/*
 * DMS.js
 * Transcription of DMS.[ch]pp into JavaScript.
 *
 * See the documentation for the C++ class.  The conversion is a literal
 * conversion from C++.
 *
 * Copyright (c) Charles Karney (2011-2019) <charles@karney.com> and licensed
 * under the MIT/X11 License.  For more information, see
 * https://geographiclib.sourceforge.io/
 */

GeographicLib.DMS = {};

(function(
  /**
   * @exports GeographicLib/DMS
   * @description Decode/Encode angles expressed as degrees, minutes, and
   *   seconds.  This module defines several constants:
   *   - hemisphere indicator (returned by
   *       {@link module:GeographicLib/DMS.Decode Decode}) and a formatting
   *       indicator (used by
   *       {@link module:GeographicLib/DMS.Encode Encode})
   *     - NONE = 0, no designator and format as plain angle;
   *     - LATITUDE = 1, a N/S designator and format as latitude;
   *     - LONGITUDE = 2, an E/W designator and format as longitude;
   *     - AZIMUTH = 3, format as azimuth;
   *   - the specification of the trailing component in
   *       {@link module:GeographicLib/DMS.Encode Encode}
   *     - DEGREE;
   *     - MINUTE;
   *     - SECOND.
   */
  d) {

  var lookup, zerofill, internalDecode, numMatch,
      hemispheres_ = "SNWE",
      signs_ = "-+",
      digits_ = "0123456789",
      dmsindicators_ = "D'\":",
      // dmsindicatorsu_ = "\u00b0\u2032\u2033"; // Unicode variants
      dmsindicatorsu_ = "\u00b0'\"", // Use degree symbol
      components_ = ["degrees", "minutes", "seconds"];
  lookup = function(s, c) {
    return s.indexOf(c.toUpperCase());
  };
  zerofill = function(s, n) {
    return String("0000").substr(0, Math.max(0, Math.min(4, n-s.length))) +
      s;
  };
  d.NONE = 0;
  d.LATITUDE = 1;
  d.LONGITUDE = 2;
  d.AZIMUTH = 3;
  d.DEGREE = 0;
  d.MINUTE = 1;
  d.SECOND = 2;

  /**
   * @summary Decode a DMS string.
   * @description The interpretation of the string is given in the
   *   documentation of the corresponding function, Decode(string&, flag&)
   *   in the {@link
   *   https://geographiclib.sourceforge.io/html/classGeographicLib_1_1DMS.html
   *   C++ DMS class}
   * @param {string} dms the string.
   * @returns {object} r where r.val is the decoded value (degrees) and r.ind
   *   is a hemisphere designator, one of NONE, LATITUDE, LONGITUDE.
   * @throws an error if the string is illegal.
   */
  d.Decode = function(dms) {
    var dmsa = dms, end,
        v = 0, i = 0, mi, pi, vals,
        ind1 = d.NONE, ind2, p, pa, pb;
    dmsa = dmsa
      .replace(/\u2212/g, '-')  // U+2212 minus sign
      .replace(/\u00b0/g, 'd')  // U+00b0 degree symbol
      .replace(/\u00ba/g, 'd')  // U+00ba alt symbol
      .replace(/\u2070/g, 'd')  // U+2070 sup zero
      .replace(/\u02da/g, 'd')  // U+02da ring above
      .replace(/\u2032/g, '\'') // U+2032 prime
      .replace(/\u00b4/g, '\'') // U+00b4 acute accent
      .replace(/\u2019/g, '\'') // U+2019 right single quote
      .replace(/\u2033/g, '"')  // U+2033 double prime
      .replace(/\u201d/g, '"')  // U+201d right double quote
      .replace(/\u00a0/g, '')   // U+00a0 non-breaking space
      .replace(/\u202f/g, '')   // U+202f narrow space
      .replace(/\u2007/g, '')   // U+2007 figure space
      .replace(/''/g, '"')      // '' -> "
      .trim();

    end = dmsa.length;
    // p is pointer to the next piece that needs decoding
    for (p = 0; p < end; p = pb, ++i) {
      pa = p;
      // Skip over initial hemisphere letter (for i == 0)
      if (i === 0 && lookup(hemispheres_, dmsa.charAt(pa)) >= 0)
        ++pa;
      // Skip over initial sign (checking for it if i == 0)
      if (i > 0 || (pa < end && lookup(signs_, dmsa.charAt(pa)) >= 0))
        ++pa;
      // Find next sign
      mi = dmsa.substr(pa, end - pa).indexOf('-');
      pi = dmsa.substr(pa, end - pa).indexOf('+');
      if (mi < 0) mi = end; else mi += pa;
      if (pi < 0) pi = end; else pi += pa;
      pb = Math.min(mi, pi);
      vals = internalDecode(dmsa.substr(p, pb - p));
      v += vals.val; ind2 = vals.ind;
      if (ind1 === d.NONE)
        ind1 = ind2;
      else if (!(ind2 === d.NONE || ind1 === ind2))
        throw new Error("Incompatible hemisphere specifies in " +
                        dmsa.substr(0, pb));
    }
    if (i === 0)
      throw new Error("Empty or incomplete DMS string " + dmsa);
    return {val: v, ind: ind1};
  };

  internalDecode = function(dmsa) {
    var vals = {}, errormsg = "",
        sign, beg, end, ind1, k,
        ipieces, fpieces, npiece,
        icurrent, fcurrent, ncurrent, p,
        pointseen,
        digcount, intcount,
        x;
    do {                       // Executed once (provides the ability to break)
      sign = 1;
      beg = 0; end = dmsa.length;
      ind1 = d.NONE;
      k = -1;
      if (end > beg && (k = lookup(hemispheres_, dmsa.charAt(beg))) >= 0) {
        ind1 = (k & 2) ? d.LONGITUDE : d.LATITUDE;
        sign = (k & 1) ? 1 : -1;
        ++beg;
      }
      if (end > beg &&
          (k = lookup(hemispheres_, dmsa.charAt(end-1))) >= 0) {
        if (k >= 0) {
          if (ind1 !== d.NONE) {
            if (dmsa.charAt(beg - 1).toUpperCase() ===
                dmsa.charAt(end - 1).toUpperCase())
              errormsg = "Repeated hemisphere indicators " +
              dmsa.charAt(beg - 1) + " in " +
              dmsa.substr(beg - 1, end - beg + 1);
            else
              errormsg = "Contradictory hemisphere indicators " +
              dmsa.charAt(beg - 1) + " and " + dmsa.charAt(end - 1) + " in " +
              dmsa.substr(beg - 1, end - beg + 1);
            break;
          }
          ind1 = (k & 2) ? d.LONGITUDE : d.LATITUDE;
          sign = (k & 1) ? 1 : -1;
          --end;
        }
      }
      if (end > beg && (k = lookup(signs_, dmsa.charAt(beg))) >= 0) {
        if (k >= 0) {
          sign *= k ? 1 : -1;
          ++beg;
        }
      }
      if (end === beg) {
        errormsg = "Empty or incomplete DMS string " + dmsa;
        break;
      }
      ipieces = [0, 0, 0];
      fpieces = [0, 0, 0];
      npiece = 0;
      icurrent = 0;
      fcurrent = 0;
      ncurrent = 0;
      p = beg;
      pointseen = false;
      digcount = 0;
      intcount = 0;
      while (p < end) {
        x = dmsa.charAt(p++);
        if ((k = lookup(digits_, x)) >= 0) {
          ++ncurrent;
          if (digcount > 0) {
            ++digcount;         // Count of decimal digits
          } else {
            icurrent = 10 * icurrent + k;
            ++intcount;
          }
        } else if (x === '.') {
          if (pointseen) {
            errormsg = "Multiple decimal points in " +
              dmsa.substr(beg, end - beg);
            break;
          }
          pointseen = true;
          digcount = 1;
        } else if ((k = lookup(dmsindicators_, x)) >= 0) {
          if (k >= 3) {
            if (p === end) {
              errormsg = "Illegal for colon to appear at the end of " +
                dmsa.substr(beg, end - beg);
              break;
            }
            k = npiece;
          }
          if (k === npiece - 1) {
            errormsg = "Repeated " + components_[k] +
              " component in " + dmsa.substr(beg, end - beg);
            break;
          } else if (k < npiece) {
            errormsg = components_[k] + " component follows " +
              components_[npiece - 1] + " component in " +
              dmsa.substr(beg, end - beg);
            break;
          }
          if (ncurrent === 0) {
            errormsg = "Missing numbers in " + components_[k] +
              " component of " + dmsa.substr(beg, end - beg);
            break;
          }
          if (digcount > 0) {
            fcurrent = parseFloat(dmsa.substr(p - intcount - digcount - 1,
                                              intcount + digcount));
            icurrent = 0;
          }
          ipieces[k] = icurrent;
          fpieces[k] = icurrent + fcurrent;
          if (p < end) {
            npiece = k + 1;
            icurrent = fcurrent = 0;
            ncurrent = digcount = intcount = 0;
          }
        } else if (lookup(signs_, x) >= 0) {
          errormsg = "Internal sign in DMS string " +
            dmsa.substr(beg, end - beg);
          break;
        } else {
          errormsg = "Illegal character " + x + " in DMS string " +
            dmsa.substr(beg, end - beg);
          break;
        }
      }
      if (errormsg.length)
        break;
      if (lookup(dmsindicators_, dmsa.charAt(p - 1)) < 0) {
        if (npiece >= 3) {
          errormsg = "Extra text following seconds in DMS string " +
            dmsa.substr(beg, end - beg);
          break;
        }
        if (ncurrent === 0) {
          errormsg = "Missing numbers in trailing component of " +
            dmsa.substr(beg, end - beg);
          break;
        }
        if (digcount > 0) {
          fcurrent = parseFloat(dmsa.substr(p - intcount - digcount,
                                            intcount + digcount));
          icurrent = 0;
        }
        ipieces[npiece] = icurrent;
        fpieces[npiece] = icurrent + fcurrent;
      }
      if (pointseen && digcount === 0) {
        errormsg = "Decimal point in non-terminal component of " +
          dmsa.substr(beg, end - beg);
        break;
      }
      // Note that we accept 59.999999... even though it rounds to 60.
      if (ipieces[1] >= 60 || fpieces[1] > 60) {
        errormsg = "Minutes " + fpieces[1] + " not in range [0,60)";
        break;
      }
      if (ipieces[2] >= 60 || fpieces[2] > 60) {
        errormsg = "Seconds " + fpieces[2] + " not in range [0,60)";
        break;
      }
      vals.ind = ind1;
      // Assume check on range of result is made by calling routine (which
      // might be able to offer a better diagnostic).
      vals.val = sign *
        ( fpieces[2] ? (60*(60*fpieces[0] + fpieces[1]) + fpieces[2]) / 3600 :
          ( fpieces[1] ? (60*fpieces[0] + fpieces[1]) / 60 : fpieces[0] ) );
      return vals;
    } while (false);
    vals.val = numMatch(dmsa);
    if (vals.val === 0)
      throw new Error(errormsg);
    else
      vals.ind = d.NONE;
    return vals;
  };

  numMatch = function(s) {
    var t, sign, p0, p1;
    if (s.length < 3)
      return 0;
    t = s.toUpperCase().replace(/0+$/, "");
    sign = t.charAt(0) === '-' ? -1 : 1;
    p0 = t.charAt(0) === '-' || t.charAt(0) === '+' ? 1 : 0;
    p1 = t.length - 1;
    if (p1 + 1 < p0 + 3)
      return 0;
    // Strip off sign and trailing 0s
    t = t.substr(p0, p1 + 1 - p0); // Length at least 3
    if (t === "NAN" || t === "1.#QNAN" || t === "1.#SNAN" || t === "1.#IND" ||
        t === "1.#R")
      return Number.NaN;
    else if (t === "INF" || t === "1.#INF")
      return sign * Number.POSITIVE_INFINITY;
    return 0;
  };

  /**
   * @summary Decode two DMS strings interpreting them as a latitude/longitude
   *   pair.
   * @param {string} stra the first string.
   * @param {string} strb the first string.
   * @param {bool} [longfirst = false] if true assume then longitude is given
   *   first (in the absence of any hemisphere indicators).
   * @returns {object} r where r.lat is the decoded latitude and r.lon is the
   *   decoded longitude (both in degrees).
   * @throws an error if the strings are illegal.
   */
  d.DecodeLatLon = function(stra, strb, longfirst) {
    var vals = {},
        valsa = d.Decode(stra),
        valsb = d.Decode(strb),
        a = valsa.val, ia = valsa.ind,
        b = valsb.val, ib = valsb.ind,
        lat, lon;
    if (!longfirst) longfirst = false;
    if (ia === d.NONE && ib === d.NONE) {
      // Default to lat, long unless longfirst
      ia = longfirst ? d.LONGITUDE : d.LATITUDE;
      ib = longfirst ? d.LATITUDE : d.LONGITUDE;
    } else if (ia === d.NONE)
      ia = d.LATITUDE + d.LONGITUDE - ib;
    else if (ib === d.NONE)
      ib = d.LATITUDE + d.LONGITUDE - ia;
    if (ia === ib)
      throw new Error("Both " + stra + " and " + strb + " interpreted as " +
                      (ia === d.LATITUDE ? "latitudes" : "longitudes"));
    lat = ia === d.LATITUDE ? a : b;
    lon = ia === d.LATITUDE ? b : a;
    if (Math.abs(lat) > 90)
      throw new Error("Latitude " + lat + " not in [-90,90]");
    vals.lat = lat;
    vals.lon = lon;
    return vals;
  };

  /**
   * @summary Decode a DMS string interpreting it as an arc length.
   * @param {string} angstr the string (this must not include a hemisphere
   *   indicator).
   * @returns {number} the arc length (degrees).
   * @throws an error if the string is illegal.
   */
  d.DecodeAngle = function(angstr) {
    var vals = d.Decode(angstr),
        ang = vals.val, ind = vals.ind;
    if (ind !== d.NONE)
      throw new Error("Arc angle " + angstr +
                      " includes a hemisphere N/E/W/S");
    return ang;
  };

  /**
   * @summary Decode a DMS string interpreting it as an azimuth.
   * @param {string} azistr the string (this may include an E/W hemisphere
   *   indicator).
   * @returns {number} the azimuth (degrees).
   * @throws an error if the string is illegal.
   */
  d.DecodeAzimuth = function(azistr) {
    var vals = d.Decode(azistr),
        azi = vals.val, ind = vals.ind;
    if (ind === d.LATITUDE)
      throw new Error("Azimuth " + azistr + " has a latitude hemisphere N/S");
    return azi;
  };

  /**
   * @summary Convert angle (in degrees) into a DMS string (using &deg;, ',
   *  and &quot;).
   * @param {number} angle input angle (degrees).
   * @param {number} trailing one of DEGREE, MINUTE, or SECOND to indicate
   *   the trailing component of the string (this component is given as a
   *   decimal number if necessary).
   * @param {number} prec the number of digits after the decimal point for
   *   the trailing component.
   * @param {number} [ind = NONE] a formatting indicator, one of NONE,
   *   LATITUDE, LONGITUDE, AZIMUTH.
   * @returns {string} the resulting string formatted as follows:
   *   * NONE, signed result no leading zeros on degrees except in the units
   *     place, e.g., -8&deg;03'.
   *   * LATITUDE, trailing N or S hemisphere designator, no sign, pad
   *     degrees to 2 digits, e.g., 08&deg;03'S.
   *   * LONGITUDE, trailing E or W hemisphere designator, no sign, pad
   *     degrees to 3 digits, e.g., 008&deg;03'W.
   *   * AZIMUTH, convert to the range [0, 360&deg;), no sign, pad degrees to
   *     3 digits, e.g., 351&deg;57'.
   */
  d.Encode = function(angle, trailing, prec, ind) {
    // Assume check on range of input angle has been made by calling
    // routine (which might be able to offer a better diagnostic).
    var scale = 1, i, sign,
        idegree, fdegree, f, pieces, ip, fp, s;
    if (!ind) ind = d.NONE;
    if (!isFinite(angle))
      return angle < 0 ? String("-inf") :
      (angle > 0 ? String("inf") : String("nan"));

    // 15 - 2 * trailing = ceiling(log10(2^53/90/60^trailing)).
    // This suffices to give full real precision for numbers in [-90,90]
    prec = Math.min(15 - 2 * trailing, prec);
    for (i = 0; i < trailing; ++i)
      scale *= 60;
    for (i = 0; i < prec; ++i)
      scale *= 10;
    if (ind === d.AZIMUTH)
      angle -= Math.floor(angle/360) * 360;
    sign = angle < 0 ? -1 : 1;
    angle *= sign;

    // Break off integer part to preserve precision in manipulation of
    // fractional part.
    idegree = Math.floor(angle);
    fdegree = (angle - idegree) * scale + 0.5;
    f = Math.floor(fdegree);
    // Implement the "round ties to even" rule
    fdegree = (f === fdegree && (f & 1) === 1) ? f - 1 : f;
    fdegree /= scale;

    fdegree = Math.floor((angle - idegree) * scale + 0.5) / scale;
    if (fdegree >= 1) {
      idegree += 1;
      fdegree -= 1;
    }
    pieces = [fdegree, 0, 0];
    for (i = 1; i <= trailing; ++i) {
      ip = Math.floor(pieces[i - 1]);
      fp = pieces[i - 1] - ip;
      pieces[i] = fp * 60;
      pieces[i - 1] = ip;
    }
    pieces[0] += idegree;
    s = "";
    if (ind === d.NONE && sign < 0)
      s += '-';
    switch (trailing) {
    case d.DEGREE:
      s += zerofill(pieces[0].toFixed(prec),
                    ind === d.NONE ? 0 :
                    1 + Math.min(ind, 2) + prec + (prec ? 1 : 0)) +
        dmsindicatorsu_.charAt(0);
      break;
    default:
      s += zerofill(pieces[0].toFixed(0),
                    ind === d.NONE ? 0 : 1 + Math.min(ind, 2)) +
        dmsindicatorsu_.charAt(0);
      switch (trailing) {
      case d.MINUTE:
        s += zerofill(pieces[1].toFixed(prec), 2 + prec + (prec ? 1 : 0)) +
          dmsindicatorsu_.charAt(1);
        break;
      case d.SECOND:
        s += zerofill(pieces[1].toFixed(0), 2) + dmsindicatorsu_.charAt(1);
        s += zerofill(pieces[2].toFixed(prec), 2 + prec + (prec ? 1 : 0)) +
          dmsindicatorsu_.charAt(2);
        break;
      default:
        break;
      }
    }
    if (ind !== d.NONE && ind !== d.AZIMUTH)
      s += hemispheres_.charAt((ind === d.LATITUDE ? 0 : 2) +
                               (sign < 0 ? 0 : 1));
    return s;
  };
})(GeographicLib.DMS);
