/***************************** 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 <math.h>

#include <fstream>
#include <memory>

#include "fstream_mars_fix.h"

#ifdef HAVE_PPROC_MIR
#include "eckit/exception/Exceptions.h"
#include "eckit/log/Log.h"

#include "mir/api/MIRJob.h"
#include "mir/input/GribMemoryInput.h"
#include "mir/input/VectorInput.h"
#include "mir/output/GribMemoryOutput.h"
#include "mir/output/VectorOutput.h"
#endif

#include "Divrot.h"


// from libMars/emos.c
extern "C" {
fortint intout_(char*, fortint[], fortfloat[], const char*, fortint, fortint);
fortint intin_(char*, fortint[], fortfloat[], const char*, fortint, fortint);
}


#ifdef HAVE_PPROC_MIR

static std::shared_ptr<mir::api::MIRJob> job;

int intuvp2(const void* vort_grib_in,
            const void* div_grib_in,
            const int& length_in,
            void* u_grib_out,
            void* v_grib_out,
            int& length_out)
{
    // eckit::Log::debug<LibMir>() << "++++++ intuvp2" << std::endl;

    try {
        // Second order packing may return different sizes
        ::memset(u_grib_out, 0, length_out);
        ::memset(v_grib_out, 0, length_out);

        mir::input::GribMemoryInput vort_input(vort_grib_in, length_in);
        mir::input::GribMemoryInput div_input(div_grib_in, length_in);


        mir::output::GribMemoryOutput u_output(u_grib_out, length_out);
        mir::output::GribMemoryOutput v_output(v_grib_out, length_out);

        mir::input::VectorInput input(vort_input, div_input);
        mir::output::VectorOutput output(u_output, v_output);

        job->set("vod2uv", true);


        static const char* capture = getenv("MIR_CAPTURE_CALLS");
        if (capture) {
            std::ofstream out(capture);
            out << "mars<<EOF" << std::endl;
            out << "retrieve,target=in.grib,";
            vort_input.marsRequest(out);
            out << std::endl;
            out << "retrieve,target=in.grib,";
            div_input.marsRequest(out);
            out << std::endl;
            out << "EOF" << std::endl;
            job->mirToolCall(out);
            out << std::endl;
        }

        job->execute(input, output);

        job->clear("vod2uv");

        ASSERT(u_output.interpolated() + u_output.saved() == 1);
        ASSERT(v_output.interpolated() + v_output.saved() == 1);

        // If packing=so, u and v will have different sizes
        // ASSERT(u_output.length() == v_output.length());
        length_out = int(std::max(u_output.length(), v_output.length()));
    }
    catch (std::exception& e) {
        eckit::Log::error() << "MIR: " << e.what() << std::endl;
        return -2;
    }

    return 0;
}

err metview_makeuv_mir(char* vo, char* d, long inlen, char* u, char* v, long* outlen, int truncation)
{
    // eckit::AutoLock<eckit::Mutex> lock(mutex_);

    err e = NOERR;
    int out = *outlen;
    int in = inlen;

    if (!job) {
        job = std::make_shared<mir::api::MIRJob>();
    }

    job->set("truncation", truncation);

    if (mars.grib_postproc == 0) {
        marslog(LOG_DBUG, "Divrot-> Env variable MARS_GRIB_POSTPROC has been set to 0 and conversion to U/V requested");
        return POSTPROC_ERROR;
    }

    //    if(!ppdata.quiet)
    {
        marslog(LOG_INFO, "Deriving U and V from vorticity and divergence");
        // ppdata.quiet = 1;
    }

    marslog(LOG_DBUG, "-> INTUV%s in=%d out=%d", mars.use_intuvp ? "P" : "S", *outlen, out);
    ASSERT(mars.use_intuvp);

    // timer_start(pptimer);
    if (mars.use_intuvp) {
        e = intuvp2(vo, d, in, u, v, out);
        // if(in != out)
        // ppdata.inter_cnt+=2;
        marslog(LOG_DBUG, "MARS_USE_INTUVP set and parameters are U/V. Avoid calling intf2");
        // ppdata.derive_uv += 2;
    }
    // timer_stop(pptimer,0);

    marslog(LOG_DBUG, "<- INTUV%s in=%d out=%d", mars.use_intuvp ? "P" : "S", *outlen, out);

    if (out > *outlen) {
        marslog(LOG_DBUG, "Divrot-> INTUV%s output is %d bytes. Buffer is only %d bytes", mars.use_intuvp ? "P" : "S", out, *outlen);
        return 0;
    }

    *outlen = out;

    /* Check if rounding to a multiple of 4 changes */
    /* the actual size.                             */
    if (*outlen > signed((inlen + sizeof(int)))) {
        marslog(LOG_DBUG, "INTUV%s returns bigger field %d > %d", mars.use_intuvp ? "P" : "S", *outlen, inlen);
        /* return -3; */
    }

    return e;
}
#endif  // HAVE_PPROC_MIR

