/*
 * (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.
 */

#include "metkit/mars/MarsLanguage.h"

#include <algorithm>
#include <fstream>
#include <optional>

#include "eckit/config/Resource.h"
#include "eckit/log/Log.h"
#include "eckit/parser/YAMLParser.h"
#include "eckit/types/Types.h"
#include "eckit/utils/StringTools.h"

#include "metkit/config/LibMetkit.h"

#include "metkit/hypercube/HyperCube.h"
#include "metkit/mars/MarsExpansion.h"
#include "metkit/mars/Type.h"
#include "metkit/mars/TypesFactory.h"

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


static pthread_once_t once = PTHREAD_ONCE_INIT;
static eckit::Value languages_;
static std::vector<eckit::Value> modifiers_{};
static std::set<std::string> verbs_;
static std::map<std::string, std::string> verbAliases_;

static void init() {
    languages_ = eckit::YAMLParser::decodeFile(metkit::LibMetkit::languageYamlFile());
    for (const auto& file : metkit::LibMetkit::modifiersYamlFiles()) {
        modifiers_.push_back(eckit::YAMLParser::decodeFile(file));
    }
    const eckit::Value verbs = languages_.keys();
    for (size_t i = 0; i < verbs.size(); ++i) {
        verbs_.insert(verbs[i]);
        eckit::Value lang = languages_[verbs[i]];
        if (lang.contains("_aliases")) {
            eckit::Value aliases = lang["_aliases"];
            ASSERT(aliases.isList());
            for (size_t j = 0; j < aliases.size(); ++j) {
                verbAliases_.emplace(aliases[j], verbs[i]);
            }
        }
    }
}

