#include "runtime_data.h"

#include "objects_cache.h"
#include "collection.h"
#include "configs/config.h"
#include "relations_manager.h"
#include "srv_attrs/registry.h"

#include <maps/wikimap/mapspro/libs/dbutils/include/parser.h>
#include <maps/wikimap/mapspro/libs/views/include/query_builder.h>
#include <yandex/maps/wiki/configs/editor/categories.h>

namespace maps {
namespace wiki {

RuntimeDataCalculator::RuntimeDataCalculator(ObjectsCache& cache)
    : cache_(cache)
{}

/**
 * Screen labels must be loaded prior to relatives loading because
 *   they are cached in master and slave infos.
 */
void
RuntimeDataCalculator::operator()(const ObjectPtr& object)
{
    const auto& policy = cache_.policy();
    if (policy.serviceAttributesLoadPolicy == ServiceAttributesLoadPolicy::Load) {
        loadServiceAttributes(object);
    }
    if (policy.tableAttributesLoadPolicy == TableAttributesLoadPolicy::Load) {
        loadTableAttributes(object);
    }
}

void
RuntimeDataCalculator::operator()(GeoObjectCollection& collection)
{
    const auto& policy = cache_.policy();
    if (policy.serviceAttributesLoadPolicy == ServiceAttributesLoadPolicy::Load) {
        loadServiceAttributes(collection);
    }
    if (policy.tableAttributesLoadPolicy == TableAttributesLoadPolicy::Load) {
        loadTableAttributes(collection);
    }
}

struct LoadInfosInfo
{
    TOIds ids;
    GeoObjectCollection objects;
    StringSet roleIds;
};

void
RuntimeDataCalculator::loadTableAttributes(const ObjectPtr& object)
{
    auto roleIds = object->category().slaveRoleIds(roles::filters::IsTable);
    if (!roleIds.empty()) {
        cache_.relationsManager().loadRelations(
            RelationType::Slave, TOIds { object->id() }, roleIds);
        readTableAttributesFromSlaveInfos(cache_, object->id());
    }
}

void
RuntimeDataCalculator::loadTableAttributes(GeoObjectCollection& collection)
{
    std::map<std::string, LoadInfosInfo> objectIdsByCategories;
    for (const auto& obj : collection) {
        auto& linkInfos = objectIdsByCategories[obj->categoryId()];
        linkInfos.ids.insert(obj->id());
        linkInfos.objects.add(obj);
    }
    for (auto it = objectIdsByCategories.begin(); it != objectIdsByCategories.end(); ) {
        auto roleIds = cfg()->editor()->categories()[it->first].slaveRoleIds(roles::filters::IsTable);
        if (!roleIds.empty()) {
            it->second.roleIds.swap(roleIds);
            ++it;
        } else {
            it = objectIdsByCategories.erase(it);
        }
    }
    for (const auto& objects : objectIdsByCategories) {
        cache_.relationsManager().loadRelations(
            RelationType::Slave, objects.second.ids, objects.second.roleIds);
        for (auto& obj : objects.second.objects) {
            readTableAttributesFromSlaveInfos(cache_, obj->id());
        }
    }
}

void
RuntimeDataCalculator::loadServiceAttributes(const ObjectPtr& object)
{
    const auto& registry = srv_attrs::ServiceAttributesRegistry::get();
    if (!registry.categoryAttrs(object->categoryId()).empty()) {
        auto serviceAttributesVals =
            serviceAttributes(object->id(), object->tablename());
        if (object->original()) {
           object->original()->setServiceAttributes(StringMap(serviceAttributesVals));
        }
        object->setServiceAttributes(std::move(serviceAttributesVals));
    }
}

void
RuntimeDataCalculator::loadServiceAttributes(GeoObjectCollection& collection)
{
    for (auto& obj : collection) {
        loadServiceAttributes(obj);
    }
}

namespace {

StringMap
loadServiceAttributes_(ObjectsCache& cache, TOid id, const std::string& tableName)
{
    views::QueryBuilder qb(cache.branchContext().branch.id());
    qb.selectFields("hstore_to_array(service_attrs)");
    qb.fromTable(tableName, "o");
    qb.whereClause("id=" + std::to_string(id));

    auto r = cache.workView().exec(qb.query());
    return r.empty() ? StringMap() : dbutils::parsePGHstore(r[0][0].c_str());
}

} // namespace

StringMap
RuntimeDataCalculator::serviceAttributes(TOid id, const std::string& tableName)
{
    auto it = cacheServiceAttrs_.find(id);
    if (it != cacheServiceAttrs_.end()) {
        return it->second;
    }
    auto insertResult = cacheServiceAttrs_.insert(
        std::make_pair(id, loadServiceAttributes_(cache_, id, tableName)));
    return insertResult.first->second;
}

} // namespace wiki
} // namespace maps
