#include <yandex/maps/wiki/common/geom.h>

#include <maps/libs/common/include/base64.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/polyline.h>
#include <maps/libs/geolib/include/vector.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/closest_point.h>
#include <maps/libs/geolib/include/segment.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/variant.h>

#include <geos/io/WKBReader.h>
#include <geos/io/WKBWriter.h>
#include <geos/io/WKTReader.h>
#include <geos/io/WKTWriter.h>
#include <geos/geom/Point.h>
#include <geos/simplify/TopologyPreservingSimplifier.h>
#include <geos/operation/buffer/BufferOp.h>
#include <geos/operation/distance/DistanceOp.h>
#include <geos/operation/linemerge/LineMerger.h>
#include <geos/operation/polygonize/Polygonizer.h>
#include <geos/geom/LineString.h>
#include <geos/geom/Polygon.h>
#include <geos/geom/MultiLineString.h>
#include <geos/geom/CoordinateFilter.h>
#include <geos/geom/CoordinateArraySequence.h>
#include <geos/geom/CoordinateArraySequenceFactory.h>

#include <util/system/compiler.h>

#define REQUIRE_LOGIC(cond, msg) \
    if (Y_LIKELY(cond)) {} else \
        throw geom::LogicError() << msg; // NOLINT

#define REQUIRE_PARAM(cond, msg) \
    if (Y_LIKELY(cond)) {} else \
        throw geom::BadParam() << msg; // NOLINT

namespace maps::wiki::common {
namespace {

class PointTransformFilter : public geos::geom::CoordinateFilter
{
public:
    PointTransformFilter(const PointTransform& transform)
        : transform_(transform)
    { }

