"use strict";

var D2R = Math.PI / 180;
var R2D = 180 / Math.PI;

var Coord = function (lon, lat)
{
  this.lon = lon;
  this.lat = lat;
  this.x = D2R * lon;
  this.y = D2R * lat;
};

Coord.prototype.view = function ()
{
  return String(this.lon).slice(0, 4) + "," + String(this.lat).slice(0, 4);
};

Coord.prototype.antipode = function ()
{
  var anti_lat = -1 * this.lat;
  var anti_lon = this.lon < 0 ? 180 + this.lon : (180 - this.lon) * -1;
  return new Coord(anti_lon, anti_lat);
};

var LineString = function ()
{
  this.coords = [];
  this.length = 0;
};

LineString.prototype.move_to = function (coord)
{
  this.length++;
  this.coords.push(coord);
};

var Arc = function (properties)
{
  this.properties = properties || {};
  this.geometries = [];
};

Arc.prototype.json = function ()
{
  if (this.geometries.length <= 0)
  {
    return {
      geometry: { type: "LineString", coordinates: null },
      type: "Feature",
      properties: this.properties
    };
  }
  else if (this.geometries.length == 1)
  {
    return {
      geometry: { type: "LineString", coordinates: this.geometries[0].coords },
      type: "Feature",
      properties: this.properties
    };
  }
  else
  {
    var multiline = [];
    for (var i = 0; i < this.geometries.length; i++)
    {
      multiline.push(this.geometries[i].coords);
    }
    return {
      geometry: { type: "MultiLineString", coordinates: multiline },
      type: "Feature",
      properties: this.properties
    };
  }
};

// TODO - output proper multilinestring
Arc.prototype.wkt = function ()
{
  var wkt_string = "";
  var wkt = "LINESTRING(";
  var collect = function (c)
  {
    wkt += c[0] + " " + c[1] + ",";
  };
  for (var i = 0; i < this.geometries.length; i++)
  {
    if (this.geometries[i].coords.length == 0)
    {
      return "LINESTRING(empty)";
    }
    else
    {
      var coords = this.geometries[i].coords;
      coords.forEach(collect);
      wkt_string += wkt.substring(0, wkt.length - 1) + ")";
    }
  }
  return wkt_string;
};

/*
 * http://en.wikipedia.org/wiki/Great-circle_distance
 *
 */
var GreatCircle = function (start, end, properties)
{
  if (!start || start.x == undefined || start.y == undefined)
  {
    throw new Error(
      "GreatCircle constructor expects two args: start and end objects with x and y properties"
    );
  }
  if (!end || end.x == undefined || end.y == undefined)
  {
    throw new Error(
      "GreatCircle constructor expects two args: start and end objects with x and y properties"
    );
  }
  this.start = new Coord(start.x, start.y);
  this.end = new Coord(end.x, end.y);
  this.properties = properties || {};

  var w = this.start.x - this.end.x;
  var h = this.start.y - this.end.y;
  var z =
    Math.pow(Math.sin(h / 2.0), 2) +
    Math.cos(this.start.y) *
      Math.cos(this.end.y) *
      Math.pow(Math.sin(w / 2.0), 2);
  this.g = 2.0 * Math.asin(Math.sqrt(z));

  if (this.g == Math.PI)
  {
    throw new Error(
      "it appears " +
        start.view() +
        " and " +
        end.view() +
        " are 'antipodal', e.g diametrically opposite, thus there is no single route but rather infinite"
    );
  }
  else if (isNaN(this.g))
  {
    throw new Error(
      "could not calculate great circle between " + start + " and " + end
    );
  }
};

/*
 * http://williams.best.vwh.net/avform.htm#Intermediate
 */
GreatCircle.prototype.interpolate = function (f)
{
  var A = Math.sin((1 - f) * this.g) / Math.sin(this.g);
  var B = Math.sin(f * this.g) / Math.sin(this.g);
  var x =
    A * Math.cos(this.start.y) * Math.cos(this.start.x) +
    B * Math.cos(this.end.y) * Math.cos(this.end.x);
  var y =
    A * Math.cos(this.start.y) * Math.sin(this.start.x) +
    B * Math.cos(this.end.y) * Math.sin(this.end.x);
  var z = A * Math.sin(this.start.y) + B * Math.sin(this.end.y);
  var lat = R2D * Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)));
  var lon = R2D * Math.atan2(y, x);
  return [lon, lat];
};

/*
 * Generate points along the great circle
 */
