#include <yandex/maps/wiki/configs/editor/config_holder.h>
#include <yandex/maps/wiki/configs/editor/attrdef.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/master_role.h>
#include <yandex/maps/wiki/configs/editor/slave_role.h>
#include <yandex/maps/wiki/configs/editor/restrictions.h>
#include <yandex/maps/wiki/configs/editor/section.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/exception.h>
#include <yandex/maps/wiki/configs/editor/magic_strings.h>

#include <boost/lexical_cast.hpp>

namespace maps {
namespace wiki {
namespace configs {
namespace editor {

class Category::Impl
{
public:
    explicit Impl(const maps::xml3::Node& node)
        : categories_(nullptr)
        , restrictions_(nullptr)
        , categoryTemplate_(nullptr)
        , id_(node.attr<std::string>("id"))
        , restrictionsId_(node.attr<std::string>("restrictions-id"))
        , templateId_(node.attr<std::string>("template-id", ""))
        , generalizationId_(node.attr<std::string>("generalization-id", ""))
        , complex_(node.attr<bool>("complex", false))
        , system_(node.attr<bool>("system", false))
        , isPrivate_(node.attr<bool>("private", false))
        , suggest_(node.attr<bool>("suggest", false))
        , syncView_(!system_ && !isPrivate_)
        , autoApprove_(node.attr<bool>("autoapprove", false))
        , hasNames_(false)
        , slavesTopologicalyContinuous_(false)
        , masterRequired_(false)
        , kind_(node.attr<std::string>("kind", ""))
        , showSlavesDiff_(node.attr<bool>("show-slaves-diff", false))
    {
        auto labelNode = node.node("label", true);
        if (!labelNode.isNull()) {
            label_ = labelNode.value<std::string>();
        }
        auto sectionRefs = node.nodes("attributes-sections/section", true);
        for (size_t i = 0; i < sectionRefs.size(); ++i) {
            sections_.push_back(sectionRefs[i].attr<std::string>("id"));
        }

        auto slavesRoles = node.nodes("relations/role", true);
        for (size_t i = 0; i < slavesRoles.size(); ++i) {
            slavesRoles_.push_back(SlaveRole(slavesRoles[i]));
        }

        auto relationsNode = node.node("relations", true);
        if (!slavesRoles_.empty() && !relationsNode.isNull()) {
            slavesTopologicalyContinuous_ = relationsNode.attr<bool>("topologicaly-continuous", false);
        }

        masterRequired_ = relationsNode.isNull()
            ? false
            : relationsNode.attr<bool>("master-required", false);

        auto slavesCountForGeomCaching = node.attr<std::string>("cache-geom-parts-threshold", "");
        if (!slavesCountForGeomCaching.empty()) {
            cacheGeomPartsThreshold_ = boost::lexical_cast<size_t>(slavesCountForGeomCaching);
            REQUIRE(*cacheGeomPartsThreshold_ > 0,
                "Zero slaves for geom caching count, category id: " << id_);
        }

        auto richContentNode = node.node("rich-content", true);
        if (!richContentNode.isNull()) {
            richContentType_ = richContentNode.node("type").value<std::string>();
            richContentFileExt_ = richContentNode.node("file-ext").value<std::string>();
        }

        hasNames_ = slavesRoles_.findByKey(NAME_TYPE_OFFICIAL) != slavesRoles_.end();
    }

    void prepare(ConfigHolder& config)
    {
        restrictions_ = &config.restrictions(restrictionsId_);
        categoryTemplate_ = &config.categoryTemplate(templateId_);

        if (id_ != CATEGORY_RELATION) {
            const auto& attrDef = config.registerSystemAttribute(
                "cat:" + id_,
                "1",
                {"1"});
            attrDefs_.push_back(attrDef);
        }

        auto& sections = (sections_.size() || templateId_.empty())
            ? sections_
            : categoryTemplate_->sections();

        for (const auto& sectionId : sections) {
            const auto& section = config.section(sectionId);
            for (const auto& attrId : section.attributes()) {
                attrDefs_.push_back(config.attribute(attrId));
            }
        }
        for (const auto& attrId : categoryTemplate_->systemAttributes()) {
            attrDefs_.push_back(config.attribute(attrId));
        }

        if (syncView_ && id_ != CATEGORY_RELATION) {
            REQUIRE(config.categoryGroups().findGroupByCategoryId(id_),
                "Category group not found for category: " << id_);
        }
    }

    void addMasterRole(MasterRole masterRole)
    {
        masterRoles_.push_back(std::move(masterRole));
    }

    const Categories* categories_;
    const Restrictions* restrictions_;
    const CategoryTemplate* categoryTemplate_;

    std::string id_;
    std::string label_;

    AttrDefsVector attrDefs_;
    StringVec sections_;

    std::string restrictionsId_;
    std::string templateId_;
    std::string generalizationId_;