    void filter_rw(geos::geom::Coordinate* c) const override
    {
        geolib3::Point2 transformed = transform_(geolib3::Point2(c->x, c->y));
        c->x = transformed.x();
        c->y = transformed.y();
    }

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

private:
    const PointTransform& transform_;
};

const std::string GEOJSON_GEOM_TYPE_NAME_POINT = "Point";
const std::string GEOJSON_GEOM_TYPE_NAME_LINE = "LineString";
const std::string GEOJSON_GEOM_TYPE_NAME_POLYGON = "Polygon";

} // namespace

const std::string& Geom::geomTypeNamePoint = "point";
const std::string& Geom::geomTypeNameLine = "polyline";
const std::string& Geom::geomTypeNamePolygon = "polygon";

Geom::Geom(const pqxx::field& ewkbField)
{

    //!This code assumes, that EWKB is a strcuture
    //! of form [4b-header:4b-ssrid:wkbData]
    //! if EWKB length=0, then there is null geometry in database
    std::string ewkb(ewkbField.c_str());
    if(!ewkb.length()){
        return;
    }
    REQUIRE_PARAM(ewkb.length() >= 16,
        "Bad WKB: [" << ewkb << ewkb.length() << "]");
    std::stringstream wkbStream;
    wkbStream << ewkb.substr(0, 8) << ewkb.substr(16, ewkb.length());
    wkbStream.seekp(0);
    geos::io::WKBReader wkbr(*geos::geom::GeometryFactory::getDefaultInstance());
    geos_ = GeometryPtr(wkbr.readHEX(wkbStream));
}

Geom::Geom(const std::string& wkb)
{
    std::stringstream wkbStream(wkb);
    geos::io::WKBReader wkbr(*geos::geom::GeometryFactory::getDefaultInstance());
    geos_ = GeometryPtr(wkbr.read(wkbStream));
}

Geom::Geom(const TGeoPoint& pt)
{
    TMercatorPoint merc = geodeticTomercator(pt.x(), pt.y());
    geos::geom::Coordinate ptCoord(merc.x(), merc.y());
    geos::geom::Point* geosPt =
        geos::geom::GeometryFactory::getDefaultInstance()->createPoint(ptCoord);
    geos_ = GeometryPtr(geosPt);
}

ConstGeosGeometryPtr
Geom::geosGeometryPtr() const
{
    return geos_.get();
}

const geos::geom::LineString*
Geom::asLineString() const
{
    return dynamic_cast<const geos::geom::LineString*>(geos_.get());
}

bool Geom::isNull() const
{
    return geos_.get() == NULL;
}

void
Geom::setLineString(geos::geom::CoordinateSequence* coordinates) {
    setLineString(std::unique_ptr<geos::geom::CoordinateSequence>(coordinates));
}

void
Geom::setLineString(std::unique_ptr<geos::geom::CoordinateSequence> coordinates)
{
    geos_ = geos::geom::GeometryFactory::getDefaultInstance()->createLineString(
        std::move(coordinates)
    );
}

std::pair<size_t, bool>
Geom::insertPoint(double x, double y, double tolerance)
{
    geos::geom::CoordinateArraySequence coordinates(*geos_->getCoordinates());
    maps::geolib3::Polyline2 polyline;
    for(size_t i = 0; i < coordinates.size(); ++i){
        polyline.add(maps::geolib3::Point2(coordinates.getAt(i).x, coordinates.getAt(i).y));
    }
    maps::geolib3::Point2 point(x, y);
    size_t segment = polyline.closestPointSegmentIndex(point);
    size_t position = 0;
    bool inserted = false;
    double distToPolyline = maps::geolib3::distance(polyline, point);
    double distToSegStart = maps::geolib3::distance(polyline.pointAt(segment), point);
    double distToSegEnd = maps::geolib3::distance(polyline.pointAt(segment + 1), point);
    if(distToSegStart < distToSegEnd && (distToSegStart <= distToPolyline || distToSegStart < tolerance)){
        position = segment;
        coordinates.setAt(geos::geom::Coordinate(x, y), segment);
    } else if(distToSegEnd < distToSegStart && (distToSegEnd <= distToPolyline || distToSegEnd < tolerance)){
        position = segment + 1;
        coordinates.setAt(geos::geom::Coordinate(x, y), segment + 1);
    } else {
        position = segment + 1;
        coordinates.add(segment + 1, geos::geom::Coordinate(x, y), /* allowRepeated = */ true);
        inserted = true;
    }
    setLineString(
        std::make_unique<geos::geom::CoordinateArraySequence>(std::move(coordinates))
    );
    return std::make_pair(position, inserted);
}

Geom
Geom::createBuffer(double width) const
{
    ASSERT(!isNull());

    switch (geos_->getGeometryTypeId()) {
        case geos::geom::GEOS_POINT:
        case geos::geom::GEOS_LINESTRING:
        case geos::geom::GEOS_MULTILINESTRING:
        case geos::geom::GEOS_POLYGON:
            return Geom(
                geos_->buffer(width, 4, geos::operation::buffer::BufferOp::CAP_SQUARE)
            );
        default:
            break;
    }
    return clone();
}

std::string
Geom::wkb() const
{
    return common::wkb(geos_.get());
}

Geom Geom::transformed(const PointTransform& transform) const
{
    Geom result = clone();
    PointTransformFilter filter(transform);
    result->apply_rw(&filter);
    return result;
}

class WriteCoordinates : public geos::geom::CoordinateFilter
{
public:
    WriteCoordinates(std::ostream& os, bool json, SpatialRefSystem outputRefSys)
        : os_(os)
        , first_(true)
        , json_(json)
        , outputRefSys_(outputRefSys)
    { }

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

