#include "snap_nodes.h"

#include "move_node.h"
#include "save_edge_helpers.h"
#include "preload_objects.h"
#include "process_events.h"
#include "../geom_tools/make_event.h"
#include "../index/spatial_index.h"
#include "../events_data.h"
#include "../editor_impl.h"
#include "../cache_impl.h"
#include "../graph.h"

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

#include <maps/libs/geolib/include/intersection.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/vector.h>
#include <maps/libs/geolib/include/closest_point.h>

#include <vector>
#include <algorithm>


namespace maps {
namespace wiki {
namespace topo {

using namespace geom;

SnapNodesOperation::SnapNodesOperation(
        const Callbacks& callbacks,
        CacheImpl& cache,
        const NodeIDSet& nodeIds,
        const TopologyRestrictions& restrictions)
    : Operation(callbacks, cache)
    , nodeIds_(nodeIds)
    , restrictions_(restrictions)
{}

/// Load nodes with given ids
///   and edges within restrictions_.pointGravityDistance
///   from total nodes bbox
void
SnapNodesOperation::preloadObjects()
{
    cache_.loadByNodes(nodeIds_);

    std::vector<NodeUpdateData> nodesData;
    for (auto nodeId : nodeIds_) {
        nodesData.push_back({nodeId, &cache_.graph().node(nodeId).pos()});
    }
    std::vector<EdgeUpdateData> edgesData;

    topo::preloadObjects(
        cache_,
        TopologyUpdateData(nodesData, edgesData),
        restrictions_);
}

boost::optional<SnapNodesOperation::SnapToEdgeResult>
SnapNodesOperation::trySnapToEdge(NodeID nodeId) const
{
    EdgeIDSet ignoredIds;
    for (auto edgePtr : cache_.graph().incidentEdges(nodeId)) {
        ignoredIds.insert(edgePtr->id());
    }

    const Node& node = cache_.graph().node(nodeId);
    auto nearestEdgeIds = cache_.graph().index().nearestEdges(
        node.pos(), restrictions_.groupJunctionGravity(), ignoredIds
    );

    if (nearestEdgeIds.empty()) {
        return boost::none;
    }

    auto nearestEdgeId = nearestEdgeIds.front();
    const Edge& nearestEdge = cache_.graph().edge(nearestEdgeId);

    const auto projection = geolib3::closestPoint(nearestEdge.geom(), node.pos());
    const auto vertex = nearestEdge.geom().pointAt(nearestEdge.geom().closestNodeIndex(projection));

    return geolib3::distance(vertex, projection) < restrictions_.groupJunctionSnapVertexGravity()
        ? SnapToEdgeResult{nearestEdgeId, vertex}
        : SnapToEdgeResult{nearestEdgeId, projection};
}

OptionalNodeID
SnapNodesOperation::trySnapToNode(
    NodeID nodeId, const geolib3::Point2& pos, NodeIDSet ignoredNodeIds) const
{
    for (auto edgePtr : cache_.graph().incidentEdges(nodeId)) {
        ignoredNodeIds.insert(edgePtr->startNode());
        ignoredNodeIds.insert(edgePtr->endNode());
    }

    auto nearestNodeIds = cache_.graph().index().nearestNodes(
        pos, restrictions_.groupJunctionGravity(), ignoredNodeIds
    );

    return
        nearestNodeIds.empty()
        ? boost::none
        : OptionalNodeID(nearestNodeIds.front());
}

void
SnapNodesOperation::operator () ()
{
    preloadObjects();

    EdgeIDSet affectedEdgeIds;
    NodeIDSet resultNodeIds;

    NodeIDSet ignoredNodeIds = nodeIds_;
    for (auto nodeId : nodeIds_) {
        auto snapToEdgeResult = trySnapToEdge(nodeId);
        if (!snapToEdgeResult) {
            continue;
        }

        auto mergedNodeId = trySnapToNode(
            nodeId, snapToEdgeResult->newPos, ignoredNodeIds
        );
        if (mergedNodeId) {
            Node& nearestNode = cache_.graph().node(*mergedNodeId);
            MergeNodesEventData mergeData(
                nodeId, nearestNode.id(), nearestNode.pos());
            ProcessEvents(cache_, callbacks_)(mergeData);
            nodeId = mergeData.idTo;
        } else {
            cache_.graph().moveNode(nodeId, snapToEdgeResult->newPos);
            cache_.storage().updateNodePos(nodeId, snapToEdgeResult->newPos);
            affectedEdgeIds.insert(snapToEdgeResult->nearestEdgeId);
        }

        resultNodeIds.insert(nodeId);
        for (auto edgePtr : cache_.graph().incidentEdges(nodeId)) {
            cache_.storage().updateEdgeGeom(
                edgePtr->id(), edgePtr->geom(), edgePtr->incidentNodes()
            );
            affectedEdgeIds.insert(edgePtr->id());
        }

        ignoredNodeIds.erase(nodeId);
    }

    nodeIds_.swap(resultNodeIds);
    saveAffectedEdges(affectedEdgeIds);
}

void
SnapNodesOperation::saveAffectedEdges(const EdgeIDSet& affectedEdgeIds)
{
    std::map<EdgeID, geolib3::Polyline2> newEdgeGeoms;
    std::map<EdgeID, IncidentNodes> oldEdgeIncidences;

    for (auto edgeId : affectedEdgeIds) {
        const Edge& edge = cache_.graph().edge(edgeId);
        newEdgeGeoms.insert({edgeId, edge.geom()});
        oldEdgeIncidences.insert({edgeId, edge.incidentNodes()});
        cache_.graph().deleteEdge(edgeId);
    }

    for (auto edgeId : affectedEdgeIds) {
        saveEdge(edgeId, newEdgeGeoms.at(edgeId), oldEdgeIncidences.at(edgeId));
    }
}

void
SnapNodesOperation::saveEdge(
    EdgeID id,
    const geolib3::Polyline2& geom,
    const IncidentNodes& oldIncidentNodes)
{
    SourceEdgeID srcEdgeId = {id, /* bool exists = */ true};

    const auto edges = cache_.graph().edges();
    geom::Intersector intersector(
        cache_.graph(), restrictions_,
        [&] (NodeID) -> bool {
            return false;
        }
    );

    geom::Intersector::Events events = intersector(srcEdgeId, geom);

    const SplitDataPtr& geomData = events.editedEdgeEvent;
    const SplitPointPtrVector& splitPoints = geomData->splitPoints;

    geomData->oldIncidentNodes = oldIncidentNodes;

    for (size_t i = 1; i < splitPoints.size() - 1; ++i) {
        if (!nodeIds_.count(*splitPoints[i]->nodeId)) {
            throw GeomProcessingErrorWithLocation(
                    ErrorCode::UnexpectedIntersection, splitPoints[i]->geom)
                << " edge " << srcEdgeId.id()
                << " unexpected intersection created at node " << *splitPoints[i]->nodeId;
        }
    }

    processEdgesSaving(cache_, callbacks_, events);
}

} // namespace topo
} // namespace wiki
} // namespace maps
