/*
 * (C) Copyright 2024- 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.
 */


#include "eccodes/geo/GribToSpec.h"

#include <algorithm>
#include <map>
#include <cstring>
#include <functional>
#include <initializer_list>
#include <memory>
#include <ostream>
#include <sstream>
#include <vector>

#include "eckit/geo/Exceptions.h"
#include "eckit/geo/PointLonLat.h"
#include "eckit/geo/util/mutex.h"
#include "eckit/log/JSON.h"
#include "eckit/types/FloatCompare.h"
#include "eckit/types/Fraction.h"
#include "eckit/utils/SafeCasts.h"

#include "eccodes/geo/eckit.h"
#include "eccodes/grib_api_internal.h"


namespace eckit::geo::util
{
const std::vector<double>& gaussian_latitudes(size_t N, bool increasing);
}


namespace eccodes::geo
{


namespace
{


struct Condition
{
    Condition() = default;

    Condition(const Condition&)            = delete;
    Condition(Condition&&)                 = delete;
    Condition& operator=(const Condition&) = delete;
    Condition& operator=(Condition&&)      = delete;

    virtual ~Condition()                   = default;
    virtual bool eval(codes_handle*) const = 0;
};


template <class T>
struct ConditionT : Condition
{
    const char* key_;
    T value_;
    bool eval(codes_handle* /*unused*/) const override;

public:
    ConditionT(const char* key, const T& value) :
        key_(key), value_(value) {}
};


template <>
bool ConditionT<long>::eval(codes_handle* h) const
{
    ASSERT(h != nullptr);

    long value = 0;
    int err    = codes_get_long(h, key_, &value);

    if (err == CODES_NOT_FOUND) {
        return false;
    }

    CHECK_ERROR(err, key_);
    return value_ == value;
}


template <>
bool ConditionT<double>::eval(codes_handle* h) const
{
    ASSERT(h != nullptr);

    double value = 0;
    int err      = codes_get_double(h, key_, &value);

    if (err == CODES_NOT_FOUND) {
        return false;
    }

    CHECK_ERROR(err, key_);
    return value_ == value;  // Want an epsilon?
}


template <>
bool ConditionT<std::string>::eval(codes_handle* h) const
{
    ASSERT(h != nullptr);

    char buffer[10240];
    size_t size = sizeof(buffer);
    int err     = codes_get_string(h, key_, buffer, &size);

    if (err == CODES_NOT_FOUND) {
        return false;
    }

    CHECK_ERROR(err, key_);
    return value_ == buffer;
}


struct ConditionOR : Condition
{
    const std::unique_ptr<const Condition> left_;
    const std::unique_ptr<const Condition> right_;
    bool eval(codes_handle* h) const override { return left_->eval(h) || right_->eval(h); }

    ConditionOR(const Condition* left, const Condition* right) :
        left_(left), right_(right) {}
};


struct ConditionAND : Condition
{
    const std::unique_ptr<const Condition> left_;
    const std::unique_ptr<const Condition> right_;
    bool eval(codes_handle* h) const override { return left_->eval(h) && right_->eval(h); }

