#include "area_checker.h"

#include "utils/geom_helpers.h"
#include "utils/searchers.h"
#include "utils/master_area_holder.h"

#include <yandex/maps/wiki/threadutils/threadpool.h>

#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/line.h>
#include <maps/libs/geolib/include/linear_ring.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/conversion_geos.h>
#include <maps/libs/geolib/include/intersection.h>

#include <functional>
#include <algorithm>
#include <thread>
#include <atomic>

namespace gl = maps::geolib3;

namespace maps {
namespace wiki {
namespace topology_fixer {

namespace {

enum class HalfplaneRelation { In, Out, OnBorder };

typedef std::function<HalfplaneRelation(const gl::Point2&)> HalfplanePointMatcher;

std::vector<gl::PointsVector>
clipPreparedGeometry(
    const gl::PointsVector& points,
    HalfplanePointMatcher halfplanePointMatcher, const gl::Line2& clipLine)
{
    REQUIRE(halfplanePointMatcher(points.front()) == HalfplaneRelation::Out,
        "First point is not outer");
    HalfplaneRelation prevRelation = HalfplaneRelation::Out;
    gl::Point2 prevPoint = points.front();

    std::vector<gl::PointsVector> result;
    gl::PointsVector buffer;

    auto intersection = [&] (const gl::Point2& p1, const gl::Point2& p2)
    {
        auto intersectionPoints = gl::intersection(clipLine, gl::Segment2(p1, p2));
        ASSERT(intersectionPoints.size() == 1);
        return intersectionPoints.front();
    };

    for (size_t i = 1; i < points.size(); ++i) {
        gl::Point2 curPoint = points[i];
        HalfplaneRelation curRelation = halfplanePointMatcher(curPoint);
        if (curRelation == HalfplaneRelation::In && prevRelation != HalfplaneRelation::In) {
            buffer.push_back(prevRelation == HalfplaneRelation::OnBorder
                ? prevPoint
                : intersection(prevPoint, curPoint));
        }
        if (curRelation == HalfplaneRelation::In) {
            buffer.push_back(curPoint);
        }
        if (curRelation != HalfplaneRelation::In && prevRelation == HalfplaneRelation::In) {
            buffer.push_back(curRelation == HalfplaneRelation::OnBorder
                ? curPoint
                : intersection(prevPoint, curPoint));
            if (buffer.size() > 2) {
                result.push_back(buffer);
            }
            buffer.clear();
        }
        prevRelation = curRelation;
        prevPoint = curPoint;
    }
    if (buffer.size() > 2) {
        result.push_back(buffer);
    }

    return result;
}

enum class GeometryType { Polyline, Ring, Polygon };

std::vector<gl::PointsVector>
clipRing(const gl::PointsVector& geom,
    HalfplanePointMatcher halfplanePointMatcher, const gl::Line2& clipLine)
{
    gl::PointsVector points = geom;
    if (points.front() != points.back()) {
        points.push_back(points.front());
    }
    REQUIRE(points.size() >= 4, "Ill-formed ring: too few points");
    REQUIRE(points.front() == points.back(), "Ill-formed ring: not closed");

    points.pop_back();
    auto it = std::find_if(points.begin(), points.end(),
        [&] (const gl::Point2& p) -> bool
        {
            return halfplanePointMatcher(p) == HalfplaneRelation::Out;
        });
    if (it == points.end()) {
        return {geom};
    }
    std::rotate(points.begin(), it, points.end()); // to begin with outer point
    points.push_back(points.front());

    return clipPreparedGeometry(points, halfplanePointMatcher, clipLine);
}

std::vector<gl::PointsVector>
clipPolyline(const gl::PointsVector& geom,
    HalfplanePointMatcher halfplanePointMatcher, const gl::Line2& clipLine)
{
    gl::PointsVector points = geom;
    REQUIRE(points.size() >= 2, "Ill-formed polyline: too few points");

    auto it = std::find_if(points.begin(), points.end(),
        [&] (const gl::Point2& p) -> bool
        {
            return halfplanePointMatcher(p) == HalfplaneRelation::Out;
        });
    if (it == points.end()) {
        return {geom};
    }

    std::vector<gl::PointsVector> result;
    gl::PointsVector part(points.begin(), std::next(it));
    if (part.size() >= 2) {
        std::reverse(part.begin(), part.end());
        for (auto&& clippedPart :
            clipPreparedGeometry(part, halfplanePointMatcher, clipLine))
        {
            std::reverse(clippedPart.begin(), clippedPart.end());
            result.push_back(std::move(clippedPart));
        }
    }
    gl::PointsVector otherPart(it, points.end());
    if (otherPart.size() >= 2) {
        for (auto&& clippedPart :
            clipPreparedGeometry(otherPart, halfplanePointMatcher, clipLine))
        {
            result.push_back(std::move(clippedPart));
        }
    }

    return result;
}

std::vector<gl::PointsVector>
buildRings(const std::vector<gl::PointsVector>& clippedBorders)
{
    std::vector<gl::PointsVector> result;
    for (const auto& border : clippedBorders) {
        auto points = border;
        if (points.front() != points.back()) {
            points.push_back(points.front());
        }
        if (points.size() >= 4) {
            result.push_back(std::move(points));
        }
    }

    return result;
}

std::vector<gl::PointsVector>
clip(GeometryType geomType, const gl::PointsVector& points, const gl::Line2& axis,
    HalfplanePointMatcher clipper)
{
    if (geomType == GeometryType::Polygon) {
        return buildRings(clipRing(points, clipper, axis));
    }
    if (geomType == GeometryType::Ring) {
        return clipRing(points, clipper, axis);
    }
    REQUIRE(geomType == GeometryType::Polyline, "Unknown geometry type");
    return clipPolyline(points, clipper, axis);
}

enum class AxisType { Min, Max };

HalfplaneRelation
coordToAxisRelation(double coord, double axisCoord, AxisType axisType)
{
    int sgn = gl::sign(coord - axisCoord, gl::EPS);
    if (sgn == 0) {
        return HalfplaneRelation::OnBorder;
    }
    if ((sgn < 0 && axisType == AxisType::Min)
        || (sgn > 0 && axisType == AxisType::Max))
    {
        return HalfplaneRelation::Out;
    }
    return HalfplaneRelation::In;
}


struct Clipper
{
    gl::Line2 axis;
    HalfplanePointMatcher clipper;
};

std::vector<Clipper>
buildClippers(const gl::BoundingBox& clipRect)
{
    gl::Point2 minXmaxY = {clipRect.minX(), clipRect.maxY()};
    gl::Point2 maxXminY = {clipRect.maxX(), clipRect.minY()};

    return std::vector<Clipper> {
        {
            gl::Line2(clipRect.lowerCorner(), maxXminY),
            [&] (const gl::Point2& p)
            {
                return coordToAxisRelation(p.y(), clipRect.minY(), AxisType::Min);
            }
        },
        {
            gl::Line2(minXmaxY, clipRect.upperCorner()),
            [&] (const gl::Point2& p)
            {
                return coordToAxisRelation(p.y(), clipRect.maxY(), AxisType::Max);
            }
        },
        {
            gl::Line2(clipRect.lowerCorner(), minXmaxY),
            [&] (const gl::Point2& p)
            {
                return coordToAxisRelation(p.x(), clipRect.minX(), AxisType::Min);
            }
        },
        {
            gl::Line2(maxXminY, clipRect.upperCorner()),
            [&] (const gl::Point2& p)
            {
                return coordToAxisRelation(p.x(), clipRect.maxX(), AxisType::Max);
            }
        }
    };
}

/**
 * for GeometryType::Polygon:
 *   Treats result of polygon clipping as a set of rings and does not restore
 *     a valid multipolygon because multipolygon restoration process is error-prone.
 *   Each ring is formed by one part of polygon border after clipping.
 *
 * for GeometryType::Ring:
 *   Treats result of ring clipping as a set of polylines
 *     representing clipped ring parts.
 *
 * for GeometryType::Polyline:
 *   Returns set of polylines.
 */
std::vector<gl::PointsVector>
clip(GeometryType geomType, const gl::PointsVector& geom, const gl::BoundingBox& clipRect)
{
    auto clippers = buildClippers(clipRect);

    std::vector<gl::PointsVector> result{geom};

    for (const auto& clipper : clippers) {
        std::vector<gl::PointsVector> clipped;
        for (const auto& g : result) {
            auto parts = clip(geomType, g, clipper.axis, clipper.clipper);
            for (const auto& part : parts) {
                if (geomType == GeometryType::Polygon) {
                    try {
                        auto p = gl::Polygon2(part);
                        clipped.push_back(part);
                    } catch (const std::exception& e) {
                        WARN() << "Error trying to cut polygon: " << e.what();
                    } catch (...) {
                        WARN() << "Error trying to cut polygon";
                    }
                } else {
                    if (isSimple(part, part.front() == part.back())) {
                        clipped.push_back(part);
                    } else {
                        WARN() << "Error trying to cut geometry: not simple";
                    }
                }
            }
        }
        result = clipped;
    }
    return result;
}


std::unordered_map<DBIdType, FaceRelations>
faceMasterRelations(const TopologyData& data, const IdSet& faceIds)
{
    std::unordered_map<DBIdType, FaceRelations> result;
    for (auto faceId : faceIds) {
        const auto& face = data.face(faceId);
        for (auto masterId : face.masterIds()) {
            const auto& master = data.master(masterId);
            FaceRelations rels;
            for (const auto& relPair: master.faceRels()) {
                rels.insert(relPair);
            }
            result.insert({master.id(), rels});
        }
    }
    return result;
}

IdSet
uniqueFaceIds(const std::unordered_map<DBIdType, FaceRelations>& faceMasterRels)
{
    IdSet allFaceIds;
    for (const auto& pair : faceMasterRels) {
        for (const auto& rel : pair.second) {
            allFaceIds.insert(rel.first);
        }
    }
    return allFaceIds;
}

typedef std::atomic<uint32_t> AtomicUInt;

template <class SearcherT, class CheckerT>
class CheckerBuilder {
public:
    virtual CheckerT operator() (
        const geolib3::BoundingBox& bbox,
        const TopologyData& originalData,
        const SearcherT& originalDataIndex,
        const TopologyData& fixedData,
        const SearcherT& fixedDataIndex,
        AtomicUInt& errorsCounter)
    const = 0;
};

/// Returns error count
template <class SearcherT, class CheckerT>
size_t run(
    const TopologyData& originalData,
    const TopologyData& fixedData,
    double clipBoxSize,
    size_t threads,
    const CheckerBuilder<SearcherT, CheckerT>& checkerBuilder)
{
    auto originalEdgeIds = originalData.edgeIds();
    auto fixedEdgeIds = fixedData.edgeIds();

    if (originalEdgeIds.empty() && fixedEdgeIds.empty()) {
        return 0u;
    }
    if (originalEdgeIds.empty()) {
        WARN() << "Original data is empty";
        return 1u;
    }
    if (fixedEdgeIds.empty()) {
        WARN() << "Fixed data is empty";
        return 1u;
    }

    auto originalSearcher = SearcherT(originalData);
    auto fixedSearcher = SearcherT(fixedData);

    boost::optional<geolib3::BoundingBox> originalBBox = originalSearcher.bbox();
    boost::optional<geolib3::BoundingBox> fixedBBox = fixedSearcher.bbox();

    if (!originalBBox && !fixedBBox) {
        WARN() << "Original and fixed data both have degenerate geometries only";
        return 0u;
    }
    geolib3::BoundingBox bbox = originalBBox ? *originalBBox : *fixedBBox;
    const auto& searcher = originalBBox ? originalSearcher : fixedSearcher;
    const double BOX_SIZE = clipBoxSize;
    bbox = geolib3::BoundingBox(
        bbox.lowerCorner(),
        {bbox.minX() + ceil(bbox.width() / BOX_SIZE) * BOX_SIZE,
         bbox.minY() + ceil(bbox.height() / BOX_SIZE) * BOX_SIZE});

    AtomicUInt errorsCounter(0u);
    ThreadPool pool(threads);

    for (double minX = bbox.minX(); minX < bbox.maxX() - gl::EPS; minX += BOX_SIZE) {
        for (double minY = bbox.minY(); minY < bbox.maxY() - gl::EPS; minY += BOX_SIZE) {
            geolib3::BoundingBox b{{minX, minY}, {minX + BOX_SIZE, minY + BOX_SIZE}};
            if (!searcher.idsIntersectingBBox(b).empty()) {
                pool.push(checkerBuilder(b,
                    originalData, originalSearcher,
                    fixedData, fixedSearcher,
                    errorsCounter));
            }
        }
    }
    pool.shutdown();
    return errorsCounter;
}

class AreaChecker {
public:
    AreaChecker(
            const gl::BoundingBox& bbox,
            const TopologyData& originalData,
            const FaceSearcher& originalFacesIndex,
            const TopologyData& fixedData,
            const FaceSearcher& fixedFacesIndex,
            double areaDifferenceThreshold,
            AtomicUInt& errorsCounter)
        : bbox_(bbox)
        , originalData_(originalData)
        , originalFacesIndex_(originalFacesIndex)
        , fixedData_(fixedData)
        , fixedFacesIndex_(fixedFacesIndex)
        , areaDifferenceThreshold_(areaDifferenceThreshold)
        , errorsCounter_(errorsCounter)

