#include "transport_thread.h"
#include "generic.h"
#include "calc.h"
#include <maps/wikimap/mapspro/services/editor/src/objects/object.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/complex_object.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/point_object.h>
#include "registry.h"
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/categories_strings.h>

#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/master_role.h>
#include <yandex/maps/wiki/common/string_utils.h>

namespace maps::wiki::srv_attrs
{

class TRANSPORT_PASSAGEWAY : public ServiceAttributesRegistry::Registrar
{
public:
    TRANSPORT_PASSAGEWAY(ServiceAttributesRegistry& registry)
        :ServiceAttributesRegistry::Registrar(registry, CATEGORY_TRANSPORT_PASSAGEWAY)
    {
        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<ComplexObject>(screenLabel))
        .depends(
        {
            {Aspect::Type::Attribute, TRANSPORT_METRO_NM},
            {
                {RelationType::Slave, ROLE_EXIT, CATEGORY_TRANSPORT_METRO_EXIT},
            }
        })
        .depends(
        {
            {Aspect::Type::Attribute, {}},
            {
                {RelationType::Slave, ROLE_BOARDING, CATEGORY_TRANSPORT_PASSAGEWAY_BOARDING},
            }
        });
    }
private:
    static std::string screenLabel(const ComplexObject* obj, ObjectsCache& cache)
    {
        auto exits = obj->slaveRelations().range(ROLE_EXIT);
        if (exits.empty()) {
            WARN() << "SRV_ATTRS: " << CATEGORY_TRANSPORT_PASSAGEWAY << " has no exists: " << obj->id();
            return {};
        };
        auto title = objectNameByType(exits.begin()->id(), NAME_TYPE_OFFICIAL, cache);
        const auto boardings = obj->slaveRelations().range(ROLE_BOARDING).size();
        if (boardings) {
            title += " (" + std::to_string(boardings) + ")";
        }
        return title;
    }

};

class TRANSPORT_TRANSITION : public ServiceAttributesRegistry::Registrar
{
public:
    TRANSPORT_TRANSITION(ServiceAttributesRegistry& registry)
        :ServiceAttributesRegistry::Registrar(registry, CATEGORY_TRANSPORT_TRANSITION)
    {
        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<ComplexObject>(screenLabel))
        .depends(
        {
            {Aspect::Type::Attribute, TRANSPORT_METRO_NM},
            {
                {RelationType::Slave, ROLE_STATION_A, CATEGORY_TRANSPORT_METRO_STATION},
            }
        })
        .depends(
        {
            {Aspect::Type::Attribute, TRANSPORT_METRO_NM},
            {
                {RelationType::Slave, ROLE_STATION_B, CATEGORY_TRANSPORT_METRO_STATION},
            }
        })
        .depends(
        {
            {Aspect::Type::Attribute, {}},
            {
                {RelationType::Slave, ROLE_BOARDING, CATEGORY_TRANSPORT_TRANSITION_BOARDING},
            }
        });
    }
private:
    static std::string screenLabel(const ComplexObject* obj, ObjectsCache& cache)
    {
        auto stationA = obj->slaveRelations().range(ROLE_STATION_A);
        auto stationB = obj->slaveRelations().range(ROLE_STATION_B);
        if (stationA.empty() || stationB.empty()) {
            WARN() << "SRV_ATTRS: " << CATEGORY_TRANSPORT_TRANSITION << " missing stations: " << obj->id();
            return {};
        }
        auto stationAName = objectNameByType(stationA.begin()->id(), NAME_TYPE_OFFICIAL, cache);
        auto stationBName = objectNameByType(stationB.begin()->id(), NAME_TYPE_OFFICIAL, cache);
        const auto& label = cfg()->editor()->categories()[CATEGORY_TRANSPORT_METRO_STATION].label();
        if (stationAName.empty()) {
            stationAName = label;
        }
        if (stationBName.empty()) {
            stationBName = label;
        }
        auto title = stationAName + NICE_DASH + stationBName;
        const auto boardings = obj->slaveRelations().range(ROLE_BOARDING).size();
        if (boardings) {
            title += " (" + std::to_string(boardings) + ")";
        }
        return title;
    }
};