namespace metkit::mars {

//----------------------------------------------------------------------------------------------------------------------
void MarsLanguage::parseModifier(ModifierType typ, std::shared_ptr<Context> ctx, size_t maxIndex,
                                 const eckit::Value& mod) {
    eckit::Value keys;
    if (typ == ModifierType::UNSET) {
        ASSERT(mod.isList());
        keys = mod;
    }
    else {
        ASSERT(mod.isMap());
        keys = mod.keys();
    }
    for (size_t j = 0; j < keys.size(); ++j) {
        std::string key = keys[j];

        auto it = types_.find(key);
        if (it != types_.end()) {
            ASSERT(!isData(key) || maxIndex <= metkit::hypercube::AxisOrder::instance().index(key));

            if (typ == ModifierType::UNSET) {
                it->second->unset(ctx);
            }
            else {
                eckit::Value vv = mod[key];
                std::vector<std::string> vals;
                if (vv.isList()) {
                    for (size_t k = 0; k < vv.size(); ++k) {
                        vals.push_back(vv[k]);
                    }
                }
                else {
                    vals.push_back(vv);
                }

                if (typ == ModifierType::DEFAULT)
                    it->second->defaults(ctx, vals);
                else if (typ == ModifierType::SET)
                    it->second->set(ctx, vals);
            }
        }
    }
}

MarsLanguage::MarsLanguage(const std::string& verb) {
    pthread_once(&once, init);

    verb_                = MarsLanguage::expandVerb(verb);
    eckit::Value lang    = languages_[verb_];
    eckit::Value params  = lang.keys();
    eckit::Value options = lang["_options"];

    for (size_t i = 0; i < params.size(); ++i) {
        std::string keyword   = params[i];
        eckit::Value settings = lang[keyword];

        if (keyword[0] == '_') {
            continue;
        }

        ASSERT(types_.find(keyword) == types_.end());

        if (options.contains(keyword)) {
            eckit::ValueMap m = options[keyword];
            for (auto j = m.begin(); j != m.end(); ++j) {
                settings[(*j).first] = (*j).second;
            }
        }

        auto [it, success] = types_.emplace(keyword, TypesFactory::build(keyword, settings));
        it->second->attach();
        keywords_.push_back(keyword);

        std::optional<eckit::Value> aliases;
        if (settings.contains("aliases")) {
            aliases = settings["aliases"];
        }
        if (settings.contains("category") && settings["category"] == "data") {
            dataKeywords_.insert(keyword);
            if (aliases) {
                for (size_t j = 0; j < aliases->size(); ++j) {
                    dataKeywords_.insert((*aliases)[j]);
                }
            }
        }
        if (settings.contains("category") && settings["category"] == "postproc") {
            postProcKeywords_.insert(keyword);
            if (aliases) {
                for (auto j = 0; j < aliases->size(); ++j) {
                    postProcKeywords_.insert((*aliases)[j]);
                }
            }
        }
        if (settings.contains("category") && settings["category"] == "sink") {
            sinkKeywords_.insert(keyword);
            if (aliases) {
                for (auto j = 0; j < aliases->size(); ++j) {
                    sinkKeywords_.insert((*aliases)[j]);
                }
            }
        }
        if (aliases) {
            for (size_t j = 0; j < aliases->size(); ++j) {
                aliases_[(*aliases)[j]] = keyword;
                keywords_.push_back((*aliases)[j]);
            }
        }
    }

    // load modifiers and associate to types
    for (const auto& modifierFile : modifiers_) {
        ASSERT(modifierFile.isList());
        for (size_t i = 0; i < modifierFile.size(); ++i) {
            eckit::Value mod = modifierFile[i];
            ASSERT(mod.isMap());
            ASSERT(mod.contains("context"));
            std::shared_ptr<Context> ctx = Context::parseContext(mod["context"]);
            size_t maxIndex              = ctx->maxAxisIndex();
            if (mod.contains("defaults")) {
                parseModifier(ModifierType::DEFAULT, ctx, maxIndex, mod["defaults"]);
            }
            if (mod.contains("set")) {
                parseModifier(ModifierType::SET, ctx, maxIndex, mod["set"]);
            }
            if (mod.contains("unset")) {
                parseModifier(ModifierType::UNSET, ctx, maxIndex, mod["unset"]);
            }
        }
    }

    if (lang.contains("_clear_defaults")) {
        const auto& keywords = lang["_clear_defaults"];
        for (auto i = 0; i < keywords.size(); ++i) {
            if (auto iter = types_.find(keywords[i]); iter != types_.end()) {
                iter->second->clearDefaults();
            }
        }
    }

    for (const std::string& a : hypercube::AxisOrder::instance().axes()) {
        Type* t = nullptr;
        auto it = types_.find(a);
        if (it != types_.end()) {
            t = it->second;
        }
        typesByAxisOrder_.emplace_back(a, t);
    }
    for (auto& [k, t] : types_) {
        if (dataKeywords_.find(k) == dataKeywords_.end()) {
            typesByAxisOrder_.emplace_back(k, t);
        }
    }
}

bool MarsLanguage::isData(const std::string& keyword) const {
    return (dataKeywords_.find(keyword) != dataKeywords_.end());
}

bool MarsLanguage::isPostProc(const std::string& keyword) const {
    return (postProcKeywords_.find(keyword) != postProcKeywords_.end());
}

bool MarsLanguage::isSink(const std::string& keyword) const {
    return (sinkKeywords_.find(keyword) != sinkKeywords_.end());
}
const std::set<std::string>& MarsLanguage::sinkKeywords() const {
    return sinkKeywords_;
}

MarsLanguage::~MarsLanguage() {
    for (auto& [k, t] : types_) {
        t->detach();
    }
}

eckit::PathName MarsLanguage::languageYamlFile() {
    return metkit::LibMetkit::languageYamlFile();
}

void MarsLanguage::reset() {
    for (auto& [k, t] : types_) {
        t->reset();
    }
}

eckit::Value MarsLanguage::jsonFile(const std::string& name) {
    // TODO: cache

    eckit::PathName path = metkit::LibMetkit::configFile(name);

    LOG_DEBUG_LIB(LibMetkit) << "MarsLanguage loading jsonFile " << path << std::endl;

    std::ifstream in(path.asString().c_str());
    if (!in) {
        throw eckit::CantOpenFile(path);
    }

    eckit::YAMLParser parser(in);

    return parser.parse();
}

static bool isnumeric(const std::string& s) {
    for (size_t i = 0; i < s.length(); i++) {
        if (!::isdigit(s[i])) {
            return false;
        }
    }

    return s.length() > 0;
}


std::string MarsLanguage::bestMatch(const std::string& name, const std::vector<std::string>& values, bool fail,
                                    bool quiet, bool fullMatch, const std::map<std::string, std::string>& aliases) {
    size_t score = (fullMatch ? name.length() : 1);
    std::vector<std::string> best;

    for (size_t i = 0; i < values.size(); ++i) {
        const std::string& value = values[i];

        size_t len = std::min(name.length(), value.length());
        size_t s   = 0;

        // Prefix comparison, break if there is a difference
        for (size_t j = 0; j < len; ++j) {
            if (::tolower(name[j]) == ::tolower(value[j])) {
                s++;
            }
            else {
                break;
            }
        }

        // if value and name are the same (in lowercase), look up aliases and return the alias
        // otherwise return the value (which is name)
        if (s == value.length() && s == name.length()) {
            if (aliases.find(value) != aliases.end()) {
                return aliases.find(value)->second;
            }
            return value;
        }

        // If fullmatch == true:
        // only option is s==score, otherwise the break above would have triggered
        // so best is only filled with exact matches
        // If fullmatch == false:
        // score keeps track of the best value found, if a better value (in terms of matching prefixes)
        // is found, clear the best vector and push the new best. If all matches are equally good, keep them in the best
        // vector
        if (s >= score) {
            if (s > score) {
                best.clear();
            }
            best.push_back(value);
            score = s;
        }
    }

    if (!quiet && best.size() > 0) {
        std::cerr << "Matching '" << name << "' with " << best << std::endl;
    }

    static bool strict = eckit::Resource<bool>("$METKIT_LANGUAGE_STRICT_MODE", true);
    if (best.size() == 1) {
        // If the best entry is a number or not name (why is this even needed)
        if (isnumeric(best[0]) && (best[0] != name)) {
            best.clear();
        }
        else {
            if (strict) {
                if (best[0] != name) {
                    std::ostringstream oss;
                    oss << "Cannot match [" << name << "] in " << values;
                    throw eckit::UserError(oss.str());
                }
            }

            if (aliases.find(best[0]) != aliases.end()) {
                return aliases.find(best[0])->second;
            }
            return best[0];
        }
    }

    // In case the match is empty, return or fail, depending on the fail flag
    static std::string empty;
    if (best.empty()) {
        if (!fail) {
            return empty;
        }

        std::ostringstream oss;
        oss << "Cannot match [" << name << "] in " << values;
        throw eckit::UserError(oss.str());
    }

    // Check the existing aliases for mappings of the matches to the canonical parameter name
    // A set is used to de-duplicate the findings
    std::set<std::string> names;
    for (std::vector<std::string>::const_iterator j = best.begin(); j != best.end(); ++j) {
        const auto k = aliases.find(*j);
        if (k == aliases.end()) {
            names.insert(*j);
        }
        else {
            names.insert((*k).second);
        }
    }

    // If there is only on match return the given one
    if (names.size() == 1) {
        if (aliases.find(best[0]) != aliases.end()) {
            return aliases.find(best[0])->second;
        }
        return best[0];
    }

    // Otherwise there is an ambiguity and we want to return or fail (depending on the fail flag)
    if (!fail) {
        return empty;
    }

    std::ostringstream oss;
    oss << "Ambiguous value '" << name << "' could be";

    for (std::vector<std::string>::const_iterator j = best.begin(); j != best.end(); ++j) {
        auto k = aliases.find(*j);
        if (k == aliases.end()) {
            oss << " '" << *j << "'";
        }
        else {
            oss << " '" << *j << "' (";
            oss << (*k).second;
            oss << ")";
        }
    }

    throw eckit::UserError(oss.str());
}

std::string MarsLanguage::expandVerb(const std::string& verb) {
    pthread_once(&once, init);
    std::string v = eckit::StringTools::lower(verb);
    auto vv       = verbs_.find(v);
    if (vv != verbs_.end()) {
        return v;
    }
    auto aa = verbAliases_.find(v);
    if (aa != verbAliases_.end()) {
        return aa->second;
    }

    std::ostringstream oss;
    oss << "Verb " << v << " is not supported";
    throw eckit::UserError(oss.str());
}

class TypeHidden : public Type {
    bool flatten() const override { return false; }
    void print(std::ostream& out) const override { out << "TypeHidden"; }
    bool expand(std::string&, const MarsRequest&) const override { return true; }

public:

