#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_METRO_EL;
using categories::TRANSPORT_METRO_JC;
using categories::TRANSPORT_METRO_THREAD;
using categories::TRANSPORT_METRO_LINE;

using categories::TRANSPORT_THREAD_STOP;

namespace {

graph::Edges
graphOutEdges(
    CheckContext* context,
    graph::NodeID sourceJunctionId,
    const std::vector<TId>& network)
{
    auto sourceJunction = context->objects<TRANSPORT_METRO_JC>().byId(sourceJunctionId);
    graph::Edges result;

    for (const auto& elementId : sourceJunction->inElements()) {
        if (std::find(network.begin(), network.end(), elementId) == network.end()) {
            continue;
        }
        auto element = context->objects<TRANSPORT_METRO_EL>().byId(elementId);
        if (context->objects<TRANSPORT_METRO_JC>().loaded(element->startJunction())) {
            result.push_back(graph::Edge(sourceJunctionId, element->startJunction()));
        }
    }
    for (const auto& elementId : sourceJunction->outElements()) {
        if (std::find(network.begin(), network.end(), elementId) == network.end()) {
            continue;
        }
        auto element = context->objects<TRANSPORT_METRO_EL>().byId(elementId);
        if (context->objects<TRANSPORT_METRO_JC>().loaded(element->endJunction())) {
            result.push_back(graph::Edge(sourceJunctionId, element->endJunction()));
        }
    }
    return result;
}

geolib3::Polygon2
buildMetroSccGeometry(
    CheckContext* context,
    const graph::NodeIds& scc)
{
    geolib3::PointsVector points;
    for (const auto& curElement : scc) {
        points.push_back(context->objects<TRANSPORT_METRO_JC>().byId(curElement)->geom());
    }
    return geolib3::bufferedConvexHull(points, utils::BUFFER_DISTANCE);
}

} //namespace

VALIDATOR_SIMPLE_CHECK(transport_metro_thread_topology_validity,
    TRANSPORT_METRO_THREAD, TRANSPORT_METRO_EL, TRANSPORT_METRO_JC,
    TRANSPORT_METRO_LINE, TRANSPORT_OPERATOR)
{
    context->objects<TRANSPORT_METRO_THREAD>().visit([&](const TransportThread* thread) {
        if (!isBoundToOperatorToValidate<TRANSPORT_METRO_LINE>(context, thread)) {
            return;
        }
        std::map<TId, size_t> junctionsValency;
        const auto& elements = thread->elements();
        for (auto elementId : elements) {
            auto element = context->objects<TRANSPORT_METRO_EL>().byId(elementId);
            ++junctionsValency[element->startJunction()];
            ++junctionsValency[element->endJunction()];
        }

        graph::StronglyConnectedComponentsFinder sccFinder;
        for (const auto& junction : junctionsValency) {
            sccFinder.exploreNode(junction.first,
                [&](graph::NodeID nodeId) {
                    return graphOutEdges(context, nodeId, elements);
                });
        };
        const auto& sccs = sccFinder.stronglyConnectedComponents();

        if (sccs.empty()) {
            context->warning(
                "empty-thread-network",
                boost::none,
                {});
            return;
        }
        if (sccs.size() == 1) {
            size_t singleUsed = 0;
            bool overBound = false;
            for (const auto& junctionValency : junctionsValency) {
                if (junctionValency.second == 1) {
                    ++singleUsed;
                    if (thread->isCircular()) {
                        context->error(
                            "thread-circular-topology-invalid",
                            context->objects<TRANSPORT_METRO_JC>().byId(junctionValency.first)->geom(),
                            {thread->id(), junctionValency.first});
                    }
                } else if (junctionValency.second != 2) {
                    overBound = true;
                    context->error(
                        "thread-topology-invalid",
                        context->objects<TRANSPORT_METRO_JC>().byId(junctionValency.first)->geom(),
                        {thread->id(), junctionValency.first});
                }
            }
            if (!overBound &&
                (!thread->isCircular() && singleUsed != 2)) {
                context->error(
                        "thread-linear-topology-invalid",
                        buildMetroSccGeometry(context, sccs.front()),
                        {thread->id()});
            }
        } else {
            auto maxSccIt = std::max_element(sccs.begin(), sccs.end(),
            [](const graph::NodeIds& a, const graph::NodeIds& b)
                { return a.size() < b.size(); });
            for (auto sccIt = sccs.cbegin(); sccIt != sccs.cend(); ++sccIt) {
                if (sccIt != maxSccIt) {
                    context->error(
                        "thread-disjoint-segments",
                        buildMetroSccGeometry(context, *sccIt),
                        {thread->id()});
                }
             }
        }
    });//visit
}

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