#include "polygon_checks.h"
#include "misc.h"
#include "geom.h"
#include <yandex/maps/wiki/validator/check_context.h>

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

#include <geos/geom/Coordinate.h>
#include <geos/geom/CoordinateFilter.h>
#include <geos/geom/Geometry.h>
#include <geos/geom/Polygon.h>
#include <geos/geom/PrecisionModel.h>
#include <geos/operation/valid/IsValidOp.h>

#include <memory>

namespace maps {
namespace wiki {
namespace validator {
namespace utils {

namespace gl = maps::geolib3;

namespace {

const double MIN_SEGMENT_LENGTH = 0.05; // meters
const double MIN_ANGLE_SIZE = 0.045; // radians

const double GEO_COORDS_PRECISION = 1e-9;

class MercatorToRPGeoPointFilter : public geos::geom::CoordinateFilter
{
public:
    MercatorToRPGeoPointFilter(double precision)
        : precisionModel_(1.0 / precision)
    {}

    void filter_rw(geos::geom::Coordinate* c) const override
    {
        auto geoPoint = gl::mercator2GeoPoint({c->x, c->y});
        c->x = geoPoint.x();
        c->y = geoPoint.y();
        precisionModel_.makePrecise(c);
    }

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

private:
    geos::geom::PrecisionModel precisionModel_;
};

} // namespace

void checkRingGeometry(
    const gl::LinearRing2& ring,
    const TId objectId,
    CheckContext* context)
{
    double distanceRatio = utils::mercatorDistanceRatio(ring.pointAt(0));
    bool isPreviousSegmentShort = false;
    for (size_t idx = 0; idx < ring.segmentsNumber(); ++idx) {
        bool isCurrentSegmentShort = (gl::length(ring.segmentAt(idx))
                * distanceRatio < MIN_SEGMENT_LENGTH);
        if (isCurrentSegmentShort && !isPreviousSegmentShort) {
            context->error(
                    "segment-too-short",
                    ring.segmentAt(idx).midpoint(), {objectId});
        }
        isPreviousSegmentShort = isCurrentSegmentShort;
    }


    for (size_t idx = 0; idx < ring.segmentsNumber(); ++idx) {
        gl::Direction2 currentDirection(ring.segmentAt(idx));
        gl::Direction2 nextDirection(ring.segmentAt(
                (idx + 1) % ring.segmentsNumber()));
        if (gl::angle(nextDirection, -currentDirection) < MIN_ANGLE_SIZE) {
            context->fatal(
                    "angle-too-small",
                    ring.segmentAt(idx).end(), {objectId});
        }
    }
}

void checkReducedPrecisionValidity(
        const gl::Polygon2& polygon,
        const TId objectId,
        CheckContext* context)
{
    static const MercatorToRPGeoPointFilter filter(GEO_COORDS_PRECISION);

    std::unique_ptr<geos::geom::Geometry> geom(
        gl::internal::geolib2geosGeometry(polygon)->clone());
    geom->apply_rw(&filter);

    geos::operation::valid::IsValidOp isValidOp(geom.get());
    if (!isValidOp.isValid()) {
        auto errorPoint = gl::geoPoint2Mercator(
            gl::internal::geosCoordinate2Point(
                isValidOp.getValidationError()->getCoordinate()));
        context->fatal(
            "bad-geometry",
            errorPoint, {objectId});
    }
}

} // namespace utils
} // namespace validator
} // namespace wiki
} // namespace maps
