/***************************** LICENSE START ***********************************

 Copyright 2021 ECMWF and INPE. This software is distributed under the terms
 of the Apache License version 2.0. In applying this license, ECMWF does not
 waive the privileges and immunities granted to it by virtue of its status as
 an Intergovernmental Organization or submit itself to any jurisdiction.

 ***************************** LICENSE END *************************************/

// geo_to_grib.cc, 010309/vk   (Q&D hack of tomatrix.cc)

#include <algorithm>
#include <cstring>
#include <list>
#include <sstream>
#include <string>
#include <vector>

#include "Metview.h"
#include "LLMatrixToGRIB.h"
#include "MvGeoPoints.h"
#include "MvStopWatch.h"


#ifndef MISSING_DATA
const double MISSING_DATA = 1E21;
#endif

const float cLatBandSize = 1.0;
const int cLatBandCount = (int)(180.0 / cLatBandSize) + 1;
const int G2G_RECIPROCAL = 0;
const int G2G_EXPONENTIAL_MEAN = 1;
const int G2G_EXPONENTIAL_SUM = 2;
const int G2G_NEAREST_MEAN = 3;
const int G2G_NEAREST_SUM = 4;
const int G2G_NEAREST_COUNT = 5;

double makeDateNumber(long date, long time);

//_____________________________________________________________________

class ToMatrix
{
    MvGeoPoints GPoints;

    double North{0.};
    double South{0.};
    double West{0.};
    double East{0.};
    double GridLat{1.};
    double GridLon{1.};
    double Tolerance{0.};
    int Weight_{0};
    int NbLat{0};
    int NbLon{0};
    int numValsInTemplateGrib{0};
    double Date{0.};
    int Parameter{-1};
    int Table2{-1};
    double* Matrix{nullptr};
    std::vector<std::list<size_t> > LatListVec;  // will store indexes
    MvField* templateField{nullptr};             // template field - defines the output grid

public:
    enum GribDefinitionMode
    {
        User,
        Grib
    };

    ToMatrix(MvRequest&);
    ToMatrix(const ToMatrix&) = delete;
    ToMatrix& operator=(const ToMatrix&) = delete;

    int load(const char*);
    int save(std::string&);
    bool estimate();
    bool estimateToUserGrid();
    bool estimateToTemplateGrid();
    bool estimateNearestToTemplateGrid();
    void setupUserMatrix(bool zero);
    bool convertUserToTemplateGrid();
    double value(float lat, float lon);
    void sortPoints();
    std::string& errorMessage() { return errorMessage_; }
    GribDefinitionMode mode() { return gribDefMode; }

protected:
    GribDefinitionMode gribDefMode;

    float long180(float x) { return x > 180 ? x - 360 : x; }
    float long360(float x) { return x < 0 ? x + 360 : x; }
    float minDistance(float x1, float x2);
    int latBand(float lat);
    std::string errorMessage_;
};

//_____________________________________________________________________

class GeoToGRIB : public MvService
{
public:
    GeoToGRIB() :
        MvService("GEO_TO_GRIB") {}
    void serve(MvRequest&, MvRequest&);
};
//_____________________________________________________________________

double makeDateNumber(long date, long time)
{
    auto myDate = double(date);
    auto myTime = double(time);

    if (myDate < 470620)  //-- must be relative => no time
        return myDate;

    if (myTime < 0)  //-- illegal or rubbish
        return myDate;

    if (myTime > 2400)  //-- illegal or rubbish
        return myDate;

    if (myTime >= 100)  //-- 01:00...24:00
        return myDate + myTime / 2400.;

    if (myTime > 59)  //-- illegal or rubbish
        return myDate;

    if (myTime > 24)  //-- 00:25...00:59
        return myDate + myTime / 60. / 24.;

    return myDate + myTime / 24.;
}

//_____________________________________________________________________

