#pragma once

#include "types.h"
#include "make_event.h"
#include "../graph.h"

#include <yandex/maps/wiki/topo/common.h>
#include <yandex/maps/wiki/topo/exception.h>

#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/polyline.h>


#include <map>


namespace maps {
namespace wiki {
namespace topo {
namespace geom {


// You can find more information here:
// https://wiki.yandex-team.ru/maps/dev/core/wikimap/mapspro/topologycal


namespace {

typedef std::map<EdgeID, SnapPolylineVector> EdgeIdToSplitParts;
struct CommonSplitParts;

}

typedef boost::optional<SnapPoint> OptionalSnapPoint;


class Intersector { // prepares events for embedding edge into existing graph.

public:

    struct Events { // split events of edited edge and intersected edges.
        SplitEdgeEventDataPtr editedEdgeEvent;
        std::map<EdgeID, SplitEdgeEventDataPtr> intersectedEdgesEvents;
        NodeIDSet unusedNodeIds;
    };

    Intersector(const Graph& graph, const TopologyRestrictions& restrictions,
        DeleteNodeChecker deleteNodeChecker);

    // Prepare data of split events.
    // * sourceEdgeId - source edge id of edited edge
    // * polyline - new geomerty of edited edge
    // * splitRequests - list of additional points where the polyline should be cut
    Events operator()(const SourceEdgeID& sourceEdgeId, const geolib3::Polyline2& polyline,
        const geolib3::PointsVector& splitRequests = geolib3::PointsVector()) const;

private:
    // If existing edge is edited and in cache
    // we must ignored some edge's nodes to snap end points.
    // If node doesn't have other incedent edges we mustn't snap to it,
    // excluding the case when the new position of the node is within the tolerance.
    NodeIDSet getIgnoredNodesToSnapEndPoint(const SourceEdgeID& sourcrEdgeId,
        const geolib3::Point2& newStart, const geolib3::Point2& newEnd) const;

    // If existing edge is edited and in cache
    // we must ignored some edges's nodes to snap inner points.
    // If node doesn't have other incedent edges we mustn'tsnap to it,
    // excluding the case when split is required at this node.
    NodeIDSet getIgnoredNodesToSnapInnerPoint(const SourceEdgeID& sourcrEdgeId,
        const geolib3::PointsVector& splitRequests) const;

    // We ignore nodes, that we snap already.
    // Also we must ignored some edges's nodes to snap end points.
    // If node doesn't have other incedent edges we mustn'tsnap to it.
    NodeIDSet getIgnoredCloseNodes(const SourceEdgeID& sourceeEdgeId,
        const SnapPolyline& partiallyAlignedPolyline) const;

    // Point is projected on the edge (point P), then one is snapped to polyline:
    // If there is vetrex V that distance(P, V) < gravity that point is snaped to V.
    // else point is snaped to P.
    SnapPoint snapPointToEdge(const geolib3::Point2& point, const Edge& edge, double gravity) const;

    // Try snap edge point to the closest node or edge with gravity.
    SnapPoint snapEdgePoint(const SourceEdgeID& sourceEdgeId, const geolib3::Point2& point,
        const NodeIDSet& ignoredNodes, double gravity) const;

    // Find close points for polyline.
    // Actually points means all vertices of the polylines and nodes.
    // In overlapping mode return points with gravity distance
    // In simple mode return nodes with gravity distance and incident = 1 and
    // all verteces with tolerance distace from polyline.
    SnapPointVector findClosePoints(const SourceEdgeID& sourceEdgeId,
        const geolib3::Polyline2& polyline, const NodeIDSet& ignoredNodeIds) const;

    // Before intersection it's essential to align new geometry of edge.
    // Common scheme of snapppig is following:
    // 1. Vertices of polyline are snapped to close nodes and edges.
    // 2. Also polyline segment should be aligned if there is rathe close point of other edge.
    // For more information see the implementation.
    SnapPolyline align(const SourceEdgeID& sourceEdgeId, const geolib3::Polyline2& polyline,
        const geolib3::PointsVector& splitRequests) const;

    // Intersect snap segment with edge segment.
    // Return snap points which are ordered along segment.
    SnapPointVector intersect(const SnapPoint& start, const SnapPoint& end,
        const Edge& edge, size_t segmentIndex) const;

