#include "object_labeler.h"

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

namespace maps::wiki::diffalert {

namespace {

const std::string EMPTY_NAME;

const std::string CATEGORY_ADDR = "addr";
const std::string ROLE_OFFICIAL = "official";

const std::string ATTR_LANG = "lang";
const std::string ATTR_NAME = "name";
const std::string ATTR_IS_LOCAL = "is_local";

const std::string LANG_RU = "ru";
const std::string LANG_EN = "en";

const std::string SUFFIX_FT_TYPE_ID = ":ft_type_id";

} // namespace

DiffLabeler::DiffLabeler(const configs::editor::ConfigHolder& editorConfig)
    : editorConfig_(editorConfig)
    , oldObjectLabeler_(editorConfig)
    , newObjectLabeler_(editorConfig)
{
}

std::pair<std::string, HasOwnName> DiffLabeler::nameForObject(const LongtaskDiffContext& diffContext)
{
    if (diffContext.newObject()) {
        auto newName = newObjectLabeler_.nameForObject(diffContext.newSnapshot(), *diffContext.newObject());
        if (!newName.empty()) {
            return std::make_pair(newName, HasOwnName::Yes);
        }
    }

    if (diffContext.oldObject()) {
        auto oldName = oldObjectLabeler_.nameForObject(diffContext.oldSnapshot(), *diffContext.oldObject());
        if (!oldName.empty()) {
            return std::make_pair(oldName, HasOwnName::Yes);
        }
    }

    ///This piece of code is gracefully taken from the editor
    std::string attrId = diffContext.categoryId() + SUFFIX_FT_TYPE_ID;
    if (editorConfig_.isAttributeDefined(attrId)) {
        const auto& attrDef = editorConfig_.attribute(attrId);
        ASSERT(attrDef);

        const auto& object = !!diffContext.oldObject()
            ? *diffContext.oldObject()
            : *diffContext.newObject();

        auto value = object.attr(attrDef->id()).value();

        auto it = std::find(attrDef->values().begin(), attrDef->values().end(), value);
        if (it != attrDef->values().end() && !it->label.empty()) {
            return std::make_pair(it->label, HasOwnName::No);
        }
        return std::make_pair(value, HasOwnName::No);
    }

    ASSERT(editorConfig_.categories().defined(diffContext.categoryId()));
    return std::make_pair(editorConfig_.categories()[diffContext.categoryId()].label(), HasOwnName::No);
}

//=================================================================

ObjectLabeler::ObjectLabeler(const configs::editor::ConfigHolder& editorConfig)
{
    for (const auto& pair : editorConfig.categories()) {
        const auto& category = pair.second;
        if (category.isSlaveRoleDefined(ROLE_OFFICIAL)) {
            categoriesWithDirectNames_.insert(category.id());
        }
    }

    std::unordered_set<std::string> processedCategoryIds;
    std::unordered_set<std::string> categoriesToProcess = categoriesWithDirectNames_;
    while (!categoriesToProcess.empty()) {
        std::unordered_set<std::string> newCategoriesToProcess;

        for (const auto& categoryId : categoriesToProcess) {
            if (processedCategoryIds.count(categoryId)) {
                continue;
            }
            const auto& category = editorConfig.categories()[categoryId];
            for (const auto& slaveRole : category.slavesRoles()) {
                if (!slaveRole.tableRow()) {
                    categoryToMasterRoles_[slaveRole.categoryId()].insert(slaveRole.roleId());
                    newCategoriesToProcess.insert(slaveRole.categoryId());
                }
            }
            processedCategoryIds.insert(categoryId);
        }

        std::swap(newCategoriesToProcess, categoriesToProcess);
    }
}

std::string ObjectLabeler::nameForObject(LongtaskSnapshot& snapshot, Object& object)
{
    std::shared_lock<std::shared_mutex> readLock(cacheMutex_);
    auto it = nameCache_.find(object.id());
    if (it != nameCache_.end()) {
        return it->second;
    }
    if (noNameCache_.count(object.id())) {
        return EMPTY_NAME;
    }
    readLock.unlock();

    auto name = loadName(snapshot, object);

    std::unique_lock<std::shared_mutex> writeLock(cacheMutex_);
    if (name.empty()) {
        noNameCache_.insert(object.id());
    } else {
        nameCache_.emplace(object.id(), name);
    }
    return name;
}

std::string ObjectLabeler::loadName(LongtaskSnapshot& snapshot, Object& object)
{
    if (object.categoryId() == CATEGORY_ADDR) {
        return loadAddrName(snapshot, object);
    }

    if (categoriesWithDirectNames_.count(object.categoryId())) {
        return loadDirectName(snapshot, object);
    }

    auto it = categoryToMasterRoles_.find(object.categoryId());
    if (it == categoryToMasterRoles_.end()) {
        return EMPTY_NAME;
    }

    return loadIndirectName(snapshot, object, it->second);
}

std::string ObjectLabeler::loadAddrName(LongtaskSnapshot& snapshot, Object& object)
{
    auto addrName = loadDirectName(snapshot, object);
    if (addrName.empty()) {
        return EMPTY_NAME;
    }

    auto it = categoryToMasterRoles_.find(CATEGORY_ADDR);
    ASSERT(it != categoryToMasterRoles_.end());

    auto masterName = loadIndirectName(snapshot, object, it->second);
    if (masterName.empty()) {
        return EMPTY_NAME;
    }

    return masterName + ", " + addrName;
}

std::string ObjectLabeler::loadDirectName(LongtaskSnapshot& snapshot, Object& object)
{
    TIds nameIds;
    for (const auto& rel : object.loadSlaveRelations()) {
        if (rel.role == ROLE_OFFICIAL) {
            nameIds.insert(rel.slaveId);
        }
    }

    std::string nameRu;
    std::string nameEn;
    std::string nameLocal;

    auto nameObjects = snapshot.objectsByIds(nameIds);
    for (const auto& nameObject : nameObjects) {
        if (nameObject->attr(ATTR_LANG).value() == LANG_RU) {
            nameRu = nameObject->attr(ATTR_NAME).value();
        } else if (nameObject->attr(ATTR_LANG).value() == LANG_EN) {
            nameEn = nameObject->attr(ATTR_NAME).value();
        } else if (nameObject->attr(ATTR_IS_LOCAL)) {
            nameLocal = nameObject->attr(ATTR_NAME).value();
        }
    }

    if (!nameRu.empty()) {
        return nameRu;
    } else if (!nameEn.empty()) {
        return nameEn;
    } else if (!nameLocal.empty()) {
        return nameLocal;
    }
    return EMPTY_NAME;
}

std::string ObjectLabeler::loadIndirectName(LongtaskSnapshot& snapshot, Object& object, const MasterRoleIds& masterRoleIds)
{
    TIds masterIds;
    for (const auto& rel : object.loadMasterRelations()) {
        if (masterRoleIds.count(rel.role)) {
            masterIds.insert(rel.masterId);
        }
    }

    auto masterObjects = snapshot.objectsByIds(masterIds);
    for (const auto& masterObject : masterObjects) {
        auto name = nameForObject(snapshot, *masterObject);
        if (!name.empty()) {
            return name;
        }
    }
    return EMPTY_NAME;
}

} // maps::wiki::diffalert