ToMatrix::ToMatrix(MvRequest& def)
{
    std::string mode = (const char*)def("GRID_DEFINITION_MODE");

    if (mode == "USER") {
        gribDefMode = User;
        North = def("AREA", 0);
        West = def("AREA", 1);
        South = def("AREA", 2);
        East = def("AREA", 3);
        GridLon = def("GRID");
        GridLat = def("GRID", 1);

        if (North < South) {
            double tmp = North;
            North = South;
            South = tmp;
        }

        if (!GridLon)
            GridLon = 1.5;
        if (!GridLat)
            GridLat = GridLon;
    }
    else {
        gribDefMode = Grib;  // user supplied a template GRIB file to define the output grid

        //-- data request => MvField
        MvRequest grb;
        def.getValue(grb, "TEMPLATE_GRIB");
        fieldset* fs = request_to_fieldset((request*)grb);
        field* f = get_field(fs, 0, expand_mem);
        templateField = new MvField(f);

        if (fs->count > 1)  //-- warning if several fields
        {
            marslog(LOG_INFO, "Info: only first field in the template GRIB will be considered");
        }
    }


    Tolerance = def("TOLERANCE");
    Parameter = def("PARAMETER");
    Table2 = def("GRIB_TABLE2_VERSION");
    std::string st = (const char*)def("INTERPOLATION_METHOD");

    if (st == "RECIPROCAL")
        Weight_ = G2G_RECIPROCAL;
    else if (st == "EXPONENTIAL_MEAN")
        Weight_ = G2G_EXPONENTIAL_MEAN;
    else if (st == "EXPONENTIAL_SUM")
        Weight_ = G2G_EXPONENTIAL_SUM;
    else if (st == "NEAREST_GRIDPOINT_MEAN")
        Weight_ = G2G_NEAREST_MEAN;
    else if (st == "NEAREST_GRIDPOINT_SUM")
        Weight_ = G2G_NEAREST_SUM;
    else
        Weight_ = G2G_NEAREST_COUNT;


    MvRequest data;
    def.getValue(data, "GEOPOINTS");
    const char* path = data("PATH");

    GPoints.load(path);
    Date = makeDateNumber(GPoints.date(0), GPoints.time(0));

    std::list<size_t> emptyList;
    LatListVec.assign(cLatBandCount + 1, emptyList);
}
//_____________________________________________________________________

double ToMatrix::value(float lat, float lon)
{
    double val = MISSING_DATA;
    double dist, coef;
    double sigma = 0;

    int band1 = latBand(std::min((int)((lat + Tolerance) + 0.5), 90));
    int band2 = latBand(std::max((int)((lat - Tolerance) - 0.5), -90));

    for (int b = band1; b >= band2; --b) {
        for (const auto& gp_iter : LatListVec[b]) {
            float pi_lat = GPoints.lat_y(gp_iter);

            if ((fabs(lat - pi_lat)) > Tolerance)
                continue;

            float pi_lon = GPoints.lon_x(gp_iter);
            if (minDistance(pi_lon, lon) > Tolerance)
                continue;

            //-- Here we have found a point inside the Interval;
            if (!GPoints.value_missing(gp_iter, 0)) {
                double x = minDistance(lon, pi_lon);
                double y = lat - pi_lat;
                dist = (x * x) + (y * y);

                // Compute weight
                if (Weight_ == G2G_RECIPROCAL) {
                    if (dist == 0)
                        return GPoints.value(gp_iter);  //-- Here the point is on the Grid
                    dist = 1 / dist;
                }
                else  // exponential
                {
                    if (Tolerance != 0)
                        dist = exp(-(dist / (pow(Tolerance, 2))));
                    else
                        dist = dist ? 0 : 1;
                }

                sigma += dist;
                coef = dist * GPoints.value(gp_iter);

                if (val == MISSING_DATA)
                    val = coef;
                else
                    val += coef;
            }
        }
    }

    if (sigma && Weight_ != G2G_EXPONENTIAL_SUM)
        val = val / sigma;

    return val;
}

