#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/detection/include/filter_dwellplaces_by_blds.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/cover_by_cells.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/detection/include/state.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/yt_utils/include/op_wrapper.h>

#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>

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

#include <util/generic/size_literals.h>

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

namespace {

const double MAX_LATITUDE = 85.1;

double toRadians(double degrees) {
    return M_PI * degrees / 180.;
}

double getMercatorScale(double latitude) {
    //Conversion from meters to mercator meters
    //https:://en.wikipedia.org/wiki/Mercator_projection#Scale_factor
    return 1. / cos(toRadians(latitude));
}

using BuildingSearcher
    = geolib3::StaticGeometrySearcher<geolib3::Polygon2, size_t>;

class FilterDwellplacesByCellsReducer
    : public NYT::IReducer<NYT::TTableReader<NYT::TNode>,
                           NYT::TTableWriter<NYT::TNode>> {
public:
    FilterDwellplacesByCellsReducer() = default;
    FilterDwellplacesByCellsReducer(double distanceInMeters)
        : distanceInMeters_(distanceInMeters)
    {}

    Y_SAVELOAD_JOB(distanceInMeters_);

    void Do(NYT::TTableReader<NYT::TNode>* reader,
            NYT::TTableWriter<NYT::TNode>* writer) override {
        std::vector<Dwellplace> places;
        std::vector<geolib3::Polygon2> mercBlds;
        for (; reader->IsValid(); reader->Next()) {
            const auto& row = reader->GetRow();
            auto tableIndex = reader->GetTableIndex();
            if (0 == tableIndex) {
                places.emplace_back(Dwellplace::fromYTNode(row));
            } else {
                mercBlds.emplace_back(Building::fromYTNode(row).toMercatorGeom());
            }
        }

        if (places.empty()) {
            return;
        }

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

        for (const auto& place : places) {
            if (isDistantFromBuildings(place, searcher)) {
                writer->AddRow(place.toYTNode());
            }
        }
    }

private:
    geolib3::BoundingBox makeSearchRect(const Dwellplace& place) const {
        geolib3::Point2 geodeticPoint = place.toGeodeticGeom();
        return geolib3::convertGeodeticToMercator(
                geolib3::BoundingBox{
                    geolib3::fastGeoShift(geodeticPoint,
                                          {-distanceInMeters_,
                                           -distanceInMeters_}),
                    geolib3::fastGeoShift(geodeticPoint,
                                          {distanceInMeters_,
                                           distanceInMeters_}),
                }
            );
    }

    bool isDistantFromBuildings(
        const Dwellplace& place,
        const BuildingSearcher& searcher)
    {
        auto searchResult = searcher.find(makeSearchRect(place));
        // no buildings found
        return searchResult.first == searchResult.second;
    }

    double distanceInMeters_;
};

REGISTER_REDUCER(FilterDwellplacesByCellsReducer);

} // namespace

void filterDwellplacesByBuildings(
    NYT::IClientBasePtr client,
    const TString& inputYTTableName,
    const TString& bldsYTTableName,
    double distanceInMeters,
    const TString& outputYTTableName)
{
    double maxDistanceInMercator
        = distanceInMeters * getMercatorScale(MAX_LATITUDE);
    double cellSizeInMercator = 10 * maxDistanceInMercator;

    NYT::TTempTable directlyCellsTable = State::getTempTable(client);
    coverObjectsByCells<Dwellplace>(
        client,
        {inputYTTableName},
        DirectlyCellsCover(cellSizeInMercator),
        directlyCellsTable.Name()
    );

    NYT::TTempTable adjacentCellsTable = State::getTempTable(client);
    coverObjectsByCells<Building>(
        client,
        {bldsYTTableName},
        AdjacentCellsCover(cellSizeInMercator, maxDistanceInMercator),
        adjacentCellsTable.Name()
    );

    YTOpExecutor::Reduce(
        client,
        YTOpExecutor::ReduceSpec()
            .AddInput(directlyCellsTable.Name())
            .AddInput(adjacentCellsTable.Name())
            .AddOutput(outputYTTableName)
            .ReduceBy({Cell::X, Cell::Y}),
        new FilterDwellplacesByCellsReducer(distanceInMeters),
        YTOpExecutor::Options()
            .Title("[Buildings detector] Filtering dwellplaces by distance to buildings")
            .MemoryLimit(4_GB)
    );
}

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