    TypeHidden() : Type("hidden", eckit::Value()) { attach(); }
};


Type* MarsLanguage::type(const std::string& name) const {
    auto k = types_.find(name);
    if (k == types_.end()) {
        if (name[0] == '_') {
            static TypeHidden hidden;
            return &hidden;
        }

        throw eckit::SeriousBug("Cannot find a type for '" + name + "'");
    }
    return k->second;
}


MarsRequest MarsLanguage::expand(const MarsRequest& r, bool inherit, bool strict) {
    MarsRequest result(verb_);

    try {
        std::vector<std::pair<std::string, std::string>> sortedParams;
        std::map<std::string, std::string> paramSet;
        std::vector<std::string> params;

        for (const auto& PP : r.params()) {
            auto c = cache_.find(PP);
            if (c != cache_.end()) {
                paramSet.emplace((*c).second, PP);
            }
            else {
                std::string p = eckit::StringTools::lower(PP);
                paramSet.emplace(cache_[p] = bestMatch(p, keywords_, true, false, true, aliases_), PP);
            }
        }

        {  // sort the parameters, following the AxisOrder
            for (const auto& k : metkit::hypercube::AxisOrder::instance().axes()) {
                auto it = paramSet.find(k);
                if (it != paramSet.end()) {
                    sortedParams.emplace_back(k, it->second);
                    paramSet.erase(it);
                }
            }
            for (const auto& [k, PP] : paramSet) {
                sortedParams.emplace_back(k, PP);
            }
        }

        for (const auto& [p, PP] : sortedParams) {
            std::vector<std::string> values = r.values(PP);

            if (values.size() == 1) {
                const std::string& s = eckit::StringTools::lower(values[0]);
                if (s == "off") {
                    result.unsetValues(p);
                    type(p)->reset();
                    continue;
                }
                if (s == "all" && type(p)->multiple()) {
                    result.setValue(p, "all");
                    continue;
                }
            }

            auto t = type(p);
            t->expand(values, result);
            result.setValuesTyped(t, values);
            t->check(values);
        }

        if (inherit) {
            for (const auto& [k, t] : typesByAxisOrder_) {
                if (t != nullptr && result.countValues(k) == 0) {
                    t->setDefaults(result);
                }
            }

            result.getParams(params);
            for (std::vector<std::string>::const_iterator k = params.begin(); k != params.end(); ++k) {
                type(*k)->setInheritance(result.values(*k));
            }
        }

        result.getParams(params);

        for (std::vector<std::string>::const_iterator k = params.begin(); k != params.end(); ++k) {
            type(*k)->pass2(result);
        }

        for (const auto& [k, t] : typesByAxisOrder_) {
            if (t != nullptr)
                t->finalise(result, strict);
        }
    }
    catch (std::exception& e) {
        std::ostringstream oss;
        oss << e.what() << " request=" << r << ", expanded=" << result;
        throw eckit::UserError(oss.str());
    }
    return result;
}


const std::string& MarsLanguage::verb() const {
    return verb_;
}

void MarsLanguage::flatten(const MarsRequest& request, const std::vector<std::string>& params, size_t i,
                           MarsRequest& result, FlattenCallback& callback) {
    if (i == params.size()) {
        callback(result);
        return;
    }

    const std::string& param = params[i];

    Type* t = type(param);
    if (!t->flatten()) {
        flatten(request, params, i + 1, result, callback);
        return;
    }

    const std::vector<std::string>& values = t->flattenValues(request);

    for (std::vector<std::string>::const_iterator j = values.begin(); j != values.end(); ++j) {
        result.setValue(param, *j);
        flatten(request, params, i + 1, result, callback);
    }
}

void MarsLanguage::flatten(const MarsRequest& request, FlattenCallback& callback) {
    std::vector<std::string> params;
    request.getParams(params);

    MarsRequest result(request);
    flatten(request, params, 0, result, callback);
}

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

}  // namespace metkit::mars
