/*
 * (C) Copyright 1996- ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation nor
 * does it submit to any jurisdiction.
 */

/// @author Baudouin Raoult
/// @author Tiago Quintino
/// @author Simon Smart

/// @date Febuary 2019

#ifndef metkit_ParamID_H
#define metkit_ParamID_H

#include <algorithm>
#include <set>
#include <string>
#include <vector>

#include "eckit/config/Resource.h"
#include "eckit/filesystem/PathName.h"
#include "eckit/log/Log.h"
#include "eckit/runtime/Metrics.h"
#include "eckit/types/Types.h"

#include "metkit/config/LibMetkit.h"
#include "metkit/mars/Param.h"

namespace metkit {

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

enum class NormalisationMode {
    Strict,  // Exact match + wind conversion
    Loose,   // Strict + partial match (dropping tables defined in param-matching.yaml)
    FullTableDropping
};

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

class ParamID {

public:  // types

    struct WindFamily {
        Param u_;
        Param v_;
        Param vo_;
        Param d_;

        WindFamily(const std::string& u, const std::string& v, const std::string& vo, const std::string& d) :
            u_(u), v_(v), vo_(vo), d_(d) {}
    };

public:  // methods

    template <typename REQUEST_T, typename AXIS_T>
    static void normalise(const REQUEST_T& r, std::vector<Param>& req, const AXIS_T& axis, bool& windConversion,
                          NormalisationMode mode = ParamID::normalisationMode());

    template <typename REQUEST_T, typename AXIS_T>
    [[deprecated]] static void normalise(const REQUEST_T& r, std::vector<Param>& req, const AXIS_T& axis,
                                         bool& windConversion, bool fullTableDropping, bool forceGRIBParamID = false);

    static const std::vector<WindFamily>& getWindFamilies();
    static const std::vector<size_t>& getDropTables();
    static NormalisationMode normalisationMode();
    [[deprecated]] static bool fullTableDropping() {
        return normalisationMode() == NormalisationMode::FullTableDropping;
    }
    static const std::set<std::string>& getMlParamsSingleLevel();
};

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

inline long replaceTable(size_t table, long paramid) {
    return (table * 1000 + paramid % 1000);
}

template <typename REQUEST_T, typename AXIS_T>
void ParamID::normalise(const REQUEST_T& request, std::vector<Param>& req, const AXIS_T& axis, bool& windConversion,
                        bool fullTableDropping, bool forceGRIBParamID) {

    normalise(request, req, axis, windConversion,
              forceGRIBParamID ? NormalisationMode::Strict
                               : (fullTableDropping ? NormalisationMode::FullTableDropping : NormalisationMode::Loose));
}

template <typename REQUEST_T, typename AXIS_T>
void ParamID::normalise(const REQUEST_T& request, std::vector<Param>& req, const AXIS_T& axis, bool& windConversion,
                        NormalisationMode mode) {

    static const bool useGRIBParamID = eckit::Resource<bool>("useGRIBParamID", false);
    bool strict                      = useGRIBParamID || mode == NormalisationMode::Strict;

    const std::vector<WindFamily>& windFamilies(getWindFamilies());

    std::vector<std::pair<Param, Param>> tableDropped;
    std::set<Param> inAxis;
    std::map<long, Param> inAxisParamID;
    std::set<Param> wind;


    for (typename AXIS_T::const_iterator j = axis.begin(); j != axis.end(); ++j) {
        Param p = *j;
        inAxis.emplace(p);
        inAxisParamID[p.paramId()] = p;
    }

    std::vector<Param> newreq;
    newreq.reserve(req.size());

    for (const auto& r : req) {
        if (inAxis.find(r) != inAxis.end()) {  // Perfect match - look no further
            newreq.push_back(r);
        }
        else {  // r is normalised to ParamID
            long paramid   = r.paramId();
            const auto& ap = inAxisParamID.find(paramid);

            // ParamID representation matching - look no further
            if (ap != inAxisParamID.end()) {
                newreq.push_back(ap->second);
            }
            else {  // Special case for U/V - exact match
                bool ok = false;
                for (const auto& wf : windFamilies) {
                    if ((paramid == wf.u_.paramId() || paramid == wf.v_.paramId() ||
                         (!strict && (paramid == wf.u_.grib1value() || paramid == wf.v_.grib1value()))) &&
                        inAxis.find(wf.vo_) != inAxis.end() && inAxis.find(wf.d_) != inAxis.end()) {

                        if (paramid == wf.u_.paramId() || (!strict && paramid == wf.u_.grib1value()))
                            newreq.push_back(wf.u_);
                        else
                            newreq.push_back(wf.v_);

                        wind.emplace(wf.vo_);
                        wind.emplace(wf.d_);
                        windConversion = true;

                        ok = true;
                        break;
                    }
                }
                // Partial match (only it table has not been specified by user)
                if (!ok && !strict && r.table() == 0 && paramid < 1000) {
                    const std::vector<size_t>& dropTables = ParamID::getDropTables();
                    for (auto t : dropTables) {
                        auto ap = inAxisParamID.find(replaceTable(t, paramid));
                        if (ap != inAxisParamID.end()) {  // ParamID representation matching - look no further
                            newreq.push_back(ap->second);
                            ok = true;
                            break;
                        }
                    }

                    if (!ok) {  // Special case for U/V - partial match
                        for (const auto& wf : windFamilies) {
                            if (paramid == wf.u_.paramId() || paramid == wf.v_.paramId()) {
                                for (auto t : dropTables) {
                                    auto vo = inAxisParamID.find(replaceTable(t, wf.vo_.paramId()));
                                    auto d  = inAxisParamID.find(replaceTable(t, wf.d_.paramId()));

                                    if (vo != inAxisParamID.end() && d != inAxisParamID.end()) {
                                        bool grib1 = vo->second.table() > 0;
                                        if (paramid == wf.u_.paramId())
                                            newreq.push_back(grib1 ? Param(t, paramid)
                                                                   : Param(0, replaceTable(t, paramid)));
                                        else
                                            newreq.push_back(grib1 ? Param(t, paramid)
                                                                   : Param(0, replaceTable(t, paramid)));

                                        wind.emplace(vo->second);
                                        wind.emplace(d->second);
                                        windConversion = true;

                                        ok = true;
                                        break;
                                    }
                                }
                                if (ok)
                                    break;
                            }
                        }
                    }
                    if (mode == NormalisationMode::FullTableDropping && !ok) {
                        // Backward compatibility - Partial match (drop completely table information)
                        for (const auto& ap : inAxisParamID) {
                            if (ap.first % 1000 == paramid % 1000) {
                                newreq.push_back(ap.second);
                                ok = true;
                                tableDropped.push_back(std::make_pair(r, ap.second));
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
    req = newreq;

    for (const auto& w : wind) {
        bool exist = false;
        for (eckit::Ordinal i = 0; i < req.size(); i++)
            if (req[i] == w) {
                exist = true;
                break;
            }
        if (!exist) {
            req.push_back(w);
        }
    }
    if (tableDropped.size() > 0) {
        eckit::MetricsPrefix prefix("paramid_normalisation");
        {
            std::ostringstream oss;
            oss << request;
            eckit::Metrics::set("user_request", oss.str());
        }
        {
            std::ostringstream oss;
            oss << tableDropped;
            eckit::Metrics::set("params", oss.str());
        }
    }
}

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

}  // namespace metkit

#endif
