#include "segments_graph.h"

#include <maps/libs/log8/include/log8.h>

#include <maps/libs/geolib/include/conversion.h>
#include <iomanip>

namespace maps {
namespace wiki {
namespace topology_fixer {
namespace utils {

const Point&
SegmentsGraph::addPoint(boost::optional<NodeId> nodeId, const gl::Point2& pos)
{
    const auto newId = gen_->getId();
    return points_.insert({newId, {newId, nodeId, pos, {}}}).first->second;
}

const Segment&
SegmentsGraph::addSegment(PointId startPointId, PointId endPointId)
{
    Point& start = points_.at(startPointId);
    Point& end = points_.at(endPointId);
    const auto newId = gen_->getId();
    const Segment& segment = segments_.insert(
            {newId, {newId, startPointId, endPointId, boost::none, false}}
        ).first->second;
    start.segmentIds.insert(segment.id);
    end.segmentIds.insert(segment.id);
    return segment;
}

gl::Segment2
SegmentsGraph::segmentGeom(SegmentId segmentId) const
{
    const Segment& segment = segments_.at(segmentId);
    return
        {points_.at(segment.startPointId).pos,
         points_.at(segment.endPointId).pos};
}

SegmentsGraph::DegreesMap
SegmentsGraph::pointDegrees() const
{
    DegreesMap result;
    for (const auto& pointData : points_) {
        result.insert({pointData.first, pointData.second.segmentIds.size()});
    }
    return result;
}

void
SegmentsGraph::removeIsolatedPoint(PointId pointId)
{
    const Point& point = points_.at(pointId);
    ASSERT(point.segmentIds.empty());
    points_.erase(pointId);
}

// Does not remove points which became isolated
void
SegmentsGraph::removeSegment(SegmentId segmentId)
{
    const Segment& segment = segments_.at(segmentId);
    points_.at(segment.startPointId).segmentIds.erase(segmentId);
    points_.at(segment.endPointId).segmentIds.erase(segmentId);
    segments_.erase(segmentId);
}

// Does not remove points which became isolated
void
SegmentsGraph::moveSegment(SegmentId segmentId, PointId fromId, PointId toId)
{
    Segment& seg = segments_.at(segmentId);
    Point& fromPoint = points_.at(fromId);
    Point& toPoint = points_.at(toId);
    REQUIRE(fromId == seg.startPointId || fromId == seg.endPointId,
        "Segment " << segmentId << " is not incident to " << fromId);
    (fromId == seg.startPointId ? seg.startPointId : seg.endPointId) = toId;
    fromPoint.segmentIds.erase(segmentId);
    toPoint.segmentIds.insert(segmentId);
    REQUIRE(seg.startPointId != seg.endPointId,
        "Segment " << segmentId <<
        " after moving from " << fromId <<
        " to " << toId << " became degenerate, point id: " << seg.startPointId);
}

bool
SegmentsGraph::haveCommonEndpoint(SegmentId segmentId1, SegmentId segmentId2) const
{
    const auto& segment1 = segments_.at(segmentId1);
    const auto& segment2 = segments_.at(segmentId2);

    return segment1.startPointId == segment2.startPointId ||
        segment1.startPointId == segment2.endPointId ||
        segment1.endPointId == segment2.startPointId ||
        segment1.endPointId == segment2.endPointId;
}

namespace {

struct OrderedPoints {
    explicit OrderedPoints(const Segment& segment)
        : pointId1(segment.startPointId)
        , pointId2(segment.endPointId)
    {
        ASSERT(pointId1 != pointId2);
        if (pointId1 > pointId2) {
            std::swap(pointId1, pointId2);
        }
    }

    bool operator == (const OrderedPoints& other) const
    {
        return pointId1 == other.pointId1 && pointId2 == other.pointId2;
    }

    struct Hasher {
        size_t operator () (const OrderedPoints& p) const
        {
            return (size_t)(1000 * p.pointId1 + p.pointId2);
        }
    };

