#include "types.h"

#include "geom_io.h"

#include <maps/libs/geolib/include/contains.h>


namespace maps {
namespace wiki {
namespace topo {
namespace geom {


using namespace geolib3;


struct SnapPointData {

    SnapPointData() = delete;

    SnapPointData(const Point2& point): point(point) {}

    SnapPointData(const Node& node): point(node.pos()), nodeId(node.id()) {}

    SnapPointData(const Edge& edge, size_t vertexIndex)
        : point(edge.geom().pointAt(vertexIndex))
        , edgeId(edge.id())
        , vertexIndex(vertexIndex)
    {}

    SnapPointData(const Edge& edge, size_t segmentIndex, const Point2& point)
        : point(point)
        , edgeId(edge.id())
        , segmentIndex(segmentIndex)
    {
        const Segment2& segment = edge.geom().segmentAt(segmentIndex);
        REQUIRE(
            contains(segment, point),
            "Point " << point << " doesn't lie on the segment " << segment
        );
    }

    Point2 point;
    OptionalNodeID nodeId;
    OptionalEdgeID edgeId;
    OptionalIndex vertexIndex;
    OptionalIndex segmentIndex;
};


SnapPoint::SnapPoint(const Point2& point):
    data_(std::make_shared<SnapPointData>(point)) {}

SnapPoint::SnapPoint(const Node& node):
    data_(std::make_shared<SnapPointData>(node)) {}

SnapPoint::SnapPoint(const Edge& edge, size_t vertexIndex):
    data_(std::make_shared<SnapPointData>(edge, vertexIndex)) {}

SnapPoint::SnapPoint(const Edge& edge, size_t segmentIndex, const Point2& point):
    data_(std::make_shared<SnapPointData>(edge, segmentIndex, point)) {}

const OptionalNodeID& SnapPoint::node() const
{
    return data_->nodeId;
}

const OptionalEdgeID& SnapPoint::edge() const
{
    return data_->edgeId;
}

const OptionalIndex& SnapPoint::vertexIndex() const
{
    return data_->vertexIndex;
}

const OptionalIndex& SnapPoint::segmentIndex() const
{
    return data_->segmentIndex;
}

const Point2& SnapPoint::geom() const
{
    return data_->point;
}

bool SnapPoint::onEdge(const Edge& edge) const
{
    return (node() == edge.startNode()) ||
           (node() == edge.endNode()) ||
           (edge.id() == this->edge());
}

bool SnapPoint::onSegment(const Edge& edge, size_t segmentIndex) const
{
    return (segmentIndex == 0 && node() == edge.startNode()) ||
        (segmentIndex + 1 == edge.geom().segmentsNumber() && node() == edge.endNode()) ||
        ( edge.id() == this->edge() &&
            (segmentIndex == this->segmentIndex() ||
             segmentIndex == this->vertexIndex() ||
             segmentIndex + 1 == this->vertexIndex())
        );
}

bool SnapPoint::operator==(const SnapPoint& other) const
{
    return data_ == other.data_;
}

bool SnapPoint::operator!=(const SnapPoint& other) const
{
    return !(*this == other);
}


namespace {

PointsVector toPointsVector(const SnapPointVector& snaps)
{
    PointsVector points;
    points.reserve(snaps.size());

    for (const auto& snap: snaps) {
        points.push_back(snap.geom());
    }

    return points;
}

} // end of anonymous namespace


struct SnapPolylineData {

    SnapPolylineData(const SnapPointVector& points)
        : polyline(Polyline2{toPointsVector(points)})
        , points(points)
    {
        REQUIRE(
            points.size(),
            "Snap polyline must consist of one point at least"
        );
    }

    Polyline2 polyline;
    SnapPointVector points;
};

SnapPolyline::SnapPolyline(const SnapPointVector& points):
    data_(std::make_shared<SnapPolylineData>(points)) {}

SnapPolyline::SnapPolyline(std::initializer_list<SnapPoint> points):
    data_(std::make_shared<SnapPolylineData>(SnapPointVector{points})) {}

const SnapPointVector& SnapPolyline::points() const
{
    return data_->points;
}

const SnapPoint& SnapPolyline::start() const
{
    return points().front();
}

const SnapPoint& SnapPolyline::end() const
{
    return points().back();
}

bool SnapPolyline::isPoint() const
{
    return points().size() == 1;
}

const Polyline2& SnapPolyline::geom() const
{
    return data_->polyline;
}

size_t SnapPolyline::size() const
{
    return points().size();
}

bool SnapPolyline::empty() const
{
    return points().empty();
}

bool SnapPolyline::operator==(const SnapPolyline& other) const
{
    return data_ == other.data_;
}

const SnapPoint& SnapPolyline::operator[](size_t index) const
{
    return points().at(index);
}

void SnapPolyline::extend(const SnapPolyline& other)
{
    if (end() != other.start()) {
        throw maps::RuntimeError() << "Polylines must have common point";
    }

    data_->points.insert(
        data_->points.end(),
        other.points().begin() + 1, other.points().end()
    );
    data_->polyline.extend(
        other.geom(), EndPointMergePolicy::MergeEqualPoints
    );
}

bool SnapPolyline::hasSameOrder(const SnapPolyline& subpolyline) const
{
    const SnapPolyline& polyline = *this;

    if (polyline.start() == subpolyline.start()) {
        return polyline[1] == subpolyline[1];
    }
    if (polyline.start() == subpolyline.end()) {
        return polyline[1] != subpolyline[subpolyline.size() - 2];
    }

    for (size_t i = 1; i < polyline.size(); ++i) {
        if (polyline[i] == subpolyline.start()) {
            return true;
        }
        if (polyline[i] == subpolyline.end()) {
            return false;
        }
    }

    throw maps::RuntimeError() << "It's not subpolyline!";
}

} // namespace maps::wiki::topo::geom
} // namespace maps::wiki::topo
} // namespace maps::wiki
} // namespace maps


namespace std {

size_t hash<maps::wiki::topo::geom::SnapPoint>::operator()(
    const maps::wiki::topo::geom::SnapPoint& point) const
{
    return std::hash<maps::wiki::topo::geom::SnapPointDataPtr>{}(point.data_);
}

size_t hash<maps::wiki::topo::geom::SnapPolyline>::operator()(
    const maps::wiki::topo::geom::SnapPolyline& polyline) const
{
    return std::hash<maps::wiki::topo::geom::SnapPolylineDataPtr>{}(polyline.data_);
}

} // end of std namespace