    void filter_ro (const geos::geom::Coordinate *c) override
    {
        if(!first_){
            os_ << (json_? ",":" ");
        }
        first_ = false;
        double x(c->x);
        double y(c->y);
        if (outputRefSys_ == SpatialRefSystem::Geodetic) {
            TGeoPoint geodetic = mercatorToGeodetic(c->x, c->y);
            x = geodetic.x();
            y = geodetic.y();
        }
        if(!json_){
            os_ << x << " " << y;
        } else {
            os_ << "[" << x << "," << y << "]";
        }
    }
    void reset()
    {
        first_ = true;
    }

private:
    std::ostream& os_;
    bool first_;
    bool json_;
    SpatialRefSystem outputRefSys_;
};

void Geom::gml(std::ostream& os, SpatialRefSystem outputRefSys) const
{
    if (!geos_) {
        return;
    }

    WriteCoordinates wc(os, false, outputRefSys);
    geos::geom::GeometryTypeId gType= geos_->getGeometryTypeId();
    switch( gType ){
    case geos::geom::GEOS_POINT:
        os<<"<gml:Point><gml:pos>";
        geos_->apply_ro(&wc);
        os<<"</gml:pos></gml:Point>";
        break;
    case geos::geom::GEOS_POLYGON:
        {
            const geos::geom::Polygon* polygon = dynamic_cast<const geos::geom::Polygon*>(geos_.get());
            os << "<gml:Polygon><gml:exterior><gml:LinearRing><gml:posList>";
            polygon->getExteriorRing()->apply_ro(&wc);
            os << "</gml:posList></gml:LinearRing></gml:exterior>";
            for(size_t i =0; i < polygon->getNumInteriorRing(); ++i){
                wc.reset();
                os << "<gml:interior><gml:LinearRing><gml:posList>";
                polygon->getInteriorRingN(i)->apply_ro(&wc);
                os << "</gml:posList></gml:LinearRing></gml:interior>";
            }
            os << "</gml:Polygon>";
        }
        break;
    case geos::geom::GEOS_LINESTRING:
        os << "<gml:MultiCurve><gml:curveMembers>";
        os<<"<gml:LineString><gml:posList>";
        geos_->apply_ro(&wc);
        os<<"</gml:posList></gml:LineString>";
        os << "</gml:curveMembers></gml:MultiCurve>";
        break;
    case geos::geom::GEOS_MULTILINESTRING:
        os << "<gml:MultiCurve><gml:curveMembers>";
        for(size_t l = 0; l < geos_->getNumGeometries(); l++){
            os<<"<gml:LineString><gml:posList>";
            geos_->getGeometryN(l)->apply_ro(&wc);
            os<<"</gml:posList></gml:LineString>";
        }
        os << "</gml:curveMembers></gml:MultiCurve>";
        break;
    default:
        break;
    }
}

namespace
{
const std::string&
geojsonGeometryTypeName(geos::geom::GeometryTypeId gType)
{
    switch( gType ){
            case geos::geom::GEOS_POINT:
                return GEOJSON_GEOM_TYPE_NAME_POINT;
            case geos::geom::GEOS_POLYGON:
                return GEOJSON_GEOM_TYPE_NAME_POLYGON;
            case geos::geom::GEOS_LINESTRING:
            case geos::geom::GEOS_MULTILINESTRING:
                return GEOJSON_GEOM_TYPE_NAME_LINE;
            default:
                break;
        }
    throw geom::LogicError() << "Unknown geometry type: " << gType;
}
} // namespace

void Geom::geoJson(std::ostream& os, SpatialRefSystem outputRefSys) const
{
    if (!geos_) {
        return;
    }

    geos::geom::GeometryTypeId gType = geos_->getGeometryTypeId();
    os << "{\"type\":\"" << geojsonGeometryTypeName(gType) << "\",\"coordinates\":";
    switch (gType) {
    default:
        break;
    case geos::geom::GEOS_POINT:
        {
            WriteCoordinates wc(os, true, outputRefSys);
            geos_->apply_ro(&wc);
        }
        break;
    case geos::geom::GEOS_POLYGON:
        {
            WriteCoordinates wc(os, true, outputRefSys);
            os << "["; //open polygon
            os << "["; //open exterior ring
            const auto& polygon = dynamic_cast<const geos::geom::Polygon&>(*geos_);
            polygon.getExteriorRing()->apply_ro(&wc);
            os << "]"; //close exterior ring
            if (polygon.getNumInteriorRing()) {
                os << ","; //open list of interior rings
                for (size_t i = 0; i < polygon.getNumInteriorRing(); ++i) {
                    if (i) {
                        os << ",";
                    }
                    os << "["; //open interior ring
                    wc.reset();
                    polygon.getInteriorRingN(i)->apply_ro(&wc);
                    os << "]"; //close interior ring
                }
                os << ""; //close list of interior rings
            }
            os << "]"; //close polygon
        }
        break;
    case geos::geom::GEOS_LINESTRING:
        {
            os << "[";
            for (size_t l = 0; l < geos_->getNumGeometries(); l++) {
                WriteCoordinates wc(os, true, outputRefSys);
                geos_->getGeometryN(l)->apply_ro(&wc);
            }
            os << "]";
        }
        break;
    case geos::geom::GEOS_MULTILINESTRING:
        {
            os << "[";
            for (size_t l = 0; l < geos_->getNumGeometries(); l++) {
                WriteCoordinates wc(os, true, outputRefSys);
                if (l) {
                    os << ",";
                }
                os << "[";
                geos_->getGeometryN(l)->apply_ro(&wc);
                os << "]";
            }
            os << "]";
        }
        break;
    }
    os << "}";
}

class WriteCoordinatesDirect:public geos::geom::CoordinateFilter
{
public:
    WriteCoordinatesDirect(std::ostream& os):os_(os), first_(true){};

