#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/impl/utility.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/coverage_rtree_reader.h>

#include <boost/format.hpp>
#include <boost/range/adaptor/map.hpp>

namespace maps::mrc::fb {

EdgeIdIterator::EdgeIdIterator(common::JoinedRangesIterator<IdIteratorType> idIterator)
    : idIterator_(idIterator)
{}

bool EdgeIdIterator::equal(const EdgeIdIterator& other) const
{
    return idIterator_ == other.idIterator_;
}

road_graph::EdgeId EdgeIdIterator::dereference() const
{
    return road_graph::EdgeId(*idIterator_);
}

void EdgeIdIterator::increment()
{
    ++idIterator_;
}

CoverageRtreeReader::CoverageRtreeReader(const road_graph::Graph& graph,
                                         const std::string& dir,
                                         EMappingMode mappingMode)
    : graph_(graph)
{
    for (db::TFc fc = FC_MIN; fc <= FC_MAX; ++fc) {
        rtrees_.emplace_back(
            dir + "/" + (boost::format(COVERAGE_RTREE_FILE) % fc).str(),
            mappingMode);
    }
    for (db::TFc fc = FC_MIN; fc <= FC_MAX; ++fc) {
        REQUIRE(graph_.version() == rtree(fc).version(),
                "inconsistent versions of graph ("
                    << graph_.version() << ") and coverage rtree fc " << fc
                    << " (" << rtree(fc).version() << ")");
    }
}

std::string_view CoverageRtreeReader::version() const
{
    return graph_.version();
}

const fb_rtree::Rtree& CoverageRtreeReader::rtree(db::TFc fc) const
{
    REQUIRE(fc >= FC_MIN && fc <= FC_MAX, "invalid FC " << fc);
    return rtrees_.at(fc - FC_MIN);
}


TIteratorRange<EdgeIdIterator>
CoverageRtreeReader::getCoveredEdgeIdsByBbox(
        const geolib3::BoundingBox& geoBbox,
        std::optional<db::TFc> maxFc) const
{
    std::vector<EdgeIdIterator::IdIteratorRangeType> ranges;
    auto filter = [&](const geolib3::BoundingBox& box, fb_rtree::Id id) {
        auto edgeId = road_graph::EdgeId(id);
        auto edgeData = graph_.edgeData(edgeId);
        auto polyline = geolib3::Polyline2(edgeData.geometry());
        return geolib3::spatialRelation(
            box, polyline.boundingBox(), geolib3::Intersects);
    };

    for (db::TFc fc = FC_MIN; fc <= FC_MAX; ++fc) {
        if (maxFc.has_value() && fc > maxFc.value()) {
            break;
        }
        ranges.push_back(rtree(fc).allIdsInWindow(geoBbox, filter));
    }
    return MakeIteratorRange(
        EdgeIdIterator(
            common::JoinedRangesIterator<EdgeIdIterator::IdIteratorType>(std::move(ranges))),
        EdgeIdIterator());
}

TIteratorRange<EdgeIdIterator>
CoverageRtreeReader::getCoveredEdgeIds() const
{
    std::vector<EdgeIdIterator::IdIteratorRangeType> ranges;
    for (db::TFc fc = FC_MIN; fc <= FC_MAX; ++fc) {
        ranges.push_back(rtree(fc).allIds());
    }
    return MakeIteratorRange(
        EdgeIdIterator(
            common::JoinedRangesIterator<EdgeIdIterator::IdIteratorType>(std::move(ranges))),
        EdgeIdIterator());
}

std::vector<road_graph::EdgeId> CoverageRtreeReader::getNearestCoveredEdgeIds(
    const geolib3::Point2& geoPos,
    size_t limit,
    db::TFc maxFc) const
{
    auto distanceToBox = fb_rtree::SquaredDistance{};
    auto distanceToId = [&](const geolib3::Point2& point, fb_rtree::Id id) {
        auto edgeId = road_graph::EdgeId(id);
        auto edgeData = graph_.edgeData(edgeId);
        auto polyline = geolib3::Polyline2(edgeData.geometry());
        return distanceToBox(point, polyline.boundingBox());
    };

    auto ids = std::vector<fb_rtree::Id>{};
    for (db::TFc fc = FC_MIN; fc <= FC_MAX; ++fc) {
        if (fc > maxFc) {
            break;
        }
        for (fb_rtree::Id id : fb_rtree::nearestIds(
                 rtree(fc), geoPos, distanceToBox, distanceToId, limit)) {
            ids.push_back(id);
        }
    }

    using TDistanceToEdgeId = std::pair<double, road_graph::EdgeId>;
    auto result = std::vector<TDistanceToEdgeId>{};
    for (fb_rtree::Id id : ids) {
        result.push_back({distanceToId(geoPos, id), road_graph::EdgeId(id)});
    }
    std::sort(result.begin(), result.end());
    if (result.size() > limit) {
        result.resize(limit);
    }
    auto rng = result | boost::adaptors::map_values;
    return {rng.begin(), rng.end()};
}

}  // namespace maps::mrc::fb