namespace {
std::string
assignedStationName(
    TOid threadStopId,
    ObjectsCache& cache,
    const std::string& stationCategoryId)
{
    auto threadStop = cache.getExisting(threadStopId);
    auto masterStations = threadStop->masterRelations().range(ROLE_ASSIGNED_THREAD_STOP);
    if (masterStations.size() != 1) {
        WARN() << "SRV_ATTRS: thread_stop wrong masters size: " << threadStopId;
        return {};
    }
    auto stationName = objectNameByType(masterStations.begin()->id(), NAME_TYPE_OFFICIAL, cache);
    return
        stationName.empty()
        ? cfg()->editor()->categories()[stationCategoryId].label()
        : stationName;
}

std::string
firstToLastStationsLabel(
    const ComplexObject* obj,
    ObjectsCache& cache,
    const std::string& stationCategoryId)
{
    auto threadStops = obj->slaveRelations().range(ROLE_PART);
    if (threadStops.empty()) {
        return s_emptyString;
    }
    TOIds previousIds;
    TOIds stationIds;
    TOid firstStationId {0};
    for (const auto& rel : threadStops) {
        stationIds.insert(rel.id());
        auto prevs = rel.relative()->slaveRelations().range(ROLE_PREVIOUS);
        if (prevs.empty()) {
            if (firstStationId) {
                WARN() << "Thread: " << obj->id() << " has broken thread_stop sequence.";
            }
            firstStationId = rel.id();
        } else {
            for (const auto& prevRel : prevs) {
                previousIds.insert(prevRel.id());
            }
        }
    }
    if (!firstStationId) {
        return s_emptyString;
    }
    std::vector<TOid> lastStations;
    std::set_difference(
        stationIds.begin(), stationIds.end(),
        previousIds.begin(), previousIds.end(),
        std::back_inserter(lastStations));
    if (lastStations.size () != 1 ) {
        WARN() << "Thread: " << obj->id() << " has broken thread_stop sequence.";
    }
    if (lastStations.empty()) {
        return s_emptyString;
    }

    return
        assignedStationName(firstStationId, cache, stationCategoryId) +
        NICE_DASH +
        assignedStationName(lastStations[0], cache, stationCategoryId);
}
}//namespace

class TRANSPORT_METRO_THREAD : public ServiceAttributesRegistry::Registrar
{
public:
    TRANSPORT_METRO_THREAD(ServiceAttributesRegistry& registry)
        :ServiceAttributesRegistry::Registrar(registry, CATEGORY_TRANSPORT_METRO_THREAD)
    {
        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<ComplexObject>(screenLabel))
            .depends(
            {
                {Aspect::Type::Attribute, TRANSPORT_METRO_NM},
                {
                    {RelationType::Slave, ROLE_PART, CATEGORY_TRANSPORT_THREAD_STOP},
                    {RelationType::Master, ROLE_ASSIGNED_THREAD_STOP, CATEGORY_TRANSPORT_METRO_STATION}
                }
            });
    }
    static std::string screenLabel(const ComplexObject* obj, ObjectsCache& cache)
    {
        return firstToLastStationsLabel(obj, cache, CATEGORY_TRANSPORT_METRO_STATION);
    }
};

class TRANSPORT_WATERWAY_THREAD : public ServiceAttributesRegistry::Registrar
{
public:
    TRANSPORT_WATERWAY_THREAD(ServiceAttributesRegistry& registry)
        :ServiceAttributesRegistry::Registrar(registry, CATEGORY_TRANSPORT_WATERWAY_THREAD)
    {
        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<ComplexObject>(screenLabel))
            .depends(
            {
                {Aspect::Type::Attribute, TRANSPORT_WATERWAY_NM},
                {
                    {RelationType::Slave, ROLE_PART, CATEGORY_TRANSPORT_THREAD_STOP},
                    {RelationType::Master, ROLE_ASSIGNED_THREAD_STOP, CATEGORY_TRANSPORT_WATERWAY_STOP}
                }
            });
    }
    static std::string screenLabel(const ComplexObject* obj, ObjectsCache& cache)
    {
        return firstToLastStationsLabel(obj, cache, CATEGORY_TRANSPORT_WATERWAY_STOP);
    }
};

