#include "schema.h"
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/common/config.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/common/config_helper.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/tds/json_helper.h>

#include <yandex/maps/wiki/common/extended_xml_doc.h>

namespace maps::wiki::json2ymapsdf::tds::schema {

namespace {

class SchemaImpl {
public:
    SchemaImpl() = default;

    std::string info() const;
    void configure(const ymapsdf::schema::Schema& ymapsdfSchema, const std::string& editorCfg, const RelationsMode& relationsMode);
    void discardRelationsDirection();

    Category& category(const std::string& catName);
    const CategoryMap& categories() const { return categories_; }

    const Relation& getRelation(const std::string& master, const std::string& role) const;
    Relations getRelations(const std::string& master, const std::string& role) const;
    void checkRelation(const std::string& master, const std::string& role) const;
    const RelationMap& relations() const { return relations_; }

    RelationsMode relationsMode() const { return relationsMode_; }

    void addRelation(
        const ymapsdf::schema::Schema& ymapsdfSchema,
        const std::string& master,
        const std::string& role,
        const std::string& slave);

private:
    void addCategory(const ymapsdf::schema::Schema& ymapsdfSchema, const std::string& category);
    void addAttribute(const ymapsdf::schema::Schema& ymapsdfSchema, const std::string& category, const std::string& attribute);

private:
    CategoryMap categories_;
    RelationMap relations_;
    RelationsMode relationsMode_;
};

SchemaImpl& schemaImpl()
{
    static SchemaImpl schema;
    return schema;
}

void
SchemaImpl::configure(const ymapsdf::schema::Schema& ymapsdfSchema, const std::string& editorCfg, const RelationsMode& relationsMode)
{
    relationsMode_ = relationsMode;

    common::ExtendedXmlDoc doc(editorCfg);

    auto categoryNodes = doc.node("/editor/categories").nodes("category");
    for (size_t i = 0; i < categoryNodes.size(); ++i) {
        const auto categoryNode = categoryNodes[i];
        std::string category = categoryNode.attr<std::string>("id");
        addCategory(ymapsdfSchema, category);

        xml3::Nodes relationNodes = categoryNode.nodes("relations/role", true);
        for (size_t i = 0; i < relationNodes.size(); ++i) {
            const auto relationNode = relationNodes[i];
            std::string role = relationNode.attr<std::string>("id");
            std::string slaveCat = relationNode.attr<std::string>("category-id");
            addRelation(ymapsdfSchema, category, role, slaveCat);
        }
    }

    auto attributeNodes = doc.node("/editor/sections").nodes("section/attribute");
    for (size_t i = 0; i < attributeNodes.size(); ++i) {
        std::string id = attributeNodes[i].attr<std::string>("id");
        auto category = categoryName(id);
        addCategory(ymapsdfSchema, category);
        auto attribute = attributeName(id);
        if (!attribute.empty()) {
            addAttribute(ymapsdfSchema, category, attribute);
        }
    }

    if (relationsMode_.mainDirection() != RelationsDirection::Unknown) {
        for (auto& [_, relation] : relations_) {
            relation->setDirection(relationsMode_.direction(relation->role()));
            if (relation->direction() == RelationsDirection::MasterToSlave) {
                category(relation->master().name()).addRelation(relation);
            } else { // relationsMode == RelationsDirection::SlaveToMaster
                category(relation->slave().name()).addRelation(relation);
            }
        }
    }
}

void
SchemaImpl::addCategory(const ymapsdf::schema::Schema& ymapsdfSchema, const std::string& category)
{
    REQUIRE (!category.empty(), "Unable to configure empty category");
    categories_.insert({category, std::make_shared<Category>(ymapsdfSchema, category)});
    categories_[category]->addAttribute(tds::ATTRIBUTE_OBJECT_ID);
    // suppose all objects have shape
    categories_[category]->addAttribute(tds::ATTRIBUTE_SHAPE);
}

void
SchemaImpl::addAttribute(const ymapsdf::schema::Schema& ymapsdfSchema, const std::string& category, const std::string& attribute)
{
    addCategory(ymapsdfSchema, category);
    categories_[category]->addAttribute(attribute);
}

void
SchemaImpl::addRelation(
    const ymapsdf::schema::Schema& ymapsdfSchema,
    const std::string& master,
    const std::string& role,
    const std::string& slave)
{
    addCategory(ymapsdfSchema, master);
    addCategory(ymapsdfSchema, slave);
    auto rel = std::make_shared<Relation> (
        category(master),
        role,
        category(slave),
        RelationsDirection::Unknown);
    relations_.insert({{master, role}, rel});
}

Category&
SchemaImpl::category(const std::string& catName)
{
    auto it = categories_.find(catName);
    REQUIRE(it != categories_.end(), "Unknown category '" << catName << "'");
    return *it->second;
}

const Relation&
SchemaImpl::getRelation(
    const std::string& master,
    const std::string& role) const
{
    auto it = relations_.find({master, role});
    REQUIRE(it != relations_.end(), "Unknown relation " << master << "." << role);
    return *it->second;
}


Relations
SchemaImpl::getRelations(
    const std::string& master,
    const std::string& role) const
{
    Relations relations;
    for (const auto& kv: relations_) {
        const auto& relMaster = kv.first.first;
        const auto& relRole = kv.first.second;
        if (relRole == role && (master.empty() || relMaster == master)) {
            relations.insert(kv.second);
        }
    }
    return relations;
}

void
SchemaImpl::checkRelation(
    const std::string& master,
    const std::string& role) const
{
    if (!master.empty()) {
        getRelation(master, role);
        return;
    }

    auto hasRole = [&]()
    {
        for (const auto& kv: relations_) {
            const auto& relRole = kv.first.second;
            if (relRole == role) {
                return true;
            }
        }
        return false;
    };

    REQUIRE(hasRole(), "Unknown role '" << role << "'");
}

void
SchemaImpl::discardRelationsDirection()
{
    relationsMode_ = RelationsMode();
}

std::string
SchemaImpl::info() const
{
    std::ostringstream infoStr;
    infoStr << "Tds schema configuration\n";

    infoStr << relationsMode_.info();

    for (const auto& kv: categories_) {
        infoStr << "cat " << kv.second->info() << "\n";
    }

    for (const auto& kv: relations_) {
        infoStr << "trel" << kv.second->info() << "\n";
    }

    return infoStr.str();
}

} // namespace

void
configure(const ymapsdf::schema::Schema& ymapsdfSchema, const std::string& editorCfg, const RelationsMode& relationsMode)
{
    return schemaImpl().configure(ymapsdfSchema, editorCfg, relationsMode);
}

void
discardRelationsDirection()
{
    return schemaImpl().discardRelationsDirection();
}

std::string
info()
{
    return schemaImpl().info();
}

Category&
category(const std::string& catName)
{
    return schemaImpl().category(catName);
}

const Relation&
relation(const std::string& master, const std::string& role)
{
    return schemaImpl().getRelation(master, role);
}

Relations
getRelations(const std::string& master, const std::string& role)
{
    return schemaImpl().getRelations(master, role);
}

void
checkRelation(const std::string& master, const std::string& role)
{
    return schemaImpl().checkRelation(master, role);
}

const CategoryMap&
categories()
{
    return schemaImpl().categories();
}

const RelationMap&
relations()
{
    return schemaImpl().relations();
}

RelationsMode
relationsMode()
{
    return schemaImpl().relationsMode();
}

void
addRelation(
    const ymapsdf::schema::Schema& ymapsdfSchema,
    const std::string& master,
    const std::string& role,
    const std::string& slave)
{
    return schemaImpl().addRelation(ymapsdfSchema, master, role, slave);
}

} // namespace maps::wiki::json2ymapsdf::tds::schema