bool Divrot::convert_DIVROT(MvRequest& out)
{
    DfileOut = marstmp();  //-- file for computed fields

    for (int f = 0; f < Dnfield; ++f)  //-- loop over input fields
    {
        char* bufferVorticity = nullptr;
        char* bufferDivergence = nullptr;
        size_t sizeVorticity = 0;
        size_t sizeDivergence = 0;
        int ret;
        err marsRet;

        MvField myField = Dfieldset[f];  //-- get grib handle for this field
        grib_handle* ghVorticity = nullptr;
        grib_handle* ghDivergence = nullptr;
        grib_handle* ghZero = nullptr;
        size_t voValuesLength = 0;   //-- data values count
        size_t divValuesLength = 0;  // ...

        // note: vorticity->rotwind, divergence->divwind, both->uvwind

        switch (DresultParam) {
            case eRotational: {
                ghVorticity = myField.getGribHandle();
                ghDivergence = grib_handle_clone(ghVorticity);  // create an all-zero valued GRIB for divergence
                ghZero = ghDivergence;                          // this is the one we will zero
                break;
            }

            case eDivergent: {
                ghDivergence = myField.getGribHandle();
                ghVorticity = grib_handle_clone(ghDivergence);  // create an all-zero valued GRIB for vorticity
                ghZero = ghVorticity;                           // this is the one we will zero
                break;
            }

            case eUV: {
                MvField myField2 = Dfieldset2[f];  //-- get grib handle for 'the other' field
                ghVorticity = myField.getGribHandle();
                ghDivergence = myField2.getGribHandle();
                break;
            }

            default: {
                setError(1, "Divrot-> params should be vorticity or divergence");
                return false;
            }
        }

        if (!isValidData_DVVALID(ghVorticity))  //-- must be vorticity or divergence
        {
            setError(1, "Divrot-> invalid vorticity data");
            return false;
        }

        if (!isValidData_DVVALID(ghDivergence))  //-- must be vorticity or divergence
        {
            setError(1, "Divrot-> invalid divergence data");
            return false;
        }

        checkOutputTruncation(ghVorticity);   //-- ensure output truncation
        checkOutputTruncation(ghDivergence);  //-- ensure output truncation

        // get the data array lengths and ensure they are the same
        if ((ret = grib_get_size(ghVorticity, "values", &voValuesLength))) {
            setError(1, "Error on GRIB decoding: grib_get_size/vorticity values returned %d in Divrot::convert_DIVROT", ret);
            return false;
        }

        if ((ret = grib_get_size(ghDivergence, "values", &divValuesLength))) {
            setError(1, "Error on GRIB decoding: grib_get_size/divergence values returned %d in Divrot::convert_DIVROT", ret);
            return false;
        }

        if (voValuesLength != divValuesLength) {
            setError(1, "Divrot: vorticity and divergence fields should have the same number of points (%d, %d)",
                     voValuesLength, divValuesLength);
            return false;
        }

        // set the 'zero' GRIB values to zero
        if (ghZero != nullptr) {
            auto* zero = new double[voValuesLength];
            memset(zero, 0, sizeof(double) * voValuesLength);

            if ((ret = grib_set_double_array(ghZero, "values", zero, voValuesLength))) {
                setError(1, "Error on GRIB decoding: grib_set_double_array returned %d in Divrot::convert_DIVROT", ret);
                return false;
            }

            delete[] zero;
        }

        // get the input GRIB message into a char buffer, because this is
        // what makeuv() wants
        if ((ret = grib_get_message(ghVorticity, (const void**)&bufferVorticity, &sizeVorticity))) {
            setError(1, "Error on GRIB decoding: grib_get_message/Vorticity returned %d in Divrot::convert_DIVROT", ret);
            return false;
        }

        if ((ret = grib_get_message(ghDivergence, (const void**)&bufferDivergence, &sizeDivergence))) {
            setError(1, "Error on GRIB decoding: grib_get_message/Divergence returned %d in Divrot::convert_DIVROT", ret);
            return false;
        }

        // compute the size of the output buffers
        DoutputValsLen = voValuesLength * 3 + 4096;  //(DtruncOut+1)*(DtruncOut+2) + 4096;
        char* u = new char[DoutputValsLen * sizeof(double)];
        char* v = new char[DoutputValsLen * sizeof(double)];

#ifdef HAVE_PPROC_MIR
        // perform conversion with MIR
        char* pprocenv = getenv("MARS_PPROC_BACKEND");
        if (pprocenv && !strcmp(pprocenv, "MIR")) {
            marsRet = metview_makeuv_mir(bufferVorticity, bufferDivergence, sizeVorticity, u, v, &DoutputValsLen, DtruncOut);

            if (marsRet != 0) {
                setError(1, "Divrot-> error converting from VO/D. Function metview_makeuv_mir");
                return false;
            }
        }
        else {
#endif

#ifdef HAVE_PPROC_EMOS
            // perform conversion with emoslib
            // set the output truncation in emoslib
            fortfloat realv[4];
            fortint truncout = DtruncOut;
            fortint e;
            char* int_parameter = "truncation";

            e = intout_(C2FORT(int_parameter), &truncout, realv, C2FORT(0), strlen(int_parameter), 0);
            e += intin_(C2FORT(int_parameter), &truncout, realv, C2FORT(0), strlen(int_parameter), 0);

            if (e != 0) {
                setError(1, "Divrot-> error when setting output truncation");
                return false;
            }

            // derive u/v using the libMars function
            marsRet = makeuv(bufferVorticity, bufferDivergence, sizeVorticity, u, v, &DoutputValsLen);
            if (marsRet != 0) {
                setError(1, "Divrot-> error converting from VO/D");
                return false;
            }
#endif  // HAVE_PPROC_EMOS

#ifdef HAVE_PPROC_MIR
        }
#endif

        grib_handle* ghU = grib_handle_new_from_message(nullptr, u, DoutputValsLen);
        grib_handle* ghV = grib_handle_new_from_message(nullptr, v, DoutputValsLen);

        appendWindComponent(ghU);  //-- (normal) wind U comp
        appendWindComponent(ghV);  //-- (normal) wind V comp

        delete[] u;
        delete[] v;

        grib_handle_delete(ghU);
        grib_handle_delete(ghV);
    }
    //-- file to FieldSet and to Request
    MvFieldSet fs(DfileOut.c_str());
    out = fs.getRequest();

    return true;
}

