#include "category_traits.h"
#include "object.h"
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/categories_strings.h>
#include <maps/wikimap/mapspro/services/editor/src/factory.h>

#include <maps/wikimap/mapspro/libs/views/include/magic_strings.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/category_groups.h>
#include <yandex/maps/wiki/configs/editor/category_template.h>
#include <yandex/maps/wiki/configs/editor/master_role.h>
#include <yandex/maps/wiki/configs/editor/topology_groups.h>
#include <map>
#include <algorithm>

namespace maps {
namespace wiki {
namespace {

const std::map<std::string, std::string> THREADS_AND_STATIONS
{
    {CATEGORY_TRANSPORT_METRO_THREAD, CATEGORY_TRANSPORT_METRO_STATION},
    {CATEGORY_TRANSPORT_TRAM_THREAD, CATEGORY_TRANSPORT_STOP},
    {CATEGORY_TRANSPORT_WATERWAY_THREAD, CATEGORY_TRANSPORT_WATERWAY_STOP},
    {CATEGORY_TRANSPORT_BUS_THREAD, CATEGORY_TRANSPORT_STOP}
};

const StringSet ROUTES {
    CATEGORY_TRANSPORT_METRO_LINE,
    CATEGORY_TRANSPORT_TRAM_ROUTE,
    CATEGORY_TRANSPORT_WATERWAY_ROUTE,
    CATEGORY_TRANSPORT_BUS_ROUTE
};

const StringSet INDOOR_GROUP_NON_POIS {
    CATEGORY_INDOOR_PLAN,
    CATEGORY_INDOOR_LEVEL,
    CATEGORY_INDOOR_BARRIER,
    CATEGORY_INDOOR_AREA,
    CATEGORY_INDOOR_RADIOMAP_CAPTURER_PATH
};

} // namespace

bool
isNamedCategory(const std::string& categoryId)
{
    return !nameAttrId(categoryId).empty();
}

std::string
nameAttrId(const Category& cat)
{
    for (const auto& attrDef : cat.attrDefs()) {
        if (!attrDef->table()) {
            continue;
        }
        const auto& attrId = attrDef->id();
        if (attrId.ends_with(SUFFIX_NM)) {
            return attrId;
        }
    }
    return s_emptyString;
}

std::string
nameAttrId(const GeoObject* obj)
{
    return nameAttrId(obj->category());
}

std::string
nameAttrId(const std::string& categoryId)
{
    ASSERT(cfg()->editor()->categories().defined(categoryId));
    return nameAttrId(cfg()->editor()->categories()[categoryId]);
}

std::string
findAttrColumnNameBySuffix(const std::string& tableAttr, const std::string& columnSuffix)
{
    const auto nmAttrDef =   cfg()->editor()->attribute(tableAttr);
    const auto& cols = nmAttrDef->columns();
    auto it = std::find_if(cols.begin(), cols.end(),
        [&columnSuffix](const std::string& colName) {
            return colName.ends_with(columnSuffix);
            });
    return it != cols.end()
        ? *it
        : s_emptyString;
}

bool
isSimpleGeomCategory(const Category& category)
{
    const auto& tmpl = category.categoryTemplate();
    return tmpl.hasGeometryType();
}

bool
isSimpleGeomCategory(const std::string& categoryId)
{
    ASSERT(cfg()->editor()->categories().defined(categoryId));
    const auto& category = cfg()->editor()->categories()[categoryId];
    return isSimpleGeomCategory(category);
}

bool
isContourCategory(const Category& category)
{
    return isContourCategory(category.id());
}

bool
isContourCategory(const std::string& categoryId)
{
    const auto& contourDefs = cfg()->editor()->contourObjectsDefs();
    return contourDefs.partType(categoryId) == ContourObjectsDefs::PartType::Contour;
}

bool isContourElementCategory(const std::string& categoryId)
{
    const auto& contourDefs = cfg()->editor()->contourObjectsDefs();
    return contourDefs.partType(categoryId) == ContourObjectsDefs::PartType::LinearElement;
}

bool isContourJunctionCategory(const std::string& categoryId)
{
    const auto& editorCfg = cfg()->editor();
    const auto& categories = editorCfg->categories();
    ASSERT(categories.defined(categoryId));
    const auto* topoGroup = editorCfg->topologyGroups().findGroup(categoryId);
    if (!topoGroup || topoGroup->junctionsCategory() != categoryId) {
        return false;
    }
    for (const auto& linearElementsCategory : topoGroup->linearElementsCategories()) {
        if (isContourElementCategory(linearElementsCategory)) {
            return true;
        }
    }
    return false;
}

bool isContourObjectCategory(const std::string& categoryId)
{
    const auto& contourDefs = cfg()->editor()->contourObjectsDefs();
    return contourDefs.partType(categoryId) == ContourObjectsDefs::PartType::Object;
}

bool
isDirectGeomMasterCategory(const Category& category)
{
    const auto& contourDefs = cfg()->editor()->contourObjectsDefs();
    if (contourDefs.partType(category.id()) == ContourObjectsDefs::PartType::Contour) {
        return true;
    }
    for (const auto& slaveRole : category.slavesRoles()) {
        if (slaveRole.geomPart() &&
            isSimpleGeomCategory(slaveRole.categoryId())) {
            return true;
        }
    }
    return false;
}

bool
isIndirectGeomMasterCategory(const Category& category)
{
    return isIndirectGeomMasterCategory(category.id());
}

bool
isIndirectGeomMasterCategory(const std::string& categoryId)
{
    const auto& contourDefs = cfg()->editor()->contourObjectsDefs();
    return contourDefs.partType(categoryId) == ContourObjectsDefs::PartType::Object;
}

bool isOutdoorPoi(const std::string& categoryId)
{
    auto group = cfg()->editor()->categoryGroups().findGroupByCategoryId(categoryId);
    return group && (group->id() == CATEGORY_GROUP_POI);
}

bool isIndoorPoi(const std::string& categoryId)
{
    if (INDOOR_GROUP_NON_POIS.contains(categoryId)) {
        return false;
    }
    auto group = cfg()->editor()->categoryGroups().findGroupByCategoryId(categoryId);
    return group && (group->id() == CATEGORY_GROUP_INDOOR);
}

bool isPoi(const std::string& categoryId)
{
    return isOutdoorPoi(categoryId) || isIndoorPoi(categoryId);
}

bool
isTransportRoute(const std::string& categoryId)
{
    return ROUTES.count(categoryId);
}

bool
isTransportThread(const std::string& categoryId)
{
    return THREADS_AND_STATIONS.count(categoryId);
}

bool
isTransportStation(const std::string& categoryId)
{
    auto it = std::find_if(THREADS_AND_STATIONS.begin(), THREADS_AND_STATIONS.end(),
        [&categoryId](const std::pair<std::string, std::string>& threadAndStation) {
            return threadAndStation.second == categoryId;
        });
    return it != THREADS_AND_STATIONS.end();
}

bool
isThreadElementRole(const std::string& categoryId, const std::string& roleId)
{
    ASSERT(cfg()->editor()->categories().defined(categoryId));
    const auto& category = cfg()->editor()->categories()[categoryId];
    return isTransportThread(categoryId) && isLinearElement(category.slaveRole(roleId).categoryId());
}

const std::string&
stationCategory(const std::string& threadCategory)
{
    ASSERT(isTransportThread(threadCategory));
    return THREADS_AND_STATIONS.at(threadCategory);
}

const std::string&
routeCategory(const std::string& threadCategory)
{
    ASSERT(isTransportThread(threadCategory));
    const auto& masterRoles = cfg()->editor()->categories()[threadCategory].masterRoles();
    for (const auto& role : masterRoles) {
        if (role.roleId() == ROLE_ASSIGNED_THREAD) {
            return role.categoryId();
        }
    }
    REQUIRE(false,
        "Thread's " <<  threadCategory
        << " relation with route isn't properly configured.");
}

bool
isLinearElement(const std::string& categoryId)
{
    const auto& editorCfg = cfg()->editor();
    const auto& categories = editorCfg->categories();
    ASSERT(categories.defined(categoryId));
    const auto* topoGroup = editorCfg->topologyGroups().findGroup(categoryId);
    return topoGroup && topoGroup->linearElementsCategories().count(categoryId);
}

bool
isJunction(const std::string& categoryId)
{
    const auto& editorCfg = cfg()->editor();
    const auto& categories = editorCfg->categories();
    ASSERT(categories.defined(categoryId));
    const auto* topoGroup = editorCfg->topologyGroups().findGroup(categoryId);
    return topoGroup && topoGroup->junctionsCategory() == categoryId;
}

bool
needCheckRelationsPermissions(const std::string& categoryId)
{
    const auto& editorCfg = cfg()->editor();
    const auto& categories = editorCfg->categories();
    ASSERT(categories.defined(categoryId));
    const auto& category = categories[categoryId];
    return category.complex() ||
        (!isLinearElement(categoryId) &&
            !category.slaveRoleIds(roles::filters::IsNeitherTableNorGeomPart).empty());

}

std::string
bestViewTable(const StringSet& categories)
{
    ASSERT(!categories.empty());

    StringSet viewTables;
    for (const auto& categoryId : categories) {
        const auto& tableName = GeoObjectFactory::objectClass(categoryId).tableName;
        if (!tableName.empty()) {
            viewTables.insert(tableName);
        }
    }
    ASSERT(!viewTables.empty());

    if (viewTables.size() == 1) {
        return *viewTables.begin();
    } else if (viewTables.count(views::TABLE_OBJECTS_C)) {
        return views::TABLE_OBJECTS;
    }
    return views::TABLE_OBJECTS_G;
}

}// namespace wiki
}// namespace maps
