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


//#define Box braz_box

#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include "fstream_mars_fix.h"

#include <eckit/parser/JSONParser.h>
#include <eckit/value/Value.h>


#include <stdlib.h>
#include <Metview.h>
#include <MvPath.hpp>
#include <MvLog.h>


//
//_____________________________________________________________________

class Meteogram : public MvService
{
public:
    Meteogram();
    void serve(MvRequest&, MvRequest&);

protected:
    void handle_local(MvRequest&, MvRequest&);
    void handle_remote(MvRequest&, MvRequest&);
    void join_outputs(const std::vector<std::string> tempFileNames, const std::string& format, MvRequest& out);
    bool download_file(const std::string& url, std::string& target, std::string& msg);
    std::string get_image_url_from_metadata(std::string& metadata_path);
    std::map<std::string, std::string> local_databases_;
    std::map<std::string, std::string> remote_databases_;
};

//_____________________________________________________________________

Meteogram::Meteogram() :
    MvService("MetPlus")
{
    MvRequest setup = mars.setup;
    setup.rewind();
    saveToPool(false);

    while (setup) {
        if (setup.getVerb() == Cached("MetPlus")) {
            local_databases_["10_days_epsgram"] = "10_days";
            local_databases_["15_days_epsgram"] = "15_days";
            local_databases_["10_days_wave_epsgram"] = "10_days_wave";
            local_databases_["10_days_plumes"] = "10_days_plumes";
            local_databases_["15_days_epsgram_with_climate"] = "15_days_with_clim";

            remote_databases_["10_days_epsgram"] = "classical_10d";
            remote_databases_["15_days_epsgram"] = "classical_15d";
            remote_databases_["10_days_wave_epsgram"] = "classical_wave";
            remote_databases_["10_days_plumes"] = "classical_plume";
            remote_databases_["15_days_epsgram_with_climate"] = "classical_15d_with_climate";

            break;
        }
        setup.advance();
    }
}


static std::string toString(double val)
{
    std::ostringstream tool;
    // tool.width(7);
    tool.precision(5);
    tool << val;
    return tool.str();
}


static std::string UpperCase(const std::string& s)
{
    std::string out;
    for (std::string::const_iterator l = s.begin(); l != s.end(); ++l)
        out.push_back(toupper(*l));
    return out;
}


bool Meteogram::download_file(const std::string& url, std::string& target, std::string& msg)
{
    // create a request and delegate the download to the Download module
    bool ok = true;
    target = marstmp();
    MvRequest downloadReq("DOWNLOAD");
    downloadReq("URL") = url.c_str();
    downloadReq("TARGET") = target.c_str();
    MvRequest downloadOut;
    callService((char*)"Download", downloadReq, downloadOut);
    downloadOut.print();
    if (std::string(downloadOut.getVerb()) == "ERROR") {
        msg = std::string(downloadOut("MESSAGE"));
        ok = false;
    }
    return ok;
}


/*
    Meteogram::join_outputs
    Returns the output request (via 'out') containing the path either to the single downloaded
    image, or the result of concatenating multiple images.
*/


