/***************************** LICENSE START ***********************************

Copyright 2021 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 "Regrid.h"

#include <algorithm>
#include <cctype>
#include <memory>
#include <ostream>
#include <string>
#include <utility>
#include <vector>

#include "eckit/config/LibEcKit.h"
#include "eckit/linalg/LinearAlgebra.h"
#include "eckit/utils/StringTools.h"
#include "eckit/utils/Translator.h"

#include "mir/api/MIRJob.h"
#include "mir/input/MIRInput.h"
#include "mir/output/MIROutput.h"
#include "mir/param/MIRParametrisation.h"
#include "mir/util/Exceptions.h"
#include "mir/util/Log.h"
#include "mir/util/MIRStatistics.h"
#include "mir/util/Trace.h"

#include "MvApplication.h"
#include "MvMiscellaneous.h"


//--------------------------------------------------------


namespace
{
// hardcoded defaults
struct defaults_t
{
    static constexpr auto grid_definition_mode = "grid";
    static constexpr auto debug = false;
    static constexpr double first_point[] = {0., 0.};
    static constexpr long multi_dimensional = 1;
};


std::string sane_key(const std::string& insane)
{
    std::string sane(insane);
    std::transform(insane.begin(), insane.end(), sane.begin(),
                   [=](unsigned char c) { return std::isalnum(c) != 0 ? std::toupper(c) : '_'; });
    return sane;
}


std::string sane_value(const std::string& insane)
{
    std::string sane(insane);
    std::transform(insane.begin(), insane.end(), sane.begin(),
                   [=](unsigned char c) { return std::isalnum(c) != 0 ? std::tolower(c) : '-'; });
    return sane;
}


// Use MvRequest satisfying MIRParametrisation interface
class RequestWrapper final : public mir::param::MIRParametrisation
{
public:
    explicit RequestWrapper(const MvRequest& request) :
        request_(request) {}

    // From MIRParametrisation
    const MIRParametrisation& userParametrisation() const override { return *this; }
    const MIRParametrisation& fieldParametrisation() const override { return *this; }

    bool has(const std::string& name) const override
    {
        auto sane = sane_key(name);
        const char* v = request_(sane.c_str());
        return (v != nullptr) && (strlen(v) > 0);
    }

    bool get(const std::string& name, std::string& v) const override { return _getValue(name, v); }
    bool get(const std::string& name, bool& v) const override { return _getValue(name, v); }
    bool get(const std::string& name, int& v) const override { return _getValue(name, v); }
    bool get(const std::string& name, long& v) const override { return _getValue(name, v); }
    bool get(const std::string& name, float& v) const override { return _getValue(name, v); }
    bool get(const std::string& name, double& v) const override { return _getValue(name, v); }

    bool get(const std::string& name, std::vector<int>& v) const override { return _getVector(name, v); }
    bool get(const std::string& name, std::vector<long>& v) const override { return _getVector(name, v); }
    bool get(const std::string& name, std::vector<float>& v) const override { return _getVector(name, v); }
    bool get(const std::string& name, std::vector<double>& v) const override { return _getVector(name, v); }
    bool get(const std::string& name, std::vector<std::string>& v) const override { return _getVector(name, v); }

    const MvRequest& request() const { return request_; }

private:
    void print(std::ostream& s) const override { s << "MvRequest[" << request_ << "]"; }

    template <typename T>
    bool _getValue(const std::string& name, T& value) const
    {
        auto sane = sane_key(name);
        const char* v = request_(sane.c_str());
        if ((v != nullptr) && (strlen(v) > 0)) {
            value = eckit::Translator<std::string, T>()(v);
            return true;
        }
        return false;
    }

    template <typename T>
    bool _getVector(const std::string& name, std::vector<T>& value) const
    {
        auto sane = sane_key(name);
        const auto* k = sane.c_str();
        const char* v = request_(k);
        if (v != nullptr && strlen(v) > 0) {
            auto c = size_t(request_.countValues(k));
            if (0 < c) {
                eckit::Translator<std::string, T> translate;
                value.clear();
                value.reserve(c);
                for (int i = 0; i < int(c); ++i) {
                    const char* v = request_(k, i);
                    value.emplace_back(translate(v));
                }
                return true;
            }
        }
        return false;
    }

    const MvRequest& request_;
};


class JobWrapper final : public mir::api::MIRJob
{
    std::string source_;
    std::string target_;
    bool target_is_temporary_;

public:
    const std::string& source() const { return source_; }
    const std::string& target() const { return target_; }
    bool target_is_temporary() const { return target_is_temporary_; }
    void source(const std::string& path) { source_ = path; }
    void target(const std::string& path, bool temporary)
    {
        target_ = path;
        target_is_temporary_ = temporary;
    }
};


std::string get_path(const RequestWrapper& param, const std::string& pathKey, const std::string& dataKey)
{
    std::string path;
    if (!param.get(pathKey, path) || path.empty()) {
        MvRequest data;
        param.request().getValue(data, dataKey.c_str());
        const RequestWrapper req(data);

        // ensure filtered/indexed fieldsets are written to disk first
        path = metview::pathFromFieldsetWrittenToDisk(req.request());
    }

    return path;
}


struct Param
{
    Param(MvProtocol& protocol, std::string name) :
        name_(std::move(name)), protocol_(protocol) {}

    Param(const Param&) = delete;
    Param(Param&&) = delete;
    void operator=(const Param&) = delete;
    void operator=(Param&&) = delete;

    virtual ~Param() = default;

    virtual void eval(const RequestWrapper&, JobWrapper&) const = 0;
    void error(const std::string& msg) const { const_cast<MvProtocol&>(protocol_).setError(1, msg.c_str()); }
    const std::string& name() const { return name_; }

private:
    const std::string name_;
    MvProtocol& protocol_;
};


template <typename T>
struct ParamT : Param
{
    using Param::Param;

    void eval(const RequestWrapper& param, JobWrapper& job) const override
    {
        T value;
        if (param.get(name(), value)) {
            job.set(name(), value);
        }
    }
};


template <>
void ParamT<std::string>::eval(const RequestWrapper& param, JobWrapper& job) const
{
    std::string value;
    if (param.get(name(), value) && !value.empty()) {
        job.set(name(), sane_value(value));  // sanitize strings
    }
};


template <typename T>
struct ParamVectorT : Param
{
    ParamVectorT(MvProtocol& protocol, const std::string& name, size_t n = 0) :
        Param(protocol, name), n_(n) {}

    void eval(const RequestWrapper& param, JobWrapper& job) const override
    {
        std::vector<T> value;
        if (get(param, value)) {
            job.set(name(), value);
        }
    }

    bool get(const mir::param::MIRParametrisation& param, std::vector<T>& value) const
    {
        std::vector<T> v;
        if (param.get(name(), v)) {
            ASSERT(!v.empty());
            if (n_ == 0 || n_ == v.size()) {
                value.swap(v);
                return true;
            }
        }
        return false;
    }

private:
    const size_t n_;
};


struct ParamArea final : ParamVectorT<double>
{
    ParamArea(MvProtocol& protocol, const std::string& name) :
        ParamVectorT(protocol, name, 4) {}

    void eval(const RequestWrapper& param, JobWrapper& job) const override
    {
        std::vector<double> value;
        if (get(param, value)) {
            ASSERT(value.size() == 4);
            std::vector<double> area{value[2] /*N*/, value[1] /*W*/, value[0] /*S*/, value[3] /*E*/};
            job.set(name(), area);
        }
    }
};