    void filter_rw (geos::geom::Coordinate *) const override
    {
    }
    void filter_ro (const geos::geom::Coordinate *c) override
    {
        if(!first_){
            os_ << ",";
        }
        first_ = false;
        os_ << "[" << c->x << "," << c->y << "]";
    }

private:
    std::ostream& os_;
    bool first_;
};


void Geom::coordinatesJson(std::ostream& os) const
{
    if (geos_){
        os << "[";
        WriteCoordinatesDirect wc(os);
        geos_->apply_ro(&wc);
        os << "]";
    }
}

std::string Geom::dump() const
{
    std::stringstream os;
    os.precision(MERCATOR_OSTREAM_PRECISION);
    os << geometryTypeName() << " ";
    coordinatesJson(os);
    return os.str();
}

Geom
Geom::startPoint() const
{
    return Geom(asLineString()->getStartPoint());
}

Geom
Geom::endPoint() const
{
    return Geom(asLineString()->getEndPoint());
}

geos::geom::Geometry*
Geom::operator -> ()
{
    REQUIRE_LOGIC(geos_, "Accessing NULL geometry");
    return geos_.get();
}

const geos::geom::Geometry*
Geom::operator -> () const
{
    REQUIRE_LOGIC(geos_, "Accessing NULL geometry");
    return geos_.get();
}

bool
Geom::equal(const Geom& g, double tolerance) const
{
    return (isNull() && g.isNull()) ||
        (!isNull() && !g.isNull() && geos_.get()->equalsExact( g.geos_.get(), tolerance ));
}

Geom
Geom::closestPoint(double x, double y) const
{
    if (!isNull()) {
        geos::geom::Coordinate ptCoord(x, y);
        std::unique_ptr<geos::geom::Point> pt(geos::geom::GeometryFactory::getDefaultInstance()->createPoint(ptCoord));
        {
            //! Geos fails with POLYGON closest point, use only exterior
            //! till they fix bug
            GeosGeometryPtr fromGeom = geos_.get();
            if (geos_->getGeometryTypeId() == geos::geom::GEOS_POLYGON) {
                fromGeom = GeosGeometryPtr(dynamic_cast<geos::geom::Polygon*>(fromGeom)->getExteriorRing());
            }
            geos::operation::distance::DistanceOp dst(fromGeom, pt.get());
            if (dst.distance() == 0) {
                return Geom(std::move(pt));
            } else {
                auto nearest = dst.nearestPoints();
                geos::geom::Point* ptRet =
                    geos::geom::GeometryFactory::getDefaultInstance()->createPoint(nearest->getAt(0));
                return Geom(ptRet);
            }
        }
    }
    return Geom();
}

Geom
Geom::center() const
{
    if(!isNull()){
        if (geos_->getGeometryTypeId() == geos::geom::GEOS_POLYGON) {
            return Geom(geos_->getInteriorPoint());
        } else if (geos_->getGeometryTypeId() == geos::geom::GEOS_LINESTRING
                   && geos_->getNumPoints() >= 3
                   && geos_->getLength() > 0.0) {
            const auto* linestringPtr = dynamic_cast<geos::geom::LineString*>(geos_.get());
            auto coords = linestringPtr->getCoordinates();
            double targetLength = geos_->getLength() * 0.5;
            double curLength = 0.0;
            size_t prev = 0;
            for(size_t v = 0; v < coords->size(); v++){
                double curSegLength = coords->getAt(v).distance(coords->getAt(prev));
                if((curSegLength + curLength) > targetLength){
                    double inSegment = targetLength - curLength;
                    double retX = (coords->getAt(v).x - coords->getAt(prev).x) * inSegment/curSegLength + coords->getAt(prev).x;
                    double retY = (coords->getAt(v).y - coords->getAt(prev).y) * inSegment/curSegLength + coords->getAt(prev).y;

                    Geom ret(geos::geom::GeometryFactory::getDefaultInstance()->createPoint(
                             geos::geom::Coordinate(retX, retY)));
                    return ret;
                }
                curLength += curSegLength;
                prev = v;
            };
        } else {
            return Geom(geos_->getCentroid());
        }
    }
    return Geom();
}

Geom
Geom::clone() const
{
    if (!geos_) {
        return Geom();
    }

    return Geom(geos::geom::GeometryFactory::getDefaultInstance()->createGeometry(geos_.get()));
}

void
Geom::simplify(double tolerance)
{
    std::unique_ptr<geos::geom::Geometry> simple(
        geos::simplify::TopologyPreservingSimplifier::simplify(geos_.get(), tolerance).release()
        );
    geos_ = GeometryPtr(simple.release());
}

double
Geom::distance(const Geom& other) const
{
    return other->distance(geos_.get());
}

namespace {
/* This function computes length line seen as straight
 * on mercator projection
 * not shortest distance over geoid
*/
double
rulerDistance(const TGeoPoint& p1, const TGeoPoint& p2)
{
    constexpr double epsilon = 1.0e-10;
    constexpr double R = 6378137;
    double dist = 0;

    if(!(fabs(p2.y()-p1.y()) < epsilon
        &&  fabs(p2.x() - p1.x()) < epsilon)){
            double latAV = ( p1.y() + (p2.y() - p1.y()) / 2 ) * M_PI / 180;
            double pathAngle = atan( ((p2.x() * 60  - p1.x() * 60)/ ( p2.y() * 60 - p1.y() * 60)) * cos(latAV) );
            double distPerDegree = 2.0 * M_PI * R / 360.0;

        dist = fabs(p2.y() - p1.y()) < epsilon
               ? fabs(((p2.x() - p1.x()) / sin(pathAngle)) * cos(latAV) * distPerDegree)
               : abs(distPerDegree * (p2.y() - p1.y()) / cos(pathAngle));
    }
    return dist;
}

class AccumulateLength:public geos::geom::CoordinateFilter
{
public:
    AccumulateLength() = default;