    {}

    void operator () () const
    {
        const auto& origFaceIds = originalFacesIndex_.idsIntersectingBBox(bbox_);
        const auto& fixedFaceIds = fixedFacesIndex_.idsIntersectingBBox(bbox_);

        if (origFaceIds.empty() && fixedFaceIds.empty()) {
            return;
        }

        double originalArea = area(originalData_, origFaceIds);
        double fixedArea = area(fixedData_, fixedFaceIds);
        const double delta = std::fabs(originalArea - fixedArea);
        if (delta > originalArea * areaDifferenceThreshold_) {
            auto lower = gl::mercator2GeoPoint(bbox_.lowerCorner());
            auto upper = gl::mercator2GeoPoint(bbox_.upperCorner());
            ++errorsCounter_;
            INFO() << "bbox [(" << lower.x() << ", " << lower.y()
                << "), (" << upper.x() << ", " << upper.y() << ")]"
                << ": area relative delta too big: delta " << delta
                << ", original area: " << originalArea;
        }
    }

private:

    double
    area(const TopologyData& data, const IdSet& faceIds) const
    {
        auto faceMasterRels = faceMasterRelations(data, faceIds);
        Area chunksArea;
        for (auto faceId : uniqueFaceIds(faceMasterRels)) {
            auto area = faceArea(data, faceId);
            for (const auto& pair : faceMasterRels) {
                auto it = pair.second.find(faceId);
                if (it == pair.second.end()) {
                    continue;
                }
                const auto relationType = it->second;
                (relationType == FaceRelationType::Exterior
                        ? chunksArea.exterior
                        : chunksArea.interior)
                    += area.exterior;
                (relationType == FaceRelationType::Exterior
                        ? chunksArea.interior
                        : chunksArea.exterior)
                    += area.interior;
            }
        }
        return std::fabs(chunksArea.exterior) - std::fabs(chunksArea.interior);
    }

