#include "cutter.h"

#include "validate.h"
#include "../utils.h"

#include <maps/libs/geolib/include/intersection.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/geolib/include/contains.h>
#include <maps/libs/geolib/include/conversion.h>

#include <stack>
#include <set>
#include <map>
#include <queue>
#include <iomanip>

namespace maps {
namespace wiki {
namespace geom_tools {

namespace {

geolib3::Point2
intersectionPoint(const geolib3::Line2& line, const geolib3::Segment2& segment)
{
    auto intersection = geolib3::intersection(line, segment);
    REQUIRE(intersection.size() == 1, "Incorrect line-segment intersection size");
    return intersection.front();
}

geolib3::Point2
snapToCutLine(const CutLine& cutLine, const geolib3::Point2& point)
{
    return cutLine.direction() == Direction::X
        ? geolib3::Point2(cutLine.coord(), point.y())
        : geolib3::Point2(point.x(), cutLine.coord());
}

geolib3::Point2 snapToCutLineNearby(
    const CutLine& cutLine, const geolib3::Point2& point, Relation relation)
{
    double delta = cutLine.tolerance() / 2.0;
    if (relation == Relation::Less) {
        delta = -delta;
    }
    return cutLine.direction() == Direction::X
        ? geolib3::Point2(cutLine.coord() + delta, point.y())
        : geolib3::Point2(point.x(), cutLine.coord() + delta);
}

} // namespace

RingAdapter
PolygonCutter::adjustRingCoords(const RingAdapter& ring, Relation relation) const
{
    REQUIRE(
        relation == Relation::Less || relation == Relation::Greater,
        "Wrong relation, must be Less or Greater");

    geolib3::PointsVector points;
    points.reserve(ring.pointsCount());
    for (size_t i = 0; i < ring.pointsCount(); ++i) {
        const auto& point = ring.point(i);
        const auto pointRelation = cutLine_.relation(point);
        points.push_back(pointRelation == Relation::Equal
            ? snapToCutLineNearby(cutLine_, point, relation)
            : point);
    }

    return RingAdapter(std::make_shared<geolib3::LinearRing2>(std::move(points)));
}

namespace {

boost::optional<size_t>
findBoundForRealtion(Relation relation, const RingAdapter& ring, const CutLine& cutLine)
{
    Relation curPointRel = cutLine.relation(ring.point(0));
    size_t curIdx = 0;
    if (curPointRel == relation) {
        do {
            curIdx = ring.prevPointNumber(curIdx);
            curPointRel = cutLine.relation(ring.point(curIdx));
        } while (curPointRel == relation && curIdx != 0);

        REQUIRE(
            curPointRel != relation, "All points are within halfplane");

        return ring.nextPointNumber(curIdx);
    }

    while (curPointRel != relation && curIdx < ring.pointsCount() - 1) {
        ++curIdx;
        curPointRel = cutLine.relation(ring.point(curIdx));
    }

    if (curPointRel == relation) {
        return curIdx;
    }

    return boost::none;
}

struct RingCutState {
    void switchToNextPoint(const CutLine& cutLine, const RingAdapter& ring)
    {
        prevIdx = curIdx;
        prevPoint = curPoint;
        prevPointRelation = curPointRelation;
        curIdx = ring.nextPointNumber(curIdx);
        curPoint = ring.point(curIdx);
        curPointRelation = cutLine.relation(curPoint);
        if (curPointRelation == Relation::Equal) {
            curPoint = snapToCutLine(cutLine, curPoint);
        }
    }

