
namespace GMap.NET.Projections
{
   using System;

   /// <summary>
   /// GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.01745329251994328,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4326\"]]
   /// PROJCS["Mapy.cz",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",15],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",134400000],PARAMETER["false_northing",-41600000],UNIT["1/32meter",0.03125]]
   /// </summary>
   public class MapyCZProjection : PureProjection
   {
      public static readonly MapyCZProjection Instance = new MapyCZProjection();

      static readonly double MinLatitude = 26;
      static readonly double MaxLatitude = 76;
      static readonly double MinLongitude = -26;
      static readonly double MaxLongitude = 38;

      #region -- Common --
      static int getLCM(int zone)
      {
         if((zone < 1) || (zone > 60))
         {
            throw new Exception("MapyCZProjection: UTM Zone number is not between 1 and 60.");
         }
         else
         {
            return ((zone * 6) - 183);
         }
      }

      static double roundoff(double xx, double yy)
      {
         var x = xx;
         var y = yy;
         x = Math.Round(x * Math.Pow(10, y)) / Math.Pow(10, y);
         return x;
      }

      static readonly double UTMSIZE = 2;
      static readonly double UNITS = 1;

      #endregion

      #region -- WGSToMapyCZ --

      public long[] WGSToPP(double la, double lo)
      {
         var utmEE = wgsToUTM(DegreesToRadians(la), DegreesToRadians(lo), 33);
         var pp = utmEEToPP(utmEE[0], utmEE[1]);
         return pp;
      }

      static long[] utmEEToPP(double east, double north)
      {
         var x = (Math.Round(east) - (-3700000.0)) * Math.Pow(2, 5);
         var y = (Math.Round(north) - (1300000.0)) * Math.Pow(2, 5);

         return new long[] { (long)x, (long)y };
      }

      double[] wgsToUTM(double la, double lo, int zone)
      {
         var latrad = la;
         var lonrad = lo;
         var latddd = RadiansToDegrees(la);
         var londdd = RadiansToDegrees(lo);

         var k = 0.9996f;
         var a = Axis;
         var f = Flattening;
         var b = a * (1.0 - f);
         var e2 = (a * a - b * b) / (a * a);
         var e = Math.Sqrt(e2);
         var ei2 = (a * a - b * b) / (b * b);
         var ei = Math.Sqrt(ei2);
         var n = (a - b) / (a + b);
         var G = a * (1.0 - n) * (1.0 - n * n) * (1.0 + (9 / 4.0) * n * n + (255.0 / 64.0) * Math.Pow(n, 4)) * (PI / 180.0);
         var w = londdd - ((double) (zone * 6 - 183));
         w = DegreesToRadians(w);
         var t = Math.Tan(latrad);
         var rho = a * (1.0 - e2) / Math.Pow(1.0 - (e2 * Math.Sin(latrad) * Math.Sin(latrad)), (3 / 2.0));
         var nu = a / Math.Sqrt(1.0 - (e2 * Math.Sin(latrad) * Math.Sin(latrad)));
         var psi = nu / rho;
         var coslat = Math.Cos(latrad);
         var sinlat = Math.Sin(latrad);
         var A0 = 1 - (e2 / 4.0) - (3 * e2 * e2 / 64.0) - (5 * Math.Pow(e2, 3) / 256.0);
         var A2 = (3 / 8.0) * (e2 + (e2 * e2 / 4.0) + (15 * Math.Pow(e2, 3) / 128.0));
         var A4 = (15 / 256.0) * (e2 * e2 + (3 * Math.Pow(e2, 3) / 4.0));
         var A6 = 35 * Math.Pow(e2, 3) / 3072.0;
         var m = a * ((A0 * latrad) - (A2 * Math.Sin(2 * latrad)) + (A4 * Math.Sin(4 * latrad)) - (A6 * Math.Sin(6 * latrad)));
         var eterm1 = (w * w / 6.0) * coslat * coslat * (psi - t * t);
         var eterm2 = (Math.Pow(w, 4) / 120.0) * Math.Pow(coslat, 4) * (4 * Math.Pow(psi, 3) * (1.0 - 6 * t * t) + psi * psi * (1.0 + 8 * t * t) - psi * 2 * t * t + Math.Pow(t, 4));
         var eterm3 = (Math.Pow(w, 6) / 5040.0) * Math.Pow(coslat, 6) * (61.0 - 479 * t * t + 179 * Math.Pow(t, 4) - Math.Pow(t, 6));
         var dE = k * nu * w * coslat * (1.0 + eterm1 + eterm2 + eterm3);
         var east = 500000.0 + (dE / UNITS);
         east = roundoff(east, UTMSIZE);
         var nterm1 = (w * w / 2.0) * nu * sinlat * coslat;
         var nterm2 = (Math.Pow(w, 4) / 24.0) * nu * sinlat * Math.Pow(coslat, 3) * (4 * psi * psi + psi - t * t);
         var nterm3 = (Math.Pow(w, 6) / 720.0) * nu * sinlat * Math.Pow(coslat, 5) * (8 * Math.Pow(psi, 4) * (11.0 - 24 * t * t) - 28 * Math.Pow(psi, 3) * (1.0 - 6 * t * t) + psi * psi * (1.0 - 32 * t * t) - psi * 2 * t * t + Math.Pow(t, 4));
         var nterm4 = (Math.Pow(w, 8) / 40320.0) * nu * sinlat * Math.Pow(coslat, 7) * (1385.0 - 3111 * t * t + 543 * Math.Pow(t, 4) - Math.Pow(t, 6));
         var dN = k * (m + nterm1 + nterm2 + nterm3 + nterm4);
         var north = (0.0 + (dN / UNITS));
         north = roundoff(north, UTMSIZE);

         return new double[] { east, north, zone };
      }

      #endregion

      #region -- MapyCZToWGS --

      public double[] PPToWGS(double x, double y)
      {
         var utmEE = ppToUTMEE(x, y);
         var ret = utmToWGS(utmEE[0], utmEE[1], 33);
         return ret;
      }

      double[] ppToUTMEE(double x, double y)
      {
         var north = y * Math.Pow(2, -5) + 1300000.0;
         var east = x * Math.Pow(2, -5) + (-3700000.0);
         east = roundoff(east, UTMSIZE);
         north = roundoff(north, UTMSIZE);

         return new double[] { east, north };
      }

      double[] utmToWGS(double eastIn, double northIn, int zone)
      {
         var k = 0.9996f;
         var a = Axis;
         var f = Flattening;
         var b = a * (1.0 - f);
         var e2 = (a * a - b * b) / (a * a);
         var e = Math.Sqrt(e2);
         var ei2 = (a * a - b * b) / (b * b);
         var ei = Math.Sqrt(ei2);
         var n = (a - b) / (a + b);
         var G = a * (1.0 - n) * (1.0 - n * n) * (1.0 + (9 / 4.0) * n * n + (255 / 64.0) * Math.Pow(n, 4)) * (PI / 180.0);
         var north = (northIn - 0) * UNITS;
         var east = (eastIn - 500000.0) * UNITS;
         var m = north / k;
         var sigma = (m * PI) / (180.0 * G);
         var footlat = sigma + ((3 * n / 2.0) - (27 * Math.Pow(n, 3) / 32.0)) * Math.Sin(2 * sigma) + ((21 * n * n / 16.0) - (55 * Math.Pow(n, 4) / 32.0)) * Math.Sin(4 * sigma) + (151 * Math.Pow(n, 3) / 96.0) * Math.Sin(6 * sigma) + (1097 * Math.Pow(n, 4) / 512.0) * Math.Sin(8 * sigma);
         var rho = a * (1.0 - e2) / Math.Pow(1.0 - (e2 * Math.Sin(footlat) * Math.Sin(footlat)), (3 / 2.0));
         var nu = a / Math.Sqrt(1.0 - (e2 * Math.Sin(footlat) * Math.Sin(footlat)));
         var psi = nu / rho;
         var t = Math.Tan(footlat);
         var x = east / (k * nu);
         var laterm1 = (t / (k * rho)) * (east * x / 2.0);
         var laterm2 = (t / (k * rho)) * (east * Math.Pow(x, 3) / 24.0) * (-4 * psi * psi + 9 * psi * (1 - t * t) + 12 * t * t);
         var laterm3 = (t / (k * rho)) * (east * Math.Pow(x, 5) / 720.0) * (8 * Math.Pow(psi, 4) * (11 - 24 * t * t) - 12 * Math.Pow(psi, 3) * (21.0 - 71 * t * t) + 15 * psi * psi * (15.0 - 98 * t * t + 15 * Math.Pow(t, 4)) + 180 * psi * (5 * t * t - 3 * Math.Pow(t, 4)) + 360 * Math.Pow(t, 4));
         var laterm4 = (t / (k * rho)) * (east * Math.Pow(x, 7) / 40320.0) * (1385.0 + 3633 * t * t + 4095 * Math.Pow(t, 4) + 1575 * Math.Pow(t, 6));
         var latrad = footlat - laterm1 + laterm2 - laterm3 + laterm4;
         var lat = RadiansToDegrees(latrad);
         var seclat = 1 / Math.Cos(footlat);
         var loterm1 = x * seclat;
         var loterm2 = (Math.Pow(x, 3) / 6.0) * seclat * (psi + 2 * t * t);
         var loterm3 = (Math.Pow(x, 5) / 120.0) * seclat * (-4 * Math.Pow(psi, 3) * (1 - 6 * t * t) + psi * psi * (9 - 68 * t * t) + 72 * psi * t * t + 24 * Math.Pow(t, 4));
         var loterm4 = (Math.Pow(x, 7) / 5040.0) * seclat * (61.0 + 662 * t * t + 1320 * Math.Pow(t, 4) + 720 * Math.Pow(t, 6));
         var w = loterm1 - loterm2 + loterm3 - loterm4;
         var longrad = DegreesToRadians(getLCM(zone)) + w;
         var lon = RadiansToDegrees(longrad);

         return new double[] { lat, lon, latrad, longrad };
      }