    /**
     * As the result of polygon clipping, we have set of rings. The idea is that
     *  sum of this ring areas (taken with correct sign) gives the same value
     *  as if we restored multipolygon after clipping and computed its area.
     * We cannot compute exact value here because summation with correct sign
     *  requires to know relation type of the face for each master it is connected to.
     * Exterior part of result holds area of rings that would have the same relation type
     *  as the original face,
     * interior part of result holds area of rings that would have changed relation type
     *  to opposite.
     */
    Area
    faceArea(const TopologyData& data, DBIdType faceId) const
    {
        try {
            gl::PointsVector ring = data.faceVertices(faceId);
            gl::Polygon2 faceVertices(ring);
            bool isCCW = topology_fixer::signedArea(ring) < 0;
            auto polygons = clip(GeometryType::Polygon, ring, bbox_);
            Area area;
            for (const auto& polyPoints : polygons) {
                try {
                    gl::Polygon2 poly(polyPoints);
                    double polyArea = topology_fixer::signedArea(polyPoints);
                    (polyArea < 0 ? area.exterior : area.interior) += std::fabs(polyArea);
                } catch (...)
                {
                    WARN() << "Could not clip face: " << faceId;
                }
            }
            if (!isCCW) {
                std::swap(area.exterior, area.interior);
            }
            return area;
        } catch (const std::exception& e) {
            WARN() << "Face " << faceId << " geometry is invalid: " << e.what();
        } catch (...) {
            WARN() << "Face " << faceId << " geometry is invalid: unknown error";
        }
        return Area();
    }