    geolib3::PointsVector borderPointsBuffer;
    size_t curIdx;
    geolib3::Point2 curPoint;
    Relation curPointRelation;
    size_t prevIdx;
    geolib3::Point2 prevPoint;
    Relation prevPointRelation;
};

RingCutState
prepareForCut(const CutLine& cutLine, const RingAdapter& ring)
{
    Relation rel = Relation::Less;
    boost::optional<size_t> pointOutOfCutLineNumber =
        findBoundForRealtion(rel, ring, cutLine);

    if (!pointOutOfCutLineNumber) {
        rel = Relation::Greater;
        pointOutOfCutLineNumber =
            findBoundForRealtion(rel, ring, cutLine);
    }

    REQUIRE(pointOutOfCutLineNumber, "All points lie on cut line");

    RingCutState result;
    result.curIdx = *pointOutOfCutLineNumber;
    result.curPoint = ring.point(result.curIdx);
    result.curPointRelation = rel;
    result.prevIdx = ring.prevPointNumber(result.curIdx);
    result.prevPoint = ring.point(result.prevIdx);
    result.prevPointRelation = cutLine.relation(result.prevPoint);

    if (result.prevPointRelation == Relation::Equal) {
        result.prevPoint = snapToCutLine(cutLine, result.prevPoint);
        result.borderPointsBuffer.push_back(result.prevPoint);
    } else {
        result.borderPointsBuffer.push_back(
            intersectionPoint(cutLine.line(), geolib3::Segment2(result.prevPoint, result.curPoint)));
    }
    result.borderPointsBuffer.push_back(result.curPoint);

    return result;
}

} // namespace

PolygonCutter::RingCutResult
PolygonCutter::cutRing(const RingAdapter& ring) const
{
    auto state = prepareForCut(cutLine_, ring);
    Relation curHalfplane = state.curPointRelation;

    RingCutResult result;

    auto flush = [&] ()
    {
        if (state.borderPointsBuffer.size() < 3) {
            return;
        }


        (curHalfplane == Relation::Less
            ? result.lessBorders
            : result.greaterBorders).push_back(state.borderPointsBuffer);

        state.borderPointsBuffer.clear();
    };

    bool needsFinalFlush = state.prevPointRelation == Relation::Equal;
    state.switchToNextPoint(cutLine_, ring);
    const auto stopIdx = needsFinalFlush ? state.prevIdx : state.curIdx;
    do {
        if (state.curPointRelation == curHalfplane) {
            if (state.prevPointRelation == Relation::Equal) {
                state.borderPointsBuffer.push_back(state.prevPoint);
            }
            state.borderPointsBuffer.push_back(state.curPoint);
        } else if (state.curPointRelation == Relation::Equal) {
            if (state.prevPointRelation != Relation::Equal) {
                state.curPoint = snapToCutLine(cutLine_, state.curPoint);
                state.borderPointsBuffer.push_back(state.curPoint);
                flush();
            }
        } else if (state.prevPointRelation == Relation::Equal) {
            state.borderPointsBuffer = geolib3::PointsVector{state.prevPoint, state.curPoint};
            curHalfplane = state.curPointRelation;
        } else {
            auto borderPoint =
                intersectionPoint(cutLine_.line(), ring.prevSegment(state.curIdx));
            state.borderPointsBuffer.push_back(borderPoint);
            flush();
            state.borderPointsBuffer = geolib3::PointsVector{borderPoint, state.curPoint};
            curHalfplane = state.curPointRelation;
        }
        state.switchToNextPoint(cutLine_, ring);
    } while (state.curIdx != stopIdx);

    if (needsFinalFlush) {
        flush();
    }

    return result;
}

namespace {

double coordAlongDirection(Direction direction, const geolib3::Point2& p)
{
    return direction == Direction::X ? p.y() : p.x();
}

enum class EndpointType { Start, End };

struct Endpoint {
    Endpoint otherEndpoint(Direction direction) const
    {
        Endpoint result = {
            borderPointsIt,
            type == EndpointType::Start ? EndpointType::End : EndpointType::Start,
            borderOrientation,
            coordAlongDirection(
                direction,
                (type == EndpointType::Start ? borderPointsIt->back() : borderPointsIt->front()))
        };
        return result;
    }

    std::list<geolib3::PointsVector>::const_iterator borderPointsIt;
    EndpointType type;
    Orientation borderOrientation;
    double coord;
};

std::string
endpointPosStr(const Endpoint& endpoint, const CutLine& cutLine)
{
    auto point = cutLine.direction() == Direction::X
        ? geolib3::Point2(cutLine.coord(), endpoint.coord)
        : geolib3::Point2(endpoint.coord, cutLine.coord());


    std::ostringstream os;
    os << std::setprecision(16) << "(" << point.x() << ", " << point.y() << ")";
    point = geolib3::mercator2GeoPoint(point);
    os << ", geo (" << point.x() << ", " << point.y() << "), type " << (endpoint.type == EndpointType::Start ? "start" : "end");
    return os.str();
}

bool
isOneWithinAnother(
    const geolib3::PointsVector& points1, const geolib3::PointsVector& points2,
    const CutLine& cutLine)
{
    typedef std::pair<double, double> Interval;

    auto coord = [&] (const geolib3::Point2& p)
        { return coordAlongDirection(cutLine.direction(), p); };

    auto containsInterval = [&] (Interval i1, Interval i2)
    {
        if (i1.first > i1.second) { std::swap(i1.first, i1.second); }
        if (i2.first > i2.second) { std::swap(i2.first, i2.second); }

        return
            i1.first - 2 * cutLine.tolerance() < i2.first &&
            i1.second + 2 * cutLine.tolerance() > i2.second;
    };

    Interval i1 = {coord(points1.front()), coord(points1.back())};
    Interval i2 = {coord(points2.front()), coord(points2.back())};
    return containsInterval(i1, i2) || containsInterval(i2, i1);
}

std::pair<bool, bool>
isOneWithinAnotherByPolygon(
    const geolib3::PointsVector& points1, const geolib3::PointsVector& points2)
{
    auto poly1 = geolib3::Polygon2(points1).boundingBox();
    auto poly2 = geolib3::Polygon2(points2).boundingBox();

    bool cmp = geolib3::spatialRelation(poly1, poly2, geolib3::Contains);

    return {cmp, !cmp};
}

class EndpointCompare {
public:
    EndpointCompare(const CutLine& cutLine, Relation halfplaneRelation)
        : cutLine_(cutLine)
        , halfplaneRelation_(halfplaneRelation)
    {}

    bool operator () (const Endpoint& e1, const Endpoint& e2) const
    {
        if (e1.borderPointsIt == e2.borderPointsIt) {
            if (e1.type == e2.type) {
                return false; //strict weak ordering requirement
            }

            return
                (e1.borderOrientation == Orientation::CCW && e1.type == EndpointType::Start) ||
                (e1.borderOrientation == Orientation::CW && e1.type == EndpointType::End);
        }

        if (geolib3::sign(e1.coord - e2.coord, cutLine_.tolerance())) {
            return (*this)(e1.coord, e2.coord);
        }

        Endpoint oe1 = e1.otherEndpoint(cutLine_.direction());
        Endpoint oe2 = e2.otherEndpoint(cutLine_.direction());

        if (!geolib3::sign(oe1.coord - oe2.coord, cutLine_.tolerance())) {
            auto cmp = isOneWithinAnotherByPolygon(*e1.borderPointsIt, *e2.borderPointsIt);
            if (cmp.first) {
                return
                    (e1.borderOrientation == Orientation::CCW && e1.type == EndpointType::Start) ||
                    (e1.borderOrientation == Orientation::CW && e1.type == EndpointType::End);
            } else {
                return
                    (e1.borderOrientation == Orientation::CW && e1.type == EndpointType::Start) ||
                    (e1.borderOrientation == Orientation::CCW && e1.type == EndpointType::End);
            }
        }
        bool cmp = (*this)(oe1.coord, oe2.coord);
        if (isOneWithinAnother(*e1.borderPointsIt, *e2.borderPointsIt, cutLine_)) {
            cmp = !cmp;
        }
        return cmp;
    }

    bool operator () (const geolib3::Point2& p1, const geolib3::Point2& p2) const
    {
        return (*this)(
            coordAlongDirection(cutLine_.direction(), p1),
            coordAlongDirection(cutLine_.direction(), p2));
    }

private:
    bool operator () (double c1, double c2) const
    {
        return halfplaneRelation_ == Relation::Greater
            ? (cutLine_.direction() == Direction::X ? c1 < c2 : c1 > c2)
            : (cutLine_.direction() == Direction::X ? c1 > c2 : c1 < c2);
    }

