/*
 * (C) Copyright 1996-2016 ECMWF & INPE.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation nor
 * does it submit to any jurisdiction.
 */


#ifdef WIN32
#pragma warning(disable : 4786)
#endif

#include <cstdlib>  //FAMI
#include <cstring>
#include <map>

using namespace std;

#include "TeDatum.h"
#include "TeException.h"
#include "TeUtils.h"

const int NUMDATUM = 9;  // number of datum
const int NUMSPH   = 8;  // number of ellipsoids

bool tokenizeWKT(char** wkt, vector<string>& tokens);

struct SphParam {
    double rd;
    double flt;
};

typedef map<string, SphParam> SphMap;

// Spheroid       Radius(Semi Major Axis)        Flattening
const char* ellipsoidList[] = {"Airy",          "6.3775634e+06", "3.340906e-03", "Clarke-1866",  "6.378206e+06",
                               "3.390060e-03",  "EarthRadius",   "6.371000e+06", "0.000000e+00", "Everest",
                               "6.3772763e+06", "3.324468e-03",  "GRS80",        "6.378137e+06", "3.352811e-03",
                               "Hayford",       "6.378388e+06",  "3.367003e-03", "UGGI67",       "6.378160e+06",
                               "3.352892e-03",  "WGS84",         "6.378137e+06", "3.352811e-03"};


// DatumName    Spheroid        Shift_X           Shift_Y           Shift_Z

const char* datumList[] = {
    "Astro-Chua",    "Hayford",       "-7.700000e+01", "2.390000e+02",  "5.000000e+00", "CorregoAlegre", "Hayford",
    "-1.387000e+02", "1.644000e+02",  "3.440000e+01",  "Indian",        "Everest",      "0.000000e+00",  "0.000000e+00",
    "0.000000e+00",  "NAD27",         "Clarke-1866",   "0.000000e+00",  "0.000000e+00", "0.000000e+00",  "NAD83",
    "GRS80",         "0.000000e+00",  "0.000000e+00",  "0.000000e+00",  "SAD69",        "UGGI67",        "0.000000e+00",
    "0.000000e+00",  "0.000000e+00",  "Spherical",     "EarthRadius",   "0.000000e+00", "0.000000e+00",  "0.000000e+00",
    "WGS84",         "WGS84",         "6.687000e+01",  "-4.370000e+00", "3.852000e+01", "Aratu",         "Hayford",
    "-7.81300e+01",  "2.9163000e+02", "-1.08480e+02"};

const char** TeGetDatumList() {
    return datumList;
}


typedef map<string, TeDatum> DatumMap;


TeDatum::TeDatum(const TeDatum& other) {
    name_ = other.name_;
    rd_   = other.rd_;
    flt_  = other.flt_;
    dx_   = other.dx_;
    dy_   = other.dy_;
    dz_   = other.dz_;
}

TeDatum& TeDatum::operator=(const TeDatum& rhs) {
    if (this != &rhs) {
        name_ = rhs.name_;
        rd_   = rhs.rd_;
        flt_  = rhs.flt_;
        dx_   = rhs.dx_;
        dy_   = rhs.dy_;
        dz_   = rhs.dz_;
    }
    return *this;
}

string TeDatum::getProj4Description() {
    double shiftX, shiftY, shiftZ;
    shiftX = shiftY = shiftZ = 0.0;

    string datumProj4 = "";
    if (name_ == "WGS84") {
        datumProj4 += " +ellps=WGS84";
        return datumProj4;
    }
    else if (name_ == "Spherical") {
        datumProj4 += " +R=" + Te2String(rd_, 6);
    }
    else if (name_ == "SAD69") {
        shiftX = -66.87;
        shiftY = 4.37;
        shiftZ = -38.52;

        datumProj4 += " +ellps=GRS67";
    }
    else if (name_ == "CorregoAlegre") {
        shiftX = -205.57;
        shiftY = 168.77;
        shiftZ = -4.12;
        datumProj4 += " +ellps=intl";
    }
    else {
        datumProj4 += " +a=" + Te2String(rd_, 6);
        datumProj4 += " +f=" + Te2String(flt_, 6);
    }

    if (shiftX != 0.0 && shiftY != 0.0 && shiftZ != 0.0)
        datumProj4 += " +towgs84=" + Te2String(shiftX, 4) + "," + Te2String(shiftY, 4) + "," + Te2String(shiftZ, 4);
    return datumProj4;
}

