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

#include "MvNetCDF.h"
#include "MvObsSet.h"
#include "MvRequestUtil.hpp"
#include "MvException.h"
#include "MvSci.h"
#include "MvLog.h"

#include <assert.h>

// using namespace metview;

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

bool BufrStation::initObsIter(MvObsSetIterator& iter) const
{
    if (pointMode_ == CoordMode) {
        MvLocation loc1(lat_ + threshold_, lon_ - threshold_);
        MvLocation loc2(lat_ - threshold_, lon_ + threshold_);
        iter.setArea(loc1, loc2);
        return true;
    }
    else if (pointMode_ == WmoNameMode) {
        iter.setIdentKey("stationOrSiteName");
        iter.setIdentKey("shipOrMobileLandStationIdentifier");
        iter.setIdentValue(wmoName_);
        return true;
    }
    else if (pointMode_ == WmoIdentMode) {
        iter.setWmoStation(wmoIdent_);
        return true;
    }
    else if (pointMode_ == WigosMode) {
        iter.setWigosId(wigosSeries_, wigosIssuer_, wigosIssueNumber_, wigosLocalName_);
        return true;
    }
    return false;
}


BufrThermo::BufrThermo() :
    Thermo("BUFR_THERMO")
{
    moduleLabel_ = "BufrThermo-> ";
}


BufrThermo::BufrThermo(const char* kw) :
    Thermo(kw)
{
    moduleLabel_ = "BufrThermo-> ";
}

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;

    auto 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
    station_ = {};

    // Retrieve point selection
    if (const char* ch = in("POINT_SELECTION")) {
        MvLog().info() << "POINT_SELECTION=" << ch;

        auto ps = std::string(ch);
        if (ps == "COORDINATES") {
            station_.pointMode_ = BufrStation::CoordMode;
            in.getValue(station_.lat_, "COORDINATES", 0);
            in.getValue(station_.lon_, "COORDINATES", 1);
            station_.threshold_ = in("THRESHOLD");
            checkThreshold(station_.threshold_);
        }
        else if (ps == "WMO_NAME") {
            station_.pointMode_ = BufrStation::WmoNameMode;
            const char* chName = in("WMO_NAME");
            if (!chName || strcmp(chName, "") == 0) {
                throw MvException(moduleLabel_ + "No WMO_NAME specified");
            }
            assert(chName);
            station_.wmoName_ = std::string(chName);
        }
        else if (ps == "WMO_IDENT") {
            station_.wmoIdent_ = in("WMO_IDENT");
            if (checkIdent(station_.wmoIdent_, false)) {
                station_.pointMode_ = BufrStation::WmoIdentMode;
            }
        }
        else if (ps == "WIGOS") {
            station_.pointMode_ = BufrStation::WigosMode;
            if (const char* wgCh = in("WIGOS_SERIES")) {
                station_.wigosSeries_ = (int)in("WIGOS_SERIES");
            }
            if (const char* wgCh = in("WIGOS_ISSUER")) {
                station_.wigosIssuer_ = (int)in("WIGOS_ISSUER");
            }
            if (const char* wgCh = in("WIGOS_ISSUE_NUMBER")) {
                station_.wigosIssueNumber_ = (int)in("WIGOS_ISSUE_NUMBER");
            }
            if (const char* wgCh = in("WIGOS_LOCAL_NAME")) {
                station_.wigosLocalName_ = std::string(wgCh);
            }
        }
        else if (ps == "STATION") {
            MvRequest stReq = in.getSubrequest("STATION");
            if (!stReq) {
                throw MvException(moduleLabel_ + "No STATION specified");
            }
            if (const char* chIdent = stReq("IDENT")) {
                station_.wmoIdent_ = stReq("IDENT");
                if (checkIdent(station_.wmoIdent_, false)) {
                    station_.pointMode_ = BufrStation::WmoIdentMode;
                }
            }
            if (station_.pointMode_ == BufrStation::NoMode) {
                station_.pointMode_ = BufrStation::CoordMode;
                station_.lat_ = stReq("LATITUDE");
                station_.lon_ = stReq("LONGITUDE");
                station_.threshold_ = stReq("THRESHOLD");
                checkThreshold(station_.threshold_);
            }
        }
    }

    if (station_.pointMode_ == BufrStation::NoMode) {
        throw MvException(moduleLabel_ + "No valid location/station specified");
    }
