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

#include "../utils/misc.h"
#include "../utils/road_utils.h"

#include <yandex/maps/wiki/validator/check.h>
#include <yandex/maps/wiki/validator/categories.h>
#include <yandex/maps/wiki/graph/strongly_connected_components.h>
#include <yandex/maps/wiki/routing/route.h>
#include <yandex/maps/wiki/routing/exception.h>

#include <maps/libs/geolib/include/convex_hull.h>

#include <boost/none_t.hpp>
#include <algorithm>

namespace maps {
namespace wiki {
namespace validator {
namespace checks {

using categories::TRANSPORT_OPERATOR;
using categories::TRANSPORT_BUS_THREAD;
using categories::TRANSPORT_BUS_ROUTE;
using categories::TRANSPORT_TRAM_THREAD;
using categories::TRANSPORT_TRAM_ROUTE;
using categories::TRANSPORT_WATERWAY_THREAD;
using categories::TRANSPORT_WATERWAY_ROUTE;
using categories::TRANSPORT_STOP;
using categories::TRANSPORT_WATERWAY_STOP;
using categories::TRANSPORT_THREAD_STOP;
using categories::COND;
using categories::RD_EL;
using categories::RD_JC;
using categories::TRANSPORT_TRAM_EL;
using categories::TRANSPORT_TRAM_JC;
using categories::TRANSPORT_WATERWAY_EL;
using categories::TRANSPORT_WATERWAY_JC;

namespace {
const double MAX_ROUTE_TO_STOP_DISTANCE = 50.0;

template <typename ElementCategory>
routing::Direction
direction(const typename ElementCategory::TObject* obj)
{
    auto dir = obj->direction();
    if (dir == routing::Direction::Both) {
        return routing::Direction::Both;
    }
    if (!obj->backBus()) {
        return dir;
    }

    if ((obj->accessId() & common::AccessId::Bus) != common::AccessId::None) {
        return routing::Direction::Both;
    }
    return
        dir == routing::Direction::Forward
        ? routing::Direction::Backward
        : routing::Direction::Forward;
}

template <typename ElementCategory>
int
fromZlev(const typename ElementCategory::TObject* obj)
{
    return obj->fromZlevel();
}

template <typename ElementCategory>
int
toZlev(const typename ElementCategory::TObject* obj)
{
    return obj->toZlevel();
}

template<>
routing::Direction
direction<TRANSPORT_TRAM_EL>(const TRANSPORT_TRAM_EL::TObject* /*obj*/)
{
    return routing::Direction::Both;
}

template<>
routing::Direction
direction<TRANSPORT_WATERWAY_EL>(const TRANSPORT_TRAM_EL::TObject* /*obj*/)
{
    return routing::Direction::Both;
}

template <>
int
fromZlev<TRANSPORT_WATERWAY_EL>(const TRANSPORT_TRAM_EL::TObject* /*obj*/)
{
    return 0;
}

template <>
int
toZlev<TRANSPORT_WATERWAY_EL>(const TRANSPORT_TRAM_EL::TObject* /*obj*/)
{
    return 0;
}

template <typename ElementCategory, typename StopCategory>
void
checkThreadRouting(CheckContext* context, const TransportThread* thread,
    const routing::Conditions& conditions)
{
    if (thread->elements().empty()) {
            context->critical(
                "transport-thread-without-elements",
                boost::none,
                { thread->id() });
        return;
    }
    if (thread->stops().size() <= 1) {
        context->critical(
            "transport-thread-too-few-stops",
            boost::none,
            { thread->id() });
        return;
    }
    routing::Stops routingStops;
    routing::Elements routingElemets;
    const auto& threadElements = thread->elements();
    for (auto elementId : threadElements) {
        if (!context->objects<ElementCategory>().loaded(elementId)) {
            context->error(
                "linked-object-is-outside-of-validation-area",
                boost::none,
                {thread->id(), elementId});
                return;
        }
        const auto* roadElement = context->objects<ElementCategory>().byId(elementId);
        routingElemets.emplace_back(
            elementId,
            direction<ElementCategory>(roadElement),
            roadElement->geom(),
            routing::ElementEnd {
                roadElement->startJunction(),
                fromZlev<ElementCategory>(roadElement)},
            routing::ElementEnd {
                roadElement->endJunction(),
                toZlev<ElementCategory>(roadElement)}
        );
    }
    std::vector<const TransportThreadStop*> stops;
    for (auto stopId : thread->stops()) {
        stops.push_back(context->objects<TRANSPORT_THREAD_STOP>().byId(stopId));
    }
    restoreThreadStopSequence(stops);
    for (const auto& stop : stops) {
        auto transportStop = stopByThreadStop<StopCategory>(context, stop->id());
        if (transportStop) {
            routingStops.emplace_back(transportStop->id(), transportStop->geom());
        }
    }
    try {
        auto result = routing::restore(
            routingElemets,
            routingStops,
            conditions,
            MAX_ROUTE_TO_STOP_DISTANCE
        );

        if (!result.unusedElementIds().empty()) {
            auto idsToReport = result.unusedElementIds();
            idsToReport.insert(thread->id());
            context->warning(
                "transport-thread-unused-elements",
                boost::none,
                {idsToReport.begin(), idsToReport.end()}
            );
        }

        for (const auto& error: result.noPathErrors()) {
            context->critical(
                "transport-thread-no-route",
                boost::none,
                {thread->id(), error.fromStopId(), error.toStopId()}
            );
        }

        for (const auto& error: result.forwardAmbiguousPathErrors()) {
            context->error(
                "transport-thread-ambiguous-route",
                boost::none,
                {thread->id(), error.fromStopId(), error.toStopId(), error.elementId()}
            );
        }

        for (const auto& error: result.backwardAmbiguousPathErrors()) {
            context->warning(
                "transport-thread-ambiguous-route",
                boost::none,
                {thread->id(), error.fromStopId(), error.toStopId(), error.elementId()}
            );
        }
    } catch (const routing::ImpossibleSnapStopError& ex) {
        auto idsToReport = ex.stopIds();
        idsToReport.insert(thread->id());
        context->critical(
            "transport-thread-station-snap-error",
            boost::none,
            {idsToReport.begin(), idsToReport.end()}
        );
    }
}

void
buildRdElSet(std::map<TId, std::set<TId>>& rdElIdToConditionIds, const TransportThread* thread)
{
    for (const auto& elId : thread->elements()) {
        rdElIdToConditionIds.insert({elId, {}});
    }
}

void
mapConditionId(std::map<TId, std::set<TId>>& rdElIdToConditionIds, const COND::TObject* cond)
{
    if (rdElIdToConditionIds.count(cond->fromRoadElement())) {
        rdElIdToConditionIds[cond->fromRoadElement()].insert(cond->id());
    }
    for (const auto& toElementId : cond->toRoadElements()) {
        if (rdElIdToConditionIds.count(toElementId.second)) {
            rdElIdToConditionIds[toElementId.second].insert(cond->id());
        }
    }
}

routing::Conditions
conditions(const std::map<TId, std::set<TId>>& rdElIdToConditionIds,
    CheckContext* context, const TransportThread* thread)
{
    routing::Conditions conditions;
    std::set<TId> foundConitions;
    for (const auto& elId : thread->elements()) {
        if (!rdElIdToConditionIds.count(elId)) {
            continue;
        }
        for (const auto& condId : rdElIdToConditionIds.at(elId)) {
            if (foundConitions.count(condId)) {
                continue;
            }
            auto cond = context->objects<COND>().byId(condId);
            if (cond->type() != common::ConditionType::Prohibited &&
                cond->type() != common::ConditionType::Uturn) {
                    continue;
            }
            if ((cond->accessId() & common::AccessId::Bus) == common::AccessId::None) {
                continue;
            }
            std::vector<TId> toElements;
            for (const auto& seqNumId : cond->toRoadElements()) {
                toElements.push_back(seqNumId.second);
            }
            foundConitions.insert(condId);
            conditions.emplace_back(cond->id(), cond->type(),
                cond->fromRoadElement(), cond->viaJunction(),
                toElements);
        }
    }
    return conditions;
}

template <typename ThreadCategory, typename RouteCategory>
void
checkRoadElementsThreadsRoutingWithConditions(CheckContext* context)
{
    std::map<TId, std::set<TId>> rdElIdToConditionIds;
    context->objects<ThreadCategory>().visit([&](const TransportThread* thread) {
        if (!isBoundToOperatorToValidate<RouteCategory>(context, thread)) {
            return;
        }
        buildRdElSet(rdElIdToConditionIds, thread);
    });
    context->objects<COND>().visit([&](const COND::TObject* cond) {
        mapConditionId(rdElIdToConditionIds, cond);
    });
    context->objects<ThreadCategory>().visit([&](const TransportThread* thread) {
        if (!isBoundToOperatorToValidate<RouteCategory>(context, thread)) {
            return;
        }
        checkThreadRouting<RD_EL, TRANSPORT_STOP>(context, thread,
            conditions(rdElIdToConditionIds, context, thread));
    });
}
}//namespace

VALIDATOR_CHECK_PART( transport_thread_topology_validity, bus_thread,
    TRANSPORT_BUS_THREAD, TRANSPORT_STOP, TRANSPORT_THREAD_STOP, RD_EL, RD_JC,
    COND, TRANSPORT_BUS_ROUTE, TRANSPORT_OPERATOR)
{
    checkRoadElementsThreadsRoutingWithConditions<TRANSPORT_BUS_THREAD, TRANSPORT_BUS_ROUTE>(context);
}

VALIDATOR_CHECK_PART( transport_thread_topology_validity, tram_thread,
    TRANSPORT_TRAM_THREAD, TRANSPORT_STOP, TRANSPORT_THREAD_STOP, TRANSPORT_TRAM_EL,
    TRANSPORT_TRAM_JC, TRANSPORT_TRAM_ROUTE, TRANSPORT_OPERATOR)
{
    context->objects<TRANSPORT_TRAM_THREAD>().visit([&](const TransportThread* thread) {
        if (!isBoundToOperatorToValidate<TRANSPORT_TRAM_ROUTE>(context, thread)) {
            return;
        }
        checkThreadRouting<TRANSPORT_TRAM_EL, TRANSPORT_STOP>(context, thread, {});
    });
}

VALIDATOR_CHECK_PART( transport_thread_topology_validity, waterway_thread,
    TRANSPORT_WATERWAY_THREAD, TRANSPORT_WATERWAY_STOP, TRANSPORT_THREAD_STOP, TRANSPORT_WATERWAY_EL,
    TRANSPORT_WATERWAY_JC,  TRANSPORT_WATERWAY_ROUTE, TRANSPORT_OPERATOR)
{
    context->objects<TRANSPORT_WATERWAY_THREAD>().visit([&](const TransportThread* thread) {
        if (!isBoundToOperatorToValidate<TRANSPORT_WATERWAY_ROUTE>(context, thread)) {
            return;
        }
        checkThreadRouting<TRANSPORT_WATERWAY_EL, TRANSPORT_WATERWAY_STOP>(context, thread, {});
    });
}

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