    // Intersect snap polyline with edge segment.
    // Return snap points which are ordered along segment.
    SnapPolylineVector intersect(const SnapPolyline& polyline, const Edge& edge,
        size_t segmentIndex) const;

    // Intersect snap polyline with edge.
    // Return snap polylines which are ordered along edge.
    SnapPolylineVector intersect(const SnapPolyline& polyline, const Edge& edge) const;

    // Try to add new split at point where new cuts are requested.
    // If new split is rather close to existing, it is not created.
    OptionalSnapPoint tryAddSplitPointForRequest(SnapPolylineVector& editedEdgeSplitParts,
        EdgeIdToSplitParts& intersectedEdgesSplitParts, const SnapPolyline& polyline,
        const geolib3::Point2& splitRequest) const;

    // Find common subpolylines (may consists of one point only) with other edges in graph.
    // Method can add new vertices to polyline,
    // but it's guranteed that all new points lie on the polyline.
    CommonSplitParts findCommonSplitParts(const SourceEdgeID& sourceEdgeId,
        const SnapPolyline& polyline, const geolib3::PointsVector& splitRequests) const;

    // Prepare events for aligned polyline.
    Events makeEvents(const SourceEdgeID& sourceEdgeId, const SnapPolyline& polyline,
        const geolib3::PointsVector& splitRequests) const;

    // In some cases node can become unused, for example closing edge
    // This function returns potential unused nodes.
    NodeIDSet getUnusedNodeIds(const SourceEdgeID& sourceEdgeId,
        const SnapPolyline& polyline) const;

    // Validate polyline after alignment.
    void checkPolyline(const geolib3::Polyline2& polyline) const;

    // Validate prepared split edge events.
    void checkResult(const Intersector::Events& events) const;


    // Helpers and shortcuts:
    const Node& getNode(const NodeID& id) const { return graph_.node(id); }

    const Edge& getEdge(const EdgeID& id) const { return graph_.edge(id); }

    const OptionalNodeID getNodeIdNear(const geolib3::Point2& point,
        double tolerance = geolib3::EPS, const NodeIDSet& ignoredNodeIds = NodeIDSet()) const
    {
        return graph_.index().nodeAt(point, tolerance, ignoredNodeIds);
    }

    const OptionalEdgeID getEdgeIdNear(const geolib3::Point2& point,
        double tolerance = geolib3::EPS, const EdgeIDSet& ignoredEdgeIds = NodeIDSet()) const
    {
        return graph_.index().edgeAt(point, tolerance, ignoredEdgeIds);
    }

    NodeIDVector findNodeIdsNear(const geolib3::Point2& point, double tolerance,
        const NodeIDSet& ignoredNodeIds = NodeIDSet()) const
    {
        return graph_.index().nearestNodes(point, tolerance, ignoredNodeIds);
    }

    NodeIDVector findNodeIdsNear(const geolib3::Polyline2& polyline, double tolerance,
        const NodeIDSet& ignoredNodeIds = NodeIDSet()) const
    {
        return graph_.index().nearestNodes(polyline, tolerance, ignoredNodeIds);
    }

    EdgeIDVector findEdgeIdsNear(const geolib3::Point2& point, double tolerance,
        const EdgeIDSet& ignoredEdgeIds = EdgeIDSet()) const
    {
        return graph_.index().nearestEdges(point, tolerance, ignoredEdgeIds);
    }

    EdgeIDVector findEdgeIdsNear(const geolib3::Polyline2& polyline, double tolerance,
        const EdgeIDSet& ignoredEdgeIds = EdgeIDSet()) const
    {
        return graph_.index().nearestEdges(polyline, tolerance, ignoredEdgeIds);
    }

    double junctionGravity() const { return restrictions_.junctionGravity(); }

    double vertexGravity() const { return restrictions_.vertexGravity(); }

    double tolerance() const { return restrictions_.tolerance(); }

    bool isMergeOfOverlappedEdgesAllowed() const {
        return restrictions_.isMergeOfOverlappedEdgesAllowed();
    }

    const Graph& graph_;
    const TopologyRestrictions restrictions_;
    DeleteNodeChecker deleteNodeChecker_;
};


} // namespace maps::wiki::topo::geom
} // namespace maps::wiki::topo
} // namespace maps::wiki
} // namespace maps
