#include <maps/libs/log8/include/log8.h>

#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/multipolygon.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>

#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/objects/include/area.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/objects/include/road.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/objects/include/building.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/objects/include/dwellplace.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/detection/include/filter_by_regions.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/yt_utils/include/op_wrapper.h>

#include <mapreduce/yt/interface/client.h>

namespace maps::wiki::autocart::pipeline {
namespace {

enum class FilterMode {
    COPY,
    MOVE
};

using BoundingBoxIndexSearcher
    = geolib3::StaticGeometrySearcher<geolib3::BoundingBox, size_t>;

template <class Object>
class FilterByRegionsMapper
    : public NYT::IMapper<NYT::TTableReader<NYT::TNode>,
                          NYT::TTableWriter<NYT::TNode>> {
public:
    FilterByRegionsMapper() = default;
    FilterByRegionsMapper(const geolib3::MultiPolygon2& regions, FilterMode mode)
        : regionsBytes_(geolib3::WKB::toBytes<geolib3::MultiPolygon2>(regions))
        , mode_(mode)
    {}

    Y_SAVELOAD_JOB(regionsBytes_, mode_);

    void Do(NYT::TTableReader<NYT::TNode>* reader,
                    NYT::TTableWriter<NYT::TNode>* writer) override {

        geolib3::MultiPolygon2 regions = geolib3::WKB::read<geolib3::MultiPolygon2>(regionsBytes_);
        std::vector<geolib3::BoundingBox> bboxes;
        for (size_t i = 0; i < regions.polygonsNumber(); i++) {
            bboxes.emplace_back(regions.polygonAt(i).boundingBox());
        }

        BoundingBoxIndexSearcher searcher;
        for (size_t i = 0; i < bboxes.size(); i++) {
            searcher.insert(&(bboxes[i]), i);
        }
        searcher.build();

        if (FilterMode::COPY == mode_) {
            for (; reader->IsValid(); reader->Next()) {
                const auto& row = reader->GetRow();
                Object object = Object::fromYTNode(row);
                if (isInRegions(object, regions, searcher)) {
                    writer->AddRow(row);
                }
            }
        } else if (FilterMode::MOVE == mode_) {
            for (; reader->IsValid(); reader->Next()) {
                const auto& row = reader->GetRow();
                Object object = Object::fromYTNode(row);
                if (isInRegions(object, regions, searcher)) {
                    writer->AddRow(row, 0);
                } else {
                    // zero table is table for extracted objects
                    size_t tableIndex = reader->GetTableIndex() + 1;
                    writer->AddRow(row, tableIndex);
                }
            }
        } else {
            WARN() << "Unsupported filter mode";
        }
    }

private:
    bool isInRegions(const Object& object,
                     const geolib3::MultiPolygon2& regions,
                     const BoundingBoxIndexSearcher& searcher) {
        typename Object::GeomType mercGeom;
        try {
            mercGeom = object.toMercatorGeom();
        } catch(const std::exception& e) {
            ERROR() << "Failed to convert geodetic to mercator";
            ERROR() << e.what();
            return false;
        }
        auto result = searcher.find(mercGeom.boundingBox());
        for (auto it = result.first; it != result.second; it++) {
            geolib3::Polygon2 region = regions.polygonAt(it->value());
            if (geolib3::spatialRelation(region, mercGeom,
                                         geolib3::SpatialRelation::Intersects)) {
                return true;
            }
        }
        return false;
    }

    std::vector<uint8_t> regionsBytes_;
    FilterMode mode_;
};

REGISTER_MAPPER(FilterByRegionsMapper<Area>);
REGISTER_MAPPER(FilterByRegionsMapper<Road>);
REGISTER_MAPPER(FilterByRegionsMapper<Building>);
REGISTER_MAPPER(FilterByRegionsMapper<Dwellplace>);

} // namespace

template <class Object>
void filterByRegions(
    NYT::IClientBasePtr client,
    const std::vector<TString>& inputYTTableNames,
    const geolib3::MultiPolygon2& regions,
    const TString& outputYTTableName)
{
    YTOpExecutor::MapSpec spec;
    for (const TString& inputYTTableName : inputYTTableNames) {
        spec.AddInput(inputYTTableName);
    }
    spec.AddOutput(outputYTTableName);
    YTOpExecutor::Map(
        client,
        spec,
        new FilterByRegionsMapper<Object>(regions, FilterMode::COPY),
        YTOpExecutor::Options()
            .Title("[Buildings detector] Filtering objects that are contained in regions")
    );
}

template void filterByRegions<Area>(
    NYT::IClientBasePtr client,
    const std::vector<TString>& inputYTTableNames,
    const geolib3::MultiPolygon2& regions,
    const TString& outputYTTableName);

template void filterByRegions<Road>(
    NYT::IClientBasePtr client,
    const std::vector<TString>& inputYTTableNames,
    const geolib3::MultiPolygon2& regions,
    const TString& outputYTTableName);

template void filterByRegions<Building>(
    NYT::IClientBasePtr client,
    const std::vector<TString>& inputYTTableNames,
    const geolib3::MultiPolygon2& regions,
    const TString& outputYTTableName);

template void filterByRegions<Dwellplace>(
    NYT::IClientBasePtr client,
    const std::vector<TString>& inputYTTableNames,
    const geolib3::MultiPolygon2& regions,
    const TString& outputYTTableName);


template <class Object>
void extractByRegions(
    NYT::IClientBasePtr client,
    const TString& inputYTTableName,
    const geolib3::MultiPolygon2& regions,
    const TString& outputYTTableName)
{
    YTOpExecutor::Map(
        client,
        YTOpExecutor::MapSpec()
            .AddInput(inputYTTableName)
            .AddOutput(outputYTTableName)
            .AddOutput(inputYTTableName),
        new FilterByRegionsMapper<Object>(regions, FilterMode::MOVE),
        YTOpExecutor::Options()
            .Title("[Buildings detector] Extracting objects that are contained in regions")
    );
}


template void extractByRegions<Area>(
    NYT::IClientBasePtr client,
    const TString& inputYTTableName,
    const geolib3::MultiPolygon2& regions,
    const TString& outputYTTableName);

template void extractByRegions<Road>(
    NYT::IClientBasePtr client,
    const TString& inputYTTableName,
    const geolib3::MultiPolygon2& regions,
    const TString& outputYTTableName);

template void extractByRegions<Building>(
    NYT::IClientBasePtr client,
    const TString& inputYTTableName,
    const geolib3::MultiPolygon2& regions,
    const TString& outputYTTableName);

template void extractByRegions<Dwellplace>(
    NYT::IClientBasePtr client,
    const TString& inputYTTableName,
    const geolib3::MultiPolygon2& regions,
    const TString& outputYTTableName);

} // namespace maps::wiki::autocart::pipeline
