#include <maps/wikimap/mapspro/services/mrc/libs/toloka_common/include/merge_rects.h>

#include <maps/libs/geolib/include/intersection.h>

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

#include <cmath>
#include <unordered_set>

namespace maps::mrc::toloka::common {
namespace {

template <class Range>
auto median(Range& rng)
{
    auto first = std::begin(rng);
    auto mid = first + std::size(rng) / 2;
    auto last = std::end(rng);
    std::nth_element(first, mid, last);
    return *mid;
}

template <class Range, class Compare, class Functor>
void forEqualRanges(Range& rng, Compare cmp, Functor f)
{
    auto first = std::begin(rng);
    auto last = std::end(rng);
    std::sort(first, last, cmp);
    while (first != last) {
        auto next = std::adjacent_find(first, last, cmp);
        if (next != last)
            next = std::next(next);
        f(boost::make_iterator_range(first, next));
        first = next;
    }
}

bool significant(const Rect& rect)
{
    constexpr size_t MAX_SIDE_SIZE_THRESHOLD_PX = 20;
    constexpr size_t MIN_SIDE_SIZE_THRESHOLD_PX = 10;
    const auto maxSide = std::max(rect.width(), rect.height());
    const auto minSide = std::min(rect.width(), rect.height());
    return maxSide >= MAX_SIDE_SIZE_THRESHOLD_PX
        && minSide >= MIN_SIDE_SIZE_THRESHOLD_PX;
}

/**
 * Returns true if \p lhs overlaps with \p rhs with relative difference by
 * each coordinate not more than 30%.
 */
bool intersected(const Rect& lhs, const Rect& rhs)
{
    constexpr int THRESH_DIFF_PIXELS = 8;
    constexpr double MAX_RELATIVE_COORD_DIFF = 0.3;
    auto intersection = geolib3::intersection(lhs, rhs);
    if (!intersection) {
        return false;
    }
    double width = std::max(lhs.width(), rhs.width());
    double height = std::max(lhs.height(), rhs.height());
    double dxMin = std::fabs(lhs.minX() - rhs.minX());
    double dxMax = std::fabs(lhs.maxX() - rhs.maxX());
    double dyMin = std::fabs(lhs.minY() - rhs.minY());
    double dyMax = std::fabs(lhs.maxY() - rhs.maxY());
    if ((dxMin > THRESH_DIFF_PIXELS
            && dxMin / width > MAX_RELATIVE_COORD_DIFF)
        || (dxMax > THRESH_DIFF_PIXELS
               && dxMax / width > MAX_RELATIVE_COORD_DIFF)
        || (dyMin > THRESH_DIFF_PIXELS
               && dyMin / height > MAX_RELATIVE_COORD_DIFF)
        || (dyMax > THRESH_DIFF_PIXELS
               && dyMax / height > MAX_RELATIVE_COORD_DIFF)) {
        return false;
    }
    return true;
}

template <class RectRange>
bool intersectedByAll(const Rect& rect, const RectRange& rects)
{
    return std::all_of(std::begin(rects), std::end(rects),
        [&rect](const auto& item) { return intersected(rect, item); });
}

template <class RectRange>
bool intersectedByAny(const Rect& rect, const RectRange& rects)
{
    return std::any_of(std::begin(rects), std::end(rects),
        [&rect](const auto& item) { return intersected(rect, item); });
}

using AssignmentToRectMap = std::unordered_map<std::string, Rect>;

/// Group rectangles marked by different users according to intersections
template <class TolokaAnswerRange>
auto groupByIntersection(const TolokaAnswerRange& answers)
{
    std::vector<AssignmentToRectMap> result;
    for (const auto& answer : answers) {
        // Try to intersect every new rectangle with every group
        for (const auto& rect : answer.rects) {
            bool matched = false;
            for (auto& group : result) {
                if (!group.count(answer.assignment)
                    && intersectedByAll(
                           rect, group | boost::adaptors::map_values)) {
                    group.insert({answer.assignment, rect});
                    matched = true;
                    break;
                }
            }
            // Create new group if rectangle did not match any existing group
            if (!matched) {
                result.push_back({{answer.assignment, rect}});
            }
        }
    }
    return result;
}

/**
 * Each coordinate (left, right, top, bottom) of the resulting rectangle
 * is calculated as the median of the corresponding coordinates
 * of all rectangles in the group.
 */
template <class RectRange>
Rect unionRects(const RectRange& rects)
{
    std::vector<double> minX, maxX, minY, maxY;
    for (const auto& item : rects) {
        minX.push_back(item.minX());
        maxX.push_back(item.maxX());
        minY.push_back(item.minY());
        maxY.push_back(item.maxY());
    }
    return rect(median(minX), median(minY), median(maxX), median(maxY));
}

/// Merge Toloka results for a single photo received from multiple users
template <class TolokaAnswerRange>
std::vector<Rect> aggregatePhoto(const TolokaAnswerRange& photoAnswers)
{
    std::vector<Rect> result;
    auto groups = groupByIntersection(photoAnswers);
    auto votes = std::size(photoAnswers);
    auto majority
        = [votes](const auto& group) { return group.size() > votes / 2; };
    for (const auto& group : groups | boost::adaptors::filtered(majority)) {
        result.push_back(unionRects(group | boost::adaptors::map_values));
    }
    return result;
}

// Approve/reject user assignment
template <class TolokaAnswerRange>
PrecisionAndRecall aggregateAssignment(
    const TolokaAnswerRange& assignmentAnswers,
    const PhotoToRectsMap& aggregatedPhotos)
{
    PrecisionAndRecall result;
    for (const auto& answer : assignmentAnswers) {
        std::vector<Rect> selectedRects;
        std::copy_if(answer.rects.begin(), answer.rects.end(),
            std::back_inserter(selectedRects), significant);
        result.selected += selectedRects.size();

        std::vector<Rect> relevantRects;
        if (aggregatedPhotos.count(answer.photo)) {
            const auto& rects = aggregatedPhotos.at(answer.photo);
            std::copy_if(std::begin(rects), std::end(rects),
                std::back_inserter(relevantRects), significant);
        }
        result.relevant += relevantRects.size();

        result.truePositive += std::count_if(selectedRects.begin(),
            selectedRects.end(), [&](const auto& rect) {
                return intersectedByAny(rect, relevantRects);
            });
    }
    return result;
}

} // anonymous namespace

std::vector<Rect> aggregatePhoto(const std::vector<TolokaAnswer>& photoAnswers)
{
    return aggregatePhoto(
        boost::make_iterator_range(photoAnswers.begin(), photoAnswers.end())
    );
}

PhotoToRectsMap aggregatePhotos(std::vector<TolokaAnswer>& answers)
{
    PhotoToRectsMap result;
    forEqualRanges(answers,
        [](const auto& lhs, const auto& rhs) {
            return lhs.photo < rhs.photo;
        },
        [&](const auto& rng) {
            result.insert({std::begin(rng)->photo, aggregatePhoto(rng)});
        });
    return result;
}

AssignmentToPrecisionAndRecallMap aggregateAssignments(
    std::vector<TolokaAnswer>& answers,
    const PhotoToRectsMap& aggregatedPhotos)
{
    AssignmentToPrecisionAndRecallMap result;
    forEqualRanges(answers,
        [](const auto& lhs, const auto& rhs) {
            return lhs.assignment < rhs.assignment;
        },
        [&](const auto& rng) {
            result.insert({std::begin(rng)->assignment,
                aggregateAssignment(rng, aggregatedPhotos)});
        });
    return result;
}

std::pair<float, float> toPair(const PrecisionAndRecall& precisionAndRecall)
{
    constexpr float EXTRA_SIGN_PENALTY = 0.2;
    if (!precisionAndRecall.selected && !precisionAndRecall.relevant) {
        // No signs detected and no signs submitted by user
        return {1, 1};
    }
    if (!precisionAndRecall.selected) {
        // Some signs detected, but no signs submitted by user
        return {1, 0};
    }
    if (!precisionAndRecall.relevant) {
        // No signs detected, but some signs submitted by user
        return {std::max<float>(
                    1 - precisionAndRecall.selected * EXTRA_SIGN_PENALTY, 0),
            1};
    }
    // Some signs detected and some signs submitted by user
    return {
        (float)precisionAndRecall.truePositive / precisionAndRecall.selected,
        (float)precisionAndRecall.truePositive / precisionAndRecall.relevant};
}

} // namespace maps::mrc::toloka::common
