#include "path_utils.h"

namespace maps {
namespace wiki {
namespace topo {

const std::string&
incidenceToStr(const IncidenceType& incidenceType)
{
    static const std::map<IncidenceType, std::string> incReprs = {
        {IncidenceType::Start, "start"},
        {IncidenceType::End, "end"}
    };
    return incReprs.at(incidenceType);
}

IncidencesByNodeMap
incidencesByEdgesToIncidencesByNodes(const IncidencesByEdgeMap& incidences)
{
    IncidencesByNodeMap result;
    for (const auto& edgeInc : incidences) {
        const EdgeID id = edgeInc.first;
        const IncidentNodes& nodes = edgeInc.second;
        result[nodes.start].push_back({id, IncidenceType::Start});
        result[nodes.end].push_back({id, IncidenceType::End});
    }
    return result;
}

IncidencesSetByNodeMap
incidencesByEdgesToIncidencesSetByNodes(const IncidencesByEdgeMap& incidences)
{
    IncidencesSetByNodeMap result;
    for (const auto& edgeInc : incidences) {
        const EdgeID id = edgeInc.first;
        const IncidentNodes& nodes = edgeInc.second;
        result[nodes.start].insert({id, nodes.start, IncidenceType::Start});
        result[nodes.end].insert({id, nodes.end, IncidenceType::End});
    }
    return result;
}

namespace {

void
removeIncidences(
    IncidencesSetByNodeMap& incidences,
    EdgeID edgeId, const IncidentNodes& nodes)
{
    auto removeInc = [&incidences, edgeId] (NodeID nodeId, IncidenceType type)
    {
        auto it = incidences.find(nodeId);
        REQUIRE(it != incidences.end(), "Node " << nodeId << " incidences not found");
        ASSERT(it->second.erase(Incidence{edgeId, nodeId, type}) == 1);
        if (it->second.empty()) {
            incidences.erase(it);
        }
    };

    removeInc(nodes.start, IncidenceType::Start);
    removeInc(nodes.end, IncidenceType::End);
}

} // namespace

PathsList
buildPaths(const IncidencesByEdgeMap& incidences)
{
    IncidencesSetByNodeMap nodeIncidencesMap =
        incidencesByEdgesToIncidencesSetByNodes(incidences);
    NodeIDSet internalPathNodes;
    for (const auto& nodeInc : nodeIncidencesMap) {
        if (nodeInc.second.size() == 2) {
            internalPathNodes.insert(nodeInc.first);
        }
    }

    PathsList result;

    auto extendPath = [&] (Path& path, NodeID curNodeId)
    {
        for (auto curNodeIt = nodeIncidencesMap.find(curNodeId);
             curNodeIt != nodeIncidencesMap.end()
                && internalPathNodes.count(curNodeIt->first);
             curNodeIt = nodeIncidencesMap.find(curNodeId))
        {
            IncidencesSet& nodeIncidences = curNodeIt->second;
            ASSERT(!nodeIncidences.empty());
            const EdgeID edgeId = nodeIncidences.begin()->edge;
            const IncidentNodes& incNodes = incidences.at(edgeId);
            if (curNodeId == path.startNodeId) {
                path.edgeIds.push_front(edgeId);
                path.nodeIds.push_front(
                    path.startNodeId == incNodes.start ? incNodes.end : incNodes.start);
            } else {
                path.edgeIds.push_back(edgeId);
                path.nodeIds.push_back(
                    path.endNodeId == incNodes.start ? incNodes.end : incNodes.start);
            }
            auto& pathNode = path.startNodeId == curNodeId
                ? path.startNodeId
                : path.endNodeId;
            pathNode = pathNode == incNodes.start
                ? incNodes.end
                : incNodes.start;
            removeIncidences(nodeIncidencesMap, edgeId, incNodes);
            curNodeId = pathNode;
        }
    };

    while (!nodeIncidencesMap.empty()) {
        const IncidencesSet& nodeIncidences = nodeIncidencesMap.begin()->second;
        ASSERT(!nodeIncidences.empty());
        const EdgeID edgeId = nodeIncidences.begin()->edge;
        const IncidentNodes& incNodes = incidences.at(edgeId);
        Path path = {
            incNodes.start, incNodes.end,
            {edgeId},
            {incNodes.start, incNodes.end}};
        removeIncidences(nodeIncidencesMap, edgeId, incNodes);
        extendPath(path, path.startNodeId);
        extendPath(path, path.endNodeId);
        result.push_back(path);
    }

    return result;
}

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