class TRANSPORT_STOP : public ServiceAttributesRegistry::Registrar
{
public:
    TRANSPORT_STOP(ServiceAttributesRegistry& registry)
        : ServiceAttributesRegistry::Registrar(registry, CATEGORY_TRANSPORT_STOP)
    {
        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<PointObject>(GenericNamedObject::screenLabel));
        registerSuggestCallback(SuggestCallbackWrapper<PointObject>(GenericNamedObject::suggestTexts));
        auto& hotspot = registerAttr(SRV_HOTSPOT_LABEL, CallbackWrapper<PointObject>(hotspotLabel));
        for (const auto& role : cfg()->editor()->categories()[CATEGORY_TRANSPORT_STOP].masterRoles()) {
            if (role.roleId() != ROLE_ASSIGNED) {
                continue;
            }
            hotspot.depends(
                {
                   {Aspect::Type::Attribute, TRANSPORT_NM},
                    {
                            {RelationType::Master, ROLE_ASSIGNED, role.categoryId()}
                    }
                }
            );
        }
        hotspot.depends(
            {
                {Aspect::Type::Attribute,
                    ftTypeAttrDefForCategory(CATEGORY_TRANSPORT_BUS_ROUTE)->id()},
                {
                    {RelationType::Master, ROLE_ASSIGNED, CATEGORY_TRANSPORT_BUS_ROUTE}
                }
            }
        );
    }
private:
    static std::string hotspotLabel(const PointObject* stop, ObjectsCache& cache)
    {
        std::string label = objectNameByType(stop->id(), NAME_TYPE_OFFICIAL, cache);
        if (label.empty()) {
            label = stop->category().label();
        }
        const auto& routes = stop->masterRelations().range(ROLE_ASSIGNED);
        std::map<std::string, std::set<std::string, common::natural_sort>> routesByLabel;
        const auto& categories = cfg()->editor()->categories();
        for (const auto& rd : routes) {
            const auto& ftTypeAttr = ftTypeAttrDefForCategory(rd.categoryId());
            auto routeLabel = ftTypeAttr
                ? valueLabel(ftTypeAttr->id(),
                        rd.relative()->attributes().value(ftTypeAttr->id()))
                : categories[rd.categoryId()].label();

            routesByLabel[routeLabel].insert(
                    objectNameByType(rd.id(), NAME_TYPE_OFFICIAL, cache));
        }
        for (const auto& routes : routesByLabel) {
            if (!label.empty()) {
                label += "\n";
            };
            label = label
                + routes.first
                + ": "
                + common::join(routes.second, ", ");
        }
        return label;

    }
};

class MASSTRANSIT_THREAD : public ServiceAttributesRegistry::Registrar
{
public:
    MASSTRANSIT_THREAD(ServiceAttributesRegistry& registry, const std::string& threadCategory)
        : ServiceAttributesRegistry::Registrar(registry, threadCategory)
    {
        auto routeRoles = cfg()->editor()->categories()[threadCategory].masterRole(ROLE_ASSIGNED_THREAD);
        ASSERT(routeRoles.size() == 1);
        auto routeCategory = routeRoles.front().categoryId();
        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<ComplexObject>(screenLabel))
            .depends(
                {
                    {Aspect::Type::Attribute, TRANSPORT_NM},
                    {
                        {RelationType::Slave, ROLE_PART, CATEGORY_TRANSPORT_THREAD_STOP},
                        {RelationType::Master, ROLE_ASSIGNED_THREAD_STOP, CATEGORY_TRANSPORT_STOP}
                    }
                })
            .depends(
                {
                    {Aspect::Type::Attribute, TRANSPORT_NM},
                    {
                        {RelationType::Master, ROLE_ASSIGNED_THREAD, routeCategory}
                    }
                });
    }

    static std::string screenLabel(
        const ComplexObject* obj,
        ObjectsCache& cache)
    {
        auto routes = obj->masterRelations().range(ROLE_ASSIGNED_THREAD);
        if (routes.empty()) {
            return s_emptyString;
        }
        return
            objectNameByType(routes.begin()->id(), NAME_TYPE_OFFICIAL, cache) +
            " (" +
            firstToLastStationsLabel(obj, cache, CATEGORY_TRANSPORT_STOP) +
            ")";
    }
};

