#include "names.h"

#include "config.h"

#include <yandex/maps/wiki/configs/editor/attrdef.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/externals.h>
#include <yandex/maps/wiki/configs/editor/magic_strings.h>

#include <maps/libs/log8/include/log8.h>

#include <algorithm>
#include <map>

using namespace std::literals;

namespace name_attr_suffix = maps::wiki::configs::editor::name_attr_suffix;

namespace maps::wiki::merge_poi {
namespace {
const std::set<std::pair<std::string, std::string>> CONTRADICTING_LANGS
{
    {"sr-latn"s, "hr"s},
};

const std::set<std::string> PROTECTED_LANGS
{
    "sr-latn",
    "kk-latn",
};

namespace tds {
const auto TRUE = "1"s;
const auto FALSE = ""s;
} // namespace tds

std::string toLower(const std::string& text)
{
    return to_lower(TString(text));
}

std::string toNMapsLang(const std::string& text)
{
    return toLower(text);
}

bool isProtectedLang(const std::string& lang)
{
    return PROTECTED_LANGS.count(toLower(lang));
}

std::unordered_map<std::string, std::string> createObjectName(
    const std::string& nmAttr,
    NameType nameType,
    IsLocal isLocal,
    Lang lang,
    std::string text)
{
    std::unordered_map<std::string, std::string> name;
    name.emplace(nmAttr + name_attr_suffix::NAME_TYPE,
        boost::lexical_cast<std::string>(nameType));
    name.emplace(nmAttr + name_attr_suffix::LANG, std::move(lang));
    name.emplace(nmAttr + name_attr_suffix::NAME, std::move(text));
    name.emplace(nmAttr + name_attr_suffix::IS_LOCAL,
        isLocal == IsLocal::Yes
            ? tds::TRUE
            : tds::FALSE);
    return name;
}

Name fromObjectNameMap(
    const std::unordered_map<std::string, std::string>& nameMap,
    const std::string& nameAttr)
{
    auto itName = nameMap.find(nameAttr + name_attr_suffix::NAME);
    auto itLang = nameMap.find(nameAttr + name_attr_suffix::LANG);
    auto itNameType = nameMap.find(nameAttr + name_attr_suffix::NAME_TYPE);
    auto itIsLocal = nameMap.find(nameAttr + name_attr_suffix::IS_LOCAL);
    ASSERT(itName != nameMap.end());
    ASSERT(itLang != nameMap.end());
    ASSERT(itNameType != nameMap.end());
    ASSERT(itIsLocal != nameMap.end());
    return {
        boost::lexical_cast<NameType>(itNameType->second),
        itIsLocal->second == tds::TRUE
            ? IsLocal::Yes
            : IsLocal::No,
        itLang->second,
        itName->second
    };
}

bool isRubricName(const Name& name, const EditorCfg& editorCfg)
{
    return maps::wiki::merge_poi::isRubricName(
        {name.lang, name.text},
        editorCfg);
}

bool hasName(const std::vector<Name>& names, const Name& other)
{
    return std::any_of(names.begin(), names.end(),
            [&](const auto& name) {
                return name.text == other.text &&
                    name.lang == other.lang &&
                    name.nameType == other.nameType;
            });
}

} // namespace

Names::Names(const poi_feed::FeedObjectData& poi)
{
    for (const auto& poiName : poi.names()) {
        names_.push_back({
            NameType::Official,
            std::nullopt,
            toNMapsLang(poiName.lang),
            poiName.value});
    }

    for (const auto& poiShortName : poi.shortNames()) {
        names_.push_back({
            NameType::RenderLabel,//Altay's short names are nmaps labels for rendering
            std::nullopt,
            toNMapsLang(poiShortName.lang),
            poiShortName.value});
    }
}

Names::Names(const editor_client::BasicEditorObject& poi, const EditorCfg& editorCfg)
{
    const auto nameAttr = guessNameAttr(poi, editorCfg);
    if (nameAttr.empty()) {
        return;
    }
    const auto it = poi.tableAttributes.find(nameAttr);
    if (it == poi.tableAttributes.end()) {
        return;
    }
    const auto& nameRows = it->second;
    names_.reserve(nameRows.size());
    for (const auto& nameRow : nameRows) {
        names_.emplace_back(fromObjectNameMap(nameRow, nameAttr));
    }
}

Names::Names(std::vector<Name> names)
    : names_(std::move(names))
{
}

std::unordered_set<Lang>
Names::localLangs() const
{
    std::unordered_set<Lang> langs;
    for (const auto& name : names_) {
        if (name.isLocal == IsLocal::Yes) {
            langs.insert(name.lang);
        }
    }
    return langs;
}

std::unordered_set<Lang>
Names::langs() const
{
    std::unordered_set<Lang> langs;
    for (const auto& name : names_) {
        langs.insert(name.lang);
    }
    return langs;
}

void
Names::writeTo(
    editor_client::BasicEditorObject& poi,
    const EditorCfg& editorCfg) const
{
    const auto nameAttr = guessNameAttr(poi, editorCfg);
    auto& tableAttributes = poi.tableAttributes;
    tableAttributes.erase(nameAttr);
    for (const auto& name : names_) {
        IsLocal isLocal = IsLocal::No;
        if (name.isLocal) {
            isLocal = *name.isLocal;
        }
        tableAttributes[nameAttr].push_back(
            createObjectName(nameAttr, name.nameType, isLocal, name.lang, name.text));
    }
}

bool
isContradictingLangs(const std::string& lang1, const std::string& lang2)
{
    const auto lower1 = toLower(lang1);
    const auto lower2 = toLower(lang2);
    return
        CONTRADICTING_LANGS.count({lower1, lower2}) ||
        CONTRADICTING_LANGS.count({lower2, lower1});
}

Names
Names::fixContradictingNamesLangs(const Names& patchNames) const
{
    std::vector<Name> fixed;
    fixed.reserve(patchNames.size());
    for (auto patchName : patchNames.names_) {
        auto objNameIt =
            std::find_if(names_.begin(), names_.end(),
                [&](const auto& objName) {
                   return
                        objName.text == patchName.text &&
                        isContradictingLangs(objName.lang, patchName.lang) &&
                        !patchNames.langs().count(objName.lang);
                });
        if (objNameIt != names_.end()) {
            patchName.lang = objNameIt->lang;
        }
        fixed.push_back(patchName);
    }
    return Names(fixed);
}

std::set<Names::UpdateResult>
Names::update(const Names& patchNames, const EditorCfg& editorCfg)
{
    if (patchNames.empty()) {
        return {};
    }
    const auto oldLangs = langs();
    if (std::any_of(oldLangs.begin(), oldLangs.end(), isProtectedLang)) {
        return {};
    }
    const auto fixedPatchNames = fixContradictingNamesLangs(patchNames);
    std::set<NameType> patchNameTypes;
    bool newNames = false;
    for (const auto& patchName : fixedPatchNames.names_) {
        patchNameTypes.insert(patchName.nameType);
        if (patchName.nameType == NameType::Official) {
            patchNameTypes.insert(NameType::RenderLabel);
        }
        newNames = newNames || !hasName(names_, patchName);
    }
    if (!newNames) {
        return {};
    }
    const auto oldLocalLangs = localLangs();
    const auto oldNames = names_;
    names_.erase(
        std::remove_if(names_.begin(), names_.end(),
            [&](const Name& name) {
                return patchNameTypes.count(name.nameType);
            }),
        names_.end());
    bool hasRubrics = false;
    for (const auto& patchName : fixedPatchNames.names_) {
        hasRubrics = hasRubrics ||
            (!hasName(oldNames, patchName) &&
                isRubricName(patchName, editorCfg));
        names_.push_back({
            patchName.nameType,
            patchName.isLocal
                ? *patchName.isLocal
                : oldLocalLangs.count(patchName.lang)
                    ? IsLocal::Yes
                    : IsLocal::No,
            patchName.lang,
            patchName.text});
    }
    std::set<Names::UpdateResult> updateResult;
    updateResult.insert(UpdateResult::Updated);
    if (hasRubrics) {
        updateResult.insert(UpdateResult::RubricName);
    }
    if (langs().size() < oldLangs.size()) {
        updateResult.insert(UpdateResult::LangLost);
    }
    return updateResult;
}

bool
isRubricName(const common::LocalizedString& name, const EditorCfg& editorCfg)
{
    return !editorCfg.externals().rubrics().rubrics(name).empty();
}

std::string
guessNameAttr(
    const editor_client::BasicEditorObject& poi,
    const EditorCfg& editorCfg)
{
    REQUIRE(editorCfg.categories().defined(poi.categoryId),
        "Category " << poi.categoryId << " not defined. Object: " << poi.id);
    const auto& category = editorCfg.categories()[poi.categoryId];
    for (const auto& attrDef : category.attrDefs()) {
        if (attrDef->id().ends_with(name_attr_suffix::NM)) {
            return attrDef->id();
        }
    }
    WARN()
        << "Category: " << poi.categoryId
        << " Object: " << poi.id
        << " No name attribute";
    return {};
}

} // maps::wiki::merge_poi
