#include "module.h"
#include "../transport-thread-common/common.h"
#include "../utils/misc.h"

#include <yandex/maps/wiki/validator/check.h>
#include <yandex/maps/wiki/validator/categories.h>
namespace maps {
namespace wiki {
namespace validator {
namespace checks {

using categories::TRANSPORT_OPERATOR;
using categories::TRANSPORT_THREAD_STOP;
using categories::TRANSPORT_BUS_THREAD;
using categories::TRANSPORT_BUS_ROUTE;
using categories::TRANSPORT_TRAM_THREAD;
using categories::TRANSPORT_TRAM_ROUTE;
using categories::TRANSPORT_TRAM_EL;
using categories::TRANSPORT_WATERWAY_THREAD;
using categories::TRANSPORT_WATERWAY_ROUTE;
using categories::TRANSPORT_WATERWAY_STOP;
using categories::TRANSPORT_WATERWAY_EL;
using categories::TRANSPORT_STOP;

using categories::RD_EL;

namespace {
template <typename StopCategory>
void
checkThreadStations(CheckContext* context, const TransportThread* thread)
{
    std::vector<const typename StopCategory::TObject*> orderedStops;
    std::vector<const TransportThreadStop*> stops;
    if (thread->stops().size() < 2) {
        context->critical(
            "transport-thread-too-few-stops",
            boost::none,
            {thread->id()});
        return;
    }
    for (auto stopId : thread->stops()) {
        stops.push_back(context->objects<TRANSPORT_THREAD_STOP>().byId(stopId));
    }
    if (checkThreadStopSequenceHasFatalErrors(context, stops, thread->id())) {
        return;
    }
    restoreThreadStopSequence(stops);
    for (const auto& stop : stops) {
        auto transportStop = stopByThreadStop<StopCategory>(context, stop->id());
        if (transportStop) {
            orderedStops.emplace_back(transportStop);
        }
    }
    if (orderedStops.size() < 2) {
        context->critical(
            "transport-stops-not-loaded",
            boost::none,
            {thread->id()});
        return;
    }
    std::set<TId> repeating;
    if (!thread->isCircular() && orderedStops.front()->id() == orderedStops.back()->id()) {
        repeating.insert(orderedStops.front()->id());
    } else if (thread->isCircular() && orderedStops.front()->id() != orderedStops.back()->id()) {
        context->critical(
            "transport-thread-stops-circularity-broken",
            boost::none,
            {orderedStops.front()->id(), orderedStops.back()->id(), thread->id()});
    }
    bool areInnerStopsRepeating = false;
    for (size_t s = 1; s < orderedStops.size(); ++s) {
        if (orderedStops[s]->id() == orderedStops[s - 1]->id()) {
            repeating.insert(orderedStops[s]->id());
            areInnerStopsRepeating = true;
        }
    }
    if (!repeating.empty()) {
        repeating.insert(thread->id());
        context->report(
            areInnerStopsRepeating
                ? Severity::Critical
                : Severity::Warning,
            "transport-thread-stops-repeating-successively",
            boost::none,
            {repeating.begin(), repeating.end()});
    }
}
void
checkElements(CheckContext* context, const TransportThread* thread)
{
    if (thread->elements().empty()) {
        context->critical("transport-thread-without-elements",
            boost::none,
            {thread->id()});
    }
}

void
checkElementsRoadElementAccess(CheckContext* context, const TransportThread* thread)
{
    for (auto elementId : thread->elements()) {
        if (!context->objects<RD_EL>().loaded(elementId)) {
            continue;
        }
        auto rdEl = context->objects<RD_EL>().byId(elementId);
        auto accessId = rdEl->accessId();
        if (!(accessId & common::AccessId::Bus) && !rdEl->backBus()) {
            context->critical(
                "transport-thread-element-invalid-access",
                utils::geomForReport(rdEl),
                {thread->id(), elementId});
        }
    }
}

}//namespace

VALIDATOR_CHECK_PART( transport_thread_relations, bus_relations,
        TRANSPORT_BUS_THREAD, TRANSPORT_THREAD_STOP, TRANSPORT_STOP, TRANSPORT_BUS_ROUTE, RD_EL,
        TRANSPORT_OPERATOR)
{
    context->objects<TRANSPORT_BUS_THREAD>().visit([&](const TransportThread* thread) {
        if (!isBoundToOperatorToValidate<TRANSPORT_BUS_ROUTE>(context, thread)) {
            return;
        }
        checkElements(context, thread);
        checkElementsRoadElementAccess(context, thread);
        checkThreadStations<TRANSPORT_STOP>(context, thread);
    });
}

VALIDATOR_CHECK_PART( transport_thread_relations, tram_relations,
        TRANSPORT_TRAM_THREAD, TRANSPORT_THREAD_STOP, TRANSPORT_STOP, TRANSPORT_TRAM_ROUTE,
        TRANSPORT_OPERATOR)
{
    context->objects<TRANSPORT_TRAM_THREAD>().visit([&](const TransportThread* thread) {
        if (!isBoundToOperatorToValidate<TRANSPORT_TRAM_ROUTE>(context, thread)) {
            return;
        }
        checkElements(context, thread);
        checkThreadStations<TRANSPORT_STOP>(context, thread);
    });
}

VALIDATOR_CHECK_PART( transport_thread_relations, waterway_relations,
        TRANSPORT_WATERWAY_THREAD, TRANSPORT_THREAD_STOP, TRANSPORT_WATERWAY_STOP,
        TRANSPORT_WATERWAY_ROUTE, TRANSPORT_OPERATOR)
{
    context->objects<TRANSPORT_WATERWAY_THREAD>().visit([&](const TransportThread* thread) {
        if (!isBoundToOperatorToValidate<TRANSPORT_WATERWAY_ROUTE>(context, thread)) {
            return;
        }
        checkElements(context, thread);
        checkThreadStations<TRANSPORT_WATERWAY_STOP>(context, thread);
    });
}

namespace {
template <typename StopCategory, typename ThreadCategory, typename RouteCategory>
void checkStopThreadRouteRelations(CheckContext* context)
{
    std::map<TId, std::set<TId>> stopsRoutes;
    context->objects<StopCategory>().visit([&](const TransportStop* stop) {
        const auto& routes = stop->routes();
        stopsRoutes.emplace(
            stop->id(),
            std::set<TId>(routes.begin(), routes.end()));
    });
    context->objects<ThreadCategory>().visit([&](const TransportThread* thread) {
        if (!isBoundToOperatorToValidate<RouteCategory>(context, thread)) {
            return;
        }
        for (auto threadStopId : thread->stops()) {
            auto transportStop = stopByThreadStop<StopCategory>(context, threadStopId);
            if (!transportStop) {
                continue;
            }
            const auto transportStopId = transportStop->id();
            if (stopsRoutes.count(transportStopId) &&
                !stopsRoutes.at(transportStopId).count(thread->line())) {
                context->critical(
                    "transport-stop-not-assigned-to-thread-route",
                    transportStop->geom(),
                    {thread->id(), transportStopId, thread->line(), threadStopId});
            }
        }

    });
}
}//namespace

VALIDATOR_CHECK_PART(transport_stop_thread_route_relations,
    bus_stop_thread_route_relations,
    TRANSPORT_THREAD_STOP, TRANSPORT_STOP,
    TRANSPORT_BUS_THREAD, TRANSPORT_BUS_ROUTE, TRANSPORT_OPERATOR)
{
    checkStopThreadRouteRelations<
        TRANSPORT_STOP,
        TRANSPORT_BUS_THREAD,
        TRANSPORT_BUS_ROUTE>(context);
}

VALIDATOR_CHECK_PART(transport_stop_thread_route_relations,
    tram_stop_thread_route_relations,
    TRANSPORT_THREAD_STOP, TRANSPORT_STOP,
    TRANSPORT_TRAM_THREAD, TRANSPORT_TRAM_ROUTE,
    TRANSPORT_TRAM_EL, TRANSPORT_OPERATOR)
{
    checkStopThreadRouteRelations<
        TRANSPORT_STOP,
        TRANSPORT_TRAM_THREAD,
        TRANSPORT_TRAM_ROUTE>(context);
}

VALIDATOR_CHECK_PART(transport_stop_thread_route_relations,
    waterway_stop_thread_route_relations,
    TRANSPORT_THREAD_STOP, TRANSPORT_WATERWAY_STOP,
    TRANSPORT_WATERWAY_THREAD, TRANSPORT_WATERWAY_ROUTE,
    TRANSPORT_WATERWAY_EL, TRANSPORT_OPERATOR)
{
    checkStopThreadRouteRelations<
        TRANSPORT_WATERWAY_STOP,
        TRANSPORT_WATERWAY_THREAD,
        TRANSPORT_WATERWAY_ROUTE>(context);
}

} // namespace checks
} // namespace validator
} // namespace wiki
} // namespace maps