#if 0
    MvLog().info() << MV_FN_INFO << "  mode=" << station_.pointMode_ << " ident=" << station_.wmoIdent_ <<
                 " lat=" << station_.lat_ << " lon=" << station_.lon_ <<
                 " wigosLocalName_=" << station_.wigosLocalName_;
#endif
    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;

    // Get location info from the station
    MvObsSet obsset(dataRequest_);
    MvObsSetIterator iter(obsset);

    if (!station_.initObsIter(iter)) {
        throw MvException(moduleLabel_ + "No valid location/station specified");
    }

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

    // we only support these subtypes. We want to skip pilot. The problem here is
    // that 101/109 are dataSubCategories defined at ECMWF for BUFR edition 3. However,
    // 4 is an internationalDataSubCategory defined in BUFR edition 4 (by WMO).
    iter.setMessageSubtype(101);
    iter.setMessageSubtype(109);
    iter.setMessageSubtype(4);

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

    // Evaluate dimensions
    // TODO: Remove this function after updating library MvNetCDF to create
    // files of type Netcdf4 (allowing multiple unlimited dimensions)

    int msgNum = 0;
    evaluateNLevels(iter, msgNum, nlev_);
    if (msgNum <= 0) {
        throw MvException(moduleLabel_ + "No BUFR messages matching the required location and observation type found in input!");
    }
    else if (nlev_ <= 0) {
        throw MvException(moduleLabel_ + "No pressure levels found in input!");
    }

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

    // Allocate memory
    allocateDataMemory();

    // Main loop
    // Compute data values
    MvObs obs;
    obsset.rewind();
    counter_ = 0;
    while ((obs = iter())) {
        //        // 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(MvSci::degToRad(myDir));
                V_[ic] = -myStr * cos(MvSci::degToRad(myDir));
            }

            // 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-> No pressure levels found in 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();
    std::ostringstream oss;
    oss << std::setfill('0')
        << std::setw(4) << tmpdate.GetYear()
        << std::setw(2) << tmpdate.GetMonth()
        << std::setw(2) << tmpdate.GetDay()
        << std::setw(2) << tmpdate.GetHour()
        << std::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_);
    std::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();
    std::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
    std::vector<long> values_ndim;
    std::vector<std::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
        metview::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;
}

void BufrThermo::evaluateNLevels(MvObsSetIterator& iter, int& msgNum, int& maxLevNum)
{
    MvObs obs;
    int nn;
    msgNum = 0;
    maxLevNum = 0;
    while ((obs = iter())) {
        msgNum++;
        nn = obs.numberOfPressureLevels();
        maxLevNum = std::max(maxLevNum, nn);
    }
}

std::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();
    std::vector<char> vtime;
    time->setCurrent(0L);
    time->get(vtime, 1, size);

    // Decode time info
    std::string str(vtime.begin(), vtime.end());
    std::string sdate = str.substr(0, 8);
    std::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 std::string(title);
}

bool BufrThermo::checkIdent(long ident, bool failOnError) const
{
    if (ident <= 0) {
        if (failOnError) {
            throw MvException(moduleLabel_ +
                              "Invalid IDENT=" + std::to_string(ident) + " specified");
        }
        return false;
    }
    return true;
}

void BufrThermo::checkThreshold(double threshold) const
{
    if (threshold < 0 || threshold > 100) {
        throw MvException(moduleLabel_ +
                          "Invalid THRESHOLD=" + std::to_string(threshold) + " specified");
    }
}
