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

 Copyright 2012 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 <string>
#include "inc_iostream.h"
#include "inc_stl.h"

#include "Metview.h"
#include "LLMatrixToGRIB.h"
#include "MvGeoPoints.h"
#include "MvStopWatch.h"
#include "min_max_macros.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;

double makeDateNumber( long date, long time );

//_____________________________________________________________________

class ToMatrix 
{
    MvGeoPoints GPoints;

    double      North;
    double      South;
    double      West;
    double      East;
    double      GridLat;
    double      GridLon;
    double      Tolerance;
    int         Weight_;
    int         NbLat;
    int         NbLon;
    int         numValsInTemplateGrib;
    double      Date;
    int         Parameter;
    int         Table2;
    double      *Matrix;
    vector< list<MvGeoP1> >  LatListVec;
    MvField     *templateField; // template field - defines the output grid

public:
    enum GribDefinitionMode {User, Grib};

    ToMatrix(MvRequest&);
    int load(const char*);
    int save(string &);
    bool estimate();
    bool estimateToUserGrid();
    bool estimateToTemplateGrid();
    double value(float lat, float lon);
    void sortPoints();
    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 );
    string errorMessage_;
};

//_____________________________________________________________________

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

double makeDateNumber( long date, long time )
{
    double myDate = date;
    double 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)
{
    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");
    string st = (const char*)def("INTERPOLATION_METHOD");

    if ( st == "RECIPROCAL" )
        Weight_ = G2G_RECIPROCAL;
    else if ( st == "EXPONENTIAL_MEAN" )
        Weight_ = G2G_EXPONENTIAL_MEAN;
    else
        Weight_ = G2G_EXPONENTIAL_SUM;


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

    GPoints.load( path );
    Date = makeDateNumber( GPoints[0].date(), GPoints[0].time() );
    
    list<MvGeoP1> 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( min( (int)((lat+Tolerance)+0.5), 90) );
    int band2 = latBand( max( (int)((lat-Tolerance)-0.5),-90) );

    for( int b=band1; b>=band2; --b)
    {
        for( list<MvGeoP1>::iterator gp_iter = LatListVec[b].begin();
             gp_iter != LatListVec[b].end();
            ++gp_iter )
        {
            float pi_lat = gp_iter->lat_y();

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

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

            //-- Here we have found a point inside the Interval;
            if( ! gp_iter->value_missing() )
            {
                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 gp_iter->value();   //-- 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 * gp_iter->value();

                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()
{
    cout << "ToMatrix::sortPoints(): Lat Band Size & Count: "
         << cLatBandSize
         << ", "
         << cLatBandCount
         << endl;
         
    for (int s = 0; s < GPoints.count(); s++)
      {
        int band = latBand( GPoints[s].lat_y() );
        LatListVec[band].push_back(GPoints[s]);
      }
}
//_____________________________________________________________________

bool ToMatrix::estimate()
{
    if (gribDefMode == User)
        return estimateToUserGrid();
    else
        return estimateToTemplateGrid();
}
//_____________________________________________________________________

bool ToMatrix::estimateToUserGrid()
{
  int   i, j;
  float lat,lon;
  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];

  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] == GEOPOINTS_MISSING_VALUE)  // convert missing value
            Matrix[i] = grd->missingValue();

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

    numValsInTemplateGrib = i;

    return true;
}
//_____________________________________________________________________

int ToMatrix::save(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 == NULL) 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);
        printf("indice  = %d \n", last);
    }
    else
    {
        // clone the template GRIB field and copy the computed values to it
        
        field *z = copy_field(templateField->libmars_field(), false);
        memcpy(z->values, Matrix, numValsInTemplateGrib*sizeof(double));
        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(13, matrix.errorMessage().c_str());
      return;
  }
  timer.lapTime( "estimate" );


  string tmpGribFile;

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

      string path;

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

      tmpGribFile = marstmp();

      if( LLMatrixToGRIB( path.c_str(), tmpGribFile.c_str() ) )
      {
          setError( 1, "geo_to_grib: convertion 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, "geo_to_grib: 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();
}