    void filter_rw (geos::geom::Coordinate *) const override
    {
    }
    void filter_ro (const geos::geom::Coordinate *c) override
    {
        TGeoPoint coord = mercatorToGeodetic(c->x, c->y);
        if(!first_){
            length_ += rulerDistance(last_, coord);
        }
        first_ = false;
        last_ = coord;
    }

    double length() const
    { return length_;}

private:
    bool first_{true};
    TGeoPoint last_;
    double length_{0};

};
} // namespace

double
Geom::realLength() const
{
    if(isNull()){
        return 0;
    }
    AccumulateLength aLength;
    geos_->apply_ro(&aLength);
    return aLength.length();
}

geolib3::Vector2
Geom::realSize() const
{
    if(isNull()){
        return geolib3::Vector2(0.0, 0.0);
    }
    const geos::geom::Envelope* env = geos_->getEnvelopeInternal();
    if(!env){
       return geolib3::Vector2(0.0, 0.0);
    }
    TGeoPoint lt = mercatorToGeodetic(env->getMinX(), env->getMinY());
    TGeoPoint lb = mercatorToGeodetic(env->getMinX(), env->getMaxY());
    TGeoPoint rt = mercatorToGeodetic(env->getMaxX(), env->getMinY());
    return geolib3::Vector2(rulerDistance(lt, rt), rulerDistance(lt, lb));

}

Geom
Geom::difference(const Geom& g) const
{
    return Geom(geos_->difference(g.geos_.get()));
}

Geom
Geom::symDifference(const Geom& g) const
{
    return Geom(geos_->symDifference(g.geos_.get()));
}

Geom
Geom::Union(const Geom& g) const
{
    return Geom(geos_->Union(g.geos_.get()));
}

namespace {

class FixLinePointsFilter : public geos::geom::CoordinateFilter
{
public:
    FixLinePointsFilter(const Geom& geom)
    {
        ASSERT(geom->getGeometryTypeId() == geos::geom::GEOS_POLYGON);

        polygon_ = dynamic_cast<const geos::geom::Polygon*>(geom.geosGeometryPtr());
    }

