#include "registry.h"
#include <maps/wikimap/mapspro/services/editor/src/objects/object.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/editor_cfg.h>
#include <maps/wikimap/mapspro/services/editor/src/objects_cache.h>
#include <maps/wikimap/mapspro/services/editor/src/relations_manager.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/relation_object.h>
#include <maps/wikimap/mapspro/services/editor/src/edit_options.h>
#include "ad.h"
#include "generic.h"
#include "hd_map.h"
#include "hydro.h"
#include "indoor.h"
#include "metro.h"
#include "misc.h"
#include "railway.h"
#include "rd.h"

#include "transport_thread.h"
#include "urban.h"

namespace maps
{
namespace wiki
{
namespace srv_attrs
{
std::unique_ptr<ServiceAttributesRegistry> ServiceAttributesRegistry::s_serviceAttributes;

// -- ServiceAttribute
ServiceAttribute::ServiceAttribute(
        const std::string& categoryId,
        const std::string& id,
        Callback callback)
    : categoryId_(categoryId)
    , id_(id)
    , callback_(callback)
{
}

ServiceAttribute::ServiceAttribute(
        const std::string& categoryId,
        const std::string& prefix,
        SuffixValueCallback suffixValueCallback)
    : categoryId_(categoryId)
    , id_(prefix)
    , suffixValueCallback_(std::move(suffixValueCallback))
{
}

ServiceAttribute::ServiceAttribute(
        const std::string& categoryId,
        const std::string& id)
    : categoryId_(categoryId)
    , id_(id)
{
}

std::string
ServiceAttribute::calc(const ObjectPtr& obj, ObjectsCache& cache) const
{
    ASSERT(callback_);
    return callback_(obj.get(), cache);
}

SuffixValues
ServiceAttribute::calcWithSuffix(const ObjectPtr& obj, ObjectsCache& cache) const
{
    ASSERT(suffixValueCallback_);
    return suffixValueCallback_(obj.get(), cache);
}

bool ServiceAttribute::isAffectedBy(const GeoObject* obj) const
{
    if (isAffectedByCallback_) {
        return isAffectedByCallback_(obj);
    }
    return true;
}

// -- SuggestCalc

SuggestCalc::SuggestCalc(
        const std::string& categoryId,
        Callback callback)
    : categoryId_(categoryId)
    , callback_(callback)
{
}

SuggestTexts
SuggestCalc::calc(const GeoObject* obj, ObjectsCache& cache) const
{
    return callback_(obj, cache);
}

// -- ServiceAttributesRegistry

void
ServiceAttributesRegistry::init(const EditorCfg& editorCfg)
{
    if (!s_serviceAttributes) {
        s_serviceAttributes.reset(new ServiceAttributesRegistry());
        s_serviceAttributes->buildSuggestDependencies(editorCfg);
        s_serviceAttributes->buildPaths();
    }
}

const ServiceAttributesRegistry&
ServiceAttributesRegistry::get()
{
    ASSERT(s_serviceAttributes);
    return *s_serviceAttributes;
}

ServiceAttributesRegistry::Registrar::Registrar(
         ServiceAttributesRegistry& registry,
         const std::string& targetCategoryId)
    : registry_(registry)
    , targetCategoryId_(targetCategoryId)
{
}

ServiceAttribute&
ServiceAttributesRegistry::Registrar::registerAttr(
        const std::string& attributeId,
        ServiceAttribute::Callback callback)
{
    return registry_.registerAttr(targetCategoryId_, attributeId, callback);
}

ServiceAttribute&
ServiceAttributesRegistry::Registrar::registerAttr(
        const std::string& attributeId,
        ServiceAttribute::SuffixValueCallback suffixValueCallback)
{
    return registry_.registerAttr(
        targetCategoryId_, attributeId, std::move(suffixValueCallback));
}


void
ServiceAttributesRegistry::Registrar::registerSuggestCallback(SuggestCalc::Callback callback)
{
    registry_.registerSuggestCallback(targetCategoryId_, callback);
}

ServiceAttributesRegistry::ServiceAttributesRegistry()
{
    ATDServiceAttributes(*this);
    RDServiceAttributes(*this);
    RAILWAYServiceAttributes(*this);
    METROServiceAttributes(*this);
    MiscServiceAttributes(*this);
    HydroServiceAttributes(*this);
    TRANSPORT_THREADServiceAttributes(*this);
    UrbanServiceAttributes(*this);
    GenericServiceAttributes(*this);
    INDOORServiceAttributes(*this);
    HDMapServiceAttributes(*this);
}

namespace
{
const std::vector<ServiceAttribute> s_emptyAttrs;
}

const std::vector<ServiceAttribute>&
ServiceAttributesRegistry::categoryAttrs(const std::string& categoryId) const
{
    auto it = attributesByCategory_.find(categoryId);
    return it == attributesByCategory_.end()
            ? s_emptyAttrs
            : it->second;
}

std::vector<ServiceAttributesRegistry::ServiceAttrSubPath>
ServiceAttributesRegistry::dependedAttrs(const std::string& categoryId) const
{
    std::vector<ServiceAttrSubPath> dependend;
    auto range = pathsThroughCategory_.equal_range(categoryId);
    for (auto it = range.first; it != range.second; ++it) {
        dependend.push_back(it->second);
    }
    return dependend;
}

StringSet
ServiceAttributesRegistry::registeredCategories() const
{
    StringSet result = readKeys(attributesByCategory_);
    StringSet fromSuggest = readKeys(suggestCallbacks_);
    result.insert(fromSuggest.begin(), fromSuggest.end());
    return result;
}

SuggestTexts
ServiceAttributesRegistry::calculateSuggest(
    const std::string& categoryId,
    const GeoObject* obj,
    ObjectsCache& cache) const
{
    auto it = suggestCallbacks_.find(categoryId);
    return it == suggestCallbacks_.end()
            ? SuggestTexts{}
            : it->second.calc(obj, cache);
}

namespace
{
    static const std::vector<Dependence> emptyDependencies;
}

const std::vector<Dependence>&
ServiceAttributesRegistry::suggestDependencies(const std::string& categoryId) const
{

    auto it = suggestCallbacks_.find(categoryId);
    return it == suggestCallbacks_.end()
            ? emptyDependencies
            : it->second.dependencies();
}

ServiceAttribute&
ServiceAttributesRegistry::registerAttr(
        const std::string& categoryId,
        const std::string& attributeId,
        ServiceAttribute::Callback callback)
{
    auto& attributes = attributesByCategory_[categoryId];
    REQUIRE(attributes.end() ==
        std::find_if(attributes.begin(), attributes.end(),
            [&](const ServiceAttribute& existingAttr) {
                return
                    existingAttr.id() == attributeId
                    ||
                    (existingAttr.resultType() == ServiceAttribute::ResultType::SuffixValue &&
                     attributeId.starts_with(existingAttr.id()));
            }),
        "Already registered category " << categoryId
        << " attribute with matching " << attributeId);
    attributes.emplace_back(categoryId, attributeId, callback);
    return attributes.back();
}

ServiceAttribute&
ServiceAttributesRegistry::registerAttr(
        const std::string& categoryId,
        const std::string& attributeId,
        ServiceAttribute::SuffixValueCallback suffixValueCallback)
{
    auto& attributes = attributesByCategory_[categoryId];
    REQUIRE(attributes.end() ==
        std::find_if(attributes.begin(), attributes.end(),
            [&](const ServiceAttribute& existingAttr) {
                return existingAttr.id().starts_with(attributeId);
            }),
        "Already registered category " << categoryId
        << " attribute with prefix matching " << attributeId);
    attributes.emplace_back(categoryId, attributeId, suffixValueCallback);
    return attributes.back();
}

void
ServiceAttributesRegistry::registerSuggestCallback(
        const std::string& categoryId,
        SuggestCalc::Callback callback)
{
    SuggestCalc name(categoryId, callback);
    suggestCallbacks_.insert({categoryId, name});
}

void
ServiceAttributesRegistry::buildSuggestDependencies(const EditorCfg& editorCfg)
{
    auto& parentRelationDefs = editorCfg.parentRelationDefs();
    if (!parentRelationDefs) {
        return;
    }
    for (auto& suggestCallback : suggestCallbacks_) {
        auto parentDefinition = parentRelationDefs->parentDefinition(
                suggestCallback.first);
        if (!parentDefinition) {
            continue;
        }
        suggestCallback.second.depends(
            {
                {Aspect::Type::Relations, STR_TO_ATTR_OWNER},
                {
                    {
                        parentDefinition->relationType,
                        parentDefinition->roleId,
                        parentDefinition->categoryId
                    }
                }
            });
    }
}

void
ServiceAttributesRegistry::buildPaths()
{
    auto parseAttrDependence = [&](const ServiceAttribute& srvAttr)
    {
        for (const auto& dep : srvAttr.dependencies()) {
            for (size_t i = 0; i < dep.path().steps().size(); ++i) {
                pathsThroughCategory_.insert(
                {
                    dep.path().steps()[i].categoryId(),
                    {&srvAttr, &dep, i}
                });
            }
        }
    };
    for (const auto& catSrvAttr : attributesByCategory_) {
        for (const auto& srvAttr : catSrvAttr.second) {
            parseAttrDependence(srvAttr);
        }
    }
}

//Scenario: Load all objects this object's srv attrs depend from
void
preloadRequiredObjects(const TOIds& clients, ObjectsCache& cache)
{
    auto collection = cache.get(clients);
    const ServiceAttributesRegistry& serviceAttributes = ServiceAttributesRegistry::get();
    std::vector<std::pair<TOid, const RelationsPath*>> sourcesAndPaths;
    for (const auto& obj : collection) {
        for (const auto& serviceAttr : serviceAttributes.categoryAttrs(obj->categoryId())) {
            for (const auto& dependence : serviceAttr.dependencies()) {
                if (!dependence.path().steps().empty()) {
                    sourcesAndPaths.push_back({obj->id(), &dependence.path()});
                }
            }
        }
        for (const auto& dependence : serviceAttributes.suggestDependencies(obj->categoryId())) {
            if (!dependence.path().steps().empty()) {
                sourcesAndPaths.push_back({obj->id(), &dependence.path()});
            }
        }
    }

    size_t numStep = 0;
    while (!sourcesAndPaths.empty()) {
        auto it = sourcesAndPaths.begin();
        std::map<TOid, StringSet> masters;
        std::map<TOid, StringSet> slaves;
        for (const auto& sourceAndPath : sourcesAndPaths) {
            auto oid = sourceAndPath.first;
            const RelationsPath* path = sourceAndPath.second;
            const RelationsStep& step = path->steps()[numStep];
            if (step.direction() == RelationType::Master) {
                masters[oid].insert(step.roleId());
            } else {
                slaves[oid].insert(step.roleId());
            }
            ++it;
        }
        cache.relationsManager().loadRelations(RelationType::Master, masters);
        cache.relationsManager().loadRelations(RelationType::Slave, slaves);
        std::vector<std::pair<TOid, const RelationsPath*>> nextLevel;
        for (const auto& sourceAndPath : sourcesAndPaths) {
            auto oid = sourceAndPath.first;
            const RelationsPath* path = sourceAndPath.second;
            if (numStep < path->steps().size() - 1) {
                const RelationsStep& step = path->steps()[numStep];
                auto relations = cache.getExisting(oid)->relations(step.direction()).range(step.roleId());
                for (const auto& relation : relations) {
                    nextLevel.push_back({relation.id(), path});
                }
            }
        }
        sourcesAndPaths.swap(nextLevel);
        ++numStep;
    }
}

namespace
{
void
notifyAll(std::vector<GeoObject*> objs,
          GeoObjectCollection& col, ObjectsCache& cache)
{
    for (auto obj: objs) {
        obj->requestPropertiesUpdate();
        col.add(cache.getExisting(obj->id()));
        DEBUG() << " Requesting update for : " << obj->id() << " of category " << obj->categoryId();
    }
}

RelationType
relationType(const std::string& text)
{
    ASSERT(!text.empty());
    return text == STR_TO_MASTER
        ? RelationType::Master
        : RelationType::Slave;
}

inline bool
toMaster(const std::string& text)
{
    return relationType(text) == RelationType::Master;
}

inline bool
toSlave(const std::string& text)
{
    return relationType(text) == RelationType::Slave;
}
}

//Scenario: Load all objects this object affects, update them
GeoObjectCollection
affectedDependentObjects(const GeoObject* obj, ObjectsCache& cache)
{
    const ServiceAttributesRegistry& serviceAttributes = ServiceAttributesRegistry::get();
    GeoObjectCollection resultCollection;
    auto dependedServiceAttrSubPaths = serviceAttributes.dependedAttrs(obj->categoryId());
    if (obj->isModifiedCategory()) {
        ASSERT(obj->original());
        auto oldDependedServiceAttrSubPaths = serviceAttributes.dependedAttrs(
            obj->original()->categoryId());
        dependedServiceAttrSubPaths.insert(dependedServiceAttrSubPaths.end(),
            oldDependedServiceAttrSubPaths.begin(), oldDependedServiceAttrSubPaths.end());
    }
    const bool isModifiedLinksToMasters = obj->isModifiedLinksToMasters();
    const bool isModifiedLinksToSlaves = obj->isModifiedLinksToSlaves();
    const bool isModifiedAttrs = obj->isModifiedAttr();
    for (const auto& dependedServiceAttrSubPath : dependedServiceAttrSubPaths) {
        const auto& srvAttr = *dependedServiceAttrSubPath.srvAttr;
        const auto& dep = *dependedServiceAttrSubPath.dependence;
        size_t stepNum = dependedServiceAttrSubPath.pathOffset;
        ASSERT(!dep.path().steps().empty());
        const auto& step = dep.path().steps()[stepNum];
        const auto& aspect = dep.aspect();
        const auto& direction = step.direction();
        bool notify = false;
        if (stepNum < dep.path().steps().size() - 1) {
            if (
                (direction == RelationType::Master
                && isModifiedLinksToSlaves)
                ||
                (direction == RelationType::Slave
                && isModifiedLinksToMasters)
                ) {
                    DEBUG() << "Requesting updates indirectly caused by: "
                        << obj->id() << " of category " << obj->categoryId()
                        << " to service attribute " << srvAttr.id() << " of "
                        << srvAttr.categoryId();
                    notify = true;
            }
        } else if (srvAttr.isAffectedBy(obj)) {
            switch (aspect.type) {
                case Aspect::Type::Geometry:
                    notify = obj->isModifiedGeom();
                    break;
                case Aspect::Type::Attribute:
                    notify = aspect.id.empty()
                                ? isModifiedAttrs
                                : obj->isModifiedAttr(dep.aspect().id);
                    break;
                case Aspect::Type::Relations:
                    notify = (isModifiedLinksToSlaves && (direction == RelationType::Master ||
                                (!aspect.id.empty() && toSlave(aspect.id))))
                            || (isModifiedLinksToMasters && (direction == RelationType::Slave ||
                                 (!aspect.id.empty() && toMaster(aspect.id))));
                    break;
            }
            if (notify) {
                   DEBUG() << "Requesting updates directly caused by: " << obj->id() << " of category " << obj->categoryId()
                       << " to service attribute " << srvAttr.id()<< " of "
                       << srvAttr.categoryId();
            }
        }
        if (notify) {
            notifyAll(dep.path().backward({obj}, stepNum), resultCollection, cache);
        }
    }
    return resultCollection;
}

void
preloadDependentObjects(const TOIds& clients, ObjectsCache& cache)
{
    TOIds relatedOids;
    std::map<std::string, TOIds> category2oids;
    for (const auto& obj : cache.get(clients)) {
        if (is<RelationObject>(obj)) {
            const RelationObject* rel = as<const RelationObject>(obj);
            relatedOids.insert(rel->masterId());
            relatedOids.insert(rel->slaveId());
            category2oids[rel->masterCategoryId()].insert(rel->masterId());
            category2oids[rel->slaveCategoryId()].insert(rel->slaveId());
        } else {
            category2oids[obj->categoryId()].insert(obj->id());
        }
    }
    cache.load(relatedOids);

    const ServiceAttributesRegistry& serviceAttributes = ServiceAttributesRegistry::get();
    for (const auto& pair : category2oids) {
        const auto& categoryId = pair.first;
        const auto& oids = pair.second;

        auto dependedServiceAttrSubPaths = serviceAttributes.dependedAttrs(categoryId);
        for (const auto& dependedServiceAttrSubPath : dependedServiceAttrSubPaths) {
            const auto& dep = *dependedServiceAttrSubPath.dependence;
            auto stepNum = dependedServiceAttrSubPath.pathOffset;

            std::vector<const GeoObject*> objects;
            for (const auto& obj : cache.get(oids)) {
                objects.push_back(obj.get());
            }
            dep.path().backward(objects, stepNum);
        }
    }
}

}//srv_attrs
}//wiki
}//maps
