/**
 * \file ProjTest.cpp
 * \brief Command line utility for testing transverse Mercator projections
 *
 * Copyright (c) Charles Karney (2008-2022) <charles@karney.com> and licensed
 * under the MIT/X11 License.  For more information, see
 * https://geographiclib.sourceforge.io/
 **********************************************************************/

#include "GeographicLib/LambertConformalConic.hpp"
#include "GeographicLib/PolarStereographic.hpp"
#include "GeographicLib/TransverseMercator.hpp"
#include "GeographicLib/TransverseMercatorExact.hpp"
#include "GeographicLib/Constants.hpp"
#include <string>
#include <limits>
#include <iostream>
#include <sstream>
#include <fstream>
#include <iomanip>
#include <stdexcept>

class DataFile {
private:
  typedef GeographicLib::Math::real real;
  std::istream* _istr;
  std::ifstream _fstr;
  //  String headers
  //    COORDINATES
  //    PROJECTION
  //    DATUM
  std::string _coords, _proj, _datum;
  //  Numeric headers
  //    CENTRAL MERIDIAN
  //    FALSE EASTING
  //    FALSE NORTHING
  //    LATITUDE OF TRUE SCALE
  //    LONGITUDE DOWN FROM POLE
  //    ORIGIN LATITUDE
  //    SCALE FACTOR
  //    STANDARD PARALLEL ONE
  //    STANDARD PARALLEL TWO
  real _lon0, _fe, _fn, _latts, _lonfp, _lat0, _k0, _lat1, _lat2;
  DataFile(const DataFile&);
  DataFile& operator=(const DataFile&);
public:
  DataFile(const std::string& file) {
    if (!(file.empty() || file == "-")) {
      _fstr.open(file.c_str());
      if (!_fstr.good())
        throw std::out_of_range("Cannot open " + file);
    }

    _istr = (file.empty() || file == "-") ? &std::cin : &_fstr;
    _coords = _proj = _datum = "NONE";
    _lon0 = _fe = _fn = _latts = _lonfp = _lat0 = _k0 = _lat1 = _lat2
      = GeographicLib::Math::NaN<real>();
    std::string s;
    while (getline(*_istr, s)) {
      if (s.empty() || s[0] == '#')
        continue;
      if (s.substr(0, 13) == "END OF HEADER")
        break;
      std::string::size_type p = s.find(':');
      if (p == std::string::npos)
        continue;
      std::string key(s, 0, p);
      p = s.find_first_not_of(" \t\v\r\n\f", p + 1);
      if (p == std::string::npos)
        continue;
      s = s.substr(p);
      p = s.find_last_not_of(" \t\v\r\n\f");
      s = s.substr(0, p + 1);
      std::istringstream is(s);
      if (key == "COORDINATES") {
        _coords = s;
      } else if (key == "PROJECTION") {
        _proj = s;
      } else if (key == "DATUM") {
        _datum = s;
      } else if (key == "CENTRAL MERIDIAN") {
        is >> _lon0;
      } else if (key == "FALSE EASTING") {
        is >> _fe;
      } else if (key == "FALSE NORTHING") {
        is >> _fn;
      } else if (key == "LATITUDE OF TRUE SCALE") {
        is >> _latts;
      } else if (key == "LONGITUDE DOWN FROM POLE") {
        is >> _lonfp;
      } else if (key == "ORIGIN LATITUDE") {
        is >> _lat0;
      } else if (key == "SCALE FACTOR") {
        is >> _k0;
      } else if (key == "STANDARD PARALLEL ONE") {
        is >> _lat1;
      } else if (key == "STANDARD PARALLEL TWO") {
        is >> _lat2;
      }
    }
  }
  bool Next(real &x, real &y) {
    char c;
    // Avoid a warning about void* changed to bool
    return (*_istr >> x >> c >> y) ? true : false;
  }
  bool Next(real &x, real &y, real &z) {
    char c, d;
    // Avoid a warning about void* changed to bool
    return (*_istr >> x >> c >> y >> d >> z) ? true : false;
  }
  const std::string& coords() const { return _coords; }
  const std::string& proj() const { return _proj; }
  const std::string& datum() const { return _datum; }
  real lon0() const { return _lon0; }
  real fe() const { return _fe; }
  real fn() const { return _fn; }
  real latts() const { return _latts; }
  real lonfp() const { return _lonfp; }
  real lat0() const { return _lat0; }
  real k0() const { return _k0; }
  real lat1() const { return _lat1; }
  real lat2() const { return _lat2; }
};

