/***************************** 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 "BufrThermo.h"
#include "MvNetCDF.h"
#include "MvObsSet.h"
#include "MvRequestUtil.hpp"

#include <assert.h>

const int TOBSTEMP     = 12001;
const int TOBSTD       = 12003;
const int TOBSDIR      = 11001;
const int TOBSSTRENGTH = 11002;
const int TOBSZ = 10003;
const double PI        = 3.14159265;

BufrThermo::BufrThermo() :
    Thermo("BUFR_THERMO"),
    counter_(0),
    pCount_(0),
    nlev_(0),
    nTIME_STR_(13)
{
}

BufrThermo::BufrThermo(const char* kw) :
    Thermo(kw)
{
}

void BufrThermo::allocateDataMemory()
{
    pCount_ = 0;

    //Deallocates memory
    T_ = std::vector<double>();
    TD_ = std::vector<double>();
    U_ = std::vector<double>();
    V_ = std::vector<double>();
    P_ = std::vector<double>();
    Z_ = std::vector<double>();

    // Allocate memory
    if (nlev_ > 0) {
        T_.resize(nlev_);
        TD_.resize(nlev_);
        U_.resize(nlev_);
        V_.resize(nlev_);
        P_.resize(nlev_);
        Z_.resize(nlev_);
    }
}

void BufrThermo::initialiseDataMemory()
{
    pCount_ = 0;

    size_t n = static_cast<size_t>(nlev_);
    assert(T_.size() == n);
    assert(TD_.size() == n);
    assert(U_.size() == n);
    assert(V_.size() == n);
    assert(P_.size() == n);
    assert(Z_.size() == n);

    for (int i = 0; i < nlev_; i++) {
        T_[i]  = MissingValue;
        TD_[i] = MissingValue;
        U_[i]  = MissingValue;
        V_[i]  = MissingValue;
        P_[i]  = MissingValue;
        Z_[i]  = MissingValue;
    }
}

bool BufrThermo::getAppParameters(MvRequest& in)
{
    // It is expected that the input request is expanded

    // Get station input parameter
    station_ = in.getSubrequest("STATION");
    if (!station_) {
        setError(1, "BufrThermo-> No Station info specified!");
        return false;
    }

    return true;
}

// The data structure contains vectors for Temp, Td, Wind and Pressure.
// There is only one Pressure vector related to the other 3 data. This
// means that these 3 data may contain different number of Missing
// values, which will be allocated in those positions when there is no
// Pressure level defined.
bool BufrThermo::processData()
{
    // If it is not a BUFR data then it has been already processed
    if (strcmp(dataRequest_.getVerb(), "BUFR") != 0)
        return true;

    // Create a temporary netCDF file
    string ncFileName = this->ncFileName();
    MvNetCDF netcdf(ncFileName, 'w');
    if (!netcdf.isValid()) {
        setError(1, "BufrThermo-> Can not open a new netCDF file");
        return false;
    }

    // Get location info from the station
    MvObsSet obsset(dataRequest_);
    MvObsSetIterator iter(obsset);
    if (!getIdent(station_, iter)) {
        if (!getLocation(station_, iter)) {
            setError(1, "BufrThermo-> WMO station number and/or location missing...");
            return false;
        }
    }

    // Initialize obs variables
    MvBufrParam obsTemp(TOBSTEMP), obsTd(TOBSTD),
        obsDirection(TOBSDIR), obsStrength(TOBSSTRENGTH),
        obsZ(TOBSZ);

    iter.setMessageType(2);  //-- Vertical soundings (other than satellite)

    // Evaluate dimensions
    // Remove this function after updating library MvNetCDF to create
    // files of type Netcdf4 (allowing multiple unlimited dimensions)
    nlev_ = this->evaluateNLevels(iter);
    if (nlev_ <= 0) {
        setError(1, "BufrThermo-> No Levels found");
        return false;
    }

    // Allocate memory
    this->allocateDataMemory();

    // Main loop
    // Compute data values
    MvObs obs;
    obsset.rewind();
    counter_ = 0;
    while (obs = iter()) {
        //FAMI20190212 only accepts BUFR sybtypes 101/109/4
        // Skip PILOT
        if (obs.messageSubtype() != 101 && obs.messageSubtype() != 109 && obs.messageSubtype() != 4)  //obs.messageSubtype() == 91 )  //-- ECMWF code for PILOT!
        {
            setError(0, "BufrThermo-> Skip PILOT soundings (full TEMP sounding needed)");
            continue;
        }

        //obs.printAllValues();

        // Initialise data values storage with MissingValue
        this->initialiseDataMemory();

        // Get/compute data values for all pressure levels
        int ic          = 0;
        float PrevLevel = FLT_MAX;
        float flevel    = obs.firstPressureLevel();
        while (flevel != MissingValue) {
            //-- Pressure levels decrease in the main sounding data.
            //-- At the end of msg there can be data for "special levels"
            //-- ( e.g. 'Maximum wind level') which refer back to lower
            //-- levels (higher pressure) and which should not be used.
            if (flevel >= PrevLevel)
                break;

            float myT   = obs.valueByPressureLevel(flevel, obsTemp);
            float myDew = obs.valueByPressureLevel(flevel, obsTd);
            float myDir = obs.valueByPressureLevel(flevel, obsDirection);
            float myStr = obs.valueByPressureLevel(flevel, obsStrength);
            float myZ   = obs.valueByPressureLevel(flevel, obsZ);

            // Temperature value, K -> C
            if (myT != MissingValue)
                T_[ic] = myT - K2C;

            // Dewpoint value, K -> C
            if (myDew != MissingValue)
                TD_[ic] = myDew - K2C;

            // Wind values, polar -> u,v
            if (myDir != MissingValue) {
                U_[ic] = -myStr * sin(myDir * PI / 180.0);
                V_[ic] = -myStr * cos(myDir * PI / 180.0);
            }

            // Geopotential
            if (myZ != MissingValue) {
                Z_[ic] = myZ;
            }

            // Pressure value
            P_[ic] = flevel;
            pCount_++;

            ic++;
            PrevLevel = flevel;
            flevel    = obs.nextPressureLevel();
        }  // end while

        // Consistency check
        if (pCount_ <= 1) {
            setError(1, "BufrThermo-> Problems with BUFR data");
            return false;
        }

        // Write info to the netCDF file
        if (!this->generate(netcdf, obs)) {
            setError(1, "BufrThermo-> Error generating netCDF file");
            return false;
        }

        counter_++;
    }  // end main loop

    if (counter_ == 0) {
        setError(1, "BufrThermo-> No data found for given Station");
        return false;
    }

    // Close netCDF file
    netcdf.close();

    return true;
}

bool BufrThermo::generate(MvNetCDF& netcdf, MvObs& obs)
{
    // Create netcdf variables, dimensions and global atributes
    if (counter_ == 0) {
        if (!this->ncInit(netcdf, obs)) {
            setError(1, "BufrThermo-> Failure to initilise a netCDF file");
            return false;
        }
    }

    // Get date and time info and generate time key (yyyymmddhhmm)
    TMetTime tmpdate = obs.obsTime();
    ostringstream oss;
    oss << setfill('0')
        << setw(4) << tmpdate.GetYear()
        << setw(2) << tmpdate.GetMonth()
        << setw(2) << tmpdate.GetDay()
        << setw(2) << tmpdate.GetHour()
        << setw(2) << tmpdate.GetMin();

    // Get netcdf variables
    MvNcVar* time  = netcdf.getVariable(sTIME);
    MvNcVar* nlev  = netcdf.getVariable(sNLEV);
    MvNcVar* temp  = netcdf.getVariable(sTEMP);
    MvNcVar* dew   = netcdf.getVariable(sDEWPOINT);
    MvNcVar* press = netcdf.getVariable(sPRESSURE);
    MvNcVar* geopot = netcdf.getVariable(sGEOPOTENTIAL);
    MvNcVar* uwind = netcdf.getVariable(sU);
    MvNcVar* vwind = netcdf.getVariable(sV);
    MvNcVar* xwind = netcdf.getVariable(sXWIND);
    if (!time || !temp || !dew || !press || !uwind || !vwind || !xwind) {
        setError(1, "BufrThermo-> Error accessing netCDF variables");
        return false;
    }

    // Add data to the variables
    std::string stime(oss.str());
    time->setCurrent(counter_);
    bool btime = time->put(stime.c_str(), 1, strlen(stime.c_str()) + 1);

    nlev->setCurrent(counter_);
    bool bnlev = nlev->put(&pCount_, 1, 1);

    temp->setCurrent(counter_);
    bool btemp = temp->put(T_, 1, nlev_);

    dew->setCurrent(counter_);
    bool bdew = dew->put(TD_, 1, nlev_);

    press->setCurrent(counter_);
    bool bpress = press->put(P_, 1, nlev_);

    geopot->setCurrent(counter_);
    bool bgeopot = geopot->put(Z_, 1, nlev_);

    uwind->setCurrent(counter_);
    bool buwind = uwind->put(U_, 1, nlev_);

    vwind->setCurrent(counter_);
    bool bvwind = vwind->put(V_, 1, nlev_);

    xwind->setCurrent(counter_);
    vector<double> xv(nlev_, 1010.5);
    bool bxwind = xwind->put(xv, 1, nlev_);

    if (!btime || !bnlev || !btemp || !bdew || !bpress || !bgeopot || !buwind || !bvwind || !bxwind) {
        setError(1, "BufrThermo-> Error writing a netCDF variable");
        return false;
    }

    return true;
}

bool BufrThermo::ncInit(MvNetCDF& netcdf, MvObs& obs)
{
    // Get geographical location
    MvLocation loc = obs.location();
    ostringstream oss;
    oss << loc.latitude() << '/' << loc.longitude();

    // Add global attributes to the netcdf file
    netcdf.addAttribute("_FILL_VALUE", MissingValue);
    netcdf.addAttribute("Station", obs.WmoIdentNumber());
    netcdf.addAttribute("Coordinates", oss.str().c_str());

    // Create variables
    vector<long> values_ndim;
    vector<string> values_sdim;
    values_ndim.push_back(0);  // unlimited
    values_sdim.push_back(sTIME);

    // Create variable for number of levels
    if (!netcdf.addVariable(sNLEV, ncInt, values_ndim, values_sdim))
        return false;

    // Create variable time
    values_ndim.push_back(nTIME_STR_);
    values_sdim.push_back(sTIME_STR);
    if (!netcdf.addVariable(sTIME, ncChar, values_ndim, values_sdim))
        return false;

    // Create variables temperature, dewpoint, pressure, geopotential and wind
    values_ndim[1] = nlev_;
    values_sdim[1] = sNLEV;
    if (!netcdf.addVariable(sTEMP, ncFloat, values_ndim, values_sdim))
        return false;
    if (!netcdf.addVariable(sDEWPOINT, ncFloat, values_ndim, values_sdim))
        return false;
    if (!netcdf.addVariable(sPRESSURE, ncFloat, values_ndim, values_sdim))
        return false;
    if (!netcdf.addVariable(sGEOPOTENTIAL, ncFloat, values_ndim, values_sdim))
        return false;
    if (!netcdf.addVariable(sU, ncFloat, values_ndim, values_sdim))
        return false;
    if (!netcdf.addVariable(sV, ncFloat, values_ndim, values_sdim))
        return false;
    if (!netcdf.addVariable(sXWIND, ncFloat, values_ndim, values_sdim))
        return false;

    return true;
}

bool BufrThermo::createOutputRequest(MvRequest& out)
{
    // Get default parameter to be plotted
    //ParamInfo* parInfo = plotVariable(params);
    //if ( !parInfo )
    //   return false;

    // Create netCDF data request
    MvRequest ncDataReq("NETCDF");
    ncDataReq("PATH")      = this->ncFileName().c_str();
    ncDataReq("TEMPORARY") = 1;

    // Create Temperature output request
    int thermoId = rand() % 10000;
    MvRequest dReq("NETCDF_XY_POINTS");
    dReq("NETCDF_DATA")       = ncDataReq;
    dReq("NETCDF_Y_VARIABLE") = sPRESSURE.c_str();
    dReq("NETCDF_X_VARIABLE") = sTEMP.c_str();
    dReq("_MODULEID")         = thermoId;

    MvRequest gReq("MGRAPH");
    gReq("GRAPH_LINE_COLOUR")            = "red";
    gReq("GRAPH_LINE_THICKNESS")         = 8;
    gReq("GRAPH_MISSING_DATA_MODE")      = "join";
    gReq("GRAPH_MISSING_DATA_STYLE")     = "solid";
    gReq("GRAPH_MISSING_DATA_THICKNESS") = 8;
    gReq("_CLASS")                       = "MGRAPH";
    gReq("_MODULEID")                    = thermoId;
    gReq("_SKIP_MACRO")                  = 1;  // skip macro converter
    MvRequest dataReq                    = dReq + gReq;

    // Create Dewpoint output request
    dReq("NETCDF_X_VARIABLE") = sDEWPOINT.c_str();
    dReq("_SKIP_MACRO")       = 1;  // skip macro converter

    gReq("GRAPH_LINE_STYLE")         = "dash",
    gReq("GRAPH_MISSING_DATA_STYLE") = "dash";

    // Create wind output request
    MvRequest wdReq("NETCDF_XY_VECTORS");
    wdReq("NETCDF_DATA")                 = ncDataReq;
    wdReq("NETCDF_Y_POSITION_VARIABLE")  = sPRESSURE.c_str();
    wdReq("NETCDF_X_POSITION_VARIABLE")  = sXWIND.c_str();
    wdReq("NETCDF_X_COMPONENT_VARIABLE") = sU.c_str();
    wdReq("NETCDF_Y_COMPONENT_VARIABLE") = sV.c_str();
    wdReq("_MODULEID")                   = thermoId;
    wdReq("_SKIP_MACRO")                 = 1;  // skip macro converter

    MvRequest wReq("MWIND");
    wReq("WIND_FIELD_TYPE")  = "flags";
    wReq("WIND_FLAG_COLOUR") = "black";
    wReq("_CLASS")           = "MWIND";
    wReq("_MODULEID")        = thermoId;
    wReq("_SKIP_MACRO")      = 1;  // skip macro converter

    dataReq = dataReq + dReq + gReq + wdReq + wReq;

    // Add information to help the Macro Converter to translate
    // the output request to a Macro code
    dataReq("_ORIGINAL_REQUEST") = this->buildMacroConverterRequest();

    // Create a default ThermoView request
    MvRequest viewReq("THERMOVIEW");
    if (origReq_ && strcmp(origReq_.getVerb(), "THERMOVIEW") == 0)
        viewReq = origReq_;
    else if ((const char*)origReq_("_CONTEXT")) {
        MvRequest req = origReq_("_CONTEXT");
        if ((const char*)req("_ORIGINAL_REQUEST"))
            viewReq = req("_ORIGINAL_REQUEST");
        // else   // use a default ThermoView request
    }
    else {
        // Initialize request from the Thermo Data module
        CopyAndRemoveParameters(origReq_, viewReq, "_");
        viewReq.unsetParam("DATA");
    }

    viewReq("_MODULEID") = thermoId;

    // Add title information.
    // It must be positioned before the Data+Visdef set of requests to avoid the
    // following problem in uPlot: if another visdef (MGRAPH or MWIND) is defined
    // after the GribThermo requests then it will not be associated to the GribThermo's
    // output data because the existence of a MTEXT request between them.
    MvRequest titleReq("MTEXT");
    titleReq("TEXT_LINE_1")    = this->title().c_str();
    titleReq("TEXT_FONT_SIZE") = 0.5;
    titleReq("_MODULEID")      = thermoId;
    titleReq("_SKIP_MACRO")    = 1;  // skip macro converter

    // If action is not visualisation related then return the netcdf data.
    // Also, add the visualisation and original requests as hidden parameters.
    // These may be used later to help the visualisation of the netcdf data.
    if (!this->isVisualise()) {
        out                  = ncDataReq;
        out("_VIEW")         = "THERMOVIEW";
        out("_VISUALISE")    = titleReq + dataReq;
        out("_VIEW_REQUEST") = viewReq;
        out("_MODULEID")     = thermoId;

        return true;
    }

    // Final output request
    out = viewReq + titleReq + dataReq;

    return true;
}

int BufrThermo::evaluateNLevels(MvObsSetIterator& iter)
{
    MvObs obs;
    int nn;
    int nlev = -1;
    while (obs = iter()) {
        nn = obs.numberOfPressureLevels();
        if (nlev < nn)
            nlev = nn;
    }

    return nlev;
}

bool BufrThermo::getIdent(MvRequest& station, MvObsSetIterator& iter)
{
    long ident = station("IDENT");
    if (ident) {
        iter.setWmoStation(ident);
        return true;
    }

    return false;
}

bool BufrThermo::getLocation(MvRequest& station, MvObsSetIterator& iter)
{
    double lat = station("LATITUDE");
    double lon = station("LONGITUDE");
    double eps = 0.0001;  //-- to allow for BUFR rounding differences

    MvLocation loc1(lat + eps, lon - eps);
    MvLocation loc2(lat - eps, lon + eps);

    iter.setArea(loc1, loc2);

    return true;
}

string BufrThermo::title()
{
    // Get first time information from the netcdf file
    MvNetCDF netcdf(this->ncFileName(), 'r');
    MvNcVar* time = netcdf.getVariable(sTIME);
    MvNcDim* dim  = time->getDimension(1);
    int size      = dim->size();
    vector<char> vtime;
    time->setCurrent(0L);
    time->get(vtime, 1, size);

    // Decode time info
    string str(vtime.begin(), vtime.end());
    string sdate = str.substr(0, 8);
    string stime = str.substr(8, 4);

    // Create title
    char title[200];
    MvNcAtt* tmpatt = netcdf.getAttribute("Coordinates");
    str             = tmpatt->as_string(0);
    int ipos        = str.find('/');
    float lat       = atof(str.substr(0, ipos).c_str());
    float lon       = atof(str.substr(ipos + 1).c_str());
    sprintf(title, "%s %s [lat,lon: %.2f,%.2f]",
            sdate.c_str(), stime.c_str(), lat, lon);

    return string(title);
}