struct ParamMultiDimensional final : Param
{
    using Param::Param;

    void eval(const RequestWrapper& param, JobWrapper& job) const override
    {
        long value = defaults_t::multi_dimensional;
        if (param.get(name(), value) && value > 1) {
            job.set("input", "{multiDimensional: " + std::to_string(value) + "}");
        }
    }
};


struct SetupInput final : Param
{
    explicit SetupInput(MvProtocol& protocol) :
        Param(protocol, "source") {}

    void eval(const RequestWrapper& param, JobWrapper& job) const override
    {
        auto src = get_path(param, "SOURCE", "DATA");
        if (src.empty()) {
            error("Regrid: 'SOURCE' or 'DATA' must be defined");
            return;
        }

        job.source(src);
    }
};


struct SetupOutput final : Param
{
    explicit SetupOutput(MvProtocol& protocol) :
        Param(protocol, "grid-definition-mode") {}

private:
    std::string mandatory_key(const RequestWrapper& param, const std::string& fromKey, const std::string& toKey) const
    {
        std::string value;
        if (!param.get(fromKey, value) || value.empty()) {
            error("Regrid: key '" + sane_key(fromKey) + "' must be defined");
        }
        return ";" + toKey + "=" + value;
    }

    std::string mandatory_keys(const RequestWrapper& param, const std::string& fromKey1, const std::string& fromKey2,
                               const std::string& toKey) const
    {
        bool ok = true;

        std::string value1;
        if (!param.get(fromKey1, value1) || value1.empty()) {
            error("Regrid: key '" + sane_key(fromKey1) + "' must be defined");
            ok = false;
        }

        std::string value2;
        if (!param.get(fromKey2, value2) || value2.empty()) {
            error("Regrid: key '" + sane_key(fromKey2) + "' must be defined");
            ok = false;
        }

        return ok ? ";" + toKey + "=" + value1 + "/" + value2 : "";
    }

