#pragma once

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

#include <geos/geom/Geometry.h>
#include <geos/geom/LineString.h>

#include <pqxx/pqxx>
#include <ostream>
#include <string>
#include <memory>

namespace maps::wiki::common {

namespace geom {

class Exception : public maps::Exception
{};

class LogicError : public maps::LogicError
{};

class BadParam : public geom::Exception
{};
} // namespace geom

using GeosGeometryPtr = geos::geom::Geometry *;
using ConstGeosGeometryPtr = const geos::geom::Geometry *;
using GeometryPtr = std::unique_ptr<geos::geom::Geometry>;
using PointTransform = std::function<geolib3::Point2 (const geolib3::Point2 &)>;

enum class SpatialRefSystem
{
    Mercator,
    Geodetic
};

const size_t MERCATOR_OSTREAM_PRECISION = 12;

class Geom
{
public:
    static const std::string& geomTypeNamePoint;
    static const std::string& geomTypeNameLine;
    static const std::string& geomTypeNamePolygon;

    Geom() = default;
    explicit Geom(GeometryPtr&& g): geos_(std::move(g)) {}
    explicit Geom(GeosGeometryPtr g): geos_(g) {}
    explicit Geom(const pqxx::field& wkbField);
    explicit Geom(const std::string& wkb);
    explicit Geom(const TGeoPoint& pt);

    Geom(Geom&& geom) noexcept { swap(geom); }
    Geom(const Geom& geom)
    {
        if (geom.geos_) {
            geom.clone().swap(*this);
        }
    }

    Geom& operator = (Geom&& geom) noexcept
    {
        if (&geom != this) {
            swap(geom);
        }
        return *this;
    }

    Geom& operator = (const Geom& geom)
    {
        if (&geom != this) {
            Geom(geom).swap(*this);
        }
        return *this;
    }

    void swap(Geom& geom) { geos_.swap(geom.geos_); }


    GeosGeometryPtr operator -> ();
    ConstGeosGeometryPtr operator -> () const;

    std::string wkb() const;

    Geom transformed(const PointTransform& transform) const;

    // These methods assume that geometry is in mercator.
    void gml(std::ostream& os, SpatialRefSystem outputRefSys) const;
    void geoJson(std::ostream& os, SpatialRefSystem outputRefSys) const;

    // for debug output
    void coordinatesJson(std::ostream& os) const;

    ConstGeosGeometryPtr geosGeometryPtr() const;

    std::string dump() const;


    //Set from lineString and take ownership of sequence
    void setLineString(geos::geom::CoordinateSequence* coordinates);
    void setLineString(std::unique_ptr<geos::geom::CoordinateSequence> coordinates);
    Geom startPoint() const;
    Geom endPoint() const;
    /**
    * Insert point into polyline and return it's index
    * tolerance is used to make vertexes points unique
    *  @return pair first=result position,second=inserted
    */
    std::pair<size_t, bool> insertPoint(double x, double y, double tolerance);

    /**
    * Get closest point to provided
    * assumed that params are in the same cooridante space
    * @param x
    * @param y
    * @return wrapped geometry
    */

    Geom closestPoint(double x, double y) const;
    /**
     * Check if geometry is NULL
     */
    bool isNull() const;
    /**
    * Create geometry object representing
    * buffer for this (see geos::geom::Geometry::buffer
    * @param width - width of buffer considered in the same units as geometry
    * @return geos::geom::Geometry derived object
    */
    Geom createBuffer(double width) const;
    /**
    * Comparing two geometries on equality
    * @param g geometry to be compared with this one
    * @param tolerance - distortion tolerance which is to be ignored
    * @return true if two geometries are equal
    */
    bool equal(const Geom& g, double tolerance) const;

    /**
    * @return center point of this geometry
    */
    Geom center() const;

    /**
    * Simplify this geometry using DP simplifier
    * @param tolerance - distance tolerance
    */
    void simplify(double tolerance);

    /**
    * Get linear distance to other geometry
    * @param other - geometry
    * @return distance
    */
    double distance(const Geom& other) const;

    /**
    * Compute real(meters) length/perimeter of the geometry
    * assuming that is it in mercator coordinate space
    */
    double realLength() const;

    geolib3::Vector2 realSize() const;

    /**
    * Calculate difference of geometries
    * @param g - other geometry
    * @return difference
    */
    Geom difference(const Geom& g) const;

    /**
    * Calculate symmetric difference of geometries
    * @param g - other geometry
    * @return symmetric difference
    */
    Geom symDifference(const Geom& g) const;

    /**
    * Calculate union of geometries
    * @param g - other geometry
    * @return union
    */
    Geom Union(const Geom& g) const; // NOLINT

    /**
    * Split this polygon
    * with line strings
    * @param splitLines - vector of line strings
    * @return vector of polygons
    */
    std::vector<Geom> splitPolyByLines(const std::vector<Geom>& splitLines) const;

    /**
    * Split this linestring
    * with points
    * @param splitPoints - vector of points
    * @return vector of linestrings
    */
    std::vector<Geom> splitLineStringByPoints(const std::vector<geolib3::Point2>& splitPoints, double tolerance) const;

    const std::string& geometryTypeName() const;
    static const std::string& geometryTypeName(ConstGeosGeometryPtr);
    static const std::string& geometryTypeName(geos::geom::GeometryTypeId);
    static const std::string& geometryTypeName(std::string postgisGeometryType);

private:
    /**
    * Create clone of this geometry
    */
    Geom clone() const;

    const geos::geom::LineString* asLineString() const;

private:
    GeometryPtr geos_;
};

std::string wkb(ConstGeosGeometryPtr g);

std::string wkb2wkt(const std::string& wkb);
std::string wkt2wkb(const std::string& wkt);

} // namespace maps::wiki::common