    ConditionAND(const Condition* left, const Condition* right) :
        left_(left), right_(right) {}
};


/*
class ConditionNOT : public Condition {
    const Condition* c_;
    bool eval(codes_handle* h) const  override { return !c_->eval(h); }
    ~ConditionNOT() override {
        delete c_;
    }

public:
    ConditionNOT(const Condition* c) : c_(c) {}
};
*/


void wrongly_encoded_grib_error(const std::string& msg)
{
    grib_context_log(nullptr, GRIB_LOG_ERROR, "%s", msg.c_str());
    throw eckit::geo::exception::GridError(msg, Here());
}


void wrongly_encoded_grib(const std::string& msg)
{
    if (static bool do_abort = codes_getenv("ECCODES_GRIB_GEO_STRICT") != nullptr; do_abort) {
        wrongly_encoded_grib_error(msg);
    }

    grib_context_log(nullptr, GRIB_LOG_WARNING, "%s", msg.c_str());
}


template <class T>
Condition* is(const char* key, const T& value)
{
    return new ConditionT<T>(key, value);
}


Condition* is(const char* key, const char* value)
{
    return new ConditionT<std::string>(key, value);
}


Condition* _and(const Condition* left, const Condition* right)
{
    return new ConditionAND(left, right);
}


Condition* _or(const Condition* left, const Condition* right)
{
    return new ConditionOR(left, right);
}


/*
 Condition *_not(const Condition *c) {
    return new ConditionNOT(c);
}
*/


Condition* is_gaussian()
{
    const auto* key = "gridType";
    return _or(is(key, "reduced_gg"),
               _or(is(key, "regular_gg"),
                   _or(is(key, "reduced_rotated_gg"),
                       _or(is(key, "regular_rotated_gg"),
                           _or(is(key, "rotated_gg"),
                               _or(is(key, "reduced_stretched_gg"),
                                   _or(is(key, "regular_stretched_gg"),
                                       _or(is(key, "reduced_stretched_rotated_gg"),
                                           _or(is(key, "regular_stretched_rotated_gg"),
                                               _or(is(key, "stretched_gg"), is(key, "stretched_rotated_gg")))))))))));
}


const char* get_key(const std::string& name, codes_handle* h)
{
    struct P
    {
        const std::string name;
        const char* key;
        const std::unique_ptr<const Condition> condition;
        P(const std::string& _name, const char* _key, const Condition* _condition = nullptr) :
            name(_name), key(_key), condition(_condition) {}
    };

    static const std::initializer_list<P> mappings{
        { "type", "gridType" },

        { "west_east_increment", "iDirectionIncrementInDegrees_fix_for_periodic_regular_grids",
          is("gridType", "regular_ll") },
        { "west_east_increment", "iDirectionIncrementInDegrees" },
        { "south_north_increment", "jDirectionIncrementInDegrees" },

        { "west", "longitudeOfLastGridPointInDegrees", is("iScansNegatively", 1L) },
        { "east", "longitudeOfFirstGridPointInDegrees", is("iScansNegatively", 1L) },
        { "west", "longitudeOfFirstGridPointInDegrees" },
        { "east", "longitudeOfLastGridPointInDegrees_fix_for_global_reduced_grids", is("gridType", "reduced_gg") },
        { "east", "longitudeOfLastGridPointInDegrees" },

        { "north", "latitudeOfFirstGridPointInDegrees_fix_for_gaussian_grids",
          _and(is("scanningMode", 0L), is_gaussian()) },
        { "south", "latitudeOfLastGridPointInDegrees_fix_for_gaussian_grids",
          _and(is("scanningMode", 0L), is_gaussian()) },
        { "north", "latitudeOfLastGridPointInDegrees_fix_for_gaussian_grids",
          _and(is("scanningMode", 1L), is_gaussian()) },
        { "south", "latitudeOfFirstGridPointInDegrees_fix_for_gaussian_grids",
          _and(is("scanningMode", 1L), is_gaussian()) },

        { "north", "latitudeOfLastGridPointInDegrees", is("jScansPositively", 1L) },
        { "south", "latitudeOfFirstGridPointInDegrees", is("jScansPositively", 1L) },
        { "north", "latitudeOfFirstGridPointInDegrees", is("scanningMode", 0L) },
        { "south", "latitudeOfLastGridPointInDegrees", is("scanningMode", 0L) },

        { "north", "latitudeOfLastGridPointInDegrees", is("jScansPositively", 1L) },
        { "south", "latitudeOfFirstGridPointInDegrees", is("jScansPositively", 1L) },
        { "north", "latitudeOfFirstGridPointInDegrees" },
        { "south", "latitudeOfLastGridPointInDegrees" },

        { "reference_lat", "latitudeOfFirstGridPointInDegrees" },
        { "reference_lon", "longitudeOfFirstGridPointInDegrees" },

        { "truncation", "pentagonalResolutionParameterJ" },  // Assumes triangular truncation
        { "accuracy", "bitsPerValue" },

        { "south_pole_latitude", "latitudeOfSouthernPoleInDegrees" },
        { "south_pole_longitude", "longitudeOfSouthernPoleInDegrees" },
        { "south_pole_rotation_angle", "angleOfRotationInDegrees" },

        { "proj", "projTargetString" },
        { "projSource", "projSourceString" },

        { "first_lat",
          "latitudeOfFirstGridPointInDegrees" },
        { "first_lon",
          "longitudeOfFirstGridPointInDegrees" },

        { "radius", "radiusInMetres" },
        { "semi_major_axis", "earthMajorAxisInMetres" },
        { "semi_minor_axis", "earthMinorAxisInMetres" },

        { "lat_0",
          "LaDInDegrees",
          _or(is("gridType", "lambert"), is("gridType", "lambert_lam")) },
        { "lon_0",
          "LoVInDegrees",
          _or(is("gridType", "lambert"), is("gridType", "lambert_lam")) },
        { "lat_1",
          "Latin1InDegrees",
          _or(is("gridType", "albers"), _or(is("gridType", "lambert"), is("gridType", "lambert_lam"))) },
        { "lat_2",
          "Latin2InDegrees",
          _or(is("gridType", "albers"), _or(is("gridType", "lambert"), is("gridType", "lambert_lam"))) },

        // This will be just called for has()
        {
            "gridded",
            "Nx",
            _or(_or(_or(is("gridType", "polar_stereographic"), is("gridType", "lambert_azimuthal_equal_area")),
                    is("gridType", "lambert")),
                is("gridType", "space_view")),
        },
        {
            "gridded",
            "Ni",
            is("gridType", "triangular_grid"),
        },
        {
            "gridded",
            "numberOfGridInReference" /*just a dummy*/,
            is("gridType", "unstructured_grid"),
        },
        { "gridded", "numberOfPointsAlongAMeridian" },  // Is that always true?
        { "gridded_regular_ll", "Ni", _or(is("gridType", "regular_ll"), is("gridType", "rotated_ll")) },
        { "gridded_named", "gridName" },

        { "grid", "gridName",
          _or(_or(_or(_or(is("gridType", "regular_gg"), is("gridType", "reduced_gg")), is("gridType", "rotated_gg")),
                  is("gridType", "reduced_rotated_gg")),
              is("gridType", "unstructured_grid")) },

        { "spectral", "pentagonalResolutionParameterJ" },

        { "uid", "uuidOfHGrid", is("gridType", "unstructured_grid") },
        { "order", "orderingConvention", is("gridType", "healpix") },

        /// FIXME: Find something that does no clash
        { "reduced", "numberOfParallelsBetweenAPoleAndTheEquator", is("isOctahedral", 0L) },
        { "regular", "N", is("gridType", "regular_gg") },
        { "octahedral", "numberOfParallelsBetweenAPoleAndTheEquator", is("isOctahedral", 1L) },

        /// TODO: is that a good idea?
        { "param", "paramId" },
        { "statistics", "" },  // (avoid ecCodes error "statistics: Function not yet implemented")
    };

    for (const auto& m : mappings) {
        if (name == m.name) {
            if (!m.condition || m.condition->eval(h)) {
                return m.key;
            }
        }
    }

    const auto* key = name.c_str();
    return key;
}


std::string get_string(codes_handle* h, const char* key)
{
    if (codes_is_defined(h, key) != 0) {
        char buffer[64] = {
            0,
        };
        size_t size = sizeof(buffer);

        CHECK_CALL(codes_get_string(h, key, buffer, &size));
        ASSERT(size < sizeof(buffer) - 1);

        if (std::strcmp(buffer, "MISSING") != 0) {
            return buffer;
        }
    }

    return "";
}


bool get_bool(codes_handle* h, const char* key)
{
    long l = 0;
    CHECK_CALL(codes_get_long(h, key, &l));

    return l != 0;
}


template <typename T>
struct ProcessingT
{
    using fun_t = std::function<bool(codes_handle*, T&)>;
    fun_t fun_;
    explicit ProcessingT(fun_t&& fun) :
        fun_(fun) {}
    ~ProcessingT()                     = default;
    ProcessingT(const ProcessingT&)    = delete;
    ProcessingT(ProcessingT&&)         = delete;
    void operator=(const ProcessingT&) = delete;
    void operator=(ProcessingT&&)      = delete;
    bool eval(codes_handle* h, T& v) const { return fun_(h, v); }
};


bool codes_is_well_defined(codes_handle* h, const char* key)
{
    int err = CODES_SUCCESS;
    return (codes_is_defined(h, key) != 0) && (codes_is_missing(h, key, &err) == 0) && (err == CODES_SUCCESS);
}


ProcessingT<double>* angular_precision()
{
    return new ProcessingT<double>([](codes_handle* h, double& value) {
        if (codes_is_well_defined(h, "basicAngleOfTheInitialProductionDomain") && codes_is_well_defined(h, "subdivisionsOfBasicAngle")) {
            value = 0.;
            return true;
        }

        long angleSubdivisions = 0;
        CHECK_CALL(codes_get_long(h, "angleSubdivisions", &angleSubdivisions));

        value = angleSubdivisions > 0 ? 1. / static_cast<double>(angleSubdivisions) : 0.;
        return true;
    });
}


ProcessingT<double>* longitudeOfLastGridPointInDegrees_fix_for_global_reduced_grids()
{
    return new ProcessingT<double>([](codes_handle* h, double& Lon2) {
        Lon2 = 0;
        CHECK_CALL(codes_get_double(h, "longitudeOfLastGridPointInDegrees", &Lon2));

        if (codes_is_defined(h, "pl") != 0) {
            double Lon1 = 0;
            CHECK_CALL(codes_get_double(h, "longitudeOfFirstGridPointInDegrees", &Lon1));

            if (eckit::types::is_approximately_equal<double>(Lon1, 0)) {
                // get pl array maximum and sum
                // if sum equals values size the grid must be global
                size_t plSize = 0;
                CHECK_CALL(codes_get_size(h, "pl", &plSize));
                ASSERT(plSize > 0);

                std::vector<long> pl(plSize, 0);
                size_t plSizeAsRead = plSize;
                CHECK_CALL(codes_get_long_array(h, "pl", pl.data(), &plSizeAsRead));
                ASSERT(plSize == plSizeAsRead);

                long plMax = 0;
                long plSum = 0;
                for (auto& p : pl) {
                    plSum += p;
                    if (plMax < p) {
                        plMax = p;
                    }
                }
                ASSERT(plMax > 0);

                long numberOfDataPoints = 0;
                CHECK_CALL(codes_get_long(h, "numberOfDataPoints", &numberOfDataPoints));

                if (static_cast<size_t>(plSum) == numberOfDataPoints) {
                    double eps = 0.;
                    ASSERT(std::unique_ptr<ProcessingT<double>>(angular_precision())->eval(h, eps));

                    eckit::Fraction Lon2_expected(360L * (plMax - 1L), plMax);

                    if (!eckit::types::is_approximately_greater_or_equal<double>(Lon2, Lon2_expected, eps)) {
                        std::ostringstream msgs;
                        msgs.precision(32);
                        msgs << "GribToSpec: wrongly encoded longitudeOfLastGridPointInDegrees:"
                             << "\n"
                                "encoded:  "
                             << Lon2
                             << "\n"
                                "expected: "
                             << static_cast<double>(Lon2_expected) << " (" << Lon2_expected << " +- " << eps << ")";

                        wrongly_encoded_grib(msgs.str());
                    }

                    Lon2 = 360.;
                }
            }
        }

        return true;
    });
}


ProcessingT<double>* latitudeOfFirstGridPointInDegrees_fix_for_gaussian_grids()
{
    return new ProcessingT<double>([](codes_handle* h, double& lat) {
        double eps = 0.;
        ASSERT(std::unique_ptr<ProcessingT<double>>(angular_precision())->eval(h, eps));

        long N = 0L;
        CHECK_CALL(codes_get_long(h, "N", &N));
        ASSERT(N > 0);

        double& Lat1 = lat;
        CHECK_CALL(codes_get_double(h, "latitudeOfFirstGridPointInDegrees", &Lat1));

        double Lat2 = 0.;
        CHECK_CALL(codes_get_double(h, "latitudeOfLastGridPointInDegrees", &Lat2));

        const auto snap = eckit::geo::util::gaussian_latitudes(N, Lat1 < Lat2).front();
        if (eckit::types::is_approximately_equal(Lat1, snap, eps)) {
            lat = snap < 0. ? eckit::geo::SOUTH_POLE.lat() : eckit::geo::NORTH_POLE.lat();
        }

        return true;
    });
}


ProcessingT<double>* latitudeOfLastGridPointInDegrees_fix_for_gaussian_grids()
{
    return new ProcessingT<double>([](codes_handle* h, double& lat) {
        double eps = 0.;
        ASSERT(std::unique_ptr<ProcessingT<double>>(angular_precision())->eval(h, eps));

        long N = 0L;
        CHECK_CALL(codes_get_long(h, "N", &N));
        ASSERT(N > 0);

        double Lat1 = 0.;
        CHECK_CALL(codes_get_double(h, "latitudeOfFirstGridPointInDegrees", &Lat1));

        double& Lat2 = lat;
        CHECK_CALL(codes_get_double(h, "latitudeOfLastGridPointInDegrees", &Lat2));

        const auto snap = eckit::geo::util::gaussian_latitudes(N, Lat1 < Lat2).back();
        if (eckit::types::is_approximately_equal(Lat2, snap, eps)) {
            lat = snap < 0. ? eckit::geo::SOUTH_POLE.lat() : eckit::geo::NORTH_POLE.lat();
        }

        return true;
    });
}


ProcessingT<double>* iDirectionIncrementInDegrees_fix_for_periodic_regular_grids()
{
    return new ProcessingT<double>([](codes_handle* h, double& we) {
        long iScansPositively = 0L;
        CHECK_CALL(codes_get_long(h, "iScansPositively", &iScansPositively));
        ASSERT(iScansPositively == 1L);

        CHECK_CALL(codes_get_double(h, "iDirectionIncrementInDegrees", &we));
        ASSERT(we > 0.);

        double Lon1 = 0.;
        double Lon2 = 0.;
        long Ni     = 0;
        CHECK_CALL(codes_get_double(h, "longitudeOfFirstGridPointInDegrees", &Lon1));
        CHECK_CALL(codes_get_double(h, "longitudeOfLastGridPointInDegrees", &Lon2));
        CHECK_CALL(codes_get_long(h, "Ni", &Ni));
        ASSERT(Ni > 0);

        Lon2 = eckit::geo::PointLonLat::normalise_angle_to_minimum(Lon2, Lon1);
        ASSERT(Lon2 >= Lon1);

        // angles are within +-1/2 precision, so (Lon2 - Lon1 + we) uses factor 3*1/2
        double eps = 0.;
        ASSERT(std::unique_ptr<ProcessingT<double>>(angular_precision())->eval(h, eps));
        eps *= 1.5;

        constexpr double GLOBE = 360;

        auto Nid = static_cast<double>(Ni);
        if (eckit::types::is_approximately_equal(Lon2 - Lon1 + we, GLOBE, eps)) {
            we = GLOBE / Nid;
        }
        else if (!eckit::types::is_approximately_equal(Lon1 + (Nid - 1) * we, Lon2, eps)) {
            // TODO refactor, not really specific to "periodic regular grids", but useful
            std::ostringstream msgs;
            msgs.precision(32);
            msgs << "GribToSpec: wrongly encoded iDirectionIncrementInDegrees:"
                    "\n"
                    "encoded: "
                 << we
                 << "\n"
                    "Ni: "
                 << Ni
                 << "\n"
                    "longitudeOfFirstGridPointInDegree: "
                 << Lon1
                 << "\n"
                    "longitudeOfLastGridPointInDegrees: "
                 << Lon2;

            wrongly_encoded_grib(msgs.str());
        }

        return true;
    });
}


ProcessingT<double>* grid_increment(const char* inc_key, const char* incgiven_key,
                                    const char* x0_key, const char* x1_key, const char* n_key, const char* sign_key)
{
    return new ProcessingT<double>([=](codes_handle* h, double& value) {
        bool given = false;
        if (long incgiven = 0; codes_is_well_defined(h, inc_key) && (codes_get_long(h, incgiven_key, &incgiven) == CODES_SUCCESS) && (incgiven != 0)) {
            CHECK_CALL(codes_get_double(h, inc_key, &value));
            given = value != CODES_MISSING_DOUBLE;
        }

        if (codes_is_well_defined(h, x0_key) && codes_is_well_defined(h, x1_key) && codes_is_well_defined(h, n_key) && codes_is_well_defined(h, sign_key)) {
            double x0 = 0.;
            CHECK_CALL(codes_get_double(h, x0_key, &x0));

            double x1 = 0.;
            CHECK_CALL(codes_get_double(h, x1_key, &x1));

            long n = 0;
            CHECK_CALL(codes_get_long(h, n_key, &n));

            if (n == 0) {
                throw eckit::geo::exception::GridError("GribToSpec: number of points is zero, invalid increment", Here());
            }

            if (n == 1) {
                value = 0.;
                return true;
            }

            long sign = 0;
            CHECK_CALL(codes_get_long(h, sign_key, &sign));

            // For longitudes, adjust x1 to respect positive/negative increment
            if (STR_EQUAL(n_key, "Ni") && (sign == 0 ? x0 < x1 : x1 < x0)) {
                x1 += sign == 0 ? -360. : 360.;
            }

            if (auto value_calculated = (x1 - x0) / static_cast<double>(sign != 0 ? (n - 1) : (1 - n)); given) {
                if (!eckit::types::is_approximately_equal(value, value_calculated, 1e-6)) {
                    wrongly_encoded_grib(
                        "GribToSpec: inconsistent increment: '" + std::string{ inc_key } +
                        "'=" + std::to_string(value) + " ~= " + std::to_string(value_calculated) +
                        " (calculated from '" + std::string{ x0_key } + "'=" + std::to_string(x0) +
                        ", '" + std::string{ x1_key } + "'=" + std::to_string(x1) +
                        ", '" + std::string{ n_key } + "'=" + std::to_string(n) +
                        ", '" + std::string{ sign_key } + "'=" + std::to_string(sign) + ")");
                    value = value_calculated;
                }
            }
            else {
                value = value_calculated;
            }

            return true;
        }

        return false;
    });
}


ProcessingT<std::vector<double>>* vector_double(std::initializer_list<std::string> keys)
{
    const std::vector<std::string> keys_(keys);
    return new ProcessingT<std::vector<double>>([=](codes_handle* h, std::vector<double>& values) {
        ASSERT(keys_.size());

        values.assign(keys_.size(), 0);
        size_t i = 0;
        for (const auto& key : keys_) {
            if (codes_is_defined(h, key.c_str()) == 0) {
                return false;
            }
            CHECK_CALL(codes_get_double(h, key.c_str(), &values[i++]));
        }
        return true;
    });
}


ProcessingT<std::string>* order()
{
    return new ProcessingT<std::string>([](codes_handle* h, std::string& value) {
        if (auto gridType = get_string(h, "gridType"); gridType == "healpix") {
            value = get_string(h, "orderingConvention");
            return true;
        }

        // scanningMode
        static const std::string P{ "+" };
        static const std::string M{ "-" };

        const auto ip = get_bool(h, "iScansPositively");
        const auto jp = get_bool(h, "jScansPositively");
        const auto a  = get_bool(h, "alternativeRowScanning");

        value = get_bool(h, "jPointsAreConsecutive") ? "j" + (jp ? P : M) + (!a ? "" : (jp ? M : P)) + "i" + (ip ? P : M)
                                                     : "i" + (ip ? P : M) + (!a ? "" : (ip ? M : P)) + "j" + (jp ? P : M);

        return true;
    });
}


ProcessingT<std::string>* packing()
{
    return new ProcessingT<std::string>([](codes_handle* h, std::string& value) {
        auto packingType = get_string(h, "packingType");
        for (const auto& prefix : std::vector<std::string>{ "grid_", "spectral_" }) {
            if (packingType.find(prefix) == 0) {
                value = packingType.substr(prefix.size());
                std::replace(value.begin(), value.end(), '_', '-');
                return true;
            }
        }

        return false;
    });
}


template <class T>
struct ConditionedProcessingT
{
    const std::string name;
    const std::unique_ptr<const T> processing;
    const std::unique_ptr<const Condition> condition;
    ConditionedProcessingT(const std::string& _name, const T* _processing, const Condition* _condition = nullptr) :
        name(_name), processing(_processing), condition(_condition) {}
};


template <class T>
using ProcessingList = std::initializer_list<ConditionedProcessingT<ProcessingT<T>>>;


template <typename T>
bool get_value(const std::string& name, codes_handle* h, T& value, const ProcessingList<T>& process)
{
    for (auto& p : process) {
        if (name == p.name) {
            if (!p.condition || p.condition->eval(h)) {
                ASSERT(p.processing);
                return p.processing->eval(h, value);
            }
        }
    }
    return false;
}


eckit::geo::util::recursive_mutex MUTEX;


class lock_type
{
    eckit::geo::util::lock_guard<eckit::geo::util::recursive_mutex> lock_guard_{ MUTEX };
};


template <typename T>
bool cache_get(const GribToSpec::cache_type& cache, const std::string& name, T& value)
{
    if (auto it = cache.find(name); it != cache.end()) {
        if (std::holds_alternative<T>(it->second)) {
            value = std::get<T>(it->second);
            return true;
        }
    }
    return false;
}


template <typename T>
void cache_set(GribToSpec::cache_type& cache, const std::string& name, const T& value)
{
    cache.insert_or_assign(name, eckit::spec::Custom::value_type{ std::in_place_type<T>, value });
}


}  // namespace


GribToSpec::GribToSpec(codes_handle* h) :
    handle_(h)
{
    ASSERT(handle_ != nullptr);

    if (eckit_geo_use_grib_fixes(h)) {
        using fixes_type = std::map<std::string, const cache_type>;
        static const fixes_type FIXES{

            // gridName=N640, edition=2
            { "51ea7dcd62e71c9707157a2d15247593", { { "longitudeOfLastGridPointInDegrees", 359.859375 } } },

            // gridName=O2560, edition=1, experimentVersionNumber=h5xa/h5zi
            { "8a6f6c4cc9ad3f64546773b87566bc72", { { "latitudeOfFirstGridPointInDegrees", 89.973 }, { "latitudeOfLastGridPointInDegrees", -89.973 } } },

            // gridName=O640, edition=1, experimentVersionNumber=h5wk/h6en/hc9k
            { "f5dc74ec36353f4c83f7de3bf46e1aef", { { "latitudeOfFirstGridPointInDegrees", 89.892 }, { "latitudeOfLastGridPointInDegrees", -89.892 } } },

            // gridType=regular_ll, edition=2, centre=egrr (Arakawa C-grid UM)
            { "026edb6c52792bc15957072536dbe7c2", { { "longitudeOfLastGridPointInDegrees", 359.0625 } } },                                                   // N96, T
            { "379bbee20b78c58b9e86e1377c14a3da", { { "longitudeOfLastGridPointInDegrees", 358.125 } } },                                                    // N96, U
            { "2e0f685a14d8ad3615dbc7252d5ebbb9", { { "longitudeOfLastGridPointInDegrees", 359.0625 } } },                                                   // N96, V
            { "fd45c3ec374370a77222de303aa00a23", { { "latitudeOfFirstGridPointInDegrees", -89.8125 }, { "latitudeOfLastGridPointInDegrees", 89.8125 } } },  // N320, T
        };

        char buffer[34];
        auto size = sizeof(buffer);
        CHECK_CALL(codes_get_string(handle_, "md5GridSection", buffer, &size));

        const std::string md5GridSection(buffer);
        ASSERT(md5GridSection.size() == 32);

        if (const auto& fix = FIXES.find(md5GridSection); fix != FIXES.end()) {
            grib_context_log(nullptr, GRIB_LOG_WARNING, "GribToSpec: applying fix for md5GridSection=%s", md5GridSection.c_str());
            cache_ = fix->second;
        }
    };

    std::string gridType;
    ASSERT(get("gridType", gridType));

    if (gridType == "healpix") {
        // ECC-2162
        double l = 0;
        ASSERT(get("longitudeOfFirstGridPointInDegrees", l));

        if (!eckit::types::is_approximately_equal(l, 45.)) {
            wrongly_encoded_grib("GribToSpec: gridType=" + gridType + ", longitudeOfFirstGridPointInDegrees should be 45.");
        }
    }

    // ECC-1642:
    // - Validate regular grid consistency
    // - Validate Ni*Nj == numberOfDataPoints for grids with both Ni and Nj

    long Ni = 0;
    long Nj = 0;
    long Nv = 0;
    int err = 0;

    if (
        !(codes_is_missing(handle_, "Ni", &err) != 0 && err == CODES_SUCCESS) && //
        !(codes_is_missing(handle_, "Nj", &err) != 0 && err == CODES_SUCCESS) && //
        codes_get_long(handle_, "Ni", &Ni) == CODES_SUCCESS && //
        codes_get_long(handle_, "Nj", &Nj) == CODES_SUCCESS && //
        codes_get_long(handle_, "numberOfDataPoints", &Nv) == CODES_SUCCESS)
    {
        if (Ni * Nj != Nv) {
            wrongly_encoded_grib_error(
                    "GribToSpec: Ni*Nj!=numberOfDataPoints (" +
                    std::to_string(Ni) + "*" + std::to_string(Nj) + "!=" +
                    std::to_string(Nv) + ")");
        }
    }
}


bool GribToSpec::has(const std::string& name) const
{
    lock_type lock;

    const auto* key = get_key(name, handle_);
    if (key == nullptr || std::strlen(key) == 0) {
        return false;
    }

    return cache_.find(key) != cache_.end() || codes_is_defined(handle_, key) != 0;
}


bool GribToSpec::get(const std::string& name, std::string& value) const
{
    lock_type lock;

    const auto* key = get_key(name, handle_);

    ASSERT(key != nullptr);
    if (std::strlen(key) == 0) {
        return false;
    }

    if (cache_get(cache_, key, value)) {
        return true;
    }

    char buffer[10240];
    size_t size = sizeof(buffer);
    int err     = codes_get_string(handle_, key, buffer, &size);

    if (err == CODES_NOT_FOUND) {
        static const ProcessingList<std::string> process{
            { "order", order() },
            { "packing", packing() },
        };

        return get_value(key, handle_, value, process);
    }

    CHECK_ERROR(err, key);

    ASSERT(size < sizeof(buffer) - 1);

    if (std::strcmp(buffer, "MISSING") == 0) {
        return false;
    }

    value = buffer;
    cache_set(cache_, name, value);
    return true;
}


bool GribToSpec::get(const std::string& name, bool& value) const
{
    lock_type lock;

    const auto* key = get_key(name, handle_);

    ASSERT(key != nullptr);
    if (std::strlen(key) == 0) {
        return false;
    }

    if (cache_get(cache_, key, value)) {
        return true;
    }

    // FIXME: make sure that 'temp' is not set if CODES_MISSING_LONG
    long temp = CODES_MISSING_LONG;
    int err   = codes_get_long(handle_, key, &temp);
    if (err == CODES_NOT_FOUND || codes_is_missing(handle_, key, &err) != 0) {
        return false;
    }

    CHECK_ERROR(err, key);

    value = temp != 0;
    cache_set(cache_, name, value);
    return true;
}


bool GribToSpec::get(const std::string& name, int& value) const
{
    if (long v = 0; get(name, v)) {
        ASSERT(static_cast<long>(static_cast<int>(v)) == v);
        value = static_cast<int>(v);
        return true;
    }

    return false;
}


bool GribToSpec::get(const std::string& name, long& value) const
{
    lock_type lock;

    const std::string key = get_key(name, handle_);
    if (key.empty()) {
        return false;
    }

    if (cache_get(cache_, key, value)) {
        return true;
    }

    // FIXME: make sure that 'value' is not set if CODES_MISSING_LONG
    int err = codes_get_long(handle_, key.c_str(), &value);
    if (err == CODES_NOT_FOUND || codes_is_missing(handle_, key.c_str(), &err) != 0) {
        return false;
    }

    CHECK_ERROR(err, key.c_str());

    cache_set(cache_, name, value);
    return true;
}


bool GribToSpec::get(const std::string& /*name*/, long long& /*value*/) const
{
    return false;
}


bool GribToSpec::get(const std::string& name, std::size_t& value) const
{
    if (cache_get(cache_, name, value)) {
        return true;
    }

    if (long value_long = 0; get(name, value_long)) {
        value = eckit::into_unsigned<size_t>(value_long);
        cache_set(cache_, name, value);
        return true;
    }

    return false;
}


bool GribToSpec::get(const std::string& name, float& value) const
{
    if (cache_get(cache_, name, value)) {
        return true;
    }

    if (double v = 0; get(name, v)) {
        value = static_cast<float>(v);
        cache_set(cache_, name, value);
        return true;
    }

    return false;
}


bool GribToSpec::get(const std::string& name, double& value) const
{
    lock_type lock;

    ASSERT(name != "grid");

    const auto* key = get_key(name, handle_);

    if (key == nullptr || std::strlen(key) == 0) {
        return false;
    }

    if (cache_get(cache_, key, value)) {
        return true;
    }

    // FIXME: make sure that 'value' is not set if CODES_MISSING_DOUBLE
    int err = codes_get_double(handle_, key, &value);
    if (err == CODES_NOT_FOUND || codes_is_missing(handle_, key, &err) != 0) {
        static const ProcessingList<double> process{
            { "angular_precision", angular_precision() },
            { "dlon", grid_increment("iDirectionIncrementInDegrees", "iDirectionIncrementGiven", "longitudeOfFirstGridPointInDegrees", "longitudeOfLastGridPointInDegrees", "Ni", "iScansPositively"), _or(is("gridType", "regular_ll"), is("gridType", "rotated_ll")) },
            { "dlat", grid_increment("jDirectionIncrementInDegrees", "jDirectionIncrementGiven", "latitudeOfFirstGridPointInDegrees", "latitudeOfLastGridPointInDegrees", "Nj", "jScansPositively"), _or(is("gridType", "regular_ll"), is("gridType", "rotated_ll")) },
            { "longitudeOfLastGridPointInDegrees_fix_for_global_reduced_grids",
              longitudeOfLastGridPointInDegrees_fix_for_global_reduced_grids() },
            { "iDirectionIncrementInDegrees_fix_for_periodic_regular_grids",
              iDirectionIncrementInDegrees_fix_for_periodic_regular_grids() },
            { "latitudeOfFirstGridPointInDegrees_fix_for_gaussian_grids",
              latitudeOfFirstGridPointInDegrees_fix_for_gaussian_grids() },
            { "latitudeOfLastGridPointInDegrees_fix_for_gaussian_grids",
              latitudeOfLastGridPointInDegrees_fix_for_gaussian_grids() },
        };

        if (get_value(key, handle_, value, process)) {
            cache_set(cache_, name, value);
            return true;
        }

        return false;
    }

    CHECK_ERROR(err, key);

    cache_set(cache_, name, value);
    return true;
}


bool GribToSpec::get(const std::string& /*name*/, std::vector<int>& /*value*/) const
{
    return false;
}


bool GribToSpec::get(const std::string& name, std::vector<long>& value) const
{
    lock_type lock;

    const auto* key = get_key(name, handle_);

    ASSERT(key != nullptr);
    if (std::strlen(key) == 0) {
        return false;
    }

    if (cache_get(cache_, key, value)) {
        return true;
    }

    size_t count = 0;
    CHECK_CALL(codes_get_size(handle_, key, &count));
    ASSERT(count > 0);

    size_t size = count;
    value.resize(size);
    CHECK_CALL(codes_get_long_array(handle_, key, value.data(), &size));
    ASSERT(count == size);

    // gridType=reduced_ll pl is (assumed) regional, otherwise assumed global
    if (name == "pl" && get_string("gridType") != "reduced_ll") {
        pl_expand_to_global(value);
    }

    cache_set(cache_, name, value);
    return true;
}


bool GribToSpec::get(const std::string& /*name*/, std::vector<long long>& /*value*/) const
{
    return false;
}


bool GribToSpec::get(const std::string& /*name*/, std::vector<std::size_t>& /*value*/) const
{
    return false;
}


bool GribToSpec::get(const std::string& name, std::vector<float>& value) const
{
    if (cache_get(cache_, name, value)) {
        return true;
    }

    if (std::vector<double> v; get(name, v)) {
        value.clear();
        value.reserve(v.size());
        for (const auto& d : v) {
            value.push_back(static_cast<float>(d));
        }

        cache_set(cache_, name, value);
        return true;
    }

    return false;
}


bool GribToSpec::get(const std::string& name, std::vector<double>& value) const
{
    lock_type lock;

    const auto* key = get_key(name, handle_);

    // NOTE: MARS client sets 'grid=vector' (deprecated) which needs to be compared against GRIB gridName
    ASSERT(key != nullptr);
    if (std::strlen(key) == 0 || std::strncmp(key, "gridName", 8) == 0) {
        return false;
    }

    if (cache_get(cache_, key, value)) {
        return true;
    }

    static const ProcessingList<std::vector<double>> process{
        { "grid", vector_double({ "xDirectionGridLengthInMetres", "yDirectionGridLengthInMetres" }),
          is("gridType", "lambert_azimuthal_equal_area") },
        { "grid", vector_double({ "DxInMetres", "DyInMetres" }),
          _or(is("gridType", "lambert"), is("gridType", "polar_stereographic")) },
        { "grid", vector_double({ "DiInMetres", "DjInMetres" }), is("gridType", "mercator") },
        { "grid", vector_double({ "dx", "dy" }), is("gridType", "space_view") },
        { "rotation", vector_double({ "latitudeOfSouthernPoleInDegrees", "longitudeOfSouthernPoleInDegrees" }),
          _or(_or(_or(is("gridType", "rotated_ll"), is("gridType", "rotated_gg")), is("gridType", "rotated_sh")),
              is("gridType", "reduced_rotated_gg")) },
    };

    if (get_value(key, handle_, value, process)) {
        cache_set(cache_, name, value);
        return true;
    }

    // FIXME make logic consistent for ::get(,*)
    if (codes_is_defined(handle_, key) == 0) {
        return false;
    }

    size_t count = 0;
    int err      = codes_get_size(handle_, key, &count);
    CHECK_ERROR(err, key);

    ASSERT(count > 0);
    size_t size = count;

    value.resize(count);

    CHECK_CALL(codes_get_double_array(handle_, key, value.data(), &size));
    ASSERT(count == size);

    ASSERT(!value.empty());

    cache_set(cache_, name, value);
    return true;
}


bool GribToSpec::get(const std::string& /*name*/, std::vector<std::string>& /*value*/) const
{
    return false;
}


void GribToSpec::pl_expand_to_global(std::vector<long>& pl) const
{
    auto N = get_long("N");
    if (pl.size() == 2 * N) {
        return;
    }

    const auto lat1 = get_double("latitudeOfFirstGridPointInDegrees");
    const auto lat2 = get_double("latitudeOfLastGridPointInDegrees");

    const auto increasing = lat1 < lat2;
    const auto& lats(eckit::geo::util::gaussian_latitudes(N, increasing));
    const auto eps = std::max(eckit::geo::PointLonLat::EPS, get_double("angular_precision"));

    auto approx              = [=](double a, double b) { return increasing ? a + eps < b : a - eps > b; };
    const auto j             = std::distance(lats.begin(), std::lower_bound(lats.begin(), lats.end(), lat1, approx));
    const auto lat2_expected = lats[j + pl.size() - 1];
    if (j + pl.size() > 2 * N || !eckit::types::is_approximately_equal<double>(lat2, lat2_expected, eps)) {
        wrongly_encoded_grib_error("GribToSpec: cropped 'pl' doesn't align to global latitudes: start=" + std::to_string(j) + " + #pl=" + std::to_string(pl.size()) + " <= #lats=" + std::to_string(lats.size()));
    }

    std::vector<long> pl_global(2 * N, 0);
    std::copy(pl.begin(), pl.end(), pl_global.begin() + j);
    pl.swap(pl_global);
}

void GribToSpec::json(eckit::JSON& j) const
{
    j.startObject();

    auto* kit = codes_keys_iterator_new(
        handle_, CODES_KEYS_ITERATOR_SKIP_DUPLICATES | CODES_KEYS_ITERATOR_SKIP_READ_ONLY, "geography");
    ASSERT(kit != nullptr);

    for (; codes_keys_iterator_next(kit) != 0;) {
        const auto* name = codes_keys_iterator_get_name(kit);

        int err         = 0;
        bool is_missing = 0 != codes_is_missing(handle_, name, &err);
        CHECK_ERROR(err, name);

        if (is_missing) {
            j << name << "MISSING";
            continue;
        }

        size_t size = 0;
        CHECK_CALL(codes_get_size(handle_, name, &size));

        int type = 0;
        CHECK_CALL(codes_get_native_type(handle_, name, &type));

        if (size > 1) {
            if (type == CODES_TYPE_LONG) {
                std::vector<long> array(size);

                auto size_read = size;
                CHECK_CALL(codes_get_long_array(handle_, name, array.data(), &size_read));
                ASSERT(size == size_read);

                j << name;
                j.startList();
                for (const auto& value : array) {
                    j << value;
                }
                j.endList();
                continue;
            }

            if (type == CODES_TYPE_DOUBLE) {
                std::vector<double> array(size);

                auto size_read = size;
                CHECK_CALL(codes_get_double_array(handle_, name, array.data(), &size_read));
                ASSERT(size == size_read);

                j << name;
                j.startList();
                for (const auto& value : array) {
                    j << value;
                }
                j.endList();
                continue;
            }

            (j << name).startList().endList();
            continue;
        }

        if (type == CODES_TYPE_LONG) {
            long value = 0;
            CHECK_CALL(codes_get_long(handle_, name, &value));
            j << name << value;
            continue;
        }

        if (type == CODES_TYPE_DOUBLE) {
            double value = 0;
            CHECK_CALL(codes_get_double(handle_, name, &value));
            j << name << value;
            continue;
        }

        if (type == CODES_TYPE_STRING) {
            char value[1024] = {
                0,
            };
            size_t length = sizeof(value);
            CHECK_CALL(codes_get_string(handle_, name, value, &length));
            j << name << value;
            continue;
        }

        j << name
          << (type == CODES_TYPE_UNDEFINED ? "UNDEFINED"
              : type == CODES_TYPE_BYTES   ? "BYTES"
              : type == CODES_TYPE_SECTION ? "SECTION"
              : type == CODES_TYPE_LABEL   ? "LABEL"
              : type == CODES_TYPE_MISSING ? "MISSING"
                                           : "?");
    }

    codes_keys_iterator_delete(kit);
    j.endObject();
}


#undef CHECK_ERROR
#undef CHECK_CALL


}  // namespace eccodes::geo