    gl::BoundingBox bbox_;
    const TopologyData& originalData_;
    const FaceSearcher& originalFacesIndex_;
    const TopologyData& fixedData_;
    const FaceSearcher& fixedFacesIndex_;
    double areaDifferenceThreshold_;

    AtomicUInt& errorsCounter_;
};

class AreaDiffCheckerBuilder : public CheckerBuilder<FaceSearcher, AreaChecker> {
public:
    explicit AreaDiffCheckerBuilder(double areaDifferenceThreshold)
        : areaDifferenceThreshold_(areaDifferenceThreshold)
    {}

    AreaChecker operator() (
        const geolib3::BoundingBox& bbox,
        const TopologyData& originalData,
        const FaceSearcher& originalDataIndex,
        const TopologyData& fixedData,
        const FaceSearcher& fixedDataIndex,
        AtomicUInt& errorsCounter)
    const override
    {
        return AreaChecker(bbox, originalData, originalDataIndex,
            fixedData, fixedDataIndex, areaDifferenceThreshold_,
            errorsCounter);
    }

private:
    double areaDifferenceThreshold_;
};



class EdgePerimeterChecker {
public:
    EdgePerimeterChecker(
            const gl::BoundingBox& bbox,
            const TopologyData& originalData,
            const StaticEdgeSearcher& originalEdgesIndex,
            const TopologyData& fixedData,
            const StaticEdgeSearcher& fixedEdgesIndex,
            double perimeterDifferenceThreshold,
            AtomicUInt& errorsCounter)
        : bbox_(bbox)
        , originalData_(originalData)
        , originalEdgesIndex_(originalEdgesIndex)
        , fixedData_(fixedData)
        , fixedEdgesIndex_(fixedEdgesIndex)
        , perimeterDifferenceThreshold_(perimeterDifferenceThreshold)
        , errorsCounter_(errorsCounter)

