#include <yandex/maps/wiki/groupedit/actions/move.h>
#include "../utils/geom.h"
#include <yandex/maps/wiki/groupedit/object.h>
#include <yandex/maps/wiki/groupedit/session.h>
#include <yandex/maps/wiki/revision/filters.h>

#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/exception.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/polyline.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/geolib/include/transform.h>
#include <maps/libs/geolib/include/variant.h>

#include <memory>
#include <sstream>

namespace maps {
namespace wiki {
namespace groupedit {
namespace actions {

namespace gl = maps::geolib3;

namespace {

const std::string GROUP_MOVED_ACTION = "group-moved";

template<typename TGeom>
std::string toWkbString(const TGeom& geom)
{
    std::ostringstream stream;
    gl::WKB::write(geom, stream);
    return stream.str();
}

gl::Polyline2
transformPolyline(
    const gl::Polygon2& cutoffPolygon,
    const gl::SimpleGeometryTransform2& transformer,
    const gl::Polyline2& polyline)
{
    const auto& points = polyline.points();
    gl::PointsVector newPoints;
    newPoints.reserve(points.size());
    for (const gl::Point2& point : points) {
        if (gl::spatialRelation(cutoffPolygon, point, gl::Intersects)) {
            newPoints.push_back(
                transformer(point, gl::TransformDirection::Forward));
        } else {
            newPoints.push_back(point);
        }
    }
    return gl::Polyline2(std::move(newPoints));
}

gl::LinearRing2
transformLinearRing2(
    const gl::Polygon2& cutoffPolygon,
    const gl::SimpleGeometryTransform2& transformer,
    const gl::LinearRing2& linearRing)
{
    const auto transfromedPolyline = transformPolyline(
        cutoffPolygon,
        transformer,
        linearRing.toPolyline());

    return gl::LinearRing2(
        transfromedPolyline.points(),
        true);
}

template<typename TGeom>
std::string transform(
    const gl::Polygon2& cutoffPolygon,
    const gl::SimpleGeometryTransform2& transformer,
    const TGeom& geom,
    std::string originalWkb,
    PolygonMode)
{
    if (!gl::spatialRelation(cutoffPolygon, geom, gl::Contains)) {
        return originalWkb;
    }

    return toWkbString(transformer(geom, gl::TransformDirection::Forward));
}

template<>
std::string transform<gl::Polyline2>(
    const gl::Polygon2& cutoffPolygon,
    const gl::SimpleGeometryTransform2& transformer,
    const gl::Polyline2& polyline,
    std::string /*originalWkb*/,
    PolygonMode)
{
    return toWkbString(transformPolyline(
        cutoffPolygon,
        transformer,
        polyline));
}

template<>
std::string transform<gl::Polygon2>(
    const gl::Polygon2& cutoffPolygon,
    const gl::SimpleGeometryTransform2& transformer,
    const gl::Polygon2& polygon,
    std::string originalWkb,
    PolygonMode polygonMode)
{
    if (polygonMode == PolygonMode::AsWhole &&
        !gl::spatialRelation(cutoffPolygon, polygon, gl::Contains))
    {
        return originalWkb;
    }

    try {
        auto transfromedExteriorRing = transformLinearRing2(
            cutoffPolygon,
            transformer,
            polygon.exteriorRing());
        std::vector<gl::LinearRing2> transfromedInteriorRings;
        const auto count = polygon.interiorRingsNumber();
        transfromedInteriorRings.reserve(count);
        for (size_t ring = 0; ring < count; ++ring) {
            transfromedInteriorRings.emplace_back(
                transformLinearRing2(
                    cutoffPolygon,
                    transformer,
                    polygon.interiorRingAt(ring)));
        }
        return toWkbString(gl::Polygon2(
            std::move(transfromedExteriorRing),
            std::move(transfromedInteriorRings),
            gl::Validate::Yes));
    } catch(const gl::ValidationError& ex) {
        return originalWkb;
    } catch(maps::Exception& ex) {
        return originalWkb;
    }
}

} // namespace

std::vector<TCommitId> moveObjects(
    const Session& session,
    const revision::filters::ProxyFilterExpr& filterExpr,
    const std::string& movePolygonWkb,
    double dx,
    double dy,
    TUserId author,
    PolygonMode polygonMode)
{
    auto movePolygon =
        geolib3::WKB::read<gl::Polygon2>(movePolygonWkb);

    gl::SimpleGeometryTransform2 transformer(gl::AffineTransform2::translate(dx, dy));

    return session.query(
            filterExpr,
            GeomPredicate::Intersects,
            movePolygonWkb).update(
        GROUP_MOVED_ACTION,
        author,
        [&](Object& obj) {
            auto optionalGeomWkb = obj.geometryWkb();
            if (!optionalGeomWkb) {
                return;
            }

            try {
                auto geometryVariant = gl::WKB::read<gl::SimpleGeometryVariant>(*optionalGeomWkb);
                obj.setGeometryWkb(
                    geometryVariant.visit(
                        [&](const auto& geometry) {
                            return transform(
                                movePolygon,
                                transformer,
                                geometry,
                                std::move(*optionalGeomWkb),
                                polygonMode);
                        }));
            } catch (const maps::RuntimeError& ex) {
                throw maps::LogicError()
                        << "Unsupported geometry type at object " << obj.id();
            }
        });
}

} // namespace actions
} // namespace groupedit
} // namespace wiki
} // namespace maps
