#include "process_events.h"

#include "../geom_tools/geom_io.h"
#include "../geom_tools/make_event.h"
#include "../geom_tools/util.h"
#include "../cache_impl.h"
#include "../graph.h"
#include "../editor_impl.h"

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

#include <algorithm>


namespace maps {
namespace wiki {
namespace topo {

using namespace geom;

void ProcessEvents::operator()(AddEdgeEventData& /*event*/)
{}

void ProcessEvents::operator()(DeleteEdgeEventData& event)
{
    cache_.graph().deleteEdge(event.id);
    cache_.storage().deleteEdge(event.id);

    callbacks_.deleteEdgeTopo.callback().process(DeleteEdgeEvent(event));
}

void ProcessEvents::operator()(MoveEdgeEventData& event)
{
    cache_.graph().deleteEdge(event.id);
    processMoveEvent(event);
}

namespace {

OptionalNodeID changedNodeId(
    OptionalNodeID newNodeId, NodeID oldNodeId)
{
    REQUIRE(oldNodeId, "Invalid node id");
    return newNodeId && *newNodeId != oldNodeId
        ? newNodeId
        : OptionalNodeID();
}

bool created(const SplitPolylinePtr polyline) {
    return polyline->edgeId.is_initialized();
};

bool allCreated(const SplitPolylinePtrVector& polyline) {
    return std::all_of(polyline.begin(), polyline.end(), created);
}

} // namespace

void ProcessEvents::operator()(SplitEdgeEventData& event)
{

    if (allCreated(event.splitPolylines)) {
        return;
    }

    // Split request part
    if (event.splitPolylines.size() > 1) {
        SplitEdgeRequest request{event};
        callbacks_.splitEdge.callback().processRequest(request);
    } else {
        event.partToKeepIDIndex = 0;
    }

    // Create split points (old endpoints are reused if it's possible).
    NodeID oldStartNode = 0;
    NodeID oldEndNode = 0;
    boost::optional<IncidentNodes> oldIncidentNodes;
    SplitPointPtrVector& splitPoints = event.splitPoints;
    if (event.sourceId.exists()) {
        if (cache_.graph().edgeExists(event.sourceId.id())) {
            const Edge& edge = cache_.graph().edge(event.sourceId.id());
            oldIncidentNodes = IncidentNodes{edge.startNode(), edge.endNode()};
            cache_.graph().deleteEdge(event.sourceId.id());
        } else {
            oldIncidentNodes = event.oldIncidentNodes;
        }
        REQUIRE(
            oldIncidentNodes,
            "If edge is droped from graph, original incidences must be set, "
            "edge id: " << event.sourceId.id()
        );
        oldStartNode = oldIncidentNodes->start;
        oldEndNode = oldIncidentNodes->end;
    }
    createNodesAtSplitPoints(splitPoints, oldIncidentNodes);

    // Create split edges
    for (size_t i = 0; i < event.splitPolylines.size(); ++i) {
        const geolib3::Polyline2& polyline = event.splitPolylines[i]->geom;
        OptionalEdgeID& splitEdgeId = event.splitPolylines[i]->edgeId;

        const bool theSameOrder = geom::near(
            polyline.pointAt(0),
            splitPoints[i]->geom
        );

        if (event.sourceId.exists() && i == event.partToKeepIDIndex) {
            const EdgeID id = event.sourceId.id();
            const OptionalNodeID newStartNode = changedNodeId(
                theSameOrder
                    ? splitPoints[i]->nodeId
                    : splitPoints[i + 1]->nodeId,
                oldIncidentNodes->start
            );
            const OptionalNodeID newEndNode = changedNodeId(
                theSameOrder
                    ? splitPoints[i + 1]->nodeId
                    : splitPoints[i]->nodeId,
                oldIncidentNodes->end
            );

            if (newStartNode || newEndNode) {
                MoveEdgeEventData data{
                    id, newStartNode, newEndNode, polyline,
                    IncidentNodes{oldStartNode, oldEndNode}
                };
                processMoveEvent(data);
            } else {
                cache_.graph().addEdge(
                    Edge{id, polyline, oldStartNode, oldEndNode}
                );
                cache_.storage().updateEdgeGeom(
                    id, polyline, IncidentNodes(oldStartNode, oldEndNode)
                );
            }
            splitEdgeId = id;
        } else if ((!event.isEditedEdge || !event.splitPolylines[i]->isShared
                    || i == event.partToKeepIDIndex) && !splitEdgeId)
        {
            IncidentNodes incidences {
                *(theSameOrder
                    ? splitPoints[i]->nodeId
                    : splitPoints[i + 1]->nodeId
                ),
                *(theSameOrder
                    ? splitPoints[i + 1]->nodeId
                    : splitPoints[i]->nodeId
                )
            };

            Edge edge = cache_.storage().createEdge(polyline, incidences);

            AddEdgeEventData addData(
                edge.id(),
                event.sourceId,
                incidences.start, incidences.end, edge.geom());
            cache_.graph().addEdge(edge);
            callbacks_.addEdgeTopo.callback().process(AddEdgeEvent{addData});
            splitEdgeId = edge.id();
        }
    }
}

void ProcessEvents::operator()(MergeEdgesEventData& event)
{
    Graph& graph = cache_.graph();
    Storage& storage = cache_.storage();
    Edge& merged = graph.edge(event.mergedId);
    Edge& deleted = graph.edge(event.deletedId);
    NodeID commonNodeId = event.mergeNodeId;
    NodeID deletedOther = deleted.oppositeNode(commonNodeId);

    DeleteEdgeEventData deleteEventData(
        deleted.id(), deleted.startNode(), deleted.endNode());
    (*this)(deleteEventData);

    MoveEdgeEventData moveEventData = (commonNodeId == merged.startNode())
        ? MoveEdgeEventData(merged.id(), deletedOther, boost::none,
            geolib3::Polyline2(event.newGeomPoints),
            IncidentNodes(merged.startNode(), merged.endNode()))
        : MoveEdgeEventData(merged.id(), boost::none, deletedOther,
            geolib3::Polyline2(event.newGeomPoints),
            IncidentNodes(merged.startNode(), merged.endNode()));
    (*this)(moveEventData);

    graph.deleteNode(commonNodeId);
    storage.deleteNode(commonNodeId);
}

void ProcessEvents::operator()(MergeNodesEventData& event)
{
    MergeNodesRequest mergeRequest(event);
    callbacks_.mergeNodes.callback().processRequest(mergeRequest);

    cache_.graph().moveNode(event.idFrom, event.pos);
    cache_.graph().moveNode(event.idTo, event.pos);

    Node& deletedNode = cache_.graph().node(event.idFrom);
    EdgePtrVector edgesToMove = cache_.graph().incidentEdges(deletedNode);
    for (auto edgePtr : edgesToMove) {
        OptionalNodeID newStart;
        OptionalNodeID newEnd;
        IncidentNodes oldIncidences(edgePtr->startNode(), edgePtr->endNode());
        OptionalNodeID& newNode =
            edgePtr->isStart(event.idFrom) ? newStart : newEnd;
        newNode.reset(event.idTo);
        MoveEdgeEventData moveEdgeData(
            edgePtr->id(), newStart, newEnd, edgePtr->geom(), oldIncidences);
        (*this)(moveEdgeData);
    }

    cache_.graph().deleteNode(event.idFrom);
    cache_.storage().updateNodePos(event.idTo, event.pos);
    cache_.storage().deleteNode(event.idFrom);

    MergeNodesEvent mergeEvent(event);
    callbacks_.mergeNodes.callback().processEvent(mergeEvent);
}

void ProcessEvents::createNodesAtSplitPoints(SplitPointPtrVector& splitPoints,
    const boost::optional<IncidentNodes>& oldIncidentNodes)
{
    REQUIRE(
        splitPoints.size() >= 2,
        "Split point list must have two points at least (end points are allways included)."
    );

    if (oldIncidentNodes) {
        reuseNode(splitPoints, splitPoints.front(), oldIncidentNodes->start);
        reuseNode(splitPoints, splitPoints.back(), oldIncidentNodes->end);
    }
    for (auto& splitPoint: splitPoints) {
        if (!splitPoint->nodeId) {
            Node node = cache_.storage().createNode(splitPoint->geom);
            splitPoint->nodeId = node.id();
            cache_.graph().addNode(node);
        }
    }
}

void
ProcessEvents::reuseNode(const SplitPointPtrVector& splitPoints,
    SplitPointPtr splitPoint, NodeID oldNodeId)
{
    auto used = [&] (const SplitPointPtr& p)
    {
        return p->nodeId == oldNodeId;
    };
    if (!splitPoint->nodeId &&
        cache_.graph().node(oldNodeId).isIsolated() &&
        cache_.storage().nodeCanBeDeleted(oldNodeId) &&
        std::none_of(splitPoints.begin(), splitPoints.end(), used))
    {
        splitPoint->nodeId = oldNodeId;
        cache_.graph().deleteNode(oldNodeId);
        cache_.graph().addNode(Node(oldNodeId, splitPoint->geom));
        cache_.storage().updateNodePos(oldNodeId, splitPoint->geom);
    }
}

/// Edge already deleted
void
ProcessEvents::processMoveEvent(MoveEdgeEventData& event)
{
    IncidentNodes nodes(
        event.newStartNode ? *event.newStartNode : event.oldIncidentNodes.start,
        event.newEndNode ? *event.newEndNode : event.oldIncidentNodes.end);
    Edge newEdge = Edge(event.id, event.newGeom, nodes.start, nodes.end);
    cache_.graph().addEdge(newEdge);
    cache_.storage().updateEdgeGeom(event.id, event.newGeom, nodes);
    callbacks_.moveEdgeTopo.callback().process(MoveEdgeEvent(event));
}

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