#include "utils.h"

#include <yandex/maps/wiki/geom_tools/polygonal/builder.h>

#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/geolib/include/contains.h>
#include <maps/libs/geolib/include/conversion_geos.h>
#include <maps/libs/geolib/include/prepared_polygon.h>

#include <geos/geom/LinearRing.h>
#include <geos/geom/Point.h>

#include <memory>

namespace maps {
namespace wiki {
namespace geom_tools {

namespace {

struct RingInfo
{
    const RingAdapter* ringPtr;
    const geolib3::Polygon2* polyPtr;
    double area;
    geolib3::PreparedPolygon2 preparedPolygon;

    RingInfo(const RingAdapter* ringPtr, const geolib3::Polygon2* polyPtr)
        : ringPtr(ringPtr)
        , polyPtr(polyPtr)
        , area(polyPtr->area())
        , preparedPolygon(*polyPtr)
    {
    }
};

} // namespace

GeolibPolygonVector
buildPolygons(
    const RingVector& exteriorRings,
    const RingVector& interiorRings,
    ValidateResult validateResult)
{
    GeolibPolygonVector exteriorPolygons;
    exteriorPolygons.reserve(exteriorRings.size());
    std::vector<RingInfo> exteriorRingInfos;
    exteriorRingInfos.reserve(exteriorRings.size());

    for (const RingAdapter& ring : exteriorRings) {
        exteriorPolygons.push_back(
            geolib3::Polygon2(ring.ring(), {}, /*bool validate = */ false));
        exteriorRingInfos.emplace_back(
                &ring,
                &exteriorPolygons.back());
    }

    std::sort(exteriorRingInfos.begin(), exteriorRingInfos.end(),
        [] (const RingInfo& l, const RingInfo& r) { return l.area < r.area; });

    std::vector<std::list<size_t>> holeIndices(exteriorRingInfos.size());
    for (size_t holeIdx = 0; holeIdx < interiorRings.size(); ++holeIdx) {
        const RingAdapter& hole = interiorRings[holeIdx];
        const geos::geom::LinearRing* geosRing =
            geolib3::internal::geolib2geosGeometry(hole.ring());
        std::unique_ptr<geos::geom::Point> geosInteriorPoint(geosRing->getInteriorPoint());
        geolib3::Point2 interiorPoint(geosInteriorPoint->getX(), geosInteriorPoint->getY());
        size_t exteriorIdx = 0;
        for ( ; exteriorIdx < exteriorRingInfos.size(); ++exteriorIdx) {
            const auto& preparedPoly = exteriorRingInfos[exteriorIdx].preparedPolygon;
            if (geolib3::spatialRelation(preparedPoly, interiorPoint, geolib3::Contains)) {
                break;
            }
        }
        if (exteriorIdx == exteriorRingInfos.size()) {
           throw PolygonBuildError() << " hole is outside any shell";
        }
        holeIndices[exteriorIdx].push_back(holeIdx);
    }

    GeolibPolygonVector result;
    result.reserve(exteriorRingInfos.size());
    for (size_t exteriorIdx = 0; exteriorIdx < exteriorRingInfos.size(); ++exteriorIdx) {
        GeolibLinearRingVector holes;
        holes.reserve(holeIndices[exteriorIdx].size());
        for (auto holeIdx : holeIndices[exteriorIdx]) {
            holes.push_back(interiorRings[holeIdx].ring());
        }
        result.push_back(geolib3::Polygon2(
            exteriorRingInfos[exteriorIdx].ringPtr->ring(),
            holes,
            validateResult == ValidateResult::Yes));
    }

    return result;
}

boost::optional<GeolibPolygonVector>
tryBuildPolygons(
    const RingVector& exteriorRings,
    const RingVector& interiorRings,
    ValidateResult validateResult)
{
    try {
        return buildPolygons(exteriorRings, interiorRings, validateResult);
    } catch (...) {
        return boost::none;
    }
}

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