#include "geom.h"

#include "utils.h"
#include "objects/junction.h"
#include "objects/linear_element.h"

#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/direction.h>
#include <maps/libs/geolib/include/distance.h>

#include <maps/libs/common/include/exception.h>
#include <yandex/maps/wiki/common/geom_utils.h>

#include <geos/geom/LineString.h>
#include <geos/geom/Point.h>
#include <geos/geom/Envelope.h>
#include <geos/geom/LineSegment.h>
#include <geos/geom/CoordinateFilter.h>
#include <geos/operation/valid/IsValidOp.h>
#include <geos/algorithm/Angle.h>

namespace maps {
namespace wiki {
namespace {
const double MIN_SEGMENT_LENGTH = 0.05; // meters
const double MIN_ANGLE_SIZE = 0.045; // radians
const double MIN_SEGMENT_DISTANCE = 0.000001; // meters
}

geolib3::Polyline2
geomToPolyline(const Geom& geom)
{
    REQUIRE(!geom.isNull(), "Null geometry");
    auto geosPtr = geom.geosGeometryPtr();
    REQUIRE(geosPtr, "Null geometry ptr");
    const geos::geom::LineString* lineString =
        dynamic_cast<const geos::geom::LineString*>(geosPtr);
    REQUIRE(lineString, "Wrong geometry type, LineString expected");
    geolib3::PointsVector points;
    points.reserve(lineString->getNumPoints());
    for (size_t i = 0; i < lineString->getNumPoints(); ++i) {
        const auto& coord = lineString->getCoordinateN(i);
        points.emplace_back(coord.x, coord.y);
    }
    return geolib3::Polyline2(points);
}

geolib3::Point2 center(const Geom& geom)
{
    ASSERT(!geom.isNull());
    return geomToPoint(geom.center());
}


Geom
polylineToGeom(const geolib3::Polyline2& geom)
{
    std::unique_ptr<std::vector<geos::geom::Coordinate>> coords(
        new std::vector<geos::geom::Coordinate>());
    coords->reserve(geom.pointsNumber());
    for (const auto& point : geom.points()) {
        coords->push_back(geos::geom::Coordinate(point.x(), point.y()));
    }
    return Geom(createPolyline(
        coords.release(),
        SpatialRefSystem::Mercator,
        AntiMeridianAdjustPolicy::Ignore));
}

geolib3::MercatorRatio mercatorRatio(const std::vector<geos::geom::Coordinate>& coords)
{
    ASSERT(!coords.empty());

    double summY = 0.0;
    for (const auto& coord : coords) {
        summY += coord.y;
    }

    return geolib3::MercatorRatio::fromMercatorPoint(geolib3::Point2(0.0, summY / coords.size()));
}

void
tryFixRing(std::vector<geos::geom::Coordinate>& points)
{
    if (points.size() > 1 &&
        points.front() == points.back()) {
        points.erase(points.begin() + points.size() - 1);
    }
    auto at = [&](size_t index) { return points[index % points.size()]; };
    const auto ratio = mercatorRatio(points);
    while (points.size() > 1) {
        bool pointRemoved = false;

        for (size_t idx = 0; idx < points.size(); ++idx) {

            auto previous = at(idx);
            auto current = at(idx + 1);
            auto next = at(idx + 2);

            if (ratio.toMeters(previous.distance(current)) < MIN_SEGMENT_LENGTH) {
                points.erase(points.begin() + ((idx + 1) % points.size()));
                pointRemoved = true;
                break;
            }

            if (points.size() > 2 &&
                    geos::algorithm::Angle::angleBetween(previous, current, next) < MIN_ANGLE_SIZE) {
                points.erase(points.begin() + ((idx + 1) % points.size()));
                pointRemoved = true;
                break;
            }
        }

        if (!pointRemoved) {
            break;
        }
    }

    WIKI_REQUIRE(points.size() > 2, ERR_TOPO_SPIKES,
        "Geometry contains unremoveable spikes and/or degenerated segments.");

    for (size_t iPoint = 0; iPoint < points.size(); ++iPoint) {
        const auto& point = points[iPoint];
        for (size_t i = 0; i < points.size() - 2; ++i) {
            geos::geom::LineSegment segment(at(iPoint + 1 + i), at(iPoint + 2 + i));
            double distance = segment.distance(point);
            WIKI_REQUIRE(distance > MIN_SEGMENT_DISTANCE, ERR_TOPO_DEGENERATE_SEGMENT,
                "Geometry contains degenerated segments.");
        }
    }
    points.push_back(points[0]);
}

geolib3::Point2
geomToPoint(const Geom& geom)
{
    REQUIRE(!geom.isNull(), "Null geometry");
    auto geosPtr = geom.geosGeometryPtr();
    REQUIRE(geosPtr, "Null geometry ptr");
    const geos::geom::Point* pos = dynamic_cast<const geos::geom::Point*>(geosPtr);
    REQUIRE(pos, "Wrong geometry type, Point expected");
    return geolib3::Point2(pos->getX(), pos->getY());
}

Geom
pointToGeom(const geolib3::Point2& pos)
{
    std::unique_ptr<std::vector<geos::geom::Coordinate>> coords(
        new std::vector<geos::geom::Coordinate>(1));
    (*coords)[0] = geos::geom::Coordinate(pos.x(), pos.y());
    return Geom(createPoint(coords.release(), SpatialRefSystem::Mercator));
}

geolib3::BoundingBox
boundingBox(const Geom& geom)
{
    REQUIRE(!geom.isNull(), "Null geometry");
    auto geosPtr = geom.geosGeometryPtr();
    REQUIRE(geosPtr, "Null geometry ptr");
    if (geosPtr->getGeometryTypeId() == geos::geom::GEOS_POINT) {
        return geolib3::BoundingBox(geomToPoint(geom),
            2.0 * CALCULATION_TOLERANCE,
            2.0 * CALCULATION_TOLERANCE);
    }
    const geos::geom::Envelope* env = geosPtr->getEnvelopeInternal();
    REQUIRE(env, "Can not obtain geom envelope, geom type: " << geosPtr->getGeometryType());
    return geolib3::BoundingBox(
        geolib3::Point2(env->getMinX(), env->getMinY()),
        geolib3::Point2(env->getMaxX(), env->getMaxY()));
}

namespace {

const double MIN_LAT_MERCATOR = geolib3::geoPoint2Mercator({-180.0, 0.0}).x();
const double MAX_LAT_MERCATOR = geolib3::geoPoint2Mercator({180.0, 0.0}).x();
const double HALF_EQUATOR_LENGTH = (MAX_LAT_MERCATOR - MIN_LAT_MERCATOR) / 2.0;

class MeridianFitter : public geos::geom::CoordinateFilter
{
public:
    MeridianFitter(double pos, double tolerance)
        : pos_(pos)
        , tolerance_(tolerance)
    {}

