/***************************** 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 *************************************/

#include <iostream>
#include <sstream>
#include <string>

#include "Metview.h"
#include "MvTimeSerie.h"
#include "MvLocation.h"
#include "MvDate.h"
#include "MvGeoPoints.h"
#include "MvObs.h"


const double cBIGDOUBLE = 3.4E35;
const char cTAB[]       = "\t";
const std::string cGRIB("GRIB");
const std::string cGPTS("GEOPOINTS");
const std::string cBUFR("BUFR");

const long cBufrType    = 252;
const long cBufrSubtype = 0;


class TimeSerie : public MvService
{
protected:
    TimeSerie(char* a) :
        MvService(a){};

    void init(MvRequest&);
    void do_geopoints();
    std::string ref_string(int param, int level, int number, const MvLocation& loca, const char* expver);
    void set_basedate(int);

    virtual bool do_the_job() = 0;

public:
    TimeSerie() :
        MvService("TIMESERIES"){};
    ~TimeSerie() { delete serie_; };
    void serve(MvRequest&, MvRequest&);

protected:
    MvRequest data_;
    MvRequest station_;
    std::string name_;
    MvRequest axis_;  //-- dummy
    MvRequest graph_;
    MvTimeSerie* serie_;
    MvDate base_date_;
    bool base_date_set_;
    bool scaling_;
    double scaleAdd_;
    double scaleMul_;
    int message_;
    double area_[4];
};


class FieldPointTimeSerie : public TimeSerie
{
public:
    FieldPointTimeSerie() :
        TimeSerie("FIELDPOINT_TIMESERIES"){};
    virtual bool do_the_job();
};


class FieldMeanTimeSerie : public TimeSerie
{
public:
    FieldMeanTimeSerie() :
        TimeSerie("FIELDMEAN_TIMESERIES"){};
    virtual bool do_the_job();
};


class GeoPointTimeSerie : public TimeSerie
{
public:
    GeoPointTimeSerie() :
        TimeSerie("GEOPOINT_TIMESERIES"){};
    virtual bool do_the_job();
};


class BufrTimeSerie : public TimeSerie
{
public:
    BufrTimeSerie() :
        TimeSerie("BUFR_TIMESERIES"){};
    virtual bool do_the_job();

protected:
    long extractDate(MvObs& obs);
};