    StringVec conversionTargets_;

    bool complex_;
    bool system_;
    bool isPrivate_;
    bool suggest_;
    bool syncView_;
    bool autoApprove_;
    bool hasNames_;

    bool slavesTopologicalyContinuous_;
    bool masterRequired_;
    std::optional<size_t> cacheGeomPartsThreshold_;

    SlaveRoles slavesRoles_;
    MasterRoles masterRoles_;

    std::string richContentType_;
    std::string richContentFileExt_;

    std::string kind_;
    bool showSlavesDiff_;
};

MOVABLE_PIMPL_DEFINITIONS(Category)

Category::Category(const maps::xml3::Node& node)
    : impl_(new Impl{node})
{}

const std::string& Category::id() const { return impl_->id_; }
const std::string& Category::label() const { return impl_->label_; }

const Restrictions& Category::restrictions() const
{
    REQUIRE(impl_->restrictions_, "Category " << impl_->id_ << " has no restrictions");
    return *impl_->restrictions_;
}

const CategoryTemplate& Category::categoryTemplate() const
{
    REQUIRE(impl_->categoryTemplate_, "Category " << impl_->id_ << " has no category template");
    return *impl_->categoryTemplate_;
}

const AttrDefsVector& Category::attrDefs() const
{
    REQUIRE(!impl_->attrDefs_.empty(), "Category " << impl_->id_ << " has no attributes");
    return impl_->attrDefs_;
}

const AttrDefPtr& Category::attribute(const std::string& id) const
{
    auto attrIt = std::find_if(impl_->attrDefs_.begin(), impl_->attrDefs_.end(),
        [&id](const AttrDefPtr& attr) {
            return attr->id() == id;
        });
    if (attrIt == impl_->attrDefs_.end()) {
        throw InitializationError() << "Attribute " << id << " not found.";
    }
    return *attrIt;
}

bool Category::isAttributeDefined(const std::string& id) const
{
    auto attrIt = std::find_if(impl_->attrDefs_.begin(), impl_->attrDefs_.end(),
        [&id](const AttrDefPtr& attr) {
            return attr->id() == id;
        });
    return attrIt != impl_->attrDefs_.end();
}

const std::string& Category::templateId() const { return impl_->templateId_; }
const std::string& Category::generalizationId() const { return impl_->generalizationId_; }

bool Category::complex() const { return impl_->complex_; }
bool Category::system() const { return impl_->system_; }
bool Category::isPrivate() const { return impl_->isPrivate_; }
bool Category::suggest() const { return impl_->suggest_; }
bool Category::syncView() const { return impl_->syncView_; }
bool Category::autoApprove() const { return impl_->autoApprove_; }
bool Category::hasNames() const { return impl_->hasNames_; }

bool Category::slavesTopologicalyContinuous() const { return impl_->slavesTopologicalyContinuous_; }
bool Category::masterRequired() const { return impl_->masterRequired_;}
std::optional<size_t> Category::cacheGeomPartsThreshold() const { return impl_->cacheGeomPartsThreshold_; }

const SlaveRoles& Category::slavesRoles() const { return impl_->slavesRoles_; }
const SlaveRole& Category::slaveRole(const std::string& roleId) const
{
    auto it = impl_->slavesRoles_.findByKey(roleId);
    REQUIRE(it != impl_->slavesRoles_.end(), "Slave role " << roleId << " not found for category:" << impl_->id_);
    return *it;
}

bool Category::isSlaveRoleDefined(const std::string& roleId) const
{
    auto it = impl_->slavesRoles_.findByKey(roleId);
    return it != impl_->slavesRoles_.end();
}

const MasterRoles& Category::masterRoles() const { return impl_->masterRoles_; }
MasterRoles Category::masterRole(const std::string& roleId) const
{
    MasterRoles result;
    for (const auto& role : impl_->masterRoles_) {
        if (role.roleId() == roleId) {
            result.push_back(role);
        }
    }
    REQUIRE(!result.empty(), "Master role " << roleId << " not found for category:" << impl_->id_);
    return result;
}

std::string Category::aclPermissionName() const { return impl_->id_; }

StringSet Category::roleIds(RelationType relationType, roles::filters::Filter filter) const
{
    return relationType == RelationType::Master
        ? masterRoleIds(filter)
        : slaveRoleIds(filter);
}

StringSet Category::slaveRoleIds(roles::filters::Filter filter) const
{
    StringSet roleIds;
    for (const auto& slaveRole : impl_->slavesRoles_) {
        if (filter(slaveRole)) {
            roleIds.insert(slaveRole.roleId());
        }
    }
    return roleIds;
}

StringSet Category::masterRoleIds(roles::filters::Filter filter) const
{
    REQUIRE(impl_->categories_, "Categories are null");

    StringSet roleIds;
    const auto categoryIds = impl_->categories_->idsByFilter(Categories::All);
    for (const auto& categoryId : categoryIds) {
        for (const auto& slaveRole : (*impl_->categories_)[categoryId].slavesRoles()) {
            if (slaveRole.categoryId() == impl_->id_ && filter(slaveRole)) {
                roleIds.insert(slaveRole.roleId());
            }
        }
    }
    return roleIds;
}

//! Ordered geom slave roles
StringVec Category::geomSlaveRoleIds() const
{
    StringVec roleIds;
    for (const auto& slaveRole : impl_->slavesRoles_) {
        if (roles::filters::IsGeom(slaveRole)) {
            roleIds.push_back(slaveRole.roleId());
        }
    }
    return roleIds;
}

StringSet Category::relativeCategoryIds(
    RelationType relationType, roles::filters::Filter filter) const
{
    return relationType == RelationType::Master
        ? masterCategoryIds(filter)
        : slaveCategoryIds(filter);
}

StringSet Category::slaveCategoryIds(roles::filters::Filter filter) const
{
    StringSet categoryIds;
    for (const auto& slaveRole : impl_->slavesRoles_) {
        if (filter(slaveRole)) {
            categoryIds.insert(slaveRole.categoryId());
        }
    }
    return categoryIds;
}

StringSet Category::masterCategoryIds(roles::filters::Filter filter) const
{
    REQUIRE(impl_->categories_, "Categories are null");

    StringSet catIds;
    const auto categoryIds = impl_->categories_->idsByFilter(Categories::All);
    for (const auto& categoryId : categoryIds) {
        for (const auto& slaveRole : (*impl_->categories_)[categoryId].slavesRoles()) {
            if (slaveRole.categoryId() == impl_->id_ && filter(slaveRole)) {
                catIds.insert(categoryId);
            }
        }
    }
    return catIds;
}

const std::string& Category::richContentType() const { return impl_->richContentType_; }
const std::string& Category::richContentFileExt() const { return impl_->richContentFileExt_; }

const std::string& Category::kind() const { return impl_->kind_; }

bool Category::showSlavesDiff() const { return impl_->showSlavesDiff_; }

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

class Categories::Impl
{
public:
    explicit Impl(const maps::xml3::Node& node)
    {
        auto categories = node.nodes("category");
        for (size_t i = 0; i < categories.size(); ++i) {
            auto ret = categories_.insert(std::make_pair(
                categories[i].attr<std::string>("id"),
                Category(categories[i])));
            REQUIRE(ret.second,
                "Duplicate category id: " << ret.first->first);
        }

        for (const auto& pair : categories_) {
            const auto& category = pair.second;
            for (const auto& slaveRole : category.slavesRoles()) {
                MasterRole masterRole{
                    slaveRole.roleId(),
                    category.id(),
                    slaveRole.masterMinOccurs(),
                    slaveRole.masterMaxOccurs()
                };
                REQUIRE(categories_.count(slaveRole.categoryId()),
                    "Slave category: " << slaveRole.categoryId() << " not defined.");
                categories_.at(slaveRole.categoryId()).impl_->addMasterRole(std::move(masterRole));
            }
        }
    }

