/***************************** 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 "Metview.h"
#include "MvPath.hpp"
#include "MvDate.h"
#include "MvMiscellaneous.h"
#include "Tokenizer.h"

#include <iostream>
#include <stdexcept>
#include <algorithm>

#define FLEXTRA_CHK(str) ({if(!(str)) {setError(13); return;} })


class MvMacroCallerService : public MvService
{
public:
    MvMacroCallerService(const char* a);

    static void progressCb(svcid* id, request* r, void* obj);
    static void replyCb(svcid* id, request* r, void* obj);

protected:
    void registerCallbacks();

    virtual void reply(const MvRequest&) = 0;
    virtual void progress(const MvRequest&) = 0;
};


MvMacroCallerService::MvMacroCallerService(const char* a) :
    MvService(a)
{
}

void MvMacroCallerService::registerCallbacks()
{
    add_progress_callback(MvApplication::getService(), nullptr,
                          progressCb, (void*)this);

    add_reply_callback(MvApplication::getService(), nullptr,
                       replyCb, (void*)this);
}

void MvMacroCallerService::progressCb(svcid* /*id*/, request* r, void* obj)
{
    // std::cout << "PROG callback: " << s << std::endl;
    MvRequest in(r);
    if (auto* b = static_cast<MvMacroCallerService*>(obj))
        b->progress(in);
}

void MvMacroCallerService::replyCb(svcid* /*id*/, request* r, void* obj)
{
    // std::cout << "REPLY: " << std::endl;
    // print_all_requests(r);
    MvRequest in(r);
    // in.print();
    if (auto* b = static_cast<MvMacroCallerService*>(obj))
        b->reply(in);
}

class Base : public MvMacroCallerService
{
    void serve(MvRequest&, MvRequest&) override;

protected:
    Base(const char* a, const std::string& id, const std::string& paramPrefix,
         const std::string& outReqType);

    std::string paramName(const std::string& p)
    {
        return paramPrefix_ + p;
    }

    void reply(const MvRequest&) override;
    void progress(const MvRequest&) override;

    std::string id_;
    std::string paramPrefix_;
    std::string outReqType_;
    std::map<std::string, std::string> modeIds_;
    std::map<std::string, std::string> onOffIds_;
    std::map<std::string, std::string> dataSourceIds_;
    std::map<std::string, std::string> areaSelectIds_;
    bool hasReply_;
    MvRequest replyReq_;
};

class FlextraPrep : public Base
{
public:
    FlextraPrep() :
        Base("FLEXTRA_PREPARE", "flextra", "FLEXTRA_", "FLEXTRA_INPUT") {}
};

class FlexpartPrep : public Base
{
public:
    FlexpartPrep() :
        Base("FLEXPART_PREPARE", "flexpart", "", "FLEXPART_INPUT") {}
};

Base::Base(const char* a, const std::string& id, const std::string& paramPrefix,
           const std::string& outReqType) :
    MvMacroCallerService(a),
    id_(id),
    paramPrefix_(paramPrefix),
    outReqType_(outReqType),
    hasReply_(false)
{
    modeIds_["FORECAST"] = "fc";
    modeIds_["PERIOD"] = "period";

    onOffIds_["ON"] = "1";
    onOffIds_["OFF"] = "0";

    dataSourceIds_["MARS"] = "mars";
    dataSourceIds_["FILE"] = "file";

    areaSelectIds_["INTERPOLATE"] = "1";
    areaSelectIds_["NATIVE"] = "0";
}

void Base::progress(const MvRequest& in)
{
    if (const char* c = in("PROGRESS")) {
        marslog(LOG_INFO, "%s", c);
    }
}

