#include "static_graph_loader.h"

#include "data_model.h"

#include <boost/algorithm/cxx11/any_of.hpp>

#include <iostream>

namespace maps::mrc::gen_targets {

namespace {

const double UTURN_SIMILAR_ANGLE_TRESHOLD = M_PI / 20;

RoadNetworkData loadStaticGraph(const road_graph::Graph& graph,
                                const succinct_rtree::Rtree& rtree,
                                const road_graph::PersistentIndex& persistentIndex,
                                const maps::geolib3::BoundingBox bbox,
                                size_t allowedUturnsFc,
                                bool allowTollRoads)
{
    // Note: There may be muptile edge IDs for the same actual edge that share
    //       its properties. One of the is called a "base edge id". Only the
    //       base IDs are found in the persistent index, so non-base IDs are
    //       filtered out.
    const auto isBaseEdgeId = [&](road_graph::EdgeId edgeId) {
        return graph.isBase(edgeId);
    };

    auto edgeIds = rtree.edgesInWindow(bbox);

    Edges edges;
    for (road_graph::EdgeId edgeId : edgeIds) {
        if (!isBaseEdgeId(edgeId)) {
            continue;
        }
        const road_graph::EdgeData& edgeData = graph.edgeData(edgeId);
        if (edgeData.type() == road_graph::EdgeType::BoatFerry
            || !(edgeData.accessIdMask() & road_graph::AccessId::Automobile)
            || (!allowTollRoads && edgeData.isToll()) // driver should pay for the road entrace
            ) {
            continue;
        }
        Edge& edge = edges[edgeId.value()];
        edge.id = edgeId.value();
        if (auto longId = persistentIndex.findLongId(edgeId)) {
            edge.persistentId = *longId;
        } else {
            continue;
        }
        edge.geom = geolib3::Polyline2(edgeData.geometry());
        edge.fc = edgeData.category();
        edge.length = edgeData.length();
        edge.time = edge.length / edgeData.speed();
        edge.isTarget = false;
        edge.isUTurn = edgeData.type() == road_graph::EdgeType::Uturn;
        edge.isJunction = edgeData.type() == road_graph::EdgeType::Junction;
        edge.isToll = edgeData.isToll();
        edge.endsWithBarrier = false;

        // check if the current edge is connected with edge with fc < allowedUturnsFc
        bool uTurnsAreAllowed = false;
        if (edgeData.category() >= allowedUturnsFc) {
            uTurnsAreAllowed = true;
            for (const auto& outgoingEdge: graph.outEdges(graph.edge(edgeId).target)) {
                if (graph.edgeData(outgoingEdge.id).category()
                    < allowedUturnsFc) {
                    uTurnsAreAllowed = false;
                    break;
                }
            }
        }

        for (const auto& outgoingEdge: graph.outEdges(graph.edge(edgeId).target)) {
            if (graph.turnData(edgeId, outgoingEdge.id).isAccessPassFor(road_graph::AccessId::Automobile)) {
                edge.endsWithBarrier = true;
            }
        }
        for (const auto& outgoingEdge: graph.outEdges(graph.edge(edgeId).target)) {
            if (!graph.turnData(edgeId, outgoingEdge.id).isAccessPassFor(road_graph::AccessId::Automobile)
                && !graph.turnData(edgeId, outgoingEdge.id).isForbiddenFor(road_graph::AccessId::Automobile)) {
                edge.endsWithBarrier = false;
            }
        }
        // allow U turns near barriers
        if (edge.endsWithBarrier) {
            uTurnsAreAllowed = true;
        }

        for (const auto& outgoingEdge: graph.outEdges(graph.edge(edgeId).target)) {
            if (graph.turnData(edgeId, outgoingEdge.id).isAccessPassFor(road_graph::AccessId::Automobile)) {
                continue;
            }

            if (graph.turnData(edgeId, outgoingEdge.id).isForbiddenFor(road_graph::AccessId::Automobile)) {
                if (!uTurnsAreAllowed) {
                    continue;
                }
                Edge edge1;
                edge1.geom = geolib3::Polyline2(edgeData.geometry());
                Edge edge2;
                edge2.geom = geolib3::Polyline2(graph.edgeData(outgoingEdge.id).geometry());
                if (geolib3::angle(edge1.outgoingDirection(),
                                   edge2.incomingDirectionReverse())
                    > UTURN_SIMILAR_ANGLE_TRESHOLD) {
                    continue;
                }
            }

            edge.outEdges.push_back(outgoingEdge.id.value());
        }
    }
    for (auto& keyValue : edges) {
        Edge& edge = keyValue.second;
        for (int i = edge.outEdges.size() - 1; i >= 0; i--) {
            if (edges.count(edge.outEdges[i])) {
                edges[edge.outEdges[i]].inEdges.push_back(keyValue.first);
            } else {
                edge.outEdges[i] = edge.outEdges.back();
                edge.outEdges.pop_back();
            }
        }

    }

    return RoadNetworkData(std::move(edges));
}

} // anonymous namespace

StaticGraphLoader::StaticGraphLoader(const std::string& graphFolder)
    : graph_(graphFolder + "/road_graph.fb")
    , persistentIndex_(graphFolder + "/edges_persistent_index.fb")
    , rtree_(graphFolder + "/rtree.fb", graph_)
    , coverage_(graphFolder + "/graph_coverage.fb")
{
    REQUIRE(graph_.version() == rtree_.version() &&
                graph_.version() == persistentIndex_.version() &&
                graph_.version() == coverage_.version(),
            "road_graph version "
                << graph_.version() << " rtree version " << rtree_.version()
                << " edges_persistent_index version "
                << persistentIndex_.version() << " graph_coverage version "
                << coverage_.version() << " are not equal");

    size_t edgesNumber = coverage_.edgesNumber();
    coveredEdgeIdToIndexMap_.reserve(edgesNumber);
    for (size_t i = 0; i < edgesNumber; ++i) {
        coveredEdgeIdToIndexMap_.insert(
            {road_graph::EdgeId(coverage_.edge(i).id), i});
    }
}

StaticGraphLoader::~StaticGraphLoader() = default;

RoadNetworkData StaticGraphLoader::loadGraph(const maps::geolib3::BoundingBox bbox,
                                             size_t allowedUturnsFc,
                                             bool allowTollRoads)
{
    return loadStaticGraph(
        graph_,
        rtree_,
        persistentIndex_,
        bbox,
        allowedUturnsFc,
        allowTollRoads
    );
}

void StaticGraphLoader::removeActualizedEdges(
    const db::ugc::TasksGroup& group,
    std::unordered_set<EdgeId>& edges) const
{
    if (!group.actualizedBefore()) {
        return;
    }
    const double COVERAGE_THRESHOLD = group.minEdgeCoverageRatio().has_value()
                                          ? group.minEdgeCoverageRatio().value()
                                          : 0.7;
    std::unordered_set<EdgeId> result{edges};
    for (auto edgeId : edges) {
        auto it = coveredEdgeIdToIndexMap_.find(road_graph::EdgeId(edgeId));
        if (it == coveredEdgeIdToIndexMap_.end()) {
            continue;
        }
        auto edge = coverage_.edge(it->second);
        std::set<db::CameraDeviation> sides;
        for (auto& coverage : edge.coverages) {
            if (coverage.coverageFraction > COVERAGE_THRESHOLD &&
                coverage.actualizationDate > *group.actualizedBefore() &&
                boost::algorithm::any_of_equal(group.cameraDeviations(),
                                               coverage.cameraDeviation)) {
                sides.insert(coverage.cameraDeviation);
            }
        }
        if (sides.size() == group.cameraDeviations().size()) {
            result.erase(edgeId);
        }
    }
    edges = std::move(result);
}

} // namespace maps::mrc::gen_targets