string TeDatum::getWKTDescription() {
    string sph_epsgcode = "";
    string dtm_epsgcode = "";
    string spheroid     = "";
    string crs_epsgcode = "";
    string datumWKT     = "GEOGCS[\"" + name_ + "\"," + "DATUM[\"" + name_ + "\", SPHEROID[\"";
    if (name_ == "Astro-Chua") {
        spheroid     = "Hayford";
        sph_epsgcode = "7022";
        dtm_epsgcode = "6224";
        crs_epsgcode = "4224";
    }
    else if (name_ == "CorregoAlegre") {
        spheroid     = "Hayford";
        sph_epsgcode = "7022";
        dtm_epsgcode = "6225";
        crs_epsgcode = "4225";
    }
    else if (name_ == "Indian") {
        spheroid     = "Everest";
        sph_epsgcode = "7015";
        dtm_epsgcode = "6239";
        crs_epsgcode = "4240";
    }
    else if (name_ == "NAD27") {
        spheroid     = "Clarke 1866";
        sph_epsgcode = "7008";
        dtm_epsgcode = "6609";
        crs_epsgcode = "4267";
    }
    else if (name_ == "NAD83") {
        spheroid     = "GRS80";
        sph_epsgcode = "7019";
        dtm_epsgcode = "6326";
        crs_epsgcode = "4269";
    }
    else if (name_ == "WGS84") {
        spheroid     = "GRS80";
        sph_epsgcode = "7019";
        dtm_epsgcode = "6326";
        crs_epsgcode = "4326";
    }
    else if (name_ == "SAD69") {
        spheroid     = "UGGI67";
        sph_epsgcode = "7036";
        dtm_epsgcode = "6291";
        crs_epsgcode = "4618";
    }
    else if (name_ == "Spherical") {
        spheroid     = "EarthRadius";
        sph_epsgcode = "7035";
    }
    else
        spheroid = "UserDefined";
    datumWKT += spheroid + "\"," + Te2String(rd_, 6) + ", " + Te2String(1.0 / flt_, 6);
    if (!sph_epsgcode.empty())
        datumWKT += ",AUTHORITY[\"EPSG\"," + sph_epsgcode + "]";
    datumWKT += "]";
    double shiftX, shiftY, shiftZ;
    shiftX = shiftY = shiftZ = 0.0;
    if (name_ == "SAD69") {
        shiftX = -66.87;
        shiftY = 4.37;
        shiftZ = -38.52;
    }
    else if (name_ == "CorregoAlegre") {
        shiftX = -205.57;
        shiftY = 168.77;
        shiftZ = -4.12;
    }
    if (shiftX != 0.0 && shiftY != 0.0 && shiftZ != 0.0)
        datumWKT += ",TOWGS84[" + Te2String(shiftX, 4) + "," + Te2String(shiftY, 4) + "," + Te2String(shiftZ, 4) +
                    ",0.0,0.0,0.0,0.0]";

    if (!dtm_epsgcode.empty())
        datumWKT += ",AUTHORITY[\"EPSG\"," + dtm_epsgcode + "]";
    datumWKT += "]";
    if (!crs_epsgcode.empty())
        datumWKT += ",AUTHORITY[\"EPSG\"," + crs_epsgcode + "]";
    datumWKT += "]";
    return datumWKT;
}

TeDatum TeDatumFactory::make(const string& name) {
    DatumMap datumMap_;
    // Step 1 - Read the Spheroids - Put in a Map

    SphMap sphMap;

    SphParam sphPar;

    int i, k = 0;

    for (i = 0; i < NUMSPH; i++) {
        string name = ellipsoidList[k++];
        sphPar.rd   = atof(ellipsoidList[k++]);
        sphPar.flt  = atof(ellipsoidList[k++]);

        sphMap[name] = sphPar;
    }

    // Step 2 - Read the Datum List, create a Map


    k = 0;

    for (i = 0; i < NUMDATUM; i++) {
        string name = datumList[k++];

        string sphname = datumList[k++];

        double dx = atof(datumList[k++]);
        double dy = atof(datumList[k++]);
        double dz = atof(datumList[k++]);

        SphMap::iterator it = sphMap.find(sphname);

        if (it == sphMap.end()) {
            throw TeException(DATUM_ERROR);
        }
        else
            sphPar = (*it).second;

        TeDatum dat(name, sphPar.rd, sphPar.flt, dx, dy, dz);

        datumMap_[name] = dat;  // create a datum map
    }

    DatumMap::iterator it = datumMap_.find(name);

    if (it != datumMap_.end())  // found it
        return (*it).second;

    return TeDatum("Spherical");
}

TeDatum TeDatumFactory::makeFromEPSGCode(const std::string epsgCode) {
    string dname;
    if (epsgCode == "6224") {
        dname = "Astro-Chua";
    }
    else if (epsgCode == "6225") {
        dname = "CorregoAlegre";
    }
    else if (epsgCode == "6240") {
        dname = "Indian";
    }
    else if (epsgCode == "6609") {
        dname = "NAD27";
    }
    else if (epsgCode == "6152") {
        dname = "NAD83";
    }
    else if (epsgCode == "6326") {
        dname = "WGS84";
    }
    else if (epsgCode == "6618") {
        dname = "SAD69";
    }
    else {
        dname = "Spherical";
    }
    TeDatum dat = TeDatumFactory::make(dname);
    return dat;
}