    static std::string first_point_keys(const RequestWrapper& param, const std::string& fromKey,
                                        const std::string& toKey1, const std::string& toKey2)
    {
        std::vector<double> value{defaults_t::first_point[0], defaults_t::first_point[1]};
        if (param.get(fromKey, value)) {
            ASSERT(value.size() == 2);
        }
        return ";" + toKey1 + "=" + std::to_string(value[0]) + ";" + toKey2 + "=" + std::to_string(value[1]);
    }

    static std::string optional_key(const RequestWrapper& param, const std::string& fromKey, const std::string& toKey)
    {
        std::string value;
        return param.get(fromKey, value) && !value.empty() ? ";" + toKey + "=" + value : "";
    }

    void eval(const RequestWrapper& param, JobWrapper& job) const override
    {
        // requires prior setup of input
        ASSERT(!job.source().empty());


        // setup output
        ASSERT(job.target().empty());
        std::string target;
        const bool temporary = !param.get("TARGET", target);
        job.target(temporary ? marstmp() : target, temporary);
        ASSERT(!job.target().empty());


        // setup mode
        std::string mode = defaults_t::grid_definition_mode;
        param.get(name(), mode);
        mode = sane_value(mode);


        if (mode == "grid") {
            std::string grid;
            if (param.get("grid", grid) && !grid.empty()) {
                std::vector<double> grid_v;

                bool list = false;
                if ((list = param.request().countValues("GRID") > 1)) {
                    param.get("grid", grid_v);
                }
                else if ((list = grid.find('/') != std::string::npos)) {
                    eckit::Translator<std::string, double> translate;
                    for (auto& v : eckit::StringTools::split("/", grid)) {
                        grid_v.emplace_back(translate(v));
                    }
                }

                if (!list) {
                    job.set("grid", grid);
                }
                else if (grid_v.size() == 2) {
                    job.set("grid", grid_v);
                }
                else {
                    error("Regrid: key 'GRID' expects two values (as list) or a non-empty string");
                }
            }
            return;
        }

        if (mode == "lambert-azimuthal-equal-area") {
            std::string grid;
            grid = "gridType=lambert_azimuthal_equal_area";

            grid += first_point_keys(param, "first-point", "latitudeOfFirstGridPointInDegrees",
                                     "longitudeOfFirstGridPointInDegrees");
            grid += mandatory_keys(param, "dx-in-metres", "dy-in-metres", "grid");
            grid += mandatory_key(param, "Nx", "Ni");
            grid += mandatory_key(param, "Ny", "Nj");
            grid += optional_key(param, "shape-of-the-earth", "shapeOfTheEarth");
            grid += optional_key(param, "radius", "radius");
            grid += optional_key(param, "earth-major-axis", "earthMajorAxis");
            grid += optional_key(param, "earth-minor-axis", "earthMinorAxis");

            grid += mandatory_key(param, "standard-parallel-in-degrees", "standardParallelInDegrees");
            grid += mandatory_key(param, "central-longitude-in-degrees", "centralLongitudeInDegrees");

            job.set("grid", grid);
            return;
        }

        if (mode == "lambert-conformal") {
            std::string grid;
            grid = "gridType=lambert";

            grid += first_point_keys(param, "first-point", "latitudeOfFirstGridPointInDegrees",
                                     "longitudeOfFirstGridPointInDegrees");
            grid += mandatory_keys(param, "dx-in-metres", "dy-in-metres", "grid");
            grid += mandatory_key(param, "Nx", "Ni");
            grid += mandatory_key(param, "Ny", "Nj");
            grid += optional_key(param, "shape-of-the-earth", "shapeOfTheEarth");
            grid += optional_key(param, "radius", "radius");
            grid += optional_key(param, "earth-major-axis", "earthMajorAxis");
            grid += optional_key(param, "earth-minor-axis", "earthMinorAxis");

            grid += mandatory_key(param, "lad-in-degrees", "LaDInDegrees");
            grid += mandatory_key(param, "lov-in-degrees", "LoVInDegrees");
            grid += optional_key(param, "latin-1-in-degrees", "Latin1InDegrees");
            grid += optional_key(param, "latin-2-in-degrees", "Latin2InDegrees");
            grid += optional_key(param, "write-lad-in-degrees", "writeLaDInDegrees");
            grid += optional_key(param, "write-lon-positive", "writeLonPositive");

            job.set("grid", grid);
            return;
        }

        if (mode == "template") {
            auto src = get_path(param, "TEMPLATE_SOURCE", "TEMPLATE_DATA");
            if (src.empty()) {
                error("Regrid: 'TEMPLATE_SOURCE' or 'TEMPLATE_DATA' must be defined");
                return;
            }

            std::unique_ptr<mir::input::MIRInput> input(mir::input::MIRInputFactory::build(src, param));
            ASSERT(input->next());

            job.representationFrom(*input);
            return;
        }

        if (mode == "filter") {
            std::unique_ptr<mir::input::MIRInput> input(mir::input::MIRInputFactory::build(job.source(), param));
            ASSERT(input->next());

            job.set("filter", true);
            job.representationFrom(*input);
            return;
        }

        error("Regrid: key '" + sane_key(name()) + "' unrecognized value '" + mode + "'");
    }
};