    CategoriesMap categories_;
};

MOVABLE_PIMPL_DEFINITIONS(Categories)

Categories::Categories(const maps::xml3::Node& node)
    : impl_(new Impl{node})
{
}

const Categories::Filter Categories::All =
    [](const Category&) -> bool { return true; };

const Categories::Filter Categories::CachedGeom =
    [](const Category& category) -> bool
    {
        return category.complex() && category.cacheGeomPartsThreshold();
    };

const Categories::Filter Categories::Suggest =
    [](const Category& category) -> bool
    {
        return category.suggest();
    };

const Category& Categories::operator[](const std::string& categoryId) const
{
    auto it = impl_->categories_.find(categoryId);
    if (it == impl_->categories_.end()) {
        throw UnknownObjectCategoryException() << " Category " << categoryId << " not found";
    }
    return it->second;
}

bool Categories::defined(const std::string& categoryId) const
{
    return impl_->categories_.count(categoryId) > 0;
}

StringSet Categories::idsByFilter(const Filter& filter) const
{
    StringSet ids;
    for (const auto& category : impl_->categories_) {
        if (filter(category.second)) {
            ids.insert(category.first);
        }
    }
    return ids;
}

Categories::CategoriesMap::const_iterator Categories::begin() const
{ return impl_->categories_.begin(); }
Categories::CategoriesMap::const_iterator Categories::end() const
{ return impl_->categories_.end(); }

void Categories::onLoad(ConfigHolder& config)
{
    for (auto& category : impl_->categories_) {
        category.second.impl_->categories_ = this;
        category.second.impl_->prepare(config);
    }
}

} // editor
} // configs
} // wiki
} // maps
