﻿namespace Mapbox.CheapRulerCs
{

	using System;


	public enum CheapRulerUnits
	{
		Kilometers,
		Miles,
		NauticalMiles,
		Meters,
		Yards,
		Feet,
		Inches
	}

	public class CheapRuler
	{


		private double _kx;
		private double _ky;


		/// <summary>
		/// Creates a ruler object that will approximate measurements around the given latitude. Units are one of: kilometers
		/// </summary>
		/// <param name="outputUnits"></param>
		public CheapRuler(double latitude, CheapRulerUnits outputUnits = CheapRulerUnits.Kilometers)
		{

			double factor;

			switch (outputUnits)
			{
				case CheapRulerUnits.Kilometers:
					factor = 1.0d;
					break;
				case CheapRulerUnits.Miles:
					factor = 1000.0d / 1609.344;
					break;
				case CheapRulerUnits.NauticalMiles:
					factor = 1000.0d / 1852.0d;
					break;
				case CheapRulerUnits.Meters:
					factor = 1000.0d;
					break;
				case CheapRulerUnits.Yards:
					factor = 1000.0d / 0.9144;
					break;
				case CheapRulerUnits.Feet:
					factor = 1000.0d / 0.3048;
					break;
				case CheapRulerUnits.Inches:
					factor = 1000.0d / 0.0254;
					break;
				default:
					factor = 1.0d;
					break;
			}

			var cos = Math.Cos(latitude * Math.PI / 180);
			var cos2 = 2 * cos * cos - 1;
			var cos3 = 2 * cos * cos2 - cos;
			var cos4 = 2 * cos * cos3 - cos2;
			var cos5 = 2 * cos * cos4 - cos3;

			// multipliers for converting longitude and latitude degrees into distance (http://1.usa.gov/1Wb1bv7)
			_kx = factor * (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5);
			_ky = factor * (111.13209 - 0.56605 * cos2 + 0.0012 * cos4);
		}


		/// <summary>
		/// Creates a ruler object from tile coordinates.
		/// </summary>
		/// <param name="y">Y TileId</param>
		/// <param name="z">Zoom Level</param>
		/// <param name="units"></param>
		/// <returns></returns>
		public static CheapRuler FromTile(int y, int z, CheapRulerUnits units = CheapRulerUnits.Kilometers)
		{
			var n = Math.PI * (1 - 2 * (y + 0.5) / Math.Pow(2, z));
			var lat = Math.Atan(0.5 * (Math.Exp(n) - Math.Exp(-n))) * 180 / Math.PI;
			return new CheapRuler(lat, units);
		}


		/// <summary>
		/// Given two points returns the distance.
		/// </summary>
		/// <param name="a">point [longitude, latitude]</param>
		/// <param name="b">point [longitude, latitude]</param>
		/// <returns>Distance</returns>
		public double Distance(double[] a, double[] b)
		{
			var dx = (a[0] - b[0]) * _kx;
			var dy = (a[1] - b[1]) * _ky;
			return Math.Sqrt(dx * dx + dy * dy);
		}


		/// <summary>
		/// Returns the bearing between two points in angles.
		/// </summary>
		/// <param name="a">a point [longitude, latitude]</param>
		/// <param name="b">b point [longitude, latitude]</param>
		/// <returns>Bearing</returns>
		public double Bearing(double[] a, double[] b)
		{
			var dx = (b[0] - a[0]) * _kx;
			var dy = (b[1] - a[1]) * _ky;
			if (dx == 0 && dy == 0)
			{
				return 0;
			}
			var bearing = Math.Atan2(dx, dy) * 180 / Math.PI;
			if (bearing > 180)
			{
				bearing -= 360;
			}
			return bearing;
		}


		/// <summary>
		/// Returns a new point given distance and bearing from the starting point.
		/// </summary>
		/// <param name="p"></param>
		/// <param name="distance"></param>
		/// <param name="bearing">point [longitude, latitude]</param>
		/// <returns></returns>
		public double[] Destination(double[] p, double distance, double bearing)
		{
			var a = (90 - bearing) * Math.PI / 180;
			return offset(
				p
				, Math.Cos(a) * distance
				, Math.Sin(a) * distance
			);
		}


		/// <summary>
		/// Returns a new point given easting and northing offsets (in ruler units) from the starting point.
		/// </summary>
		/// <param name="p">point [longitude, latitude]</param>
		/// <param name="dx">dx easting</param>
		/// <param name="dy">dy northing</param>
		/// <returns>point [longitude, latitude]</returns>
		private double[] offset(double[] p, double dx, double dy)
		{
			return new double[]
			{
				p[0] + dx / _kx,
				p[1] + dy / _ky
			};
		}



	}
}
