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

#include "../poi/poi_categories.h"
#include "../utils/category_traits.h"
#include "../utils/face_builder.h"
#include "../utils/face_checks.h"
#include "../utils/object_elements_within_aoi.h"

#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/polygon.h>

#include <functional>
#include <list>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <mutex>

using maps::wiki::validator::categories::HYDRO;
using maps::wiki::validator::categories::HYDRO_FC;
using maps::wiki::validator::categories::HYDRO_FC_EL;
using maps::wiki::validator::categories::ADDR;
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_ROADNET;
using maps::wiki::validator::categories::URBAN_ROADNET_FC;
using maps::wiki::validator::categories::URBAN_ROADNET_EL;
using maps::wiki::validator::categories::URBAN_AREAL;
using maps::wiki::validator::categories::URBAN_ROADNET_AREAL;

namespace maps {
namespace geolib3 {

// Helpers for range-for
typename StaticGeometrySearcher<
        Polygon2, const wiki::validator::Face*>::Iterator begin(
    const typename StaticGeometrySearcher<
        Polygon2, const wiki::validator::Face*>::SearchResult& searchResult)
{ return searchResult.first; }

typename StaticGeometrySearcher<
        Polygon2, const wiki::validator::Face*>::Iterator end(
    const typename StaticGeometrySearcher<
        Polygon2, const wiki::validator::Face*>::SearchResult& searchResult)
{ return searchResult.second; }

} // namespace geolib3

namespace wiki {
namespace validator {
namespace checks {

namespace gl = maps::geolib3;

namespace {

// -------------------------------------------------------------------------------------------------

struct FloodingInfo
{
    TId hydroFaceId;
    gl::Point2 point;
};

class ReportData {
public:
    bool isIgnored(TId id) const;

    void insert(TId id, FloodingInfo&& floodingInfo);
    void insertFrom(ReportData&& rdata);

    void setIgnore(TId id);

    const std::unordered_map<TId, FloodingInfo>& report() const
    {
        return floodingInfos_;
    }

private:
    mutable std::mutex mutex_;

    std::unordered_map<TId, FloodingInfo> floodingInfos_;
    std::unordered_set<TId> ignoredIds_;
};

bool ReportData::isIgnored(TId id) const
{
    std::lock_guard<std::mutex> g(mutex_);
    return ignoredIds_.count(id) > 0;
}

void ReportData::insert(TId id, FloodingInfo&& floodingInfo)
{
    std::lock_guard<std::mutex> g(mutex_);
    if (ignoredIds_.count(id) > 0) {
        return;
    }

    floodingInfos_.insert(std::make_pair(id, std::move(floodingInfo)));
}

void ReportData::setIgnore(TId id)
{
    std::lock_guard<std::mutex> g(mutex_);

    floodingInfos_.erase(id);
    ignoredIds_.insert(id);
}

void ReportData::insertFrom(ReportData&& other)
{
    std::lock_guard<std::mutex> g(mutex_);

    for (auto& val : other.floodingInfos_) {
        floodingInfos_.insert(
            std::make_pair(
                val.first,
                std::move(val.second)
            )
        );
    }
}

// -------------------------------------------------------------------------------------------------

const size_t VISIT_BATCH_SIZE = 1000;
const size_t VISIT_BBOX_BATCH_SIZE = 1000;

const double HEURISTIC_BUFFER_SIZE = 400; // mercator meters

const TFeatureType SPORT_FEATURE_TYPE = 191;
const TFeatureType SQUARE_FEATURE_TYPE = 241;

bool urbanFtTypeFilter(const TFeatureType type)
{ return type == SPORT_FEATURE_TYPE; };

bool urbanRoadnetFtTypeFilter(const TFeatureType type)
{ return type == SQUARE_FEATURE_TYPE; };

template<typename TLhsGeom, typename TRhsGeom>
gl::Point2 geomForReport(const TLhsGeom& lhs, const TRhsGeom& rhs)
{
    auto bbox = gl::intersection(lhs.boundingBox(), rhs.boundingBox());
    ASSERT(bbox);
    return bbox->center();
}

template<class FeatureCategory>
class FaceSearcher
{
public:
    typedef std::function<bool(TFeatureType)> FeatureTypePredicate;
    typedef gl::StaticGeometrySearcher<gl::Polygon2, const Face*>::SearchResult
        SearchResult;