//_____________________________________________________________________________
//--
//-- This is a copy of Velstr::isValidData_DVVALID.
//-- The original Fortran code is still left in file
//-- ../Velstr/velstr-exFortran.cc
//--
//--  Modified for METV-2977.
bool Divrot::isValidData_DVVALID(grib_handle* gh)
{
    const int cBUFLEN = 50;
    char charBuf[cBUFLEN + 1];

    size_t len = cBUFLEN;  //-- is spectral or not?
    int ret;
    if ((ret = grib_get_string(gh, "gridType", charBuf, &len))) {
        setError(1, "Error on GRIB decoding: grib_get_string/gridType returned %d in Divrot::isValidData_DVVALID", ret);
        return false;
    }

    if (std::string(charBuf) != cSPECTRAL) {
        setError(1, "Divrot-> data is '%s', must be spectral ('%s')", charBuf, cSPECTRAL.c_str());
        return false;
    }

    long param;
    if ((ret = grib_get_long(gh, "paramId", &param))) {
        setError(1, "Error on GRIB decoding: grib_get_long/paramId returned %d in Divrot::isValidData_DVVALID", ret);
        return false;
    }

    //    if (param == atol(cVORTICITY.c_str())) {
    if (param == vorParamId_) {
        Dparam = eVorticity;
        return true;
    }

    //    if (param == atol(cDIVERGENCE.c_str())) {
    if (param == divParamId_) {
        Dparam = eDivergence;
        return true;
    }

    setError(1, "Divrot-> param is '%s', must be vorticity (%s) or divergence (%s)", charBuf, cVORTICITY.c_str(), cDIVERGENCE.c_str());
    return false;
}
//_____________________________________________________________________________
//--
//-- This is a copy of Velstr::smooth_BPPSMTH.
//-- The original Fortran code is still left in file
//-- ../Velstr/velstr-exFortran.cc
//--
bool Divrot::smooth_BPPSMTH(grib_handle* gh)
{
    size_t len = 0;  //-- data values count
    int ret;
    if ((ret = grib_get_size(gh, "values", &len))) {
        setError(1, "Error on GRIB decoding: grib_get_size returned %d in Divrot::smooth_BPPSMTH", ret);
        return false;
    }

    auto* data = new double[len];  //-- get data values
    if ((ret = grib_get_double_array(gh, "values", data, &len))) {
        setError(1, "Error on GRIB decoding: grib_get_double_array returned %d in Divrot::smooth_BPPSMTH", ret);
        return false;
    }

    //-- prepare smoothing stuff
    double fltcons = -1. / pow(Dfltc * (Dfltc + 1), Dmfltexp);
    sendProgress("Spatial smoothing, Fltc = %g, Mfltexp = %g", Dfltc, Dmfltexp);

    long itinp1 = DtruncOut;           //-- NPREL4
    auto* dummy = new double[itinp1];  //-- compute final smoothing coefficients
    dummy[0] = 1.0;
    for (int n = 1; n < itinp1; ++n) {
        dummy[n] = exp(fltcons * pow((double)(n * (n + 1)), Dmfltexp));
    }

    int i = 0;  //-- do the smoothing
    int ii = 0;
    for (int m = 0; m < itinp1; ++m)
        for (int n = m; n < itinp1; ++n) {
            data[ii] = dummy[n] * data[i];
            data[ii + 1] = dummy[n] * data[i + 1];
            i += 2;
            ii += 2;
        }
    //-- put back the smoothed data
    if ((ret = grib_set_double_array(gh, "values", data, len))) {
        setError(1, "Error on GRIB decoding: grib_set_double_array returned %d in Divrot::smooth_BPPSMTH", ret);
        return false;
    }

    delete[] dummy;
    delete[] data;

    return true;
}

bool Divrot::checkOutputTruncation(grib_handle* gh)
{
    long myTtruncIn = 0;  //-- get truncation
    int ret;
    if ((ret = grib_get_long(gh, "pentagonalResolutionParameterJ", &myTtruncIn))) {
        setError(1, "Error on GRIB decoding: pentagonalResolutionParameterJ returned %d in Divrot::checkOutputTruncation", ret);
        return false;
    }

    DtruncIn = myTtruncIn;

    if (DtruncOut > DtruncIn) {
        setError(0, "Divrot-> Requested output truncation %d greater than input %d. Setting output truncation to %d", DtruncOut, (int)DtruncIn, (int)DtruncIn);

        DtruncOut = DtruncIn;  //-- fall back to input truncation
    }

    return true;
}