//_____________________________________________________________________

float ToMatrix::minDistance(float x1, float x2)
{
    // calculate two ways, in case given values are
    // on the different sides of discontinuity line!

    float min1 = long180(x1) - long180(x2);
    if (min1 < 0.0)
        min1 = -min1;

    float min2 = long360(x1) - long360(x2);
    if (min2 < 0.0)
        min2 = -min2;

    return min1 < min2 ? min1 : min2;
}
//_____________________________________________________________________

int ToMatrix::latBand(float lat)
{
    return int((lat + 90.5) / cLatBandSize);
}
//_____________________________________________________________________

void ToMatrix::sortPoints()
{
    std::ostringstream oss;
    oss << "ToMatrix::sortPoints(): Lat Band Size & Count: "
        << cLatBandSize
        << ", "
        << cLatBandCount;
    marslog(LOG_INFO, oss.str().c_str());

    for (size_t s = 0; s < GPoints.count(); s++) {
        int band = latBand(GPoints.lat_y(s));
        LatListVec[band].push_back(s);
    }
}
//_____________________________________________________________________

bool ToMatrix::estimate()
{
    if (Weight_ == G2G_NEAREST_MEAN || Weight_ == G2G_NEAREST_SUM || Weight_ == G2G_NEAREST_COUNT) {
        //  if User, convert to Template, because we need that on order to use these modes

        if (gribDefMode == User)
            if (!convertUserToTemplateGrid())
                return false;

        return estimateNearestToTemplateGrid();
    }
    else {
        if (gribDefMode == User)
            return estimateToUserGrid();
        else
            return estimateToTemplateGrid();
    }
}


//_____________________________________________________________________

void ToMatrix::setupUserMatrix(bool zero)
{
    double epsilon = 0.001;

    double oriEast = East;

    if ((East - West) == 360)
        East -= GridLon;

    double dNbLon = (East - West) / GridLon;
    NbLon = static_cast<int>(rint(dNbLon)) + 1;

    double dNbLat = (North - South) / GridLat;
    NbLat = static_cast<int>(rint(dNbLat)) + 1;

    if (fabs(rint(dNbLon) - dNbLon) > epsilon) {
        marslog(LOG_WARN, "Target grid resolution does not match the domain in West-East direction!");
        marslog(LOG_WARN, "--> Dx=%f West=%f East=%f", GridLon, West, oriEast);
        marslog(LOG_WARN, "Please check grid geometry in the resulting GRIB!");
    }

    if (fabs(rint(dNbLat) - dNbLat) > epsilon) {
        marslog(LOG_WARN, "Target grid resolution does not match the domain in North-South direction!");
        marslog(LOG_WARN, "--> Dy=%f North=%f South=%f", GridLat, North, South);
        marslog(LOG_WARN, "Please check grid geometry in the resulting GRIB!");
    }

// This is the old method, which did not always work properly. Sometimes there were
//  one less column or row in the target grid than expected!
#if 0  
  NbLon = static_cast< int >( (East  - West ) / GridLon + 1 );
  NbLat = static_cast< int >( (North - South) / GridLat + 1 );
#endif

    Matrix = new double[NbLon * NbLat];

    if (zero)
        memset(Matrix, 0, NbLon * NbLat * sizeof(double));  // set all values to zero
}

//_____________________________________________________________________

bool ToMatrix::estimateToUserGrid()
{
    int i, j;
    float lat, lon;

    setupUserMatrix(false);

    lat = North;
    for (j = 0; j < NbLat; j++) {
        lon = West;
        for (i = 0; i < NbLon; i++) {
            Matrix[i + (j * NbLon)] = value(lat, lon);
            lon += GridLon;
        }
        lat -= GridLat;
    }

    return true;
}
//_____________________________________________________________________