GeographicLib::Math::real
dist(GeographicLib::Math::real a, GeographicLib::Math::real f,
     GeographicLib::Math::real lat0, GeographicLib::Math::real lon0,
     GeographicLib::Math::real lat1, GeographicLib::Math::real lon1) {
  using namespace GeographicLib;
  using std::cos; using std::sin; using std::sqrt; using std::hypot;
  typedef Math::real real;
  real
    phi = lat0 * Math::degree(),
    e2 = f * (2 - f),
    sinphi = sin(phi),
    n = 1/sqrt(1 - e2 * sinphi * sinphi),
      // See Wikipedia article on latitude
    hlon = cos(phi) * n,
    hlat = (1 - e2) * n * n * n,
    dlon = lon1 - lon0;
  if (dlon >= 180) dlon -= 360;
  else if (dlon < -180) dlon += 360;
  return a * Math::degree() *
    hypot((lat1 - lat0) * hlat, dlon * hlon);
}

int usage(int retval) {
  ( retval ? std::cerr : std::cout ) <<
"ProjTest latlonfile projfile\n\
\n\
Checks projections against NGS GoldData files\n";
  return retval;
}

int main(int argc, char* argv[]) {
  using namespace GeographicLib;
  typedef Math::real real;

  if (argc != 3)
    return usage(1);
  try {
    real eps = 2*std::numeric_limits<real>::epsilon();

    enum { undef = 0, tm = 1, ps = 2, lcc = 3 };
    int m = 0;
    std::string geof(argv[++m]);
    DataFile geo(geof);
    std::string projf(argv[++m]);
    DataFile proj(projf);
    if (geo.coords() != "Geodetic")
      throw std::out_of_range("Unsupported coordinates " + geo.coords());
    real a, f;
    if (geo.datum() == "WGE") {
      a = Constants::WGS84_a();
      f = Constants::WGS84_f();
    } else if (geo.datum() == "Test_sphere") {
      a = 20000000/Math::pi();
      f = 0;
    } else if (geo.datum() == "Test_SRMmax") {
      a = 6400000;
      f = 1/real(150);
    } else
      throw std::out_of_range("Unsupported datum " + geo.datum());
    if (proj.datum() != geo.datum())
      throw std::out_of_range("Datum mismatch " +
                              geo.datum() + " " + proj.datum());
    real fe, fn, lat0, lat1, lat2, lon0, k1;
    int type = undef;
    if (proj.proj() == "Lambert Conformal Conic (1 parallel)") {
      fe = proj.fe();
      fn = proj.fn();
      k1 = proj.k0();
      lon0 = proj.lon0();
      lat1 = lat2 = lat0 = proj.lat0();
      type = lcc;
    } else if (proj.proj() == "Lambert Conformal Conic (2 parallel)") {
      fe = proj.fe();
      fn = proj.fn();
      k1 = 1;
      lon0 = proj.lon0();
      lat0 = proj.lat0();
      lat1 = proj.lat1();
      lat2 = proj.lat2();
      type = lcc;
    } else if (proj.proj() == "Mercator") {
      using std::isfinite;
      fe = proj.fe();
      fn = proj.fn();
      k1 = proj.k0();
      if (!isfinite(k1))
        k1 = 1;
      lon0 = proj.lon0();
      lat0 = 0;
      lat1 = proj.latts();
      if (!isfinite(lat1))
        lat1 = 0;
      lat2 = -lat1;
      type = lcc;
    } else if (proj.proj() == "Polar Stereographic") {
      using std::isfinite;
      fe = proj.fe();
      fn = proj.fn();
      lon0 = proj.lonfp();
      lat0 = 90;
      k1 = proj.k0();
      if (!isfinite(k1))
        k1 = 1;
      real latts = proj.latts();
      if (isfinite(latts)) {
        if (latts < 0)
          lat0 = -lat0;
        LambertConformalConic tx(a, f, lat0, 1);
        real x, y, gam, k;
        tx.Forward(0, latts, 10, x, y, gam, k);
        k1 = 1/k;
      }
      lat1 = lat2 = lat0;
      type = lon0 == 0 ? ps : lcc;
    } else if (proj.proj() == "Transverse Mercator") {
      fe = proj.fe();
      fn = proj.fn();
      lon0 = proj.lon0();
      k1 = proj.k0();
      lat0 = lat1 = lat2 = proj.lat0();
      type = tm;
    } else
      throw std::out_of_range("Unsupported projection " + proj.proj());
    LambertConformalConic tx(a, f, lat1, lat2, k1);
    PolarStereographic txa(a, f, k1);
    TransverseMercator txb(a, f, k1);
    TransverseMercatorExact txc(a, f == 0 ? real(0.1)/eps : f, k1);
    std::cout << a << " 1/" << 1/f << " " << k1 << " " << type << "\n";
    real x0, y0, gam, k;
    if (type == lcc)
      tx.Forward(lon0, lat0, lon0, x0, y0, gam, k);
    else if (type == tm)
      txb.Forward(lon0, lat0, lon0, x0, y0, gam, k);
    else
      x0 = y0 = 0;
    real lata, lona, xa, ya;
    unsigned count = 0;
    real maxerrx = 0, maxerry = 0, maxerr = 0, maxerrk = 0, maxerrr = 0;
    std::cout << std::fixed << std::setprecision(7);
    while (geo.Next(lata, lona) && proj.Next(xa, ya)) {
      using std::abs; using std::hypot;
      ++count;
      // Suppress bogus uninitialized warnings for lat and lon
      real lat = 0, lon = 0, x, y, xx, yy;
      x = xa - fe + x0;
      y = ya - fn + y0;
      xx = x/k1;
      yy = y/k1;
      switch (type) {
      case lcc:
        tx.Reverse(lon0, x, y, lat, lon, gam, k);
        break;
      case ps:
        txa.Reverse(lat1 > 0, x, y, lat, lon, gam, k);
        break;
      case tm:
        txb.Reverse(lon0, x, y, lat, lon, gam, k);
        break;
      default: break;           // To suppress warning
      }
      real errr = dist(a, f, lata, lona, lat, lon);
      maxerrr = std::max(errr, maxerrr);
      if (!(errr < 1e-6))
        std::cout << "REV: "
                  << lata << " "
                  << lona << " "
                  << xx << " "
                  << yy << " "
                  << errr << "\n";

      switch (type) {
      case lcc:
        tx.Forward(lon0, lata, lona, x, y, gam, k);
        break;
      case ps:
        txa.Forward(lat1 > 0, lata, lona, x, y, gam, k);
        break;
      case tm:
        txb.Forward(lon0, lata, lona, x, y, gam, k);
        break;
      default: break;           // To suppress warning
      }
      x -= x0;
      y -= y0;
      x += fe;
      y += fn;
      real
        errx = abs(x - xa),
        erry = abs(y - ya),
        err = hypot(errx, erry),
        errk = err/std::max(real(1),k);
      std::ostringstream sx, sxa, sy, sya;
      sx << std::fixed << std::setprecision(6) << x;
      sxa << std::fixed << std::setprecision(6) << xa;
      sy << std::fixed << std::setprecision(6) << y;
      sya << std::fixed << std::setprecision(6) << ya;
      // if (sx.str() != sxa.str() || sy.str() != sya.str())
      if (!(errk < 1e-6))
        std::cout << "FOR: "
                  << lata << " "
                  << lona << " "
                  << xx << " "
                  << yy << " "
                  << errk << "\n";
      maxerrx = std::max(errx, maxerrx);
      maxerry = std::max(erry, maxerry);
      maxerr = std::max(err, maxerr);
      maxerrk = std::max(errk, maxerrk);
    }
    std::cout << count << " records; maxerr "
              << maxerrk << " " << maxerrr << "\n";
  }
  catch (const std::exception& e) {
    std::cout << "ERROR: " << e.what() << "\n";
    return 1;
  }
  return 0;
}
