#pragma once

#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/segment.h>
#include <maps/libs/geolib/include/polyline.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/linear_ring.h>
#include <maps/libs/geolib/include/bounding_box.h>

#include <maps/libs/common/include/exception.h>

#include <boost/optional.hpp>

#include <vector>
#include <list>

namespace maps {
namespace wiki {
namespace geom_tools {

enum class Orientation { CW, CCW };

class OutOfRangeException : public maps::LogicError {};

class RingAdapter {
public:
    explicit RingAdapter(const geolib3::LinearRing2* ring)
        : ring_(ring)
    {}

    explicit RingAdapter(const std::shared_ptr<geolib3::LinearRing2>& ring)
        : ownedRing_(ring)
        , ring_(ring.get())
    {}

    size_t pointsCount() const { return ring().pointsNumber(); }

    geolib3::Point2 point(size_t number) const;
    size_t prevPointNumber(size_t number) const { return number > 0 ? number - 1 : pointsCount() - 1; }
    geolib3::Point2 prevPoint(size_t number) const;
    size_t nextPointNumber(size_t number) const { return number < pointsCount() - 1 ? number + 1 : 0; }
    geolib3::Point2 nextPoint(size_t number) const;

    geolib3::Segment2 segment(size_t number) const;
    geolib3::Segment2 prevSegment(size_t number) const;
    geolib3::Segment2 nextSegment(size_t number) const;

    geolib3::BoundingBox boundingBox() const;

    double signedArea() const;
    Orientation orientation() const;

    void setOrientation(Orientation requiredOrientation);

    const geolib3::LinearRing2& ring() const { return *ring_; }

private:
    double signedAreaImpl() const;

    void checkPointNumber(size_t number) const;

    void cacheArea() const;
    void cacheBBox() const;
    void cacheOrientation() const;

    Orientation actualOrientation() const;

    std::shared_ptr<geolib3::LinearRing2> ownedRing_;
    const geolib3::LinearRing2* ring_;

    // cached in orientation call
    mutable boost::optional<Orientation> requestedOrientation_;

    // cached data

    mutable boost::optional<double> area_;
    mutable boost::optional<geolib3::BoundingBox> bbox_;
    mutable boost::optional<Orientation> actualOrientation_;
};

typedef std::vector<RingAdapter> RingVector;
typedef std::list<RingAdapter> RingList;

RingVector
convertRingsListToVector(RingList ringList); // sink argument

class PolygonAdapter {
public:
    PolygonAdapter(RingAdapter shell, RingVector holes)
        : shell_(std::move(shell))
        , holes_(std::move(holes))
    {}

    explicit PolygonAdapter(const geolib3::Polygon2& polygon);

    const RingAdapter& shell() const { return shell_; }

    size_t holesCount() const { return holes_.size(); }
    const RingAdapter& hole(size_t number) const;

    size_t pointsCount() const;

    geolib3::BoundingBox boundingBox() const { return shell().boundingBox(); }

    double area() const;

    /// Set exterior ring orientation to CCW and interior rings orientation to CW
    void normalize();

    geolib3::Polygon2 convertToGeolibPolygon() const;

private:
    void cacheArea() const;
    void cachePointsCount() const;

    RingAdapter shell_;
    RingVector holes_;

    // cached data

    mutable boost::optional<double> area_;
    mutable boost::optional<size_t> pointsCount_;
};

typedef std::vector<PolygonAdapter> PolygonVector;

} // namespace geom_tools
} // namespace wiki
} // namespace maps