    FaceSearcher(CheckContext* context, FeatureTypePredicate featureTypeFilter)
    {
        typedef typename utils::FaceCategory<FeatureCategory>::type
            FaceCategory;
        typedef typename utils::ElementCategory<FaceCategory>::type
            ElementCategory;

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

        context->objects<FeatureCategory>().visit([&](
                const typename FeatureCategory::TObject* feature)
        {
            if (!featureTypeFilter(feature->featureType())
                    || !utils::objectFacesWithinAoi<FeatureCategory>(
                        context, feature)) {
                // object is not interesting or not fully loaded, skipping
                return;
            }

            std::list<gl::Polygon2> faceGeometries;
            std::vector<std::pair<const gl::Polygon2*, const Face*>>
                searcherInitData;

            for (TId faceId : feature->faces()) {
                const Face* face = viewFace.byId(faceId);
                utils::FaceBuilder faceBuilder(
                    face,
                    viewElement);
                auto faceGeom = utils::facePolygon(faceBuilder);

                if (!faceGeom) {
                    // object has invalid faces, skipping
                    return;
                }

                faceGeometries.push_back(*faceGeom);
                searcherInitData.emplace_back(&faceGeometries.back(), face);
            }

            this->geometryStore_.splice(
                std::end(this->geometryStore_),
                std::move(faceGeometries));
            for (const auto& faceGeometryPair : searcherInitData) {
                this->searcher_.insert(
                    faceGeometryPair.first,
                    faceGeometryPair.second);
            }
        });
        searcher_.build();
    }

