#include "module.h"
#include "../utils/face_builder.h"
#include "../utils/face_checks.h"
#include <yandex/maps/wiki/validator/check.h>
#include <yandex/maps/wiki/validator/categories.h>
#include <yandex/maps/wiki/geom_tools/polygonal/builder.h>

#include <maps/libs/geolib/include/algorithm.h>
#include <maps/libs/geolib/include/linear_ring.h>
#include <maps/libs/geolib/include/conversion_geos.h>

#include <geos/geom/Polygon.h>
#include <geos/geom/MultiPolygon.h>

using maps::wiki::validator::categories::REGION;
using maps::wiki::validator::categories::AD;
using maps::wiki::validator::categories::AD_FC;
using maps::wiki::validator::categories::AD_EL;
using maps::wiki::validator::categories::AD_NEUTRAL;
using maps::wiki::validator::categories::AD_NEUTRAL_FC;
using maps::wiki::validator::categories::AD_NEUTRAL_EL;

namespace maps {
namespace wiki {
namespace validator {
namespace checks {

namespace {

const double MAX_MERCATOR_X = 20037508.343; //180 degrees
const double MAX_MERCATOR_Y = 18764656.23; //84 degrees
const double THRESHOLD = 2.0;

geolib3::BoundingBox ALL_MAP_BB(
    geolib3::Point2(-MAX_MERCATOR_X + THRESHOLD, -MAX_MERCATOR_Y + THRESHOLD),
    geolib3::Point2(MAX_MERCATOR_X - THRESHOLD, MAX_MERCATOR_Y - THRESHOLD));

boost::optional<geolib3::LinearRing2> faceLinearRing(
    const utils::FaceBuilder& faceBuilder)
{
    if (faceBuilder.valid()) {
        auto points = faceBuilder.points();
        if (points.size() >= 4) {
            try {
                return geolib3::LinearRing2(points);
            } catch (const maps::Exception&) {
                // validation failed
            }
        }
    }

    return boost::none;
}

template<typename CompoundObjectCategory>
std::vector<geolib3::Polygon2> contourObjectPolygon(CheckContext* context, TId id)
{
    typedef typename utils::FaceCategory<CompoundObjectCategory>::type FaceCategory;
    typedef typename utils::ElementCategory<FaceCategory>::type ElementCategory;

    std::vector<geolib3::LinearRing2> exterionRings;
    std::vector<geolib3::LinearRing2> interiorRings;

    auto viewFace = context->objects<FaceCategory>();
    auto viewElement = context->objects<ElementCategory>();

    const auto* object = context->objects<CompoundObjectCategory>().byId(id);
    for (auto faceId : object->faces()) {
        const auto* face = viewFace.byId(faceId);
        utils::FaceBuilder faceBuilder(face, viewElement);
        auto faceGeom = faceLinearRing(faceBuilder);
        if (faceGeom) {
            if (face->isInterior()) {
                interiorRings.push_back(std::move(*faceGeom));
            } else {
                exterionRings.push_back(std::move(*faceGeom));
            }
        }
    }

    return geom_tools::PolygonBuilder::build(
        exterionRings, interiorRings, geom_tools::ValidateResult::Yes);
}

std::vector<geolib3::Polygon2> computeDifference(
    const geolib3::Polygon2& worldPolygon,
    const geolib3::Polygon2& allRegionsPolygon)
{
    auto allRegionsPolygonGeos = geolib3::internal::geolib2geosGeometry(allRegionsPolygon);
    auto worldPolygonGeos = geolib3::internal::geolib2geosGeometry(worldPolygon);

    std::unique_ptr<geos::geom::Geometry> difference(worldPolygonGeos->difference(allRegionsPolygonGeos.get()));
    if (!difference) {
        return {};
    }

    std::vector<geolib3::Polygon2> result;

    auto geometryTypeId = difference->getGeometryTypeId();
    if (geometryTypeId == geos::geom::GEOS_POLYGON) {
        const auto* polygon = dynamic_cast<const geos::geom::Polygon*>(difference.get());
        result.push_back(geolib3::internal::geos2geolibGeometry(polygon));

    } else if (geometryTypeId == geos::geom::GEOS_MULTIPOLYGON ||
            geometryTypeId == geos::geom::GEOS_GEOMETRYCOLLECTION) {
        const auto* collection = dynamic_cast<const geos::geom::GeometryCollection*>(difference.get());
        for (size_t i = 0; i < collection->getNumGeometries(); i++) {
            const auto* part = dynamic_cast<const geos::geom::Polygon*>(collection->getGeometryN(i));
            if (part) {
                result.push_back(geolib3::internal::geos2geolibGeometry(part));
            }
        }
    }

    return result;
}

} // namespace

VALIDATOR_SIMPLE_CHECK( region_geometry, REGION, AD, AD_FC, AD_EL, AD_NEUTRAL, AD_NEUTRAL_FC, AD_NEUTRAL_EL )
{
    std::vector<TId> allRegionIds;
    std::vector<geolib3::Polygon2> allPolygons;

    context->objects<REGION>().visit([&](const Region* region) {
        allRegionIds.push_back(region->id());

        for (auto admUnitId : region->admUnits()) {
            auto polygons = contourObjectPolygon<AD>(context, admUnitId);

            allPolygons.insert(allPolygons.end(),
                std::make_move_iterator(polygons.begin()),
                std::make_move_iterator(polygons.end()));
        }

        for (auto adNeutralId : region->adNeutrals()) {
            auto polygons = contourObjectPolygon<AD_NEUTRAL>(context, adNeutralId);

            allPolygons.insert(allPolygons.end(),
                std::make_move_iterator(polygons.begin()),
                std::make_move_iterator(polygons.end()));
        }
    });

    auto allRegionsMultiPolygon = geolib3::unitePolygons(allPolygons);

    if (allRegionsMultiPolygon.polygonsNumber() == 0) {
        context->critical(
            "all-regions-geometry-is-empty",
            boost::none,
            allRegionIds);
        return;
    } else if (allRegionsMultiPolygon.polygonsNumber() > 1) {
        for (size_t i = 0; i < allRegionsMultiPolygon.polygonsNumber(); i++) {
            context->critical(
                "all-regions-geometry-has-isolated-part",
                allRegionsMultiPolygon.polygonAt(i),
                allRegionIds);
        }
        return;
    }

    auto allRegionsPolygon = allRegionsMultiPolygon.polygonAt(0);

    if (allRegionsPolygon.interiorRingsNumber() > 0) {
        for (size_t i = 0; i < allRegionsPolygon.interiorRingsNumber(); i++) {
            context->critical(
                "all-regions-geometry-has-hole",
                geolib3::Polygon2(allRegionsPolygon.interiorRingAt(i), {}),
                allRegionIds);
        }
    }

    auto worldPolygon = ALL_MAP_BB.polygon();

    if (!spatialRelation(allRegionsPolygon, worldPolygon, geolib3::SpatialRelation::Contains)) {
        auto difference = computeDifference(worldPolygon, allRegionsPolygon);

        if (difference.empty()) {
            context->critical(
                "all-regions-geometry-does-not-cover-entire-map",
                boost::none,
                allRegionIds);
            return;
        }
        for (const auto& polygon : difference) {
            context->critical(
                "all-regions-geometry-does-not-cover-entire-map",
                polygon,
                allRegionIds);
        }
    }
}

} // namespace checks
} // namespace validator
} // namespace wiki
} // namespace maps