    PointId pointId1;
    PointId pointId2;
};

} // namespace

SegmentsGraph::OrigToUnitedSegmentIdsMap
SegmentsGraph::uniteDuplicatedSegments()
{
    std::unordered_map<OrderedPoints, SegmentIdsSet, OrderedPoints::Hasher>
        pointsToSegmentsMapping;
    for (const auto& segmentData : segments_) {
        pointsToSegmentsMapping[OrderedPoints(segmentData.second)].insert(
            segmentData.first);
    }
    OrigToUnitedSegmentIdsMap result;
    for (const auto& pointsToSegmentsData : pointsToSegmentsMapping) {
        const auto& dupSegmentsIds = pointsToSegmentsData.second;
        if (dupSegmentsIds.size() == 1) {
            continue;
        }
        const auto segmentId = *dupSegmentsIds.begin();
        segments_.at(segmentId).isShared = true;
        for (auto it = ++dupSegmentsIds.begin(); it != dupSegmentsIds.end(); ++it) {
            result.insert({*it, segmentId});
        }
    }

    for (const auto& segmentIdsPair : result) {
        removeSegment(segmentIdsPair.first);
    }
    return result;
}

void
SegmentsGraph::mergePoints(PointId fromId, PointId toId)
{
    const Point& from = points_.at(fromId);
    REQUIRE(!from.nodeId,
        "Point " << fromId << " is snapped to " << *from.nodeId << " node");
    IdSet movedSegmentIds = from.segmentIds;
    for (auto segmentId : movedSegmentIds) {
        moveSegment(segmentId, fromId, toId);
    }
    removeIsolatedPoint(fromId);
}

SegmentsGraph::PointIncidencesType
SegmentsGraph::pointIncidencesType(PointId pointId) const
{
    const Point& point = points_.at(pointId);
    bool hasShared = false;
    bool hasNonShared = false;
    for (auto segmentId : point.segmentIds) {
        const bool isShared = segments_.at(segmentId).isShared;
        hasShared = hasShared || isShared;
        hasNonShared = hasNonShared || !isShared;
    }
    static const std::map<std::pair<bool, bool>, PointIncidencesType> mapping = {
        {{false, false}, PointIncidencesType::Isolated},
        {{false, true}, PointIncidencesType::SharedOnly},
        {{true, false}, PointIncidencesType::NonSharedOnly},
        {{true, true}, PointIncidencesType::Mixed}
    };
    return mapping.at({hasNonShared, hasShared});
}

PointId
SegmentsGraph::otherPointId(const Segment& segment, PointId pointId) const
{
    REQUIRE(pointId == segment.startPointId || pointId == segment.endPointId,
            "Segment " << segment.id << " is not incident to " << pointId);
    return segment.startPointId == pointId
        ? segment.endPointId
        : segment.startPointId;
}

SegmentIdsList
SegmentsGraph::sharedSegmentIds() const
{
    SegmentIdsList result;
    for (const auto& segmentData : segments_) {
        if (segmentData.second.isShared) {
            result.push_back(segmentData.first);
        }
    }
    return result;
}

std::list<SegmentsGraph::Circuit>
SegmentsGraph::buildPartition() const
{
    const auto& mergeablePoints = mergeablePointIncidences();
    auto isMergeable = [&] (PointId pointId) -> bool
    {
        return mergeablePoints.count(pointId) > 0;
    };
    SegmentIdsSet toProcess;
    for (const auto& segmentData : segments_) {
        toProcess.insert(segmentData.first);
    }
    std::list<Circuit> result;
    while (!toProcess.empty()) {
        SegmentId curSegId = *toProcess.begin();
        const Segment& curSeg = segments_.at(curSegId);
        toProcess.erase(curSegId);
        Circuit curCircuit;
        curCircuit.pointIdsList.push_front(curSeg.startPointId);
        curCircuit.pointIdsList.push_back(curSeg.endPointId);
        curCircuit.segmentIdsList.push_back(curSegId);
        bool canBeExtendedFront, canBeExtendedBack;
        while (!toProcess.empty() &&
               ((canBeExtendedFront = isMergeable(curCircuit.pointIdsList.front())) ||
               (canBeExtendedBack = isMergeable(curCircuit.pointIdsList.back()))))
        {
            curSegId = canBeExtendedFront
                ? curCircuit.segmentIdsList.front()
                : curCircuit.segmentIdsList.back();
            const auto curPointId = canBeExtendedFront
                ? curCircuit.pointIdsList.front()
                : curCircuit.pointIdsList.back();
            const auto& pointIncidences = mergeablePoints.at(curPointId);
            ASSERT(pointIncidences.size() == 2);
            const auto nextSegId = *pointIncidences.begin() == curSegId
                ? *++pointIncidences.begin()
                : *pointIncidences.begin();
            curCircuit.segmentIdsList.insert(
                (canBeExtendedFront
                    ? curCircuit.segmentIdsList.begin()
                    : curCircuit.segmentIdsList.end()),
                nextSegId);
            auto nextPointId = otherPointId(segments_.at(nextSegId), curPointId);
            curCircuit.pointIdsList.insert(
                (canBeExtendedFront
                    ? curCircuit.pointIdsList.begin()
                    : curCircuit.pointIdsList.end()),
                nextPointId);
            toProcess.erase(nextSegId);
        }
        result.push_back(curCircuit);
    }
    ASSERT(toProcess.empty());
    return result;
}


void
SegmentsGraph::print() const
{
    std::ostringstream os;
    os << std::setprecision(12);
    os << "Nodes: [";
    for (const auto& point : points_) {
        gl::Point2 p = gl::mercator2GeoPoint(point.second.pos);
        os << "{" << point.first << ", (" << p.x() << ", " << p.y() << "), ";
        if (point.second.nodeId) {
            os << "node id: " << *point.second.nodeId;
        } else {
            os << "node id: none";
        }
        os << "} ";
    }
    os << "]; Edges: [";
    for (const auto& segment : segments_) {
        os << "{" << segment.first << ", (" << segment.second.startPointId << ", " << segment.second.endPointId << "), ";
        if (segment.second.edgeId) {
            os << "edge id: " << *segment.second.edgeId;
        } else {
            os << "edge id: none";
        }
        os << ", is shared: " << (segment.second.isShared ? "true" : "false");
        os << "} ";
    }
    os << "]";
    INFO() << os.str();
}


SegmentsGraph::PointIncidencesMap
SegmentsGraph::mergeablePointIncidences() const
{
    auto pointIsMergeable = [&] (const Point& point) -> bool
    {
        if (point.nodeId) {
            return false;
        }
        const auto incidencesType = pointIncidencesType(point.id);
        REQUIRE(point.segmentIds.size() == 2 &&
            (incidencesType == PointIncidencesType::NonSharedOnly ||
             incidencesType == PointIncidencesType::SharedOnly),
            "Point " << point.id << " incidences are not correct");
        return true;
    };
    PointIncidencesMap result;
    for (const auto& pointData : points_) {
        if (pointIsMergeable(pointData.second)) {
            result.insert({pointData.first, pointData.second.segmentIds});
        }
    }
    return result;
}

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