    const SearchResult find(const gl::BoundingBox& searchBox) const
    { return searcher_.find(searchBox); }

private:
    gl::StaticGeometrySearcher<gl::Polygon2, const Face*> searcher_;
    std::list<gl::Polygon2> geometryStore_;
};

// See description for checkFeaturesFlooding
template<class Category>
void checkObjectsFlooding(
        CheckContext* context,
        const Face* hydroFace, const gl::Polygon2& hydroFaceGeom,
        std::function<bool(const typename Category::TObject*)> objectFilter,
        ReportData* reportData)
{
    ReportData faceReportData;

    auto viewObjects = context->objects<Category>();

    auto visitor = [&](const typename Category::TObject* object) {
        const bool shouldIgnore = faceReportData.isIgnored(object->id())
                || reportData->isIgnored(object->id())
                || !objectFilter(object);

        if (shouldIgnore) {
            return;
        }

        auto bufferBbox = gl::resizeByValue(
                object->geom().boundingBox(),
                HEURISTIC_BUFFER_SIZE);
        if (gl::spatialRelation(bufferBbox, hydroFaceGeom, gl::Disjoint)) {
            for (const auto* object_
                     : viewObjects.byBbox(bufferBbox)) {
                if (gl::spatialRelation(
                        bufferBbox,
                        object_->geom().boundingBox(),
                        gl::Contains)
                    && objectFilter(object_)) {
                    faceReportData.setIgnore(object_->id());
                }
            }
            return;
        }

        if (hydroFace->isInterior()) {
            if (gl::spatialRelation(hydroFaceGeom, object->geom(),
                    gl::Contains)) {
                reportData->setIgnore(object->id());
            }
        } else {
            if (gl::spatialRelation(hydroFaceGeom, object->geom(),
                    gl::Intersects)) {
                reportData->insert(
                    object->id(),
                    FloodingInfo{
                        hydroFace->id(),
                        geomForReport(hydroFaceGeom, object->geom())
                    }
                );
            }
        }
    };

    viewObjects.batchVisitObjects(
        viewObjects.byBbox(hydroFaceGeom.boundingBox()),
        visitor,
        VISIT_BBOX_BATCH_SIZE
    );
}

/*
Searches for intersections between provided hydro object face and features
of FeatureCategory.

Objects' geometries are assumed to be correct, i.e. all internal faces of
an object are contained in its external faces and there are no faces within
internal faces.

Provided reportData and ignoredIds are assumed to be local for current hydro
object.

We process all feature faces intersecting current hydro face by bboxes.

First, heuristics to reduce the number of heavy geometry relation
calculations for hydro faces with large bboxes are applied.
We relate hydro face with feature face bbox resized by HEURISTIC_BUFFER_SIZE
in each direction. If they are disjoint, we add all feature faces lying
within this bbox in the ignore list for current face.

Next, there are four cases:
  * both faces are interior
  Has no significance, skip.

  * hydro interior, feature exterior
  If feature face is within hydro face, then feature face cannot have
  intersections with the hydro object. Add it to the ignore list for current
  object and clean up report data for the object.

  * hydro exterior, feature interior
  If hydro face is within feature face, then hydro face cannot have
  intersections with the feature object. As we don't iterate over feature
  objects, we add all of feature object faces to the ignore list for
  current face and remove them from corresponding report data.

  * both faces exterior
  If faces intersect, mark it as a flooding in the report data local for
  current face.

Finally, we dump all confirmed floodings to the object's report data.

Code for objects flooding is similar, objects are treated as features with
a single external face.
*/
template<class FeatureCategory>
void checkFeaturesFlooding(
        CheckContext* context,
        const Face* hydroFace, const gl::Polygon2& hydroFaceGeom,
        const FaceSearcher<FeatureCategory>& featureFaceSearcher,
        ReportData* reportData)
{
    ReportData faceReportData;

    for (const auto& featureFaceDatum :
            featureFaceSearcher.find(hydroFaceGeom.boundingBox())) {
        const Face* featureFace = featureFaceDatum.value();
        const auto& featureFaceGeom = featureFaceDatum.geometry();

        if (faceReportData.isIgnored(featureFace->id())
                || reportData->isIgnored(featureFace->id())) {
            continue;
        }

        auto bufferBbox = gl::resizeByValue(
                featureFaceGeom.boundingBox(),
                HEURISTIC_BUFFER_SIZE);
        if (gl::spatialRelation(bufferBbox, hydroFaceGeom, gl::Disjoint)) {
            for (const auto& featureFaceDatum_
                     : featureFaceSearcher.find(bufferBbox)) {
                if (gl::spatialRelation(
                        bufferBbox,
                        featureFaceDatum.geometry().boundingBox(),
                        gl::Contains)) {
                    faceReportData.setIgnore(featureFaceDatum_.value()->id());
                }
            }
            continue;
        }

        if (hydroFace->isInterior()) {
            if (featureFace->isInterior()) {
                // both interior
            } else {
                // hydro interior, feature exterior
                if (gl::spatialRelation(hydroFaceGeom, featureFaceGeom,
                        gl::Contains)) {
                    reportData->setIgnore(featureFace->id());
                }
            }
        } else {
            if (featureFace->isInterior()) {
                // hydro exterior, feature interior
                if (gl::spatialRelation(hydroFaceGeom, featureFaceGeom,
                        gl::Within)) {
                    const auto* feature =
                        context->objects<FeatureCategory>().byId(
                            featureFace->parent());
                    for (TId featureFaceId : feature->faces()) {
                        faceReportData.setIgnore(featureFaceId);
                    }
                }
            } else {
                // both exterior
                if (gl::spatialRelation(hydroFaceGeom, featureFaceGeom,
                        gl::Intersects)) {
                    faceReportData.insert(
                        featureFace->id(),
                        FloodingInfo{
                            hydroFace->id(),
                            geomForReport(hydroFaceGeom, featureFaceGeom)
                        }
                    );
                }
            }
        }
    }

    reportData->insertFrom(std::move(faceReportData));
}

template<class CategoryName>
bool performPoiFloodingCheck(
    CheckContext* context,
    const Face* hydroFace, const gl::Polygon2& hydroFaceGeom,
    ReportData* reportData)
{
    checkObjectsFlooding<CategoryName>(
                context,
                hydroFace, hydroFaceGeom,
                [](const Poi*) { return true; },
                reportData);
    return true;
}

template<class... Categories>
void performPoiFloodingChecks(
    CheckContext* context,
    const Face* hydroFace, const gl::Polygon2& hydroFaceGeom,
    ReportData* reportData)
{
    auto performed = {
        performPoiFloodingCheck<Categories>(
            context,
            hydroFace, hydroFaceGeom,
            reportData)...
    };
    (void)performed;
}

} // namespace

VALIDATOR_SIMPLE_CHECK( flooding,
        HYDRO, HYDRO_FC, HYDRO_FC_EL,
        ADDR, BLD,
        URBAN, URBAN_FC, URBAN_EL,
        URBAN_AREAL,
        URBAN_ROADNET, URBAN_ROADNET_FC, URBAN_ROADNET_EL,
        URBAN_ROADNET_AREAL,
        POI_CATEGORIES)
{
    FaceSearcher<URBAN> urbanFaceSearcher(context, urbanFtTypeFilter);
    FaceSearcher<URBAN_ROADNET> roadnetFaceSearcher(context, urbanRoadnetFtTypeFilter);

    auto viewHydroFc = context->objects<HYDRO_FC>();
    auto viewHydroFcEl = context->objects<HYDRO_FC_EL>();

    context->objects<HYDRO>().batchVisit([&](const ContourFeature* hydro)
    {
        if (!utils::objectFacesWithinAoi<HYDRO>(context, hydro)) {
            return;
        }

        ReportData reportData;

        for (TId hydroFaceId : hydro->faces()) {
            const Face* hydroFace = viewHydroFc.byId(hydroFaceId);
            utils::FaceBuilder faceBuilder(
                hydroFace,
                viewHydroFcEl);
            auto hydroFaceGeom = utils::facePolygon(faceBuilder);

            if (!hydroFaceGeom) {
                // hydro object has invalid faces, skipping
                return;
            }

            checkObjectsFlooding<ADDR>(
                context,
                hydroFace, *hydroFaceGeom,
                [](const AddressPoint*) { return true; },
                &reportData);
            checkObjectsFlooding<BLD>(
                context,
                hydroFace, *hydroFaceGeom,
                [](const Building*) { return true; },
                &reportData);

            checkFeaturesFlooding<URBAN>(
                context,
                hydroFace, *hydroFaceGeom,
                urbanFaceSearcher,
                &reportData);
            checkFeaturesFlooding<URBAN_ROADNET>(
                context,
                hydroFace, *hydroFaceGeom,
                roadnetFaceSearcher,
                &reportData);

            checkObjectsFlooding<URBAN_AREAL>(
                context,
                hydroFace, *hydroFaceGeom,
                [](const PolygonFeature* o) { return urbanFtTypeFilter(o->featureType()); },
                &reportData);
            checkObjectsFlooding<URBAN_ROADNET_AREAL>(
                context,
                hydroFace, *hydroFaceGeom,
                [](const PolygonFeature* o) { return urbanRoadnetFtTypeFilter(o->featureType()); },
                &reportData);
            performPoiFloodingChecks<POI_CATEGORIES>(
                context,
                hydroFace,
                *hydroFaceGeom,
                &reportData);
        }

        for (const auto& idFloodingInfoPair : reportData.report()) {
            context->error(
                "flooding",
                idFloodingInfoPair.second.point,
                { idFloodingInfoPair.first,
                  idFloodingInfoPair.second.hydroFaceId });
        }
    }, VISIT_BATCH_SIZE);
}

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