    const CutLine& cutLine_;
    Relation halfplaneRelation_;
};

Orientation
borderOrientation(const geolib3::PointsVector& border, const EndpointCompare& compare)
{
    REQUIRE(border.size() >= 3, "Empty border points");

    size_t minIdx, maxIdx;
    minIdx = maxIdx = 0;

    for (size_t i = 1; i < border.size(); ++i) {
        const auto& p = border[i];
        if (compare(p, border[minIdx])) {
            minIdx = i;
        }
        if (compare(border[maxIdx], p)) {
            maxIdx = i;
        }
    }

    REQUIRE(minIdx != maxIdx, "One point has max and min coord at the same time");

    const Orientation endpointsOrientation = compare(border.front(), border.back())
        ? Orientation::CCW
        : Orientation::CW;
    const Orientation res = minIdx < maxIdx ? Orientation::CCW : Orientation::CW;

    REQUIRE(
        endpointsOrientation == res,
        "Border orientation computed by endpoints and extremal points are not the same");

    return res;
}

std::vector<Endpoint>
sortedEndpoints(
    const CutLine& cutLine,
    const std::list<geolib3::PointsVector>& borders, Relation relation)
{
    REQUIRE(!borders.empty(), "Empty borders list");

    EndpointCompare compare(cutLine, relation);

    auto coord = [&] (const geolib3::Point2& p)
        { return coordAlongDirection(cutLine.direction(), p); };

    std::vector<Endpoint> endpoints;
    endpoints.reserve(borders.size() * 2);
    for (auto borderIt = borders.begin(); borderIt != borders.end(); ++borderIt) {
        auto orientation = borderOrientation(*borderIt, compare);
        endpoints.push_back(
            Endpoint{borderIt, EndpointType::Start, orientation, coord(borderIt->front())});
        endpoints.push_back(
            Endpoint{borderIt, EndpointType::End, orientation, coord(borderIt->back())});
    }

    std::sort(endpoints.begin(), endpoints.end(), compare);

    REQUIRE(endpoints.front().type == EndpointType::Start,
            "First point is not start " << endpointPosStr(endpoints.front(), cutLine));
    for (size_t i = 0; i < endpoints.size() - 1; ++i) {
        REQUIRE(endpoints[i].type != endpoints[i + 1].type,
        "Endpoints type alternation error at "
            << endpointPosStr(endpoints[i], cutLine) << ", "
            << endpointPosStr(endpoints[i + 1], cutLine));
    }

    return endpoints;
}

} // namespace

std::list<RingAdapter>
PolygonCutter::restoreRings(
    const std::list<geolib3::PointsVector>& borders, Relation relation) const
{
    auto endpoints = sortedEndpoints(cutLine(), borders, relation);

    auto appendPoints = [&] (geolib3::PointsVector& to, const geolib3::PointsVector& from)
    {
        to.insert(
            to.end(),
            (!to.empty() && geolib3::distance(to.back(), from.front()) < cutLine_.tolerance()
                ? std::next(from.begin())
                : from.begin()),
            from.end());
    };

    std::list<RingAdapter> result;
    std::stack<Endpoint> endpointsStack;
    for (const Endpoint& endpoint : endpoints) {
        if (endpoint.borderOrientation == Orientation::CW ||
            endpoint.type == EndpointType::Start)
        {
            endpointsStack.push(endpoint);
            continue;
        }
        // CCW polyline part end point
        geolib3::PointsVector points;
        // pop border ends till CCW polyline start point, form a ring
        auto borderIt = endpoint.borderPointsIt;
        for (;;) {
            REQUIRE(
                !endpointsStack.empty(),
                "Ill-formed ring borders: no exterior ring start point prior to end point");
            Endpoint e = endpointsStack.top();
            endpointsStack.pop();
            if (e.type == EndpointType::Start) {
                appendPoints(points, *e.borderPointsIt);
                if (e.borderOrientation == Orientation::CCW &&
                    e.borderPointsIt == borderIt)
                {
                    break; // found start point of poly we began with
                }
            }
        }
        if (points.size() >= 3) {
            result.push_back(
                RingAdapter(std::make_shared<geolib3::LinearRing2>(std::move(points))));
        }
    }
    return result;
}

boost::optional<GeolibPolygonVector>
PolygonCutter::buildPolygons(RingList&& shells, RingList&& holes) const
{
    RingList rings;
    rings.insert(rings.end(), shells.begin(), shells.end());
    rings.insert(rings.end(), holes.begin(), holes.end());

    if (!findIncorrectIntersections(cutLine_, rings).empty()) {
        return boost::none;
    }

    auto polygons = tryBuildPolygons(
        convertRingsListToVector(std::move(shells)),
        convertRingsListToVector(std::move(holes)),
        ValidateResult::Yes);

    if (!polygons) {
        return boost::none;
    }

    return polygons;
}

boost::optional<PolygonCutter::Result>
PolygonCutter::operator () (const geolib3::Polygon2& polygon_) const
{
        const auto rel = cutLine_.relation(polygon_);
        if (rel == Relation::Less) {
            return Result{{polygon_}, {}};
        } else if (rel == Relation::Greater) {
            return Result{{}, {polygon_}};
        } else if (rel == Relation::Equal) {
            return Result{{}, {}};
        }

        PolygonAdapter polygon(polygon_);
        polygon.normalize();

        std::list<RingAdapter> lessHoles, greaterHoles;

        RingCutResult cutRingBorders = cutRing(polygon.shell());
        for (size_t hIdx = 0; hIdx < polygon.holesCount(); ++hIdx) {
            const auto& hole = polygon.hole(hIdx);
            const auto holeRel = cutLine_.relation(hole);
            if (holeRel == Relation::Equal) {
                continue;
            }
            if (holeRel == Relation::Greater || holeRel == Relation::Less) {
                (holeRel == Relation::Greater ? greaterHoles : lessHoles).push_back(
                    adjustRingCoords(hole, holeRel));
                continue;
            }
            auto cutResult = cutRing(hole);
            cutRingBorders.lessBorders.splice(
                cutRingBorders.lessBorders.end(), std::move(cutResult.lessBorders));
            cutRingBorders.greaterBorders.splice(
                cutRingBorders.greaterBorders.end(), std::move(cutResult.greaterBorders));
        }

        auto lessPolygons = buildPolygons(
            restoreRings(cutRingBorders.lessBorders, Relation::Less),
            std::move(lessHoles));

        if (!lessPolygons) {
            return boost::none;
        }

        auto greaterPolygons = buildPolygons(
            restoreRings(cutRingBorders.greaterBorders, Relation::Greater),
            std::move(greaterHoles));

        if (!greaterPolygons) {
            return boost::none;
        }

        return Result{std::move(*lessPolygons), std::move(*greaterPolygons)};
}

} // namespace geom_tools
} // namespace wiki
} // namespace maps