void Meteogram::join_outputs(const std::vector<std::string> tempFileNames, const std::string& format, MvRequest& out)
{
    std::string outclass;
    if (format == "ps")
        outclass = "PSFILE";
    else
        outclass = UpperCase(format);
    MvRequest data(outclass.c_str());


    if (true) {
        // check if any of the returned files are either non-existent or of zero-length

        bool allFilesOk = true;

        for (size_t i = 0; i < tempFileNames.size(); i++) {
            if (!FileHasValidSize(tempFileNames[i].c_str())) {
                allFilesOk = false;
                break;
            }
        }

        if (allFilesOk) {
            // copy the temporary files to their proper destinations

            if (tempFileNames.size() > 1) {
                std::string outputFileName = marstmp();
                data("PATH") = outputFileName.c_str();

                // multiple metgrams - join them into one; format-dependent code here

                std::string mpRunning("Meteogram-> Running: ");

                if (format == "ps") {
                    std::string psJoinCommand = "psjoin -p ";

                    for (size_t i = 0; i < tempFileNames.size(); i++) {
                        psJoinCommand += tempFileNames[i] + " ";
                    }

                    psJoinCommand += " > " + outputFileName;

                    sendProgress(mpRunning + psJoinCommand);
                    system(psJoinCommand.c_str());
                }

                else if (format == "pdf") {
                    std::string pdfJoinCommand = "gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -dPDFSETTINGS=/prepress -sOutputFile=";
                    pdfJoinCommand += outputFileName + " ";

                    for (size_t i = 0; i < tempFileNames.size(); i++) {
                        pdfJoinCommand += tempFileNames[i] + " ";
                    }

                    sendProgress(mpRunning + pdfJoinCommand);
                    system(pdfJoinCommand.c_str());
                }
                else {
                    data("PATH") = tempFileNames[0].c_str();
                }
            }

            else {
                data("PATH") = tempFileNames[0].c_str();
            }


            // return the path to the output file
            // - this will allow the user to save or visualise the result

            // data("TEMPORARY") = 1;
            out = data;
        }

        else  // not all the files are ok
        {
            // the script appeared to connect to the server ok, but it seems
            // that we did not get a valid file back from it
            // this will cause the program to fail and abort

            setError(1, "Meteogram-> Invalid file returned from server (non-existant or zero size)");
            return;
        }
    }
}


std::string Meteogram::get_image_url_from_metadata(std::string& metadata_path)
{
    std::ifstream in(metadata_path, std::ifstream::binary);
    if (in) {
        try {
            eckit::JSONParser p(in);
            eckit::Value v = p.parse();
            eckit::ValueMap data = v["data"];
            eckit::ValueMap link = data["link"];
            std::string url = link["href"];
            return url;
        }
        catch (eckit::Exception e) {
            return "ERROR: could not parse JSON metadata for data/link/href";
        }
    }
    else {
        return "ERROR: Could not open " + metadata_path + " to read meteogram metadata";
    }
}


void Meteogram::handle_remote(MvRequest& in, MvRequest& out)
{
    // the first URL to get the metadata looks like this:
    // https://apps.ecmwf.int/webapps/opencharts-api/v1/products/opencharts_meteogram/?epsgram=classical_15d&
    // lon=-1&station_name=Reading&base_time=2022-02-16T00%3A00%3A00Z&lat=51.43&format=pdf
    // - then we parse this JSON file to get the image URL, and download that

    // QUESTIONS:
    // 1) can we get different expvers?


    std::string restUrl = "https://charts.ecmwf.int/opencharts-api/v1/products/opencharts_meteogram/";

    // allow to override the base URL so that if this changes, users don't need to move to a new
    // version of Metview in order to use it
    if (const char* base_url = getenv("METVIEW_METEOGRAM_BASE_URL")) {
        restUrl = base_url;
    }


    std::string type = (const char*)in("type");
    std::string type2 = remote_databases_[type];
    if (!type2.empty())
        type = type2;
    restUrl += "?epsgram=" + type;


    restUrl += "&user=ecmwf";


    std::string dataSelectionType = (const char*)in("DATA_SELECTION_TYPE");

    if (dataSelectionType == "DATE") {
        // get the date and tias a full string
        int time = in("FORECAST_RUN_TIME");
        MvDate date = in("DATE");
        date += (double)time / 24.0;
        std::ostringstream date_option;
        date_option << date.ISO8601();
        restUrl += "&base_time=" + date_option.str();
    }

    else if (dataSelectionType == "LATEST") {
        // simply do not add a base_time string
    }


    // output format, e.g. png
    std::string format = (const char*)in("format");
    restUrl += "&format=" + format;


    // --- loop through the stations

    std::vector<std::string> tempFileNames;  // will store a list of all the temporary filenames used
    MvRequest station = in("station");
    while (station) {
        // get the station name for the title
        std::string title_option = (const char*)station("NAME");
        restUrl += "&station_name=" + title_option;


        // get the station coordinates

        std::string latitude_option = toString(station("LATITUDE"));
        std::string longitude_option = toString(station("LONGITUDE"));
        restUrl += "&lat=" + latitude_option;
        restUrl += "&lon=" + longitude_option;


        // get the station height

        if (station.countValues("HEIGHT") != 0) {
            std::string height_option = toString(station("HEIGHT"));

            if (!height_option.empty())
                restUrl += "&altitude=" + height_option;
        }


        // download the metadata using the REST API
        MvLog().info() << "Meteogram: downloading metadata via url " << restUrl;
        std::string tmpOutputFileName, msg;
        if (!download_file(restUrl, tmpOutputFileName, msg)) {
            setError(1, "Meteogram-> error downloading metadata - %s", msg.c_str());
            return;
        }


        // parse the metadata to extract the URL of the image
        std::string image_url = get_image_url_from_metadata(tmpOutputFileName);
        if (image_url.size() > 5 && image_url.substr(0, 5) == "ERROR") {
            setError(1, "Meteogram-> error parsing metadata - %s", image_url.c_str());
            return;
        }


        // download the image
        MvLog().info() << "Meteogram: downloading image via url " << image_url;
        if (!download_file(image_url, tmpOutputFileName, msg)) {
            setError(1, "Meteogram-> downloading image - %s", msg.c_str());
            return;
        }
        tempFileNames.push_back(tmpOutputFileName);  // remember which filename we used

        station.advance();
    }


    // we only support multiple stations with PostScript output
    // (for backwards compatibility with the previous metgram module)

    if (tempFileNames.size() > 1 && (format != "ps") && (format != "pdf")) {
        setError(1, "Meteogram-> Multiple stations only supported with PDF or PostScript output; use 'pdf' output or use multiple calls.");
        return;
    }


    // final step: generate the output request - either containing a single image or the result
    // of contatenating multiple images
    join_outputs(tempFileNames, format, out);
}


