#include "tools.h"

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

#include <boost/algorithm/string.hpp>

namespace maps::mrc::tools {
namespace {

struct TolokaAnswer {
    std::string url;
    geolib3::BoundingBox bbox;
    std::string houseNumber;
};

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;
    }
}

auto parseClassificationResult(const json::Value& val)
{
    std::vector<TolokaAnswer> result;
    for (const auto& item : val) {
        auto inputValues = item["inputValues"];
        auto url = inputValues["image"].as<std::string>();
        auto bbox = geolib3::BoundingBox{
            geolib3::Point2(inputValues["bbox"][0][0].as<float>(),
                inputValues["bbox"][0][1].as<float>()),
            geolib3::Point2(inputValues["bbox"][1][0].as<float>(),
                inputValues["bbox"][1][1].as<float>())};
        std::string houseNumber;
        auto outputValues = item["outputValues"];
        if (outputValues.isObject() && outputValues.hasField("result")) {
            auto result = outputValues["result"];
            if (result.isString()) {
                houseNumber = result.as<std::string>();
            }
        }
        result.push_back({url, bbox, houseNumber});
    }
    return result;
}

/// Merge Toloka results for a single bbox received from multiple users.
template <class TolokaAnswerRange>
std::string aggregateHouseNumber(const TolokaAnswerRange& answers)
{
    // Moore’s Voting Algorithm
    std::string result;
    long count = 0;
    for (const auto& answer : answers) {
        if (count == 0) {
            result = answer.houseNumber;
            count = 1;
        } else if (result == answer.houseNumber) {
            ++count;
        } else {
            --count;
        }
    }

    auto first = std::begin(answers);
    auto last = std::end(answers);
    count = std::count_if(first, last, [result](const auto& answer) {
        return answer.houseNumber == result;
    });
    return count > std::distance(first, last) / 2 ? result : std::string{};
}

} // anonymous namespace

UrlToHouseNumbersMap aggregateHouseNumbers(const json::Value& val)
{
    UrlToHouseNumbersMap result;
    auto answers = parseClassificationResult(val);
    forEqualRanges(answers,
        [](const auto& lhs, const auto& rhs) {
            return std::make_tuple(lhs.url, lhs.bbox.minX(), lhs.bbox.maxX(),
                       lhs.bbox.minY(), lhs.bbox.maxY())
                < std::make_tuple(rhs.url, rhs.bbox.minX(), rhs.bbox.maxX(),
                      rhs.bbox.minY(), rhs.bbox.maxY());
        },
        [&](const auto& rng) {
            const auto& front = *std::begin(rng);
            result[front.url][front.bbox] = aggregateHouseNumber(rng);
        });
    return result;
}

void dump(const UrlToHouseNumbersMap& urlToHouseNumbersMap,
    std::ostream& os)
{
    json::Builder builder(os);
    builder << [&](json::ObjectBuilder b) {
        b["images_with_objects"] << [&](json::ArrayBuilder b) {
            for (const auto& urlToHouseNumbers : urlToHouseNumbersMap) {
                const auto& url = urlToHouseNumbers.first;
                const auto& bboxToHouseNumberMap = urlToHouseNumbers.second;

                b << [&](json::ObjectBuilder b) {
                    b["source"] = url;
                    b["objects"] << [&](json::ArrayBuilder b) {
                        for (const auto& bboxToHouseNumber :
                            bboxToHouseNumberMap) {
                            const auto& bbox = bboxToHouseNumber.first;
                            const auto& houseNumber
                                = bboxToHouseNumber.second;
                            b << [&](json::ObjectBuilder b) {
                                b["num"] = houseNumber;
                                b["type"] = "house_number_sign";
                                b["bbox"] << [&](json::ArrayBuilder b) {
                                    b << [&](json::ArrayBuilder b) {
                                        b << round(bbox.minX())
                                          << round(bbox.minY());
                                    };
                                    b << [&](json::ArrayBuilder b) {
                                        b << round(bbox.maxX())
                                          << round(bbox.maxY());
                                    };
                                };
                            };
                        }
                    };
                };
            }
        };
    };
}

} // maps::mrc::tools
