#include "tasks_generator.h"

#include "check_generator_result.h"
#include "filtered_connected_components.h"
#include "minimal_overhead.h"
#include "unrecommended_maneuvers.h"
#include "route_generator.h"
#include "split_path.h"
#include "task_area.h"

#include <maps/libs/log8/include/log8.h>

namespace maps::mrc::gen_targets {

namespace {

template<typename Polygon>
std::unordered_set<EdgeId> selectTargetEdgesImpl(
    const RoadNetworkData& roadNetwork,
    const Polygon& polygon,
    EdgeFilter filter)
{
    auto bbox = polygon.boundingBox();
    geolib3::PreparedPolygon2 preparedPolygon(polygon);

    std::unordered_set<EdgeId> targetEdges;

    for (const auto& edgePair : roadNetwork.edges()) {
        const Edge& edge = edgePair.second;
        if (!edge.isUTurn && // U turns connectors are not interesting for driving
            filter(edge) &&
            polylineIsMostlyInsidePolygon(edge.geom, preparedPolygon, bbox))
        {
            targetEdges.insert(edge.id);
        }
    }

    return targetEdges;
}


} // namespace


std::unordered_set<EdgeId> selectTargetEdges(
    const RoadNetworkData& roadNetwork,
    const geolib3::Polygon2& polygon,
    EdgeFilter filter)
{
    return selectTargetEdgesImpl(roadNetwork, polygon, filter);
}

std::unordered_set<EdgeId> selectTargetEdges(
    const RoadNetworkData& roadNetwork,
    const geolib3::MultiPolygon2& polygon,
    EdgeFilter filter)
{
    return selectTargetEdgesImpl(roadNetwork, polygon, filter);
}


Paths convertLoopsPathsToPaths(const std::unordered_set<EdgeId>& targetEdges,
                               const LoopsPaths& loopsPaths)
{
    Paths paths;
    std::unordered_set<EdgeId> visitedEdges;
    for (const LoopsPath& loopsPath : loopsPaths) {
        Path path;
        bool isUsefull = false;
        for (const LoopEdge& loopEdge : loopsPath) {
            PathEdge pathEdge;
            pathEdge.edgeId = loopEdge.edgeId;
            if (targetEdges.count(pathEdge.edgeId) &&
                    visitedEdges.insert(pathEdge.edgeId).second)
            {
                pathEdge.isUsefulAsTarget = true;
                isUsefull = true;
            } else {
                pathEdge.isUsefulAsTarget = false;
            }
            path.push_back(pathEdge);
        }
        if (isUsefull) {
            // some very small districts(several edges) can be visited
            // during another district route
            paths.push_back(path);
        }
    }
    return paths;
}

double getCoveredTargetsLength(const RoadNetworkData& roadNetwork,
                               const LoopsPath& path)
{
    double length = 0;
    std::unordered_set<EdgeId> visitedEdges;
    for (LoopEdge loopEdge : path) {
        const Edge& edge = roadNetwork.edge(loopEdge.edgeId);
        if (edge.isTarget && visitedEdges.insert(edge.id).second) {
            length += edge.length;
        }
    }
    return length;
}

// @brief generates one or more tasks which cover most of target edges
Paths generateTasks(RoadNetworkData& roadNetwork,
                    const std::unordered_set<EdgeId>& targetEdges,
                    double minTaskLength)
{
    LoopsPaths tasks;

    std::vector<std::vector<EdgeId>> connectedComponents
        = getFilteredConnectedComponentsOfTargets(roadNetwork, targetEdges);

    for (auto& componentEdges : connectedComponents) {
        roadNetwork.eraseTargets();
        for (EdgeId edgeId : componentEdges) {
            roadNetwork.addTarget(edgeId);
        }

        ExtendedRoadNetwork optimizedRoadNetwork(roadNetwork.getEdges());
        OptimalOverheadCreator optimalOverheadCreator = OptimalOverheadCreator(
            optimizedRoadNetwork);
        optimizedRoadNetwork.setUnrecommendedManeuvers(
            UnrecommendedManeuversCalculator(
                optimizedRoadNetwork).getUnrecommendedManeuvers());

        EdgeId initEdge = -1;
        for (EdgeId edgeId : componentEdges) {
            if (optimizedRoadNetwork.edge(edgeId).isTarget) {
                initEdge = edgeId;
                break;
            }
        }
        REQUIRE(initEdge != -1, "Connected component doesn't have target edges");

        LoopsPath path = RouteGenerator(optimizedRoadNetwork, initEdge).getResult();
        for (auto& edge : path) {
            edge.edgeId = optimalOverheadCreator.getOriginalId(edge.edgeId);
        }
        auto newTasks = splitLoopRouteIntoTasks(roadNetwork, path,
                                                minTaskLength);
        tasks.insert(tasks.end(), newTasks.begin(), newTasks.end());
    }

    Paths result = convertLoopsPathsToPaths(targetEdges, tasks);
    return result;
}

// @brief generates one or more tasks for each district.
std::vector<std::pair<District, Path>>
generateTasksWithinDistricts(
    RoadNetworkData& roadNetwork,
    const std::vector<District>& districts,
    int maxRoadFc,
    double minTaskLength,
    std::unordered_set<EdgeId>& prohibitedEdges)
{
    std::vector<std::pair<District, Path>> tasks;
    std::unordered_set<EdgeId> allTargets;

    for (const auto& district : districts) {
        auto targetEdgesVec = roadNetwork.getEdgesWithinPolygon(district.area,
                                                             0,
                                                             maxRoadFc,
                                                             prohibitedEdges);

        std::unordered_set<EdgeId> targetEdges(targetEdgesVec.begin(), targetEdgesVec.end());
        allTargets.insert(targetEdges.begin(), targetEdges.end());
        Paths currentTasks = generateTasks(roadNetwork, targetEdges,
                                           minTaskLength);
        for (size_t i = 0; i < currentTasks.size(); i++) {
            const auto& task = currentTasks[i];
            for (const auto& pathEdge : task) {
                if (pathEdge.isUsefulAsTarget) {
                    // avoid target edges copies
                    prohibitedEdges.insert(pathEdge.edgeId);
                }
            }
            tasks.push_back({district, task});
            if (currentTasks.size() > 1) {
                tasks.back().first.name.name += "_" + std::to_string(i + 1);
                correctTaskArea(tasks.back(), roadNetwork);
            }

        }
    }

    std::vector<const Path*> pathRefs;
    pathRefs.reserve(tasks.size());
    for(const auto& task : tasks) {
        pathRefs.push_back(&task.second);
    }

    checkResult(roadNetwork, allTargets, pathRefs);
    return tasks;
}

} // namespace maps::mrc::gen_targets