void Meteogram::handle_local(MvRequest& in, MvRequest& out)
{
    std::vector<std::string> tempFileNames;  // will store a list of all the temporary filenames used

    std::string outclass = "BAD";
    std::string outputFileName = marstmp();


    // ----------------------------------------------------
    // start creating our xml metgram request. We wil
    // write this to the file 'mgrequestfile'
    // --- construct the string that will be written to the
    // --- xml metgram request file
    // ----------------------------------------------------

    std::string format;
    std::string mgrequest;
    std::string mainTag;


    // get the data selection type (local, date or latest)

    std::string dataSelectionType = (const char*)in("DATA_SELECTION_TYPE");


    // start creating our xml metgram request. We will write this to the file 'mgrequestfile'

    mgrequest = "<" + mainTag + " ";


    // get the template we'll use (e.g. 10 days eps)
    // first, convert the name to what the metgram client expects

    std::string type = (const char*)in("type");
    type = local_databases_[type];
    mgrequest += "template='" + type + "' ";


    if (dataSelectionType == "DATE") {
        // get the date as a full string

        MvDate date = in("DATE");
        std::ostringstream date_option;
        date_option << date.YyyyMmDd();
        mgrequest += "date='" + date_option.str() + "' ";


        // get the time as a full string

        int time = in("FORECAST_RUN_TIME");
        std::ostringstream time_option;
        time_option << std::setw(4) << std::setfill('0') << time * 100;
        mgrequest += "time='" + time_option.str() + "' ";
    }

    else if (dataSelectionType == "LATEST") {
        mgrequest += "date='latest' ";
    }

    else if (dataSelectionType == "LOCAL") {
        std::string database = (const char*)in("database");

        mgrequest += "database='" + database + "' ";
    }


    // get the experiment number

    std::string experiment = (const char*)in("EXPERIMENT");

    if (!experiment.empty())
        mgrequest += "expver='" + experiment + "' ";


    // get the size (A4, A3, etc)
    // - also make a note of where we are in the string because we might want
    //   to hardcode this to A3 if the user wants a raster file generated.

    std::string size = "a4";
    mgrequest += "format='" + size + "' ";
    size_t a4_char_position = mgrequest.size() - 3;


    // close the eps tag

    mgrequest += ">\n    ";


    // --- loop through the stations

    MvRequest station = in("station");

    while (station) {
        // start the embedded station tag

        mgrequest += "<station ";


        // get the station name for the title

        std::string title_option = (const char*)station("NAME");
        mgrequest += "name='" + title_option + "' ";


        // get the station coordinates

        std::string latitude_option = toString(station("LATITUDE"));
        std::string longitude_option = toString(station("LONGITUDE"));
        mgrequest += "latitude='" + latitude_option + "' ";
        mgrequest += "longitude='" + longitude_option + "' ";


        // get the station height

        if (station.countValues("HEIGHT") != 0) {
            std::string height_option = toString(station("HEIGHT"));

            if (!height_option.empty())
                mgrequest += "height='" + height_option + "' ";
        }


        // get the file format option

        format = (const char*)in("format");
        std::string format_option;

        if (format == "ps")
            format_option = "psfile";
        else if (format == "png")
            format_option = "pngfile";
        else if (format == "gif")
            format_option = "giffile";
        else if (format == "jpeg")
            format_option = "jpgfile";
        else if (format == "pdf")
            format_option = "pdffile";
        else
            format_option = "pdffile";
        // SVG????????????


        std::string tmpOutputFileName = marstmp();
        mgrequest += format_option + "='" + tmpOutputFileName + "' ";


        tempFileNames.push_back(tmpOutputFileName);  // remember which filename we used


        // if we are creating a raster image, then set the format to a3 so
        // that the conversion looks better (the web metgram server actually
        // generates a ps file and then converts that to raster).

        if (format == "png" || format == "gif" || format == "jpeg") {
            mgrequest.replace(a4_char_position, 1, "3");
        }


        // close the station tag

        mgrequest += "/>\n";

        station.advance();
    }


    // finally add the closing eps tag

    mgrequest += "</" + mainTag + ">\n";


    // we only support multiple stations with PostScript output
    // (for backwards compatibility with the previous metgram module)

    if (tempFileNames.size() > 1 && (format != "ps") && (format != "pdf")) {
        setError(1, "Meteogram-> Multiple stations only supported with PDF or PostScript output; use 'pdf' output or use multiple calls.");
        return;
    }

    // write the request to a text file

    std::string mgrequestfile = marstmp();  // the name of the temporary file we'll write our xml request to


    std::ofstream xmlrequestfile;
    xmlrequestfile.open(mgrequestfile.c_str());

    if (xmlrequestfile.is_open()) {
        xmlrequestfile << mgrequest;
        xmlrequestfile.close();
    }
    else {
        setError(1, "Meteogram-> Failed to write xml metgram request file: %s", mgrequestfile.c_str());
        return;
    }


    // run the metgram command
    // which command to run to get the metgram - the 'classic' metgram requires a different script

    std::string command;
    command = getenv("METVIEW_BIN");
    command += "/mv_metgram " + mgrequestfile;


    sendProgress("Meteogram-> Running command: %s", command.c_str());
    int return_code = system(command.c_str());
    sendProgress("Meteogram-> Returned: %d", return_code);


    // successful execution?

    if (return_code == 0) {
        // final step: generate the output request - either containing a single image or the result
        // of contatenating multiple images
        join_outputs(tempFileNames, format, out);
    }

    else {
        // execution failed
        setError(1, "Meteogram-> Failed to generate the meteogram");
        return;
    }

    std::cout << "--------------Meteogram::serve() out--------------" << std::endl;
    out.print();
}


void Meteogram::serve(MvRequest& in, MvRequest& out)
{
    std::cout << "--------------Meteogram::serve() in--------------" << std::endl;
    in.print();


    // get the data selection type (local, date or latest)
    std::string dataSelectionType = (const char*)in("DATA_SELECTION_TYPE");


    if (dataSelectionType == "LOCAL")
        handle_local(in, out);
    else
        handle_remote(in, out);


    std::cout << "--------------Meteogram::serve() out--------------" << std::endl;
    out.print();
}

//_____________________________________________________________________

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