TeDatum TeDatumFactory::makeFromWKT(const std::string wktDatumDescription) {
    TeDatum dat;
    vector<string> tokens;
    char* wktchar = new char[wktDatumDescription.size()];
    strcpy(wktchar, wktDatumDescription.c_str());
    if (!tokenizeWKT((char**)&wktchar, tokens)) {
        return dat;
    }
    unsigned int i = 0;
    while (i < tokens.size() && tokens[i] != "DATUM")
        ++i;

    if (i == tokens.size())
        return dat;
    ++i;

    map<string, string> ewktToTeDatum;
    ewktToTeDatum["D_Chua"]                = "Astro-Chua";
    ewktToTeDatum["D_Corrego_Alegre"]      = "CorregoAlegre";
    ewktToTeDatum["D_Indian_1975"]         = "Indian";
    ewktToTeDatum["D_North_American_1927"] = "NAD27";
    ewktToTeDatum["D_North_American_1983"] = "NAD83";
    ewktToTeDatum["D_South_American_1969"] = "SAD69";
    ewktToTeDatum["D_WGS_1984"]            = "WGS84";
    ewktToTeDatum["D_Aratu"]               = "Aratu";
    // lets the ESRI's WKT format
    TeTrim(tokens[i]);
    map<string, string>::iterator it = ewktToTeDatum.find(tokens[i]);
    if (it != ewktToTeDatum.end()) {
        dat = TeDatumFactory::make(it->second);
        return dat;
    }

    map<string, string> wktToTeDatum;
    wktToTeDatum["Chua"]                      = "Astro-Chua";
    wktToTeDatum["Corrego_Alegre"]            = "CorregoAlegre";
    wktToTeDatum["Indian_1975"]               = "Indian";
    wktToTeDatum["North_American_Datum_1927"] = "NAD27";
    wktToTeDatum["North_American_Datum_1983"] = "NAD83";
    wktToTeDatum["South_American_Datum_1969"] = "SAD69";
    wktToTeDatum["Unknown"]                   = "Spherical";
    wktToTeDatum["WGS_1984"]                  = "WGS84";
    wktToTeDatum["Aratu"]                     = "Aratu";

    // lets try OGC's SFS WKT format
    it = wktToTeDatum.find(tokens[i]);
    if (it != wktToTeDatum.end()) {
        dat = TeDatumFactory::make(it->second);
        return dat;
    }

    // lets try EPSG authority codes
    unsigned int is = i;
    while (i < tokens.size() && tokens[i] != "AUTHORITY")
        ++i;
    if (i == tokens.size())
        return dat;
    ++i;
    if (tokens[i] == "EPSG") {
        ++i;
        dat = TeDatumFactory::makeFromEPSGCode(tokens[i]);
        return dat;
    }

    // so let's try according to the parameters
    i = is;
    dat.name(tokens[i]);
    while (i < tokens.size() && tokens[i] != "SPHEROID")
        ++i;
    if (i == tokens.size())
        return dat;
    ++i;
    ++i;
    dat.radius(atof(tokens[i].c_str()));
    ++i;
    dat.flattening(1 / atof(tokens[i].c_str()));
    ++i;
    if (tokens[i] == "TOWGS84" && i < tokens.size()) {
        ++i;
        double dx = (atof(tokens[i].c_str()));
        ++i;
        double dy = (atof(tokens[i].c_str()));
        ++i;
        double dz = (atof(tokens[i].c_str()));
        ++i;
        ++i;
        ++i;
        ++i;
        if (dx == 0.0 && dy == 0 && dz == 0.0) {
            dat = TeDatum("WGS84", 6378137, 0.003352811, 66.87, -4.37, 38.52);
        }
        // this is a SAD69
        else if (dx == -66.87 && dy == 4.37 && dz == -38.52) {
            dat = TeDatum("SAD69", 6378160, 0.003352892, 0.0, 0.0, 0.0);
        }
        else {
            dat.dx_ = dx;
            dat.dy_ = dy;
            dat.dz_ = dz;
        }
    }
    return dat;
}


bool findDatum(double semiMajor, double flatenning, TeDatum& datum) {
    DatumMap datumMap_;

    // Step 1 - Read the Spheroids - Put in a Map

    SphMap sphMap;
    SphParam sphPar;

    int i, k = 0;

    for (i = 0; i < NUMSPH; i++) {
        string name = ellipsoidList[k++];
        sphPar.rd   = atof(ellipsoidList[k++]);
        sphPar.flt  = atof(ellipsoidList[k++]);

        sphMap[name] = sphPar;
    }

    // Step 2 - Read the Datum List, create a Map
    k = 0;

    for (i = 0; i < NUMDATUM; i++) {
        string name    = datumList[k++];
        string sphname = datumList[k++];
        double dx      = atof(datumList[k++]);
        double dy      = atof(datumList[k++]);
        double dz      = atof(datumList[k++]);

        SphMap::iterator it = sphMap.find(sphname);

        if (it == sphMap.end()) {
            throw TeException(DATUM_ERROR);
        }
        else
            sphPar = (*it).second;
        TeDatum dat(name, sphPar.rd, sphPar.flt, dx, dy, dz);
        datumMap_[name] = dat;  // create a datum map
    }

    DatumMap::iterator it = datumMap_.begin();
    while (it != datumMap_.end()) {
        TeDatum dt = (*it).second;
        if (TeCompareDouble(dt.radius(), semiMajor, 6) && TeCompareDouble(dt.flattening(), flatenning, 6)) {
            datum = dt;
            return true;
        }
        ++it;
    }
    return false;
}
