using System;
using System.Collections.Generic;
using LeanCloud.Storage.Internal;

namespace LeanCloud
{
    /// <summary>
    /// AVGeoPoint represents a latitude / longitude point that may be associated
    /// with a key in a AVObject or used as a reference point for geo queries.
    /// This allows proximity-based queries on the key.
    ///
    /// Only one key in a class may contain a GeoPoint.
    /// </summary>
    public struct AVGeoPoint : IJsonConvertible
    {
        /// <summary>
        /// Constructs a AVGeoPoint with the specified latitude and longitude.
        /// </summary>
        /// <param name="latitude">The point's latitude.</param>
        /// <param name="longitude">The point's longitude.</param>
        public AVGeoPoint(double latitude, double longitude)
          : this()
        {
            Latitude = latitude;
            Longitude = longitude;
        }

        private double latitude;
        /// <summary>
        /// Gets or sets the latitude of the GeoPoint. Valid range is [-90, 90].
        /// Extremes should not be used.
        /// </summary>
        public double Latitude
        {
            get
            {
                return latitude;
            }
            set
            {
                if (value > 90 || value < -90)
                {
                    throw new ArgumentOutOfRangeException("value",
                      "Latitude must be within the range [-90, 90]");
                }
                latitude = value;
            }
        }

        private double longitude;
        /// <summary>
        /// Gets or sets the longitude. Valid range is [-180, 180].
        /// Extremes should not be used.
        /// </summary>
        public double Longitude
        {
            get
            {
                return longitude;
            }
            set
            {
                if (value > 180 || value < -180)
                {
                    throw new ArgumentOutOfRangeException("value",
                      "Longitude must be within the range [-180, 180]");
                }
                longitude = value;
            }
        }

        /// <summary>
        /// Get the distance in radians between this point and another GeoPoint. This is the smallest angular
        /// distance between the two points.
        /// </summary>
        /// <param name="point">GeoPoint describing the other point being measured against.</param>
        /// <returns>The distance in between the two points.</returns>
        public AVGeoDistance DistanceTo(AVGeoPoint point)
        {
            double d2r = Math.PI / 180; // radian conversion factor
            double lat1rad = Latitude * d2r;
            double long1rad = longitude * d2r;
            double lat2rad = point.Latitude * d2r;
            double long2rad = point.Longitude * d2r;
            double deltaLat = lat1rad - lat2rad;
            double deltaLong = long1rad - long2rad;
            double sinDeltaLatDiv2 = Math.Sin(deltaLat / 2);
            double sinDeltaLongDiv2 = Math.Sin(deltaLong / 2);
            // Square of half the straight line chord distance between both points.
            // [0.0, 1.0]
            double a = sinDeltaLatDiv2 * sinDeltaLatDiv2 +
              Math.Cos(lat1rad) * Math.Cos(lat2rad) * sinDeltaLongDiv2 * sinDeltaLongDiv2;
            a = Math.Min(1.0, a);
            return new AVGeoDistance(2 * Math.Asin(Math.Sqrt(a)));
        }

        IDictionary<string, object> IJsonConvertible.ToJSON()
        {
            return new Dictionary<string, object>
            {
                { "__type", "GeoPoint" },
                { "latitude", Latitude },
                { "longitude", Longitude }
            };
        }
    }
}