    void filter_rw(geos::geom::Coordinate* coordinateToFix) const override
    {
        constexpr double TOLERANCE = 0.01;

        //First, try to snap the point to a polygon vertex
        auto snapToVertex = [&](const geos::geom::LineString* ring) -> bool
        {
            const auto* coordinatesRO = ring->getCoordinatesRO();
            for (size_t i = 0; i < coordinatesRO->size(); i++) {
                const auto& ringCoordinate = coordinatesRO->getAt(i);
                if (ringCoordinate.distance(*coordinateToFix) < TOLERANCE) {
                    *coordinateToFix = ringCoordinate;
                    return true;
                }
            }
            return false;
        };

        if (snapToVertex(polygon_->getExteriorRing())) {
            return;
        }
        for (size_t i = 0; i < polygon_->getNumInteriorRing(); i++) {
            if (snapToVertex(polygon_->getInteriorRingN(i))) {
                return;
            }
        }

        //Otherwise, try to mirror the point about a polygon edge if the point is inside the polygon
        std::unique_ptr<geos::geom::Point> pointToFix(geos::geom::GeometryFactory::getDefaultInstance()->createPoint(*coordinateToFix));
        if (!polygon_->contains(pointToFix.get())) {
            return;
        }

        auto mirrorAboutEdge = [&](const geos::geom::LineString* ring) -> bool
        {
            geos::operation::distance::DistanceOp dst(ring, pointToFix.get());
            if (dst.distance() > 0 && dst.distance() < TOLERANCE) {
                auto nearest = dst.nearestPoints();

                //mirror point to the outside
                coordinateToFix->x = 2.0 * nearest->getAt(0).x - coordinateToFix->x;
                coordinateToFix->y = 2.0 * nearest->getAt(0).y - coordinateToFix->y;

                return true;
            }
            return false;
        };

        if (mirrorAboutEdge(polygon_->getExteriorRing())) {
            return;
        }
        for (size_t i = 0; i < polygon_->getNumInteriorRing(); i++) {
            if (mirrorAboutEdge(polygon_->getInteriorRingN(i))) {
                return;
            }
        }
    }

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

private:
    const geos::geom::Polygon* polygon_;
};

} // namespace

/**
Inspired by PostGIS ST_Split function (lwpoly_split_by_line)
https://github.com/postgis/postgis/blob/3beb3f82ba78cb98478637f380070839472c8fb3/liblwgeom/lwgeom_geos_split.c
*/
std::vector<Geom>
Geom::splitPolyByLines(const std::vector<Geom>& splitLines) const
{
    REQUIRE_PARAM(
        (*this)->getGeometryTypeId() == geos::geom::GEOS_POLYGON,
        "Expected geom type is polygon, but input is "
           << (*this)->getGeometryType());

    Geom geometryToPolygonize((*this)->getBoundary());
    REQUIRE_LOGIC(!geometryToPolygonize.isNull(), "Boundary is null");

    FixLinePointsFilter fixLinePointsFilter(*this);

    for (const auto& splitLine : splitLines) {
        REQUIRE_PARAM(splitLine->getGeometryTypeId() == geos::geom::GEOS_LINESTRING,
            "Expected param geom type is line string, but input is "
                << splitLine->getGeometryType());

        Geom splitLineClone = splitLine.clone();
        splitLineClone->apply_rw(&fixLinePointsFilter);
        geometryToPolygonize = geometryToPolygonize.Union(splitLineClone);
        REQUIRE_LOGIC(!geometryToPolygonize.isNull(),
            "Boundary and line union is null");
    }

    geos::operation::polygonize::Polygonizer polygonizer;
    polygonizer.add(geometryToPolygonize.geosGeometryPtr());

    auto polygons = polygonizer.getPolygons();
    REQUIRE_LOGIC(polygons, "Polygons do not exist");
    REQUIRE_LOGIC(!polygons->empty(), "Polygons are empty");

    std::vector<Geom> resultPolygons;
    for (const auto& polygon : *polygons) {
        Geom interiorPos(polygon->getInteriorPoint());
        REQUIRE_LOGIC(!interiorPos.isNull(), "No interior point");

        if ((*this)->contains(interiorPos.geosGeometryPtr())) {
            resultPolygons.emplace_back(polygon->clone());
        }
    }
    return resultPolygons;
}

namespace {
class Vertex
{
public:
    Vertex(double x, double y)
        : position_(x, y)
        , isJunction_(false)
        {}
    explicit Vertex(const geolib3::Point2& p)
        : position_(p)
        , isJunction_(true)
        {}
    void setIsJunction() { isJunction_ = true; }
    bool isJunction() const { return isJunction_; }
    const geolib3::Point2& position() const { return position_; }
    geos::geom::Coordinate coordinate() const
    {
        return {position_.x(), position_.y()};
    }
private:
    geolib3::Point2 position_;
    bool isJunction_;
};

class ReadAllCoordinates : public geos::geom::CoordinateFilter
{
public:
    explicit ReadAllCoordinates(std::vector<Vertex>& vertexes)
        : vertexes_(vertexes)
    {}
    void filter_ro (const geos::geom::Coordinate* coord) override
    {
        vertexes_.emplace_back(coord->x, coord->y);
    }
    std::vector<Vertex>& vertexes_;
};
} // namespace

std::vector<Geom>
Geom::splitLineStringByPoints(
    const std::vector<geolib3::Point2>& splitPoints,
    double tolerance) const
{
    const auto& geom = *this;
    REQUIRE_PARAM(geom->getGeometryTypeId() == geos::geom::GEOS_LINESTRING,
        "Expected geom type is linestring, but input is "
            << geom.geometryTypeName());
    REQUIRE_PARAM(!splitPoints.empty(), "Empty splitpoints.");

    std::vector<Vertex> vertexes;
    vertexes.reserve(geom->getNumPoints() + splitPoints.size());
    ReadAllCoordinates reader(vertexes);
    geom->apply_ro(&reader);
    vertexes.front().setIsJunction();
    vertexes.back().setIsJunction();
    const double squaredTolerance = tolerance * tolerance;
    for (const auto& splitPoint : splitPoints) {
        size_t closestVertexIndex = vertexes.size();
        double shortestVertexDistance =  std::numeric_limits<double>::max();
        for (size_t v = 0; v < vertexes.size(); ++v) {
            double distance = geolib3::squaredDistance(splitPoint, vertexes[v].position());
            if (distance <  shortestVertexDistance) {
                shortestVertexDistance = distance;
                closestVertexIndex = v;
            }
        }
        if (shortestVertexDistance < squaredTolerance) {
            vertexes[closestVertexIndex].setIsJunction();
            continue;
        }
        size_t closestEdgeIndex = vertexes.size();
        double shortestEdgeDistance =  std::numeric_limits<double>::max();
        for (size_t v = 0; v < vertexes.size() - 1; ++v) {
            double distance = geolib3::squaredDistance(
                geolib3::Segment2(vertexes[v].position(), vertexes[v + 1].position()),
                splitPoint);
            if (distance <  shortestEdgeDistance) {
                shortestEdgeDistance = distance;
                closestEdgeIndex = v;
            }
        }
        REQUIRE_LOGIC(
            shortestEdgeDistance < squaredTolerance,
            "Split point away from linestring ("
                << sqrt(shortestEdgeDistance) << ").");
        auto insertPosition = vertexes.begin();
        std::advance(insertPosition, closestEdgeIndex + 1);
        vertexes.insert(insertPosition, Vertex(splitPoint));
    }
    std::vector<Geom> resultingGeoms;
    size_t startVertexIndex = 0;
    while (startVertexIndex < vertexes.size() - 1) {
        std::unique_ptr<std::vector<geos::geom::Coordinate>>
            coordinates(new std::vector<geos::geom::Coordinate>());
        coordinates->emplace_back(
            vertexes[startVertexIndex].coordinate());
        size_t endVertexIndex = startVertexIndex + 1;
        while (endVertexIndex < vertexes.size() &&
            !vertexes[endVertexIndex].isJunction()) {
            coordinates->emplace_back(
                vertexes[endVertexIndex].coordinate());
            ++endVertexIndex;
        }
        if (endVertexIndex < vertexes.size()) {
            coordinates->emplace_back(
                vertexes[endVertexIndex].coordinate());
        }
        Geom newLineString;
        newLineString.setLineString(
            geos::geom::CoordinateArraySequenceFactory::instance()->create(
                coordinates.release()));
        resultingGeoms.push_back(std::move(newLineString));
        startVertexIndex = endVertexIndex;
    }
    return resultingGeoms;
}


const std::string&
Geom::geometryTypeName() const
{
    return geometryTypeName(geos_.get());
}

const std::string&
Geom::geometryTypeName(ConstGeosGeometryPtr geos)
{
    REQUIRE_LOGIC(geos, "Geometry isNull");
    return geometryTypeName(geos->getGeometryTypeId());
}

const std::string&
Geom::geometryTypeName(geos::geom::GeometryTypeId gType)
{
    switch( gType ){
            case geos::geom::GEOS_POINT:
                return geomTypeNamePoint;
            case geos::geom::GEOS_POLYGON:
                return geomTypeNamePolygon;
            case geos::geom::GEOS_LINESTRING:
            case geos::geom::GEOS_MULTILINESTRING:
                return geomTypeNameLine;
            default:
                break;
        }
    throw geom::LogicError() << "Unknown geometry type: " << gType;
}

const std::string&
Geom::geometryTypeName(std::string postgisGeometryType)
{
    for (auto& c : postgisGeometryType) {
        c = std::tolower(c);
    }

    if (postgisGeometryType == "st_point") {
        return geomTypeNamePoint;
    }
    if (postgisGeometryType == "st_polygon") {
        return geomTypeNamePolygon;
    }
    if (postgisGeometryType == "st_linestring" ||
        postgisGeometryType == "st_multilinestring")
    {
        return geomTypeNameLine;
    }
    throw geom::LogicError() << "Unknown ST_GeometryType: " << postgisGeometryType;
}

std::string
wkb(ConstGeosGeometryPtr g)
{
    std::stringstream hexwkb;
    geos::io::WKBWriter w;
    w.write(*g, hexwkb);
    return hexwkb.str();
}

std::string wkb2wkt(const std::string& wkb)
{
    return geolib3::WKT::toString<geolib3::SimpleGeometryVariant>(
        geolib3::WKB::read<geolib3::SimpleGeometryVariant>(wkb));
}

std::string wkt2wkb(const std::string& wkt)
{
    return geolib3::WKB::toString<geolib3::SimpleGeometryVariant>(
        geolib3::WKT::read<geolib3::SimpleGeometryVariant>(wkt));
}

} // namespace maps::wiki::common