//_____________________________________________________________________________
void TimeSerie::init(MvRequest& in)
{
    data_    = in("DATA");
    station_ = in("STATION");
    //axis_   =  in("VERTICAL_AXIS");
    graph_ = in("ATTRIBUTES");

    base_date_set_ = false;

    if ((const char*)in("NAME"))  //-- NAME for geopoints and BUFR
        name_ = (const char*)in("NAME");
    else
        name_ = "Curve without a name";

    if ((const char*)in("SCALING")) {
        scaleAdd_ = in("SCALING");
        if (in.countValues("SCALING") < 2)
            scaleMul_ = 1;
        else
            scaleMul_ = in("SCALING", 1);
    }
    else {
        scaleAdd_ = 0;
        scaleMul_ = 1;
    }
    scaling_ = scaleAdd_ != 0 || scaleMul_ != 1 ? true : false;

    if ((const char*)in("MESSAGE"))  //-- for BUFR only
        message_ = (int)in("MESSAGE");
    else
        message_ = 1;

    if ((const char*)in("AREA")) {
        for (int i = 0; i < 4; ++i)
            area_[i] = in("AREA", i);
    }

    serie_ = new MvTimeSerie(axis_, graph_);

    //-- if no graph attributes given then set a timeseries to return a request
    //-- containing only 'HorAxis+Curve', otherwise 'HorAxis+Curve+GraphAttr'
    if (graph_)
        serie_->setReqFilter(eTsReqsCurveAndGraph);
    else
        serie_->setReqFilter(eTsReqsCurveOnly);
}
//_____________________________________________________________________________
void TimeSerie::set_basedate(int bd)
{
    if (!base_date_set_) {
        base_date_     = MvDate((double)bd);
        base_date_set_ = true;

        serie_->setDate(bd, 0);
    }
#if 0
  else if( MvDate( (double)bd ) < base_date_ )
    {
      std::cerr << "we should give error msg because another basedate is smaller" << std::endl;
      marslog( LOG_EROR, "TimeSerie::set_basedate: different basedate!!" );
    }
#endif
}
//_____________________________________________________________________________
void TimeSerie::serve(MvRequest& in, MvRequest& out)
{
    std::cout << "TimeSerie::serve() in" << std::endl;
    in.print();

    init(in);

    if (!do_the_job())
        return;

    //-- this one increases legend texts to be visible/readable --//
    MvRequest biggerLegText("PTEXT");
    biggerLegText("TEXT_LEGEND_MAGICS_STYLE")   = "ON";
    biggerLegText("LEGEND_COLUMN_COUNT")        = 2;
    biggerLegText("LEGEND_ENTRY_MAXIMUM_WIDTH") = 12.0;

    out = serie_->getRequest() + biggerLegText;

    std::cout << "TimeSerie::serve() out" << std::endl;
    out.print();
}
//_____________________________________________________________________________
string
TimeSerie::ref_string(int param, int level, int number, const MvLocation& loca, const char* expver)
{
    std::ostringstream oref;
    oref << param
         << "-"
         << level
         << "-"
         << number
         << "-"
         << loca
         << "-"
         << expver;

    return oref.str();
}
//_____________________________________________________________________________
bool FieldPointTimeSerie::do_the_job()
{
    int param  = -1;
    int level  = -1;
    int number = -1;

    MvRequest last_station;

    MvFieldSet fs(data_);
    MvFieldSetIterator iter(fs);  //-- fieldset iterator

    iter.sort("LEVELIST");
    iter.sort("NUMBER");
    iter.sort("PARAM");

    MvField field;
    while (field = iter())  //-- iterate through all fields
    {
        MvFieldExpander expand(field);
        MvRequest r = field.getRequest();

        int req_date           = r("DATE");
        int req_time           = r("TIME");
        double req_step        = r("STEP");
        const char* req_expver = r("EXPVER");

        set_basedate(req_date);

        //-- compute time offset between base date and current data date --//
        MvDate cur_date((double)req_date + (double)req_time / 2400.0 + (double)req_step / 24);
        double x_offset = base_date_.time_interval_hours(cur_date);

        //cout << req_date << cTAB << req_time << cTAB << req_step << cTAB << x_offset; //-- << std::endl;

        int req_par = r("PARAM");
        int req_lev = r("LEVELIST");
        int req_num = r("NUMBER");

        //-- check if new time series --//
        if (req_par != param || req_lev != level || req_num != number) {
            param                = req_par;
            level                = req_lev;
            number               = req_num;
            std::string par_name = (const char*)field.marsName();

            while (station_) {
                std::string station_name = (const char*)station_("NAME");
                const char* icon_name    = station_("_CLASS");
                if (icon_name)
                    last_station = station_;

                double lat = station_("LATITUDE");
                double lon = station_("LONGITUDE");
                MvLocation loca(lat, lon);

                std::ostringstream otit;
                otit << par_name.c_str();

                if (level)
                    otit << ", level "
                         << level;

                otit << ", "
                     << station_name.c_str()
                     << " "
                     << loca
                     << ends;

                std::string title = otit.str();
                std::string ref   = ref_string(param, level, number, loca, req_expver);

                serie_->createNewGroup(ref.c_str(), title.c_str());

                serie_->addIconInfo(ref.c_str(), "DATA", data_);
                serie_->addIconInfo(ref.c_str(), "STATION", last_station);

                //serie_->setTitle( ref.c_str(), title.c_str() );

                station_.advance();
            }
            station_.rewind();
        }

        //-- pick the point value, for each location --//
        while (station_) {
            double lat = station_("LATITUDE");
            double lon = station_("LONGITUDE");
            MvLocation loca(lat, lon);
            std::string ref = ref_string(param, level, number, loca, req_expver);
            //cout << cTAB << ref.c_str() << std::endl;

            double y = field.interpolateAt(lon, lat);
            if (y < cBIGDOUBLE) {
                if (scaling_)
                    y = (y + scaleAdd_) * scaleMul_;

                serie_->addPoint(ref.c_str(), x_offset, y);
            }

            station_.advance();
        }
        station_.rewind();
    }

    return true;
}
//_____________________________________________________________________________
bool FieldMeanTimeSerie::do_the_job()
{
    int param  = -1;
    int level  = -1;
    int number = -1;

    MvRequest last_station;

    MvFieldSet fs(data_);
    MvFieldSetIterator iter(fs);  //-- fieldset iterator

    iter.sort("LEVELIST");
    iter.sort("NUMBER");
    iter.sort("PARAM");

    MvField field;
    while (field = iter())  //-- iterate through all fields
    {
        MvFieldExpander expand(field);
        MvRequest r = field.getRequest();

        int req_date           = r("DATE");
        int req_time           = r("TIME");
        double req_step        = r("STEP");
        const char* req_expver = r("EXPVER");

        set_basedate(req_date);

        //-- compute time offset between base date and current data date --//
        MvDate cur_date((double)req_date + (double)req_time / 2400.0 + (double)req_step / 24);
        double x_offset = base_date_.time_interval_hours(cur_date);

        //cout << req_date << cTAB << req_time << cTAB << req_step << cTAB << x_offset; //-- << std::endl;

        int req_par = r("PARAM");
        int req_lev = r("LEVELIST");
        int req_num = r("NUMBER");

        MvLocation loca(area_[0], area_[1]);

        //-- check if new time series --//
        if (req_par != param || req_lev != level || req_num != number) {
            param                = req_par;
            level                = req_lev;
            number               = req_num;
            std::string par_name = (const char*)field.marsName();

            std::ostringstream otit;
            otit << par_name.c_str();

            if (level)
                otit << ", level "
                     << level;

            otit << ", area("
                 << area_[0] << ","
                 << area_[1] << ","
                 << area_[2] << ","
                 << area_[3] << ")"
                 << ends;

            std::string title = otit.str();

            std::string ref = ref_string(param, level, number, loca, req_expver);

            serie_->createNewGroup(ref.c_str(), title.c_str());

            serie_->addIconInfo(ref.c_str(), "DATA", data_);
        }

        std::string ref = ref_string(param, level, number, loca, req_expver);
        std::cout << cTAB << ref.c_str() << std::endl;

        double y = field.integrate(area_[0], area_[1], area_[2], area_[3]);
        if (y < cBIGDOUBLE) {
            if (scaling_)
                y = (y + scaleAdd_) * scaleMul_;

            serie_->addPoint(ref.c_str(), x_offset, y);
        }
    }

    return true;
}
//_____________________________________________________________________________
bool GeoPointTimeSerie::do_the_job()
{
    const char* myPath = data_("PATH");
    MvGeoPoints gpts(myPath);

    int cnt = gpts.count();
    if (cnt < 2) {
        setError(1, "TimeSeries-> Empty geopoints file");
        return false;
    }

    if (gpts.format() == eGeoString) {
        setError(1, "TimeSeries-> Geopoints format 'XYV' not supported!");
        return false;
    }
    if (gpts.format() == eGeoString) {
        setError(1, "TimeSeries-> String valued geopoints are not supported!");
        return false;
    }

    MvGeoP1 gp1 = gpts[0];
    set_basedate(gp1.date());

    serie_->setDate(gp1.date(), 0);

    list<string> ref_list;
    list<string>::iterator lit;

    for (int i = 0; i < cnt; ++i) {
        MvGeoP1 gp = gpts[i];
        MvDate cur_date((double)gp.date() + (double)gp.time() / 2400);
        double x_offset = base_date_.time_interval_hours(cur_date);

        //cout << gp.date() << cTAB << gp.time() << cTAB << x_offset; //-- << std::endl;

        double lat = gp.lat_y();
        double lon = gp.lon_x();
        MvLocation loca(lat, lon);
        std::string ref = ref_string(0, (int)gp.height(), 0, loca, "NA");
        //cout << cTAB << ref.c_str() << std::endl;

        if (find(ref_list.begin(), ref_list.end(), ref) == ref_list.end()) {
            ref_list.push_back(ref);

            std::ostringstream otit;
            if (name_ != "AUTOMATIC")
                otit << name_;
            else
                otit << "Point "
                     << loca;

            std::string title = otit.str();

            serie_->createNewGroup(ref.c_str(), title.c_str());
            serie_->addIconInfo(ref.c_str(), "DATA", data_);
        }

        double y = gp.value();
        if (scaling_)
            y = (y + scaleAdd_) * scaleMul_;

        serie_->addPoint(ref.c_str(), x_offset, y);
    }

    return true;
}
//_____________________________________________________________________________
bool BufrTimeSerie::do_the_job()
{
    MvObsSet obsSet(data_);
    MvObsSetIterator obsIter(obsSet);

    obsIter.setMessageType(cBufrType);
    obsIter.setMessageSubtype(cBufrSubtype);

    MvObs obs = obsIter();
    if (message_ > 1)
        for (int i = 1; i < message_; ++i)  //-- skip until the requested msg
            obs = obsIter();

    if (obs) {
        int myDate = extractDate(obs);          //-- extract date info
        int myTime = 100 * obs.intValue(4004);  //-- multiply to make format HHMM

        set_basedate(myDate);
        serie_->setDate(myDate, myTime);

        double lat = obs.value(5001);  //-- extract location
        double lon = obs.value(6001);
        MvLocation loca(lat, lon);

        int startIndex    = 9;  //-- check if xtra level coord descr
        double mayBeLevel = obs[startIndex];
        long currDescr    = obs.currentDescriptor();

        std::string levelInfo("");
        if (currDescr == 7004 || currDescr == 7007) {
            std::ostringstream oslev;  //-- level: add level info into default title

            if (currDescr == 7004)  //-- convert Pa to hPa
                oslev << mayBeLevel / 100 << " hPa ";
            else
                oslev << mayBeLevel << " m ";  //-- hardcoded unit (obs.unit() would return "M")

            levelInfo = oslev.str();

            ++startIndex;  //-- adjust data starting index
        }

        double parval      = obs[startIndex + 1];  //-- extract parameter name
        std::string parnam = obs.name();

        //-- non-orthodoxic but valid use of ref_string parameters --//
        std::string ref = ref_string(myDate, myTime, currDescr, loca, parnam.c_str());

        std::string title;
        if (name_ != "AUTOMATIC") {
            title = name_;  //-- user provided title
        }
        else {
            std::ostringstream os;
            os << levelInfo  //-- automatic title
               << parnam
               << " "
               << loca;
            title = os.str();
        }

        serie_->createNewGroup(ref.c_str(), title.c_str());

        int mySteps = obs.numberOfLevels(4024);  //-- 0'04'024 time period of displacement
        for (int s = 0; s < mySteps; ++s) {
            int x_offset = (int)obs[startIndex + 2 * s];
            double y_val = obs[startIndex + 2 * s + 1];

            if (y_val != kBufrMissingValue) {
                if (scaling_)
                    y_val = (y_val + scaleAdd_) * scaleMul_;

                serie_->addPoint(ref.c_str(), x_offset, y_val);
            }
        }
    }

    return true;
}
//_____________________________________________________________________________
long BufrTimeSerie::extractDate(MvObs& obs)
{
    long y = obs.intValue(4001);
    long m = obs.intValue(4002);
    long d = obs.intValue(4003);

    return 10000 * y + 100 * m + d;
}
//_____________________________________________________________________________
int main(int argc, char** argv)
{
    MvApplication theApp(argc, argv);

    FieldPointTimeSerie fieldpointtimeserie;
    FieldMeanTimeSerie fieldmeantimeserie;
    GeoPointTimeSerie geotimeserie;
    BufrTimeSerie bufrtimeserie;

    theApp.run();
}