bool ToMatrix::estimateToTemplateGrid()
{
    MvGridBase* grd = templateField->mvGrid();  // be careful with this pointer because MvField is in charge!

    if (!grd->hasLocationInfo())  //-- check that it is ok
    {
        errorMessage_ = "Template GRIB field grid locations cannot be read by Metview.";
        return false;
    }


    // for each gridpoint, get the location and compute the geopoint value there

    Matrix = new double[templateField->countValues()];

    MvGridPoint gp;
    int i = 0;

    do {
        gp = grd->gridPoint();

        Matrix[i] = value(gp.loc_.latitude(), gp.loc_.longitude());

        if (Matrix[i] == MISSING_DATA)  // convert missing value
            Matrix[i] = grd->missingValue();

        i++;
    } while (grd->advance());

    numValsInTemplateGrib = i;

    return true;
}

//_____________________________________________________________________

bool ToMatrix::estimateNearestToTemplateGrid()
{
    unsigned int* numClosest;
    templateField->setShape(expand_mem);
    MvGridBase* grid = templateField->mvGrid();  // be careful with this pointer because MvField is in charge!

    if (!grid->hasLocationInfo())  //-- check that it is ok
    {
        errorMessage_ = "Template GRIB field grid locations cannot be read by Metview.";
        return false;
    }

    numValsInTemplateGrib = templateField->countValues();


    Matrix = new double[numValsInTemplateGrib];                 // create the matrix to hold the result
    memset(Matrix, 0, numValsInTemplateGrib * sizeof(double));  // set all values to zero


    // create matrix (zeroed) to store how many gpts are assinged to each grid point
    numClosest = new unsigned int[numValsInTemplateGrib];
    memset(numClosest, 0, numValsInTemplateGrib * sizeof(unsigned int));


    // for each geopoint, find the nearest gridpoint and add the geopoint's value to that gridpoint
    long ngeo = GPoints.count();
    for (long i = 0; i < ngeo; i++) {
        if (!GPoints.value_missing(i, 0)) {
            MvGridPoint gridpt = grid->nearestGridpoint(GPoints.lat_y(i), GPoints.lon_x(i), false);
            if (Weight_ == G2G_NEAREST_COUNT)
                Matrix[gridpt.index_]++;  // G2G_NEAREST_COUNT
            else
                Matrix[gridpt.index_] += GPoints.value(i);  // G2G_NEAREST_MEAN or G2G_NEAREST_SUM

            numClosest[gridpt.index_]++;
        }
    }


    // mean? then divide by the number of points (or insert missing value if no near points)
    // sum? then insert missing value if no near points
    if (Weight_ == G2G_NEAREST_MEAN) {
        for (long j = 0; j < numValsInTemplateGrib; j++)
            if (numClosest[j] != 0)
                Matrix[j] /= numClosest[j];
            else
                Matrix[j] = grid->missingValue();
    }

    else if (Weight_ == G2G_NEAREST_SUM) {
        for (long j = 0; j < numValsInTemplateGrib; j++)
            if (numClosest[j] == 0)
                Matrix[j] = grid->missingValue();
    }


    delete[] numClosest;

    return true;
}

//_____________________________________________________________________

bool ToMatrix::convertUserToTemplateGrid()
{
    // the user has supplied grid parameters, but we want to convert this into
    // an actual GRIB file so that we can use it as a template

    setupUserMatrix(true);

    std::string llmatrixpath;
    if (save(llmatrixpath)) {
        marslog(LOG_INFO, "geo_to_grib: can not write matrix into file %s", llmatrixpath.c_str());
        return false;
    }

    std::string tmpGribFile = marstmp();

    if (LLMatrixToGRIB(llmatrixpath.c_str(), tmpGribFile.c_str())) {
        marslog(LOG_INFO, "geo_to_grib: conversion from geo/LLMatrix to GRIB failed");
        return false;
    }

    unlink(llmatrixpath.c_str());  // remove the temporary LLMATRIX file


    MvFieldSet fieldset(tmpGribFile.c_str());  // load it up
    templateField = new MvField(fieldset[0].libmars_field());

    gribDefMode = Grib;

    return true;
}