    {}

    void operator () () const
    {
        const auto& origEdgeIds = originalEdgesIndex_.idsIntersectingBBox(bbox_);
        const auto& fixedEdgeIds = fixedEdgesIndex_.idsIntersectingBBox(bbox_);

        if (origEdgeIds.empty() && fixedEdgeIds.empty()) {
            return;
        }

        double originalPerimeter = perimeter(originalData_, origEdgeIds);
        double fixedPerimeter = perimeter(fixedData_, fixedEdgeIds);
        const double delta = originalPerimeter - fixedPerimeter;
        if (delta > originalPerimeter * perimeterDifferenceThreshold_) {
            auto lower = gl::mercator2GeoPoint(bbox_.lowerCorner());
            auto upper = gl::mercator2GeoPoint(bbox_.upperCorner());
            ++errorsCounter_;
            INFO() << "bbox [(" << lower.x() << ", " << lower.y()
                << "), (" << upper.x() << ", " << upper.y() << ")]"
                << ": perimeter delta too big: delta " << delta
                << ", original perimeter: " << originalPerimeter;
        }
    }

private:

    double
    perimeter(const TopologyData& data, const IdSet& edgeIds) const
    {
        double result = 0.0;
        for (auto edgeId : edgeIds) {
            result += edgePerimeter(data, edgeId) * data.edge(edgeId).masterIds().size();
        }
        return result;
    }

    double
    edgePerimeter(const TopologyData& data, DBIdType edgeId) const
    {
        double perimeter = 0.0;
        try {
            const auto& edgePoints = data.edge(edgeId).linestring().points();
            for (const auto& part : clip(GeometryType::Polyline, edgePoints, bbox_)) {
                perimeter += topology_fixer::perimeter(part);
            }
        } catch (const std::exception& e) {
            WARN() << "Edge " << edgeId << " could not be processed: " << e.what();
        } catch (...) {
            WARN() << "Edge " << edgeId << " could not be processed: unknown error";
        }
        return perimeter;
    }

    gl::BoundingBox bbox_;
    const TopologyData& originalData_;
    const StaticEdgeSearcher& originalEdgesIndex_;
    const TopologyData& fixedData_;
    const StaticEdgeSearcher& fixedEdgesIndex_;
    double perimeterDifferenceThreshold_;

    AtomicUInt& errorsCounter_;
};

class PerimeterDiffCheckerBuilder :
    public CheckerBuilder<StaticEdgeSearcher, EdgePerimeterChecker> {
public:
    explicit PerimeterDiffCheckerBuilder(double perimeterDifferenceThreshold)
        : perimeterDifferenceThreshold_(perimeterDifferenceThreshold)
    {}

    EdgePerimeterChecker operator() (
        const geolib3::BoundingBox& bbox,
        const TopologyData& originalData,
        const StaticEdgeSearcher& originalDataIndex,
        const TopologyData& fixedData,
        const StaticEdgeSearcher& fixedDataIndex,
        AtomicUInt& errorsCounter)
    const override
    {
        return EdgePerimeterChecker(bbox, originalData, originalDataIndex,
            fixedData, fixedDataIndex, perimeterDifferenceThreshold_,
            errorsCounter);
    }

private:
    double perimeterDifferenceThreshold_;
};

} // namespace

size_t
AreaDiffChecker::operator () (
    const TopologyData& originalData,
    const TopologyData& fixedData) const
{
    AreaDiffCheckerBuilder builder(areaDifferenceThreshold_);
    return run<FaceSearcher, AreaChecker>(
        originalData, fixedData,
        clipBoxSize_, threads_, builder);
}

size_t
PerimeterDiffChecker::operator () (
    const TopologyData& originalData,
    const TopologyData& fixedData) const
{
    PerimeterDiffCheckerBuilder builder(perimeterDifferenceThreshold_);
    return run<StaticEdgeSearcher, EdgePerimeterChecker>(
        originalData, fixedData,
        clipBoxSize_, threads_, builder);
}

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