      #endregion

      public override RectLatLng Bounds
      {
         get
         {
            return RectLatLng.FromLTRB(MinLongitude, MaxLatitude, MaxLongitude, MinLatitude);
         }
      }

      public override GSize TileSize
      {
         get
         {
            return new GSize(256, 256);
         }
      }

      public override double Axis
      {
         get
         {
            return 6378137;
         }
      }

      public override double Flattening
      {
         get
         {
            return (1.0 / 298.257223563);
         }
      }

      public override GPoint FromLatLngToPixel(double lat, double lng, int zoom)
      {
         GPoint ret = GPoint.Empty;

         lat = Clip(lat, MinLatitude, MaxLatitude);
         lng = Clip(lng, MinLongitude, MaxLongitude);

         var size = GetTileMatrixSizePixel(zoom);
         {
            var l = WGSToPP(lat, lng);
            ret.X = (long)l[0] >> (20 - zoom);
            ret.Y = size.Height - ((long)l[1] >> (20 - zoom));
         }
         return ret;
      }

      public override PointLatLng FromPixelToLatLng(long x, long y, int zoom)
      {
         PointLatLng ret = PointLatLng.Empty;

         var size = GetTileMatrixSizePixel(zoom);

         var oX = x << (20 - zoom);
         var oY = (size.Height - y) << (20 - zoom);
         {
            var l = PPToWGS(oX, oY);
            ret.Lat = Clip(l[0], MinLatitude, MaxLatitude);
            ret.Lng = Clip(l[1], MinLongitude, MaxLongitude);
         }
         return ret;
      }

      public override GSize GetTileMatrixSizeXY(int zoom)
      {
         return new GSize((long)Math.Pow(2, zoom), (long)Math.Pow(2, zoom));
      }

      public override GSize GetTileMatrixSizePixel(int zoom)
      {
         GSize s = GetTileMatrixSizeXY(zoom);
         return new GSize(s.Width << 8, s.Height << 8);
      }

      public override GSize GetTileMatrixMinXY(int zoom)
      {
         long wh = zoom > 3 ? (3 * (long)Math.Pow(2, zoom - 4)) : 1;
         return new GSize(wh, wh);
      }

      public override GSize GetTileMatrixMaxXY(int zoom)
      {
         long wh = (long)Math.Pow(2, zoom) - (long)Math.Pow(2, zoom - 2);
         return new GSize(wh, wh);
      }
   }
}