//_____________________________________________________________________

int ToMatrix::save(std::string& path)  // returns path to caller
{
    if (gribDefMode == User) {
        // create an LLMatrix text file to be later converted to GRIB

        path = marstmp();
        FILE* f = fopen(path.c_str(), "w");
        if (f == nullptr)
            return 1;

        fprintf(f, "#LLMATRIX\n");
        fprintf(f, "GRID = %g/%g\n", GridLon, GridLat);
        fprintf(f, "NORTH = %g\n", North);
        fprintf(f, "SOUTH = %g\n", South);
        fprintf(f, "EAST = %g\n", East);
        fprintf(f, "WEST = %g\n", West);
        fprintf(f, "NLAT = %d\n", NbLat);
        fprintf(f, "NLON = %d\n", NbLon);
        fprintf(f, "DATE = %fl\n", Date);
        fprintf(f, "MISSING = '%g'\n", MISSING_DATA);
        fprintf(f, "PARAM   = %d\n", Parameter);
        fprintf(f, "TABLE2  = %d\n", Table2);
        fprintf(f, "#DATA\n");

        // int last;

        for (int j = 0; j < NbLat; j++) {
            for (int i = 0; i < NbLon; i++) {
                // last = i + ( j*NbLon);
                fprintf(f, "%g\n", Matrix[i + (j * NbLon)]);
            }
        }
        fclose(f);
    }
    else {
        // clone the template GRIB field and copy the computed values to it

        field* z = copy_field(templateField->libmars_field(), false);
        std::memcpy(z->values, Matrix, numValsInTemplateGrib * sizeof(double));

        // there could be missing values in the generated GRIB
        z->bitmap = true;
        MvField resultField(z);

        if (Parameter != 255) {
            resultField.setGribKeyValueLong("table2Version", Table2);
            resultField.setGribKeyValueLong("paramId", Parameter);  // paramId to ensure GRIB2 ok
        }

        MvFieldSet fs;
        fs += resultField;
        MvRequest req(fs.getRequest());   // this forces save-to-file
        path = (const char*)req("PATH");  // return path to caller
    }

    return 0;
}
//_____________________________________________________________________

void GeoToGRIB::serve(MvRequest& in, MvRequest& out)
{
    MvStopWatch timer("GeoToGRIB");

    ToMatrix matrix(in);
    timer.lapTime("init");

    matrix.sortPoints();
    timer.lapTime("sortPoints");

    if (!matrix.estimate()) {
        setError(1, "GeoToGRIB-> %s", matrix.errorMessage().c_str());
        return;
    }
    timer.lapTime("estimate");


    std::string tmpGribFile;

    if (matrix.mode() == ToMatrix::User) {
        // create an intermediate LLMatrix file, then convert to GRIB

        std::string path;

        if (matrix.save(path)) {
            setError(1, "GeoToGRIB-> can not write matrix into file %s", path.c_str());
            return;
        }

        tmpGribFile = marstmp();

        if (LLMatrixToGRIB(path.c_str(), tmpGribFile.c_str())) {
            setError(1, "GeoToGRIB-> conversion from geo/LLMatrix to GRIB failed");
            return;
        }

        unlink(path.c_str());  // remove the temporary LLMATRIX file
    }
    else {
        // write the GRIB directly rather than creating an intermediate LLMatrix file

        if (matrix.save(tmpGribFile)) {
            setError(1, "GeoToGRIB-> can not write GRIB into file %s", tmpGribFile.c_str());
            return;
        }
    }

    MvRequest grib = "GRIB";
    grib("PATH") = tmpGribFile.c_str();
    grib("TEMPORARY") = 1;

    out = grib;
}
//_____________________________________________________________________

int main(int argc, char** argv)
{
    MvApplication theApp(argc, argv);
    GeoToGRIB tool;
    theApp.run();
}