void Base::reply(const MvRequest& in)
{
    hasReply_ = true;
    replyReq_ = in;
}

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

    // Etadot is not stored in MARS for dates earlier than this
    MvDate etaDotDate("20080604");

    std::string flextraMacro;
    char* mvbin = getenv("METVIEW_BIN");
    if (mvbin == nullptr) {
        marslog(LOG_EROR, "No METVIEW_BIN env variable is defined. Cannot locate mv_flextra_prep.mv macro!");
        setError(13);
        return;
    }
    else {
        flextraMacro = std::string(mvbin) + "/mv_flextra_prep.mv";
    }

    std::vector<std::string> param;
    std::string str;

    // The first param indicates that we are not in test mode
    param.emplace_back("0");

    // The second param is the model type id
    param.push_back(id_);

    // Add perparation mode
    std::string prepMode;
    FLEXTRA_CHK(in.getValueId(paramName("PREPARE_MODE"), prepMode, modeIds_));
    param.push_back(prepMode);

    // Add reuse(check) input status
    FLEXTRA_CHK(in.getValueId(paramName("REUSE_INPUT"), str, onOffIds_));
    param.push_back(str);

    // Data source
    std::string dataSource;
    FLEXTRA_CHK(in.getValueId(paramName("INPUT_SOURCE"), dataSource, dataSourceIds_));
    param.push_back(dataSource);

    if (prepMode == "period" && dataSource == "file") {
        marslog(LOG_EROR, "When %s is \"period\" %s must be set to \"mars\"!",
                paramName("PREPARE_MODE").c_str(), paramName("INPUT_SOURCE").c_str());
        setError(13);
        return;
    }

    // Add outpath
    std::string outPath;
    FLEXTRA_CHK(in.getPath(paramName("OUTPUT_PATH"), outPath, false));
    param.push_back(outPath);

    // Add Available file
    std::string fAvailable = outPath + "/AVAILABLE";
    param.push_back(fAvailable);

    std::string interpolate = "1";
    if (dataSource == "file") {
        FLEXTRA_CHK(in.getValueId(paramName("GRID_INTERPOLATION"), interpolate, onOffIds_));
    }

    param.push_back(interpolate);

    std::vector<std::string> area;
    std::vector<std::string> grid;
    if (interpolate == "1") {
        // Add area
        FLEXTRA_CHK(in.getValue(paramName("AREA"), area));
        param.push_back(metview::merge(area, "/"));

        // Add grid resolution
        FLEXTRA_CHK(in.getValue(paramName("GRID"), grid));
        param.push_back(metview::merge(grid, "/"));
    }

    else {
        param.emplace_back("0");
        param.emplace_back("0");
    }

    // Add top level
    std::string topL;
    FLEXTRA_CHK(in.getValue(paramName("TOP_LEVEL"), topL));
    param.push_back(topL);

    std::string topUnits;
    FLEXTRA_CHK(in.getValue(paramName("TOP_LEVEL_UNITS"), topUnits));
    param.push_back(topUnits);

    std::vector<std::string> numXy;
    std::string errStr;
    if (interpolate == "1" && !metview::checkGrid(area, grid, numXy, errStr)) {
        marslog(LOG_EROR, "Inconsistency between %s and %s!", paramName("AREA").c_str(), paramName("GRID").c_str());
        marslog(LOG_EROR, "%s", errStr.c_str());
        setError(13);
        return;
    }

    if (prepMode == "fc") {
        // Add mars expver
        std::string marsExpver;
        FLEXTRA_CHK(in.getValue(paramName("FC_MARS_EXPVER"), marsExpver));
        param.push_back(marsExpver);

        FLEXTRA_CHK(in.getValue(paramName("DATE"), str));
        // std::cout << "date=" << str << std::endl;

        param.push_back(str);
        MvDate md(str.c_str());
        if (md < etaDotDate) {
            marslog(LOG_EROR, "Cannot prepare data for the date specified in FLEXTRA_DATE: %s", str.c_str());
            marslog(LOG_EROR, "Etadot is not avaliable in MARS for this date! It has only been archived since 4 June 2008.");
            setError(13);
            return;
        }

        FLEXTRA_CHK(in.getValue(paramName("TIME"), str));
        param.push_back(str);

        std::vector<std::string> steps;
        FLEXTRA_CHK(in.getValue(paramName("STEP"), steps));
        param.push_back(metview::merge(steps, "/"));
    }
    else {
        // Add mars class
        std::string marsClass;
        FLEXTRA_CHK(in.getValue(paramName("FC_MARS_CLASS"), marsClass));
        metview::toLower(marsClass);
        param.push_back(marsClass);

        // Add mars class
        std::string marsAnClass;
        FLEXTRA_CHK(in.getValue(paramName("AN_MARS_CLASS"), marsAnClass));
        metview::toLower(marsAnClass);
        param.push_back(marsAnClass);

        // Add mars expver
        std::string marsExpver;
        FLEXTRA_CHK(in.getValue(paramName("FC_MARS_EXPVER"), marsExpver));
        param.push_back(marsExpver);

        // Add mars expver
        std::string marsAnExpver;
        FLEXTRA_CHK(in.getValue(paramName("AN_MARS_EXPVER"), marsAnExpver));
        param.push_back(marsAnExpver);

        FLEXTRA_CHK(in.getDate(paramName("PERIOD_START_DATE"), str));
        param.push_back(str);
        MvDate mdStart(str.c_str());
        if (mdStart < etaDotDate) {
            marslog(LOG_EROR, "Cannot prepare data for the date specified in %s: %s",
                    str.c_str(), paramName("PERIOD_START_DATE").c_str());
            marslog(LOG_EROR, "Etadot is not avaliable in MARS for this date! It has only been archived since 4 June 2008.");
            setError(13);
            return;
        }

        FLEXTRA_CHK(in.getValue(paramName("PERIOD_START_TIME"), str));
        param.push_back(str);
        FLEXTRA_CHK(in.getDate(paramName("PERIOD_END_DATE"), str));
        param.push_back(str);
        MvDate mdEnd(str.c_str());
        if (mdEnd < etaDotDate) {
            marslog(LOG_EROR, "Cannot prepare data for the date specified in %s: %s",
                    str.c_str(), paramName("PERIOD_END_DATE").c_str());
            marslog(LOG_EROR, "Etadot is not avaliable in MARS for this date! It has only been archived since 4 June 2008.");
            setError(13);
            return;
        }

        FLEXTRA_CHK(in.getValue(paramName("PERIOD_END_TIME"), str));
        param.push_back(str);
        FLEXTRA_CHK(in.getValue(paramName("PERIOD_STEP"), str));
        param.push_back(str);

        if (mdEnd < mdStart) {
            marslog(LOG_EROR, "Inconsistency in period definition! %s precedes %s!",
                    paramName("PERIOD_END_DATE").c_str(), paramName("PERIOD_START_DATE").c_str());
            setError(13);
            return;
        }
    }

    // Input files
    if (dataSource == "file") {
        std::string inputFiles;
        FLEXTRA_CHK(in.getValue(paramName("INPUT_FILE"), inputFiles));
        param.push_back(inputFiles);
    }


    // Build request to be sent to Macro
    MvRequest req("MACRO");

    std::string processName = MakeProcessName("FlextraPrepare");
    MvRequest macroReq("MACRO");
    req("PATH") = flextraMacro.c_str();
    req("_CLASS") = "MACRO";
    req("_ACTION") = "execute";
    req("_REPLY") = processName.c_str();
    req("_EXTENDMESSAGE") = "1";

    // Define argument list for the macro!
    for (auto& it : param) {
        req.addValue("_ARGUMENTS", it.c_str());
    }

    // const char *cdir=getenv("PWD");
    // marslog(LOG_INFO,"PWD=%s",cdir);

    // Run macro
    marslog(LOG_INFO, "Execute macro: %s", flextraMacro.c_str());

    svc* mysvc = MvApplication::getService();
    registerCallbacks();
    MvApplication::callService("macro", req, this);

    for (;;) {
        if (hasReply_)
            break;

        svc_connect(mysvc);
        if (process_service(mysvc))
            break; /* Timeout */
    }

    if (const char* v = replyReq_.getVerb()) {
        if (strcmp(v, "ERROR") == 0) {
            marslog(LOG_EROR, "Failed to run FLEXTRA input preparation! Macro failed!");
            if (const char* cc = replyReq_("MESSAGE")) {
                marslog(LOG_EROR, "Message: %s", cc);
            }
            setError(13);
        }
        else if (strcmp(v, "NUMBER") == 0) {
            if (const char* numCh = replyReq_("VALUE")) {
                if (strcmp(numCh, "0") != 0) {
                    marslog(LOG_EROR, "Failed to run FLEXTRA input preparation!");
                    marslog(LOG_EROR, "Macro exited with code:: %s", numCh);
                }
            }
        }
    }
    else {
        marslog(LOG_EROR, "Failed to run FLEXTRA input preparation!");
        setError(13);
    }

#if 0
    //MvRequest reply = MvApplication::waitService("macro",req,error);

	//Call myself to process the macro reply request
	//if(!error && reply)
    if(!error)
    {
		//const char* myname = reply("_REPLY");
		//MvApplication::callService(myname,reply,0);
	}
	else
	{
	  	marslog(LOG_EROR,"Failed to run FLEXTRA input preparation! Macro failed!");
		setError(13);
		return;
	}
#endif
    // reply.print();

    out = MvRequest(outReqType_.c_str());
    out("INPUT_DATA_PATH") = outPath.c_str();
    out("AVAILABLE_FILE_PATH") = fAvailable.c_str();

    out.print();
}


int main(int argc, char** argv)
{
    MvApplication theApp(argc, argv, "FlextraPrepare");

    FlextraPrep flextra;
    FlexpartPrep flexpart;

    theApp.run();
}
