#include "edges_fixer.h"

#include "topology_data_helpers.h"

#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/line.h>
#include <maps/libs/geolib/include/direction.h>
#include <maps/libs/geolib/include/projection.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/intersection.h>

#include <initializer_list>
#include <algorithm>
#include <deque>

namespace maps {
namespace wiki {
namespace topology_fixer {
namespace utils {

bool
EdgesFixer::edgesShouldBeFixed(EdgeId edgeId1, EdgeId edgeId2)
{
    const auto& edge1 = data_.edge(edgeId1);
    const auto& edge2 = data_.edge(edgeId2);
    return gl::intersects(
        gl::resizeByValue(edge1.boundingBox(),
            maxInteractingEdgesDistance_ + gl::EPS),
        gl::resizeByValue(edge2.boundingBox(),
            maxInteractingEdgesDistance_ + gl::EPS));
}

bool
EdgesFixer::edgesFixingAllowed(EdgeId edgeId1, EdgeId edgeId2) const
{
    const auto& edge1 = data_.edge(edgeId1);
    const auto& edge2 = data_.edge(edgeId2);

    return edge1.fZlev() == edge1.tZlev()
        && edge1.fZlev() == edge2.fZlev()
        && edge1.fZlev() == edge2.tZlev();
}

void
EdgesFixer::ProcessingData::addEdge(EdgeId edgeId)
{
    auto insertEdgeDataRes = edgesData.insert(
        {edgeId,
         addEdgeToSegmentsGraph(*dataCopy, edgeId, segmentsGraph, nodeIdToPointIdMap)});
    ASSERT(insertEdgeDataRes.second);
    const Edge& edge = dataCopy->edge(edgeId);
    REQUIRE(edge.fZlev() == edge.tZlev(), "Edge " << edgeId << " zlevels differ");
    const ZLevelType edgeZLev = edge.fZlev();
    if (edgesData.size() == 1) {
        ASSERT(!zLev);
        zLev = edgeZLev;
    } else {
        ASSERT(zLev);
        REQUIRE(edgeZLev == *zLev, "Edge " << edgeId << " zlevel " << edgeZLev
            << " differs from current " << *zLev);
    }
}

void
EdgesFixer::ProcessingData::createNewNodes()
{
    for (const auto& pointDegree : segmentsGraph.pointDegrees()) {
        const auto pointId = pointDegree.first;
        auto& point = segmentsGraph.point(pointId);
        if (!point.nodeId && pointDegree.second > 2) {
            const auto newNodeId = dataCopy->addNode(point.pos);
            nodeIdToPointIdMap.insert({newNodeId, pointId});
            point.nodeId = newNodeId;
        }
    }
}

void
EdgesFixer::ProcessingData::createNewEdges()
{
    ASSERT(zLev);
    for (const SegmentsGraph::Circuit& circuit : segmentsGraph.buildPartition()) {
        auto edgeId = findMatchInExistingEdges(circuit.segmentIdsList);
        if (!edgeId) {
            const auto& startPoint = segmentsGraph.point(circuit.pointIdsList.front());
            ASSERT(startPoint.nodeId);
            const auto& endPoint = segmentsGraph.point(circuit.pointIdsList.back());
            ASSERT(endPoint.nodeId);
            gl::PointsVector points;
            points.reserve(circuit.pointIdsList.size());
            for (auto pointId : circuit.pointIdsList) {
                points.push_back(segmentsGraph.point(pointId).pos);
            }
            edgeId = dataCopy->addEdge(
                gl::Polyline2(std::move(points)), *startPoint.nodeId, *endPoint.nodeId, *zLev, *zLev);
        }
        for (auto segmentId : circuit.segmentIdsList) {
            segmentsGraph.segment(segmentId).edgeId = *edgeId;
        }
    }
}

boost::optional<EdgeId>
EdgesFixer::ProcessingData::findMatchInExistingEdges(
    const SegmentIdsList& segIds) const
{
    for (const auto& edgeSegs : edgesData) {
        const auto& edgeSegIds = edgeSegs.second;
        if (segIds.size() != edgeSegIds.size()) {
            continue;
        }
        if (std::equal(segIds.begin(), segIds.end(), edgeSegIds.begin()) ||
            std::equal(segIds.rbegin(), segIds.rend(), edgeSegIds.begin()))
        {
            return edgeSegs.first;
        }
    }
    return boost::none;
}

void
EdgesFixer::ProcessingData::fixEdgesInOriginalData()
{
    auto locks = locker.lockFaces(dataCopy->faceIds());

    TopologyDataDiff diff = computeDiff(*dataCopy, *originalDataCopy);
    applyDiff(fullData, *dataCopy, diff);
}

SegmentIdsList
addEdgeToSegmentsGraph(const TopologyData& data, EdgeId edgeId,
    SegmentsGraph& graph, NodeIdToPointIdMap& nodeIdsMap)
{
    auto addExistingNode = [&] (NodeId nodeId) -> PointId
    {
        auto nodeIt = nodeIdsMap.find(nodeId);
        const Node& node = data.node(nodeId);
        const auto pointId = nodeIt == nodeIdsMap.end()
            ? graph.addPoint(node.id(), node.point()).id
            : graph.point(nodeIt->second).id;
        if (nodeIt == nodeIdsMap.end()) {
            nodeIdsMap.insert({nodeId, pointId});
        }
        return pointId;
    };
    const Edge& edge = data.edge(edgeId);
    SegmentIdsList edgeSegments;
    auto currentPointId = addExistingNode(edge.fnodeId());
    const auto& points = edge.linestring().points();
    ASSERT(points.size() >= 2);
    for (size_t i = 1; i < points.size() - 1; ++i) {
        const auto& newPoint = graph.addPoint(boost::none, points[i]);
        edgeSegments.push_back(graph.addSegment(currentPointId, newPoint.id).id);
        currentPointId = newPoint.id;
    }
    edgeSegments.push_back(
        graph.addSegment(currentPointId, addExistingNode(edge.tnodeId())).id);
    return edgeSegments;
}

} // namespace utils
} // namespace topology_fixer
} // namespace wiki
} // namespace maps
