#include "feature_processor.h"
#include "validate.h"

#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/configs/editor/attrdef.h>
#include <maps/libs/log8/include/log8.h>
#include <boost/algorithm/string.hpp>
#include <boost/regex.hpp>

#include <array>

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

namespace maps {
namespace wiki {
namespace importer {

namespace {

const std::string BLD_COMPLEX_LAYER_NAME = "bld_complex";
const std::string BUSINESS_ENTRANCE_LAYER_NAME = "business_entrance";
const std::string FT_TYPE_ID = "ft_type_id";

const char ATTRIBUTE_SEPARATOR = '_';
const std::string DELETE_ATTR_VALUE = "DELETE";
const std::string DELETE_MASTERS_VALUE = "DELETE";
const std::string DELETE_MASTERS_PREFIX = "DELETE:";

const std::string ADD_NAME_REGEX_PATTERN = "nm_([[:digit:]])_([-a-zA-Z]+)(_l)?"; //example: nm_0_ru_l
const std::string DELETE_NAME_REGEX_PATTERN = "d_nm_([[:digit:]])_([-a-zA-Z]+)"; //example: d_nm_0_ru
const std::string MULTIVALUE_DELIMITER = "|";

std::string roleIdFromIndexStr(const std::string& roleIndexStr)
{
    REQUIRE(roleIndexStr.size() == 1, "Wrong role index " << roleIndexStr);
    int64_t roleIndex = roleIndexStr[0] - '0';
    REQUIRE(roleIndex >=0 && static_cast<size_t>(roleIndex) < NAME_ROLE_IDS.size(), "Wrong role index" << roleIndexStr);
    return NAME_ROLE_IDS[roleIndex];
}

std::string rebuildAllowedMultiValue(
    const std::string& valuesStr,
    const configs::editor::AttributeDef& def)
{
    if (!def.multiValue()) {
        return def.allowedValue(valuesStr);
    }
    auto values = common::split(valuesStr, MULTIVALUE_DELIMITER);
    for (auto& value : values) {
        value = def.allowedValue(value);
    }
    return common::join(values, MULTIVALUE_DELIMITER);
}

} // namespace

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

FeatureProcessor::FeatureProcessor(const EditorConfig& editorConfig, const cfg::Category& category, const RubricsMatcher& rubricsMatcher)
    : editorConfig_(editorConfig)
    , category_(category)
    , rubricsMatcher_(rubricsMatcher)
{
}

Objects FeatureProcessor::processGeometry(ObjectPtr& object, const OGRGeometry* geometry) const
{
    if (geometryProcessor_) {
        return geometryProcessor_(object, geometry);
    }
    return { object };
}

void FeatureProcessor::processField(
    ObjectPtr& object,
    const std::string& fieldName,
    const std::string& value) const
{
    auto it = fieldProcessors_.find(fieldName);
    if (it != fieldProcessors_.end()) {
        it->second(object, value);
    }
}

void FeatureProcessor::setGeometryProcessor(const GeometryProcessor& processor)
{
    geometryProcessor_ = processor;
}

bool FeatureProcessor::hasFieldProcessor(const std::string& fieldName) const
{
    return fieldProcessors_.find(fieldName) != fieldProcessors_.end();
}

void FeatureProcessor::setFieldProcessor(
    const std::string& fieldName,
    const FeatureProcessor::FieldProcessor& processor)
{
    fieldProcessors_[fieldName] = processor;
}

void FeatureProcessor::addMasterIndoorLevel(
    ObjectPtr& object,
    const std::string& role,
    ID masterIndoorLevelId) const
{
    object->addFutureMaster(role, std::move(masterIndoorLevelId));
}

void FeatureProcessor::addNormalField(
    const std::string& fieldName,
    const std::string& attributeId,
    OGRFieldType fieldType)
{
    const auto& attribute = category_.attribute(attributeId);
    validateFieldType(attribute, fieldType);

    setFieldProcessor(fieldName,
        [&attribute](ObjectPtr& object, const std::string& value) {
            if (value == DELETE_ATTR_VALUE) {
                object->deleteAttribute(attribute->id());
            } else {
                object->addAttribute(
                    attribute->id(),
                    rebuildAllowedMultiValue(value, *attribute));
            }
        });
}

void FeatureProcessor::addMasterField(
    const std::string& fieldName,
    const std::string& roleId,
    const cfg::Category& masterCategory)
{
    setFieldProcessor(fieldName,
        [roleId, &masterCategory](ObjectPtr& object, const std::string& value) {
            if (value == DELETE_MASTERS_VALUE) {
                object->deleteMaster(roleId);
            } else if (value.starts_with(DELETE_MASTERS_PREFIX)) {
                auto masterIdValue = value.substr(DELETE_MASTERS_PREFIX.length());
                auto masterDbId = cast<TObjectId>(masterIdValue);
                REQUIRE(masterDbId != EMPTY_DB_ID, "Empty master db id");
                object->deleteMaster(roleId, masterDbId);
            } else {
                auto masterDbId = cast<TObjectId>(value);
                REQUIRE(masterDbId != EMPTY_DB_ID, "Empty master db id");
                object->addMaster(roleId, masterDbId, masterCategory);
            }
        });
}

void FeatureProcessor::addFutureMasterField(
    const std::string& fieldName,
    const std::string& roleId,
    const std::string& masterLayer)
{
    setFieldProcessor(fieldName,
        [roleId, masterLayer](ObjectPtr& object, const std::string& value) {
            object->addFutureMaster(
                roleId,
                ID{masterLayer, cast<TFeatureId>(value)});
        });
}

void FeatureProcessor::addDbIdField(
    const std::string& fieldName)
{
    setFieldProcessor(fieldName,
        [](ObjectPtr& object, const std::string& value) {
            object->setDbId(cast<TObjectId>(value), ObjectState::Existing);
        });
}

void FeatureProcessor::addTempBldComplexField(
    const std::string& fieldName)
{
    const auto& bldComplexCategory = editorConfig_.category(cat::BLD_COMPLEX);

    setFieldProcessor(fieldName,
        [&bldComplexCategory](ObjectPtr& object, const std::string& value) {
            auto complexTempId = cast<TFeatureId>(value);
            if (complexTempId == 0) {
                return;
            }
            auto master = object->cache().getOrCreateObject(
                {BLD_COMPLEX_LAYER_NAME, complexTempId}, bldComplexCategory);
            object->addMaster(role::PART, master);
        });
}

void FeatureProcessor::addIndoorRubricField(
    const std::string& fieldName,
    const std::string& attrName) // ex: poi:business_rubric_id
{
    const auto& rubricsMatcher = rubricsMatcher_;
    const auto& category = category_;

    setFieldProcessor(fieldName,
        [fieldName, attrName, &rubricsMatcher, &category](ObjectPtr& object, const std::string& value) {
              auto ftType = rubricsMatcher.getFtType(fieldName, value);
              REQUIRE(ftType, "Undefined rubric value: " + value + "for type: " + fieldName);
              object->addAttribute(category.id() + ":" + FT_TYPE_ID, *ftType);
              if (category.isAttributeDefined(attrName)) {
                object->addAttribute(attrName, value);
              }
        });
}

void FeatureProcessor::addMasterPoiIdsField(
    const std::string& fieldName)
{
    const auto& poiCategory = editorConfig_.category(fieldName);

    setFieldProcessor(fieldName,
        [&poiCategory](ObjectPtr& object, const std::string& value) {
            auto poiIdsStr = common::split(value, IDS_DELIM);
            for (const auto& poiIdStr : poiIdsStr) {
                auto poiId = cast<TFeatureId>(poiIdStr);
                if (poiId == 0) {
                    continue;
                }
                object->addMaster(role::ENTRANCE_ASSIGNED, poiId, poiCategory);
            }
        });
}

bool FeatureProcessor::tryAddNameField(
    const std::string& fieldName,
    OGRFieldType fieldType)
{
    boost::regex pattern(ADD_NAME_REGEX_PATTERN);
    boost::smatch match;
    if (!boost::regex_match(fieldName, match, pattern)) {
        return false;
    }

    REQUIRE(match.size() == 4, "Wrong pattern");

    auto roleId = roleIdFromIndexStr(match[1]);
    std::string lang = match[2];
    bool local = !match[3].str().empty();

    const auto& role = category_.slaveRole(roleId);
    const auto& slaveCategory = editorConfig_.category(role.categoryId());

    const auto& nameAttribute = slaveCategory.attribute(slaveCategory.id() + ":name");
    validateFieldType(nameAttribute, fieldType);

    const auto& langAttribute = slaveCategory.attribute(slaveCategory.id() + ":lang");

    setFieldProcessor(fieldName,
        [&role, &slaveCategory, &nameAttribute, &langAttribute, lang, local](ObjectPtr& object, const std::string& value) {
            auto& slave = object->addSlave(role.roleId(), slaveCategory).relatedObject;
            slave->addAttribute(nameAttribute->id(), nameAttribute->allowedValue(value));
            slave->addAttribute(langAttribute->id(), langAttribute->allowedValue(lang));
            if (local) {
                slave->addAttribute(slaveCategory.id() + ":is_local", "1");
            }
        });

    return true;
}

bool FeatureProcessor::tryDeleteNameField(
    const std::string& fieldName,
    OGRFieldType /*fieldType*/)
{
    boost::regex pattern(DELETE_NAME_REGEX_PATTERN);
    boost::smatch match;
    if (!boost::regex_match(fieldName, match, pattern)) {
        return false;
    }

    REQUIRE(match.size() == 3, "Wrong pattern");

    auto roleId = roleIdFromIndexStr(match[1]);
    std::string lang = match[2];

    const auto& role = category_.slaveRole(roleId);

    setFieldProcessor(fieldName,
        [&role, lang, fieldName](ObjectPtr& object, const std::string& value) {
            REQUIRE(value == "all", "Value of the field '" << fieldName << "' must be 'all'");
            object->deleteSlave(DeleteRelation{role.roleId(), lang});
        });

    return true;
}

bool FeatureProcessor::tryParseRelation(
    const std::string& fieldName)
{
    auto sepPos = fieldName.find(ATTRIBUTE_SEPARATOR);
    while (sepPos != std::string::npos && !hasFieldProcessor(fieldName)) {
        auto tryRoleId = fieldName.substr(0, sepPos);
        auto tryMasterCategoryId = fieldName.substr(sepPos + 1);
        if (editorConfig_.hasCategory(tryMasterCategoryId)) {
            const auto& masterCategory = editorConfig_.category(tryMasterCategoryId);
            if (masterCategory.isSlaveRoleDefined(tryRoleId)) {
                addMasterField(fieldName, tryRoleId, masterCategory);
            }
        }

        sepPos = fieldName.find(ATTRIBUTE_SEPARATOR, sepPos + 1);
    }

    return hasFieldProcessor(fieldName);
}

} // importer
} // wiki
} // maps
