#include "module.h"
#include "../utils/geom.h"
#include "../utils/misc.h"
#include "../utils/face_builder.h"
#include "../utils/face_checks.h"
#include "../utils/object_elements_within_aoi.h"

#include <yandex/maps/wiki/validator/categories.h>
#include <yandex/maps/wiki/validator/check.h>
#include <yandex/maps/wiki/validator/common.h>

#include <maps/libs/common/include/exception.h>
#include <yandex/maps/wiki/common/misc.h>

#include <maps/libs/geolib/include/linear_ring.h>
#include <maps/libs/geolib/include/polygon.h>

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

#include <boost/optional.hpp>

#include <cmath>
#include <vector>
#include <memory>
#include <algorithm>

using maps::wiki::validator::categories::BLD;
using maps::wiki::validator::categories::URBAN;
using maps::wiki::validator::categories::URBAN_FC;
using maps::wiki::validator::categories::URBAN_EL;
using maps::wiki::validator::categories::URBAN_AREAL;

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

namespace gl = maps::geolib3;

namespace {

const double URBAN_BUFFER_SIZE = 2.0; //meters

const size_t URBAN_BATCH_SIZE = 100;
const size_t BUILDING_BATCH_SIZE = 1000;

const TFeatureType RESIDENTIAL_FEATURE_TYPE = 101;
const TFeatureType INDUSTRIAL_FEATURE_TYPE = 102;

typedef std::vector<gl::Polygon2> PolygonsVector;

bool isCheckingFtType(TFeatureType ftType)
{
    return common::isIn(
            ftType, {RESIDENTIAL_FEATURE_TYPE, INDUSTRIAL_FEATURE_TYPE});
}

template<class UrbanObject, class BldObject>
void checkFtType(
    CheckContext* context,
    const UrbanObject* urban,
    const BldObject* building)
{
    if (urban->featureType() != building->featureType()
            && isCheckingFtType(urban->featureType())
            && isCheckingFtType(building->featureType())) {
        context->error(
                "bld-urban-different-ft_type",
                building->geom(),
                {urban->id(), building->id()});
    }
}


template <class FaceCategory, class EdgeCategory>
boost::optional<gl::Polygon2> buildFace(
    CheckContext* context,
    const typename FaceCategory::TObject* face)
{
    if (utils::objectElementsWithinAoi<FaceCategory>(context, face)) {
        auto viewEdge = context->objects<EdgeCategory>();

        utils::FaceBuilder faceBuilder(face, viewEdge);
        return utils::facePolygon(faceBuilder);
    }
    return boost::none;
}


template <class FaceCategory, class EdgeCategory>
PolygonsVector buildPolygons(
    CheckContext* context,
    const std::vector<TId>& faces)
{
    auto viewFace = context->objects<FaceCategory>();

    PolygonsVector exteriorFaces;
    PolygonsVector interiorFaces;
    for (const TId& faceId : faces) {
        if (!viewFace.loaded(faceId)) {
            //possibly invalid interior face
            return PolygonsVector();
        }
        const auto& face = viewFace.byId(faceId);
        auto faceGeom = buildFace<FaceCategory, EdgeCategory>(context, face);
        if (!faceGeom && face->isInterior()) {
            //invalid interior face
            return PolygonsVector();
        }
        if (faceGeom) {
            auto& faces = (face->isInterior() ? interiorFaces : exteriorFaces);
            faces.push_back(*faceGeom);
        }
    }

    PolygonsVector polygons;
    for (const auto& exteriorFace : exteriorFaces) {
        std::vector<gl::LinearRing2> interiorRings;
        for (const auto& interiorFace : interiorFaces) {
            if (utils::contains(exteriorFace, interiorFace)) {
                interiorRings.push_back(interiorFace.exteriorRing());
            }
        }
        try {
            polygons.emplace_back(exteriorFace.exteriorRing(), interiorRings);
        } catch (const maps::Exception&) {
            //Polygon2 validation failed
        }
    }
    return polygons;
}


boost::optional<gl::Polygon2> buffer(
    const gl::Polygon2& polygon,
    double bufferSize)
{
    const std::shared_ptr<const geos::geom::Polygon> geosGeom =
            gl::internal::geolib2geosGeometry(polygon);
    std::unique_ptr<geos::geom::Geometry> geosGeomWithBuffer(
            geosGeom->buffer(bufferSize));
    auto geosPolygonWithBuffer = dynamic_cast<geos::geom::Polygon*>(
            geosGeomWithBuffer.get());
    if (geosPolygonWithBuffer != nullptr) {
        try {
            return gl::internal::geos2geolibGeometry(geosPolygonWithBuffer);
        } catch (const maps::Exception&) {
            //Polygon2 validation failed
        }
    }
    return boost::none;
}


template <class BuildingCategory, class UrbanCategory>
void checkUrban(
    CheckContext* context,
    const gl::Polygon2& urbanGeom,
    const typename UrbanCategory::TObject* urban)
{
    double bufferSize = URBAN_BUFFER_SIZE /
            utils::mercatorDistanceRatio(urbanGeom);
    auto urbanGeomWithBuffer = buffer(urbanGeom, bufferSize);
    const auto& buildings = context->objects<BuildingCategory>().byBbox(
            urbanGeom.boundingBox());
    context->objects<BuildingCategory>().batchVisitObjects(
        buildings,
        [&](const Building* building) {
            if (utils::contains(urbanGeom, building->geom())) {
                checkFtType(context, urban, building);
            } else if (utils::intersects(urbanGeom, building->geom())) {
                if (urbanGeomWithBuffer && utils::contains(
                            *urbanGeomWithBuffer, building->geom())) {
                    checkFtType(context, urban, building);
                    context->warning(
                            "bld-urban-border-intersection",
                            building->geom(),
                            {urban->id(), building->id()});
                } else {
                    context->error(
                            "bld-urban-border-intersection",
                            building->geom(),
                            {urban->id(), building->id()});

                }
            }
        }, BUILDING_BATCH_SIZE);
}


} // namespace

VALIDATOR_CHECK_PART( bld_urban_relations, contour_urban,
        BLD, URBAN, URBAN_FC, URBAN_EL )
{
    context->objects<URBAN>().batchVisit(
            [&](const ContourFeature* urban)
    {
        PolygonsVector urbanGeoms = buildPolygons<URBAN_FC, URBAN_EL>(
                context, urban->faces());
        for (const gl::Polygon2& urbanGeom : urbanGeoms) {
            checkUrban<BLD, URBAN>(context, urbanGeom, urban);
        }
    }, URBAN_BATCH_SIZE);
}

VALIDATOR_CHECK_PART( bld_urban_relations, areal_urban, BLD, URBAN_AREAL )
{
    context->objects<URBAN_AREAL>().batchVisit(
            [&](const PolygonFeature* urban)
    {
        checkUrban<BLD, URBAN_AREAL>(context, urban->geom(), urban);
    }, URBAN_BATCH_SIZE);
}

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