    void filter_rw(geos::geom::Coordinate* coord) const override
    {
        *coord = geos::geom::Coordinate(adjust(coord->x), coord->y);
    }

    void filter_ro (const geos::geom::Coordinate*) override {}

    double adjust(double coord) const
    {
        if (!geolib3::sign(coord - MIN_LAT_MERCATOR, tolerance_)
            || !geolib3::sign(coord - MAX_LAT_MERCATOR, tolerance_))
        {
            return pos_;
        }
        return coord;
    }

private:

    double pos_;
    double tolerance_;
};

struct LatitudeBounds
{
    double min;
    double max;
    boost::optional<double> minPositive;
    boost::optional<double> maxNegative;
};

LatitudeBounds
coordinatesBounds(const geos::geom::CoordinateSequence* coords)
{
    REQUIRE(coords->size(), "Empty polyline(polygon)");
    LatitudeBounds result;
    result.min = result.max = (*coords)[0].x;
    for (size_t i = 0; i < coords->size(); ++i) {
        double coord = (*coords)[i].x;
        result.min = std::min(result.min, coord);
        result.max = std::max(result.max, coord);
        if (coord > 0.0) {
            result.minPositive = result.minPositive ? std::min(*result.minPositive, coord) : coord;
        }
        if (coord < 0.0) {
            result.maxNegative = result.maxNegative ? std::max(*result.maxNegative, coord) : coord;
        }
    }
    return result;
}

boost::optional<AntiMeridianId>
antiMeridianId(const LatitudeBounds& bounds, double tolerance)
{
    if (bounds.max - bounds.min < HALF_EQUATOR_LENGTH) {
        if (MAX_LAT_MERCATOR - bounds.max < tolerance) {
            return AntiMeridianId::East;
        }
        if (bounds.min - MIN_LAT_MERCATOR < tolerance) {
            return AntiMeridianId::West;
        }
        return boost::none;
    }

    if (bounds.min < 0.0 && bounds.max > 0.0) {
        REQUIRE(bounds.minPositive && bounds.maxNegative, "Invalid inverted latitude range");
        const double distanceToMin = *bounds.maxNegative - MIN_LAT_MERCATOR;
        const double distanceToMax = MAX_LAT_MERCATOR - *bounds.minPositive;
        if (distanceToMin + distanceToMax < HALF_EQUATOR_LENGTH) {
            /// intersects 180 meridian
            if (distanceToMin < tolerance) {
                return AntiMeridianId::East;
            }
            if (distanceToMax < tolerance) {
                return AntiMeridianId::West;
            }
        }
    }
    THROW_WIKI_LOGIC_ERROR(ERR_TOPO_TOO_LONG,
        "Distance too long: minLat = " << bounds.min << ", maxLat = " << bounds.max);
}

} // namespace

void
adjustToAntiMeridian(geos::geom::CoordinateSequence* coords, double tolerance)
{
    LatitudeBounds bounds = coordinatesBounds(coords);
    if (!bounds.maxNegative || !bounds.minPositive) {
        return;
    }
    auto hemisphereId = wiki::antiMeridianId(bounds, tolerance);
    if (!hemisphereId) {
        return;
    }
    MeridianFitter fitter{
        *hemisphereId == AntiMeridianId::West ? MIN_LAT_MERCATOR : MAX_LAT_MERCATOR,
        tolerance};
    coords->apply_rw(&fitter);
}

boost::optional<AntiMeridianAdjustResult>
adjustToAntiMeridian(const Geom& geom, double tolerance)
{
    if (geom.isNull() ||
        (geom->getGeometryTypeId() != geos::geom::GEOS_LINESTRING &&
         geom->getGeometryTypeId() != geos::geom::GEOS_POLYGON))
    {
        return boost::none;
    }
    std::unique_ptr<geos::geom::CoordinateSequence> coords(geom->getCoordinates());
    auto hemisphereId = wiki::antiMeridianId(
        coordinatesBounds(coords.get()), tolerance);
    if (!hemisphereId) {
        return boost::none;
    }
    MeridianFitter fitter{
        *hemisphereId == AntiMeridianId::West ? MIN_LAT_MERCATOR : MAX_LAT_MERCATOR,
        tolerance};
    Geom result = geom;
    result->apply_rw(&fitter);
    geos::operation::valid::IsValidOp isValidOp(result.geosGeometryPtr());
    if (!isValidOp.isValid()) {
        const std::string& msg =
            isValidOp.getValidationError()->getMessage();
        THROW_WIKI_LOGIC_ERROR(ERR_TOPO_INVALID_GEOMETRY,
            "While adjusting geom to 180 meridian: " << msg);
    }
    return AntiMeridianAdjustResult{result, *hemisphereId};
}

namespace {

bool
isOnAntiMeridian(const geolib3::Point2& pos, double tolerance)
{
    return !geolib3::sign(pos.x() - MAX_LAT_MERCATOR, tolerance) ||
        !geolib3::sign(pos.x() - MIN_LAT_MERCATOR, tolerance);
}

AntiMeridianId
pointAntiMeridianId(const geolib3::Point2& pos)
{
    return pos.x() < 0.0 ? AntiMeridianId::West : AntiMeridianId::East;
}

} // namespace

geolib3::Point2
adjustToAntiMeridian(
    const geolib3::Point2& oldPos, const geolib3::Point2& newPos,
    double tolerance)
{
    AntiMeridianId id = pointAntiMeridianId(oldPos);
    bool isBorderPoint = isOnAntiMeridian(newPos, tolerance);

    if (id == AntiMeridianId::West && isBorderPoint) {
        return {MIN_LAT_MERCATOR, newPos.y()};
    }
    if (id == AntiMeridianId::East && isBorderPoint) {
        return {MAX_LAT_MERCATOR, newPos.y()};
    }
    double min = std::min(oldPos.x(), newPos.x());
    double max = std::max(oldPos.x(), newPos.x());
    if (max - min < HALF_EQUATOR_LENGTH) {
        return newPos;
    }
    THROW_WIKI_LOGIC_ERROR(ERR_TOPO_TOO_LONG,
        "Distance too long: minLat = " << min << ", maxLat = " << max);
}

geolib3::Point2
adjustToAntiMeridian(
    AntiMeridianId id, const geolib3::Point2& pos, double tolerance)
{
    AntiMeridianId pointId = pointAntiMeridianId(pos);
    if (pointId != id && !isOnAntiMeridian(pos, tolerance)) {
        THROW_WIKI_LOGIC_ERROR(ERR_TOPO_INTERSECTS_180_MERIDIAN,
            "Can not move point from " << pos.x() << ", " << pos.y()
            << ": through 180 meridian");
    }
    MeridianFitter fitter{
        id == AntiMeridianId::West ? MIN_LAT_MERCATOR : MAX_LAT_MERCATOR,
        tolerance};
    return {fitter.adjust(pos.x()), pos.y()};
}

} // namespace wiki
} // namespace maps
