#include "find_garbage.h"
#include "tools.h"

#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/parallel_for_each.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/visibility.h>
#include <maps/libs/common/include/make_batches.h>

#include <boost/geometry.hpp>
#include <boost/geometry/geometries/point_xy.hpp>
#include <boost/geometry/index/rtree.hpp>
#include <boost/range/adaptor/filtered.hpp>

#include <map>

namespace bg = boost::geometry;

namespace maps {
namespace mrc {
namespace features_deleter {
namespace {

using BPoint = bg::model::d2::point_xy<double>;
using BBox = bg::model::box<BPoint>;

BPoint toBoost(const geolib3::Point2& point)
{
    return {point.x(), point.y()};
}

struct BIndexable {
    using result_type = BPoint;

    BPoint operator()(const db::Feature& feature) const
    {
        return toBoost(feature.geodeticPos());
    }
};

using BFeatureRTree
    = bg::index::rtree<db::Feature, bg::index::quadratic<16>, BIndexable>;

BFeatureRTree loadFeatureRTree(Context& ctx)
{
    static constexpr size_t BATCH_SIZE = 100000;
    db::TIds featureIds = ctx.loadFeaturesIds();
    INFO() << "loaded " << featureIds.size() << " feature ids";
    std::sort(featureIds.begin(), featureIds.end());

    db::Features features;
    features.reserve(featureIds.size());
    auto batches = maps::common::makeBatches(featureIds, BATCH_SIZE);
    common::parallelForEach(batches.begin(), batches.end(),
        [&](std::mutex& guard, const auto& batch) {
            auto range
                = ctx.loadFeatures(
                    db::table::Feature::isPublished
                    && db::table::Feature::id.greaterOrEquals(*batch.begin())
                    && db::table::Feature::id.lessOrEquals(*std::prev(batch.end())));
            std::lock_guard<std::mutex> lock{guard};
            features.insert(features.end(), range.begin(), range.end());
            INFO() << "loaded " << features.size() << " features";
        });
    return {features.begin(), features.end()};
}

bool isBad(const db::Feature& feature)
{
    static constexpr double BAD_QUALITY = 0.05;
    return feature.hasQuality() && feature.quality() < BAD_QUALITY
        // Walk features with low quality are not considered 'bad'
        && !db::isStandalonePhotosDataset(feature.dataset());
}

bool isGood(const db::Feature& feature)
{
    static constexpr double GOOD_QUALITY = 0.5;
    return feature.hasQuality() && feature.quality() > GOOD_QUALITY;
}

using FeatureIdToEdgeIdMMap = std::unordered_multimap<db::TId, db::TId>;

template <typename Features>
FeatureIdToEdgeIdMMap makeBadFeatureToEdgeMMap(Context& ctx,
                                               const Features& features)
{
    FeatureIdToEdgeIdMMap result;
    auto bads = features | ba::filtered(isBad);
    common::parallelForEach(bads.begin(), bads.end(),
        [&](std::mutex& guard, const db::Feature& feature) {
            FeatureIdToEdgeIdMMap subResult;
            auto edges = ctx.graph().edgesInWindow(
                db::addVisibilityMargins(feature.geodeticPos().boundingBox()));
            for (const auto& edge : edges) {
                if (db::isVisible(feature, edge.geom)) {
                    subResult.insert({feature.id(), edge.edgeId});
                }
            }

            if (!subResult.empty()) {
                std::lock_guard<std::mutex> lock{guard};
                result.insert(subResult.begin(), subResult.end());
            }
        });
    return result;
}

db::Features selectNearestFeatures(const BFeatureRTree& features,
                                   const Edge& edge)
{
    db::Features result;
    auto window = db::addVisibilityMargins(edge.geom.boundingBox());
    features.query(bg::index::intersects(BBox{toBoost(window.lowerCorner()),
                                              toBoost(window.upperCorner())}),
                   std::back_inserter(result));
    return result;
}

template <typename Features>
db::Segments getUncovered(const Features& features, const Edge& edge)
{
    db::Segments result{edge.geom.segments().begin(),
                        edge.geom.segments().end()};
    for (const auto& feature : features) {
        auto cuttings = db::getUncovered(feature.geodeticPos(),
                                         db::direction(feature),
                                         db::CameraDeviation::Front,
                                         result);
        result = std::move(cuttings);
        if (result.empty()) {
            break;
        }
    }
    return result;
}

} // anonymous namespace

std::set<db::TId> findGarbageFeatureIds(Context& ctx)
{
    auto features = loadFeatureRTree(ctx);
    auto badFeatureToEdgeMMap = makeBadFeatureToEdgeMMap(ctx, features);
    auto edgeIds = mapValues(badFeatureToEdgeMMap);
    INFO() << "found " << edgeIds.size() << " edges to check";
    common::parallelForEach(edgeIds.begin(), edgeIds.end(),
        [&](std::mutex& guard, const db::TId& edgeId) {
            auto edge = ctx.graph().edgeById(edgeId);
            auto nearestFeatures = selectNearestFeatures(features, edge);
            auto uncovered
                = getUncovered(nearestFeatures | ba::filtered(isGood), edge);

            for (const auto& feature : nearestFeatures | ba::filtered(isBad)) {
                if (db::isVisible(feature, uncovered)) {
                    std::lock_guard<std::mutex> lock{guard};
                    badFeatureToEdgeMMap.erase(feature.id());
                }
            }
        });
    auto result = mapKeys(badFeatureToEdgeMMap);
    INFO() << "found " << result.size() << " features to delete";
    return result;
}

} // features_deleter
} // mrc
} // maps