struct SetupWindProcessing final : Param
{
    using Param::Param;

    void eval(const RequestWrapper& param, JobWrapper& job) const override
    {
        std::string wind;
        if (param.get(name(), wind) && !wind.empty()) {
            wind = sane_value(wind);

            if (wind == "uv-to-uv") {
                job.set("uv2uv", true);
                return;
            }

            if (wind == "vod-to-uv") {
                job.set("vod2uv", true);
                return;
            }

            error("Regrid: key '" + sane_key(name()) + "' unrecognized value '" + wind + "'");
        }
    }
};


}  // namespace


//--------------------------------------------------------


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


    // * Initialize variables from user interface
    // * input is 1) from "SOURCE", or 2) from "DATA" (only getting "PATH", nothing fancy)
    // * output is a temporary GRIB file

    const RequestWrapper args(in);

    // set logging
    mir::Log::debug().reset();
    eckit::Log::debug<eckit::LibEcKit>().reset();

    mir::Log::info().setStream(std::cout);
    mir::Log::warning().setStream(std::cerr);
    mir::Log::error().setStream(std::cerr);

    std::string debug;
    if (args.get("debug", debug) && !debug.empty() ? eckit::Translator<std::string, bool>()(debug)
                                                   : defaults_t::debug) {
        mir::Log::debug().setStream(std::cout);
        eckit::Log::debug<eckit::LibEcKit>().setStream(std::cout);
    }

    static const std::vector<Param*> parameters{
        new SetupInput(*this),
        new SetupOutput(*this),
        new SetupWindProcessing(*this, "wind-processing"),
        new ParamT<double>(*this, "climate-filter-delta"),
        new ParamArea(*this, "area"),
        new ParamVectorT<double>(*this, "rotation", 2),
        new ParamT<std::string>(*this, "truncation"),
        new ParamT<std::string>(*this, "intgrid"),
        new ParamT<std::string>(*this, "spectral-order"),
        new ParamVectorT<long>(*this, "pl"),
        new ParamT<std::string>(*this, "bitmap"),
        new ParamT<int>(*this, "frame"),
        new ParamT<std::string>(*this, "nabla"),
        new ParamT<bool>(*this, "nabla-poles-missing-values"),
        new ParamT<std::string>(*this, "interpolation"),
        new ParamT<std::string>(*this, "interpolation-statistics"),
        new ParamT<std::string>(*this, "l2-projection-input-method"),
        new ParamT<std::string>(*this, "l2-projection-output-method"),
        new ParamT<std::string>(*this, "nearest-method"),
        new ParamT<std::string>(*this, "distance-weighting"),
        new ParamT<std::string>(*this, "distance-weighting-with-lsm"),
        new ParamT<int>(*this, "nclosest"),
        new ParamT<double>(*this, "distance"),
        new ParamT<double>(*this, "distance-weighting-gaussian-stddev"),
        new ParamT<double>(*this, "distance-weighting-shepard-power"),
        new ParamT<double>(*this, "distance-tolerance"),
        new ParamT<double>(*this, "climate-filter-delta"),
        new ParamT<std::string>(*this, "non-linear"),
        new ParamT<double>(*this, "simulated-missing-value"),
        new ParamT<double>(*this, "simulated-missing-value-epsilon"),
        new ParamT<bool>(*this, "lsm"),
        new ParamT<std::string>(*this, "lsm-interpolation-input"),
        new ParamT<std::string>(*this, "lsm-selection-input"),
        new ParamT<std::string>(*this, "lsm-named-input"),
        new ParamT<std::string>(*this, "lsm-file-input"),
        new ParamT<double>(*this, "lsm-value-threshold-input"),
        new ParamT<std::string>(*this, "lsm-interpolation-output"),
        new ParamT<std::string>(*this, "lsm-selection-output"),
        new ParamT<std::string>(*this, "lsm-named-output"),
        new ParamT<std::string>(*this, "lsm-file-output"),
        new ParamT<double>(*this, "lsm-value-threshold-output"),
        new ParamT<bool>(*this, "pre-globalise"),
        new ParamT<bool>(*this, "globalise"),
        new ParamT<long>(*this, "accuracy"),
        new ParamT<std::string>(*this, "packing"),
        new ParamT<int>(*this, "edition"),
        new ParamT<bool>(*this, "delete-local-definition"),
        new ParamT<std::string>(*this, "input-statistics"),
        new ParamT<std::string>(*this, "output-statistics"),
        new ParamT<int>(*this, "precision"),
        new ParamT<std::string>(*this, "vector-space"),
        new ParamT<std::string>(*this, "matrix-loader"),
        new ParamT<std::string>(*this, "legendre-loader"),
        new ParamT<std::string>(*this, "point-search-trees"),
        new ParamT<bool>(*this, "atlas-trans-flt"),
        new ParamT<std::string>(*this, "atlas-trans-type"),
        new ParamT<std::string>(*this, "plan"),
        new ParamT<std::string>(*this, "plan-script"),
        new ParamMultiDimensional(*this, "multi-dimensional"),
    };

    JobWrapper job;
    ASSERT(getError() == 0);

    for (const auto* p : parameters) {
        p->eval(args, job);
        if (getError() != 0) {
            mir::Log::error() << "Regrid: error in setup, cannot continue" << std::endl;
            return;
        }
    }

    // check setup of both input & output
    ASSERT(!job.source().empty() && !job.target().empty());

    // additional, non-automatic logic
    std::string nabla;
    if (job.get("nabla", nabla) && nabla.length() > 2 && !job.has("vod2uv")) {
        if (nabla.substr(0, 3) == "uv-") {
            job.set("uv2uv", true);
        }
    }

    if (job.has("plan") || job.has("plan-script")) {
        job.set("style", "custom");
    }

    // linear algebra dense/sparse backends
    // If we want to control the backend, we can move that to MIRJob
    std::string backend;
    if (args.get("dense-backend", backend) || args.get("backend", backend)) {
        if (eckit::linalg::LinearAlgebra::hasDenseBackend(backend)) {
            eckit::linalg::LinearAlgebra::denseBackend(backend);
        }
    }
    if (args.get("sparse-backend", backend) || args.get("backend", backend)) {
        if (eckit::linalg::LinearAlgebra::hasSparseBackend(backend)) {
            eckit::linalg::LinearAlgebra::sparseBackend(backend);
        }
    }


    //----------


    std::unique_ptr<mir::output::MIROutput> output(mir::output::MIROutputFactory::build(job.target(), args));
    ASSERT(output);

    std::unique_ptr<mir::input::MIRInput> input(mir::input::MIRInputFactory::build(job.source(), job));
    ASSERT(input);

    process(job, *input, *output, "field");


    // Set output
    out.setVerb("GRIB");
    out("PATH") = job.target().c_str();
    out("TEMPORARY") = job.target_is_temporary() ? 1 : 0;


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


void Regrid::process(const mir::api::MIRJob& job, mir::input::MIRInput& input, mir::output::MIROutput& output,
                     const std::string& what)
{
    mir::trace::ResourceUsage usage("regrid");
    mir::trace::Timer timer("Total time");

    mir::util::MIRStatistics statistics;
    mir::Log::debug() << "Using '" << eckit::linalg::LinearAlgebra::backend() << std::endl;

    job.mirToolCall(mir::Log::debug());

    size_t i = 0;
    while (input.next()) {
        mir::Log::debug() << "============> " << what << ": " << (++i) << std::endl;
        job.execute(input, output, statistics);
    }

    statistics.report(mir::Log::info());

    mir::Log::info() << i << " " << what << "(s) in " << timer.elapsedSeconds()
                     << ", rate: " << double(i) / double(timer.elapsed()) << " " << what << "/s" << std::endl;
}


//--------------------------------------------------------


int main(int argc, char** argv)
{
    MvApplication theApp(argc, argv, "regrid");
    Regrid perc("REGRID");

    // The applications don't try to read or write from pool, this
    // should not be done with the new PlotMod.
    // a.addModeService("GRIB", "DATA");
    // c.saveToPool(false);
    // perc.saveToPool(false);

    theApp.run();
}