GreatCircle.prototype.Arc = function (npoints, options)
{
  var first_pass = [];
  if (!npoints || npoints <= 2)
  {
    first_pass.push([this.start.lon, this.start.lat]);
    first_pass.push([this.end.lon, this.end.lat]);
  }
  else
  {
    var delta = 1.0 / (npoints - 1);
    for (var i = 0; i < npoints; ++i)
    {
      var step = delta * i;
      var pair = this.interpolate(step);
      first_pass.push(pair);
    }
  }
  /* partial port of dateline handling from:
      gdal/ogr/ogrgeometryfactory.cpp

      TODO - does not handle all wrapping scenarios yet
    */
  var bHasBigDiff = false;
  var dfMaxSmallDiffLong = 0;
  // from http://www.gdal.org/ogr2ogr.html
  // -datelineoffset:
  // (starting with GDAL 1.10) offset from dateline in degrees (default long. = +/- 10deg, geometries within 170deg to -170deg will be splited)
  var dfDateLineOffset = options && options.offset ? options.offset : 10;
  var dfLeftBorderX = 180 - dfDateLineOffset;
  var dfRightBorderX = -180 + dfDateLineOffset;
  var dfDiffSpace = 360 - dfDateLineOffset;

  // https://github.com/OSGeo/gdal/blob/7bfb9c452a59aac958bff0c8386b891edf8154ca/gdal/ogr/ogrgeometryfactory.cpp#L2342
  for (var j = 1; j < first_pass.length; ++j)
  {
    var dfPrevX = first_pass[j - 1][0];
    var dfX = first_pass[j][0];
    var dfDiffLong = Math.abs(dfX - dfPrevX);
    if (
      dfDiffLong > dfDiffSpace &&
      ((dfX > dfLeftBorderX && dfPrevX < dfRightBorderX) ||
        (dfPrevX > dfLeftBorderX && dfX < dfRightBorderX))
    )
    {
      bHasBigDiff = true;
    }
    else if (dfDiffLong > dfMaxSmallDiffLong)
    {
      dfMaxSmallDiffLong = dfDiffLong;
    }
  }

  var poMulti = [];
  if (bHasBigDiff && dfMaxSmallDiffLong < dfDateLineOffset)
  {
    var poNewLS = [];
    poMulti.push(poNewLS);
    for (var k = 0; k < first_pass.length; ++k)
    {
      var dfX0 = parseFloat(first_pass[k][0]);
      if (k > 0 && Math.abs(dfX0 - first_pass[k - 1][0]) > dfDiffSpace)
      {
        var dfX1 = parseFloat(first_pass[k - 1][0]);
        var dfY1 = parseFloat(first_pass[k - 1][1]);
        var dfX2 = parseFloat(first_pass[k][0]);
        var dfY2 = parseFloat(first_pass[k][1]);
        if (
          dfX1 > -180 &&
          dfX1 < dfRightBorderX &&
          dfX2 == 180 &&
          k + 1 < first_pass.length &&
          first_pass[k - 1][0] > -180 &&
          first_pass[k - 1][0] < dfRightBorderX
        )
        {
          poNewLS.push([-180, first_pass[k][1]]);
          k++;
          poNewLS.push([first_pass[k][0], first_pass[k][1]]);
          continue;
        }
        else if (
          dfX1 > dfLeftBorderX &&
          dfX1 < 180 &&
          dfX2 == -180 &&
          k + 1 < first_pass.length &&
          first_pass[k - 1][0] > dfLeftBorderX &&
          first_pass[k - 1][0] < 180
        )
        {
          poNewLS.push([180, first_pass[k][1]]);
          k++;
          poNewLS.push([first_pass[k][0], first_pass[k][1]]);
          continue;
        }

        if (dfX1 < dfRightBorderX && dfX2 > dfLeftBorderX)
        {
          // swap dfX1, dfX2
          var tmpX = dfX1;
          dfX1 = dfX2;
          dfX2 = tmpX;
          // swap dfY1, dfY2
          var tmpY = dfY1;
          dfY1 = dfY2;
          dfY2 = tmpY;
        }
        if (dfX1 > dfLeftBorderX && dfX2 < dfRightBorderX)
        {
          dfX2 += 360;
        }

        if (dfX1 <= 180 && dfX2 >= 180 && dfX1 < dfX2)
        {
          var dfRatio = (180 - dfX1) / (dfX2 - dfX1);
          var dfY = dfRatio * dfY2 + (1 - dfRatio) * dfY1;
          poNewLS.push([
            first_pass[k - 1][0] > dfLeftBorderX ? 180 : -180,
            dfY
          ]);
          poNewLS = [];
          poNewLS.push([
            first_pass[k - 1][0] > dfLeftBorderX ? -180 : 180,
            dfY
          ]);
          poMulti.push(poNewLS);
        }
        else
        {
          poNewLS = [];
          poMulti.push(poNewLS);
        }
        poNewLS.push([dfX0, first_pass[k][1]]);
      }
      else
      {
        poNewLS.push([first_pass[k][0], first_pass[k][1]]);
      }
    }
  }
  else
  {
    // add normally
    var poNewLS0 = [];
    poMulti.push(poNewLS0);
    for (var l = 0; l < first_pass.length; ++l)
    {
      poNewLS0.push([first_pass[l][0], first_pass[l][1]]);
    }
  }

  var arc = new Arc(this.properties);
  for (var m = 0; m < poMulti.length; ++m)
  {
    var line = new LineString();
    arc.geometries.push(line);
    var points = poMulti[m];
    for (var j0 = 0; j0 < points.length; ++j0)
    {
      line.move_to(points[j0]);
    }
  }
  return arc;
};

if (typeof module !== "undefined" && typeof module.exports !== "undefined")
{
  // nodejs
  module.exports.Coord = Coord;
  module.exports.Arc = Arc;
  module.exports.GreatCircle = GreatCircle;
}
else
{
  // browser
  var arc = {};
  arc.Coord = Coord;
  arc.Arc = Arc;
  arc.GreatCircle = GreatCircle;
}