class MASSTRANSIT_THREAD_CONNECTOR : public ServiceAttributesRegistry::Registrar
{
public:
    MASSTRANSIT_THREAD_CONNECTOR(ServiceAttributesRegistry& registry, const std::string& threadConnectorCategory)
        : ServiceAttributesRegistry::Registrar(registry, threadConnectorCategory)
    {
        const auto& threadRole = cfg()->editor()->categories()[threadConnectorCategory].slaveRole(ROLE_SRC_THREAD);
        const auto threadCategory = threadRole.categoryId();

        const auto& routeRoles = cfg()->editor()->categories()[threadCategory].masterRole(ROLE_ASSIGNED_THREAD);
        ASSERT(routeRoles.size() == 1);
        const auto routeCategory = routeRoles.front().categoryId();

        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<ComplexObject>(screenLabel))
            .depends(
                {
                    {Aspect::Type::Attribute, TRANSPORT_NM},
                    {
                        {RelationType::Slave, ROLE_SRC_THREAD, threadCategory},
                        {RelationType::Slave, ROLE_PART, CATEGORY_TRANSPORT_THREAD_STOP},
                        {RelationType::Master, ROLE_ASSIGNED_THREAD_STOP, CATEGORY_TRANSPORT_STOP}
                    }
                })
            .depends(
                {
                    {Aspect::Type::Attribute, TRANSPORT_NM},
                    {
                        {RelationType::Slave, ROLE_DST_THREAD, threadCategory},
                        {RelationType::Slave, ROLE_PART, CATEGORY_TRANSPORT_THREAD_STOP},
                        {RelationType::Master, ROLE_ASSIGNED_THREAD_STOP, CATEGORY_TRANSPORT_STOP}
                    }
                })
            .depends(
                {
                    {Aspect::Type::Attribute, TRANSPORT_NM},
                    {
                        {RelationType::Slave, ROLE_SRC_THREAD, threadCategory},
                        {RelationType::Master, ROLE_ASSIGNED_THREAD, routeCategory}
                    }
                })
            .depends(
                {
                    {Aspect::Type::Attribute, TRANSPORT_NM},
                    {
                        {RelationType::Slave, ROLE_DST_THREAD, threadCategory},
                        {RelationType::Master, ROLE_ASSIGNED_THREAD, routeCategory}
                    }
                });
    }

private:
    static std::string screenLabel(
        const ComplexObject* obj,
        ObjectsCache& cache)
    {
        auto srcThreads = obj->slaveRelations().range(ROLE_SRC_THREAD);
        auto dstThreads = obj->slaveRelations().range(ROLE_DST_THREAD);

        if (srcThreads.empty() || dstThreads.empty()) {
            return s_emptyString;
        }

        auto srcThread = dynamic_pointer_cast<ComplexObject>(
            cache.getExisting(srcThreads.begin()->id()));
        auto dstThread = dynamic_pointer_cast<ComplexObject>(
            cache.getExisting(dstThreads.begin()->id()));

        return
            MASSTRANSIT_THREAD::screenLabel(srcThread.get(), cache) +
            " / " +
            MASSTRANSIT_THREAD::screenLabel(dstThread.get(), cache);
    }
};

TRANSPORT_THREADServiceAttributes::TRANSPORT_THREADServiceAttributes(ServiceAttributesRegistry& registry)
{
    TRANSPORT_METRO_THREAD metroThread(registry);
    TRANSPORT_TRANSITION transition(registry);
    TRANSPORT_PASSAGEWAY passageway(registry);

    TRANSPORT_WATERWAY_THREAD waterwayThread(registry);

    MASSTRANSIT_THREAD busThread(registry, CATEGORY_TRANSPORT_BUS_THREAD);
    MASSTRANSIT_THREAD tramThread(registry, CATEGORY_TRANSPORT_TRAM_THREAD);
    TRANSPORT_STOP stop(registry);

    MASSTRANSIT_THREAD_CONNECTOR busThreadConnector(registry, CATEGORY_TRANSPORT_BUS_THREAD_CONNECTOR);
    MASSTRANSIT_THREAD_CONNECTOR tramThreadConnector(registry, CATEGORY_TRANSPORT_TRAM_THREAD_CONNECTOR);
    MASSTRANSIT_THREAD_CONNECTOR waterwayThreadConnector(registry, CATEGORY_TRANSPORT_WATERWAY_THREAD_CONNECTOR);
    MASSTRANSIT_THREAD_CONNECTOR metroThreadConnector(registry, CATEGORY_TRANSPORT_METRO_THREAD_CONNECTOR);
}

} // namespace maps::wiki::srv_attrs
