#include "edges_overlaps_fixer.h"

#include "segments/segments_overlapper.h"
#include "segments/segments_intersector.h"
#include "topology_data_helpers.h"
#include "geom_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 {

namespace {

const double SEGMENT_LENGTH_EPS = 0.00000001;

//only for very-very bad data
const bool ENABLE_INTERSECTIONS_FOR_CONTOUR_OBJECTS = true;

} // namespace

bool
EdgesOverlapsFixer::edgesFixingAllowed(EdgeId edgeId1, EdgeId edgeId2) const
{
    if (!EdgesFixer::edgesFixingAllowed(edgeId1, edgeId2)) {
        return false;
    }
    const auto& edge1 = data_.edge(edgeId1);
    const auto& edge2 = data_.edge(edgeId2);
    auto edgeMasterIds = [&] (const Edge& edge) -> IdSet
    {
        IdSet masterIds;
        for (auto faceId : edge.faceIds()) {
            const auto& faceMasterIds =  data_.face(faceId).masterIds();
            masterIds.insert(faceMasterIds.begin(), faceMasterIds.end());
        }
        return masterIds;
    };
    return idsIntersection(edgeMasterIds(edge1), edgeMasterIds(edge2)).empty();
}

std::unique_ptr<EdgesFixer::ProcessingData>
EdgesOverlapsFixer::initProcessingData(EdgeId edgeId1, EdgeId edgeId2)
{
    return std::unique_ptr<EdgesFixer::ProcessingData>(
        new ProcessingData(
            edgeId1, edgeId2, data_, locker_, tolerance_, maxAngleBetweenSegments_));
}

SegmentsProcessingResult EdgesOverlapsFixer::ProcessingData::processEdges()
{
    ASSERT(edgesData.size() == 2);
    auto result = processOverlaps(
        segmentsGraph, edgesData.at(edgeId1), edgesData.at(edgeId2),
        tolerance_, maxAngleBetweenSegments_);

    if (!ENABLE_INTERSECTIONS_FOR_CONTOUR_OBJECTS ||
            dataCopy->ftGroup()->id() != TopologyGroupId::Ad) {
        return result;
    }

    bool hasChanges = result.hasChanges;

    result = processIntersections(
        segmentsGraph, result.segmentIds1, result.segmentIds2);
    result.hasChanges = hasChanges || result.hasChanges;

    collapseZeroLengthSegments(edgeId1, result.segmentIds1);
    collapseZeroLengthSegments(edgeId2, result.segmentIds2);

    return result;
}

void EdgesOverlapsFixer::ProcessingData::collapseZeroLengthSegments(EdgeId, SegmentIdsList& segmentIds)
{
    for (auto it = segmentIds.begin(); it != segmentIds.end();) {
        auto id = *it;
        const gl::Segment2& segGeom = segmentsGraph.segmentGeom(id);
        if (gl::squaredLength(segGeom) > SEGMENT_LENGTH_EPS) {
            ++it;
            continue;
        }

        const auto& seg = segmentsGraph.segment(id);
        auto startId = seg.startPointId;
        auto endId = seg.endPointId;

        const Point& start = segmentsGraph.point(startId);
        const Point& end = segmentsGraph.point(endId);
        if (start.nodeId && end.nodeId) {
            ++it;
            continue;
        }

        if (start.nodeId) {
            std::swap(startId, endId); //merge point without nodeId to point with nodeId
        }

        segmentsGraph.removeSegment(id);
        segmentsGraph.mergePoints(startId, endId);

        it = segmentIds.erase(it);
    }
}

/**
 * Edges touching is not allowed 'cause it results in touching polygons.
 * => all newly shared points must have at least one incident shared edge.
 */
bool
EdgesOverlapsFixer::ProcessingData::newNodesValid() const
{
    for (const auto& pointDegree : segmentsGraph.pointDegrees()) {
        const auto pointId = pointDegree.first;
        const auto newPointDegree = pointDegree.second;
        const auto incidencesType = segmentsGraph.pointIncidencesType(pointId);

        if (ENABLE_INTERSECTIONS_FOR_CONTOUR_OBJECTS && dataCopy->ftGroup()->id() == TopologyGroupId::Ad) {
            if (incidencesType == SegmentsGraph::PointIncidencesType::Isolated) {
                return false;
            }
        } else {
            ASSERT(incidencesType != SegmentsGraph::PointIncidencesType::Isolated);
            if (!segmentsGraph.point(pointId).nodeId &&
                !((incidencesType != SegmentsGraph::PointIncidencesType::NonSharedOnly &&
                newPointDegree > 2) ||
                (incidencesType != SegmentsGraph::PointIncidencesType::Mixed &&
                newPointDegree == 2)))
            {
                WARN() << "Point " << pointId
                    << " incidences count: " << newPointDegree
                    << ", incidences type: " <<
                    (incidencesType == SegmentsGraph::PointIncidencesType::NonSharedOnly
                            ? "NonShared"
                            : "Mixed");
                segmentsGraph.print();
                return false;
            }
        }
    }
    return true;
}

bool
EdgesOverlapsFixer::ProcessingData::restoreTopologyInCopy()
{
    IdSet faceIds;
    for (const auto& edgeData : edgesData) {
        EdgeIdsSet newEdgeIds;
        for (auto segId : edgeData.second) {
            const auto& seg = segmentsGraph.segment(segId);
            ASSERT(seg.edgeId);
            newEdgeIds.insert(*seg.edgeId);
        }
        ASSERT(!newEdgeIds.empty());
        originalToSplittedEdgesMap.insert({edgeData.first, newEdgeIds});
        const auto& edgeFaceIds = dataCopy->edge(edgeData.first).faceIds();
        faceIds.insert(edgeFaceIds.begin(), edgeFaceIds.end());
        if (newEdgeIds.size() > 1 || *newEdgeIds.begin() != edgeData.first) {
            IdSet edgeIds(newEdgeIds.begin(), newEdgeIds.end());
            dataCopy->replaceEdge(edgeData.first, edgeIds);
        }
    }

    for (auto faceId : faceIds) {
        try {
            gl::Polygon2(dataCopy->faceVertices(faceId));
        } catch (const std::exception& e) {
            WARN() << "Face " << faceId << " new geometry is invalid: " << e.what();
            return false;
        }
    }
    return true;
}

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