#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/merge_candidates.h>

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_attrs.h>

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

namespace maps::mrc::eye {

namespace {

using AnyObjectAttrs = std::variant<
    db::eye::SignAttrs,
    db::eye::HouseNumberAttrs,
    db::eye::TrafficLightAttrs,
    db::eye::RoadMarkingAttrs
>;

AnyObjectAttrs getObjectAttrs(const db::eye::Object& object) {
    switch (object.type()) {
        case db::eye::ObjectType::Sign:
            return object.attrs<db::eye::SignAttrs>();
        case db::eye::ObjectType::HouseNumber:
            return object.attrs<db::eye::HouseNumberAttrs>();
        case db::eye::ObjectType::TrafficLight:
            return object.attrs<db::eye::TrafficLightAttrs>();
        case db::eye::ObjectType::RoadMarking:
            return object.attrs<db::eye::RoadMarkingAttrs>();
    };
}

bool match(
    const db::eye::HouseNumberAttrs& lhs,
    const db::eye::HouseNumberAttrs& rhs)
{
    return lhs.number == rhs.number;
}

bool match(
    const db::eye::SignAttrs& lhs,
    const db::eye::SignAttrs& rhs)
{
    return lhs.type == rhs.type
        && lhs.temporary == rhs.temporary;
}

constexpr bool match(
    const db::eye::TrafficLightAttrs&,
    const db::eye::TrafficLightAttrs&)
{
    return true;
}

bool match(
    const db::eye::RoadMarkingAttrs& lhs,
    const db::eye::RoadMarkingAttrs& rhs)
{
    return lhs.type == rhs.type;
}

// Предполагается, что attrs мы взяли у объекта того же типа, что и object
bool matchAttrs(
    const AnyObjectAttrs& attrs,
    const db::eye::Object& object)
{
    switch(object.type()) {
        case db::eye::ObjectType::Sign:
            return match(
                std::get<db::eye::SignAttrs>(attrs),
                object.attrs<db::eye::SignAttrs>()
            );
        case db::eye::ObjectType::HouseNumber:
            return match(
                std::get<db::eye::HouseNumberAttrs>(attrs),
                object.attrs<db::eye::HouseNumberAttrs>()
            );
        case db::eye::ObjectType::TrafficLight:
            return match(
                std::get<db::eye::TrafficLightAttrs>(attrs),
                object.attrs<db::eye::TrafficLightAttrs>()
            );
        case db::eye::ObjectType::RoadMarking:
            return match(
                std::get<db::eye::RoadMarkingAttrs>(attrs),
                object.attrs<db::eye::RoadMarkingAttrs>()
            );
    }
}

class ObjectSearcher {
public:
    ObjectSearcher(const std::vector<ObjectsInPassage>& objectsByPassages) {
        for (size_t passageIndx = 0; passageIndx < objectsByPassages.size(); passageIndx++) {
            const auto& objectsInPassage = objectsByPassages[passageIndx];
            for (const auto& [primaryId, location] : objectsInPassage.locationByPrimaryId) {
                searcher_.insert(
                    &location.mercatorPosition,
                    ObjectPassageIndx{primaryId, passageIndx}
                );
            }
        }
        searcher_.build();
    }

    std::vector<ObjectPassageIndx> find(const geolib3::Point2& pos, double radiusMerc) {
        const geolib3::BoundingBox searchBox(pos, 2 * radiusMerc, 2 * radiusMerc);

        const auto searchResult = searcher_.find(searchBox);

        std::vector<ObjectPassageIndx> result;
        for (auto it = searchResult.first; it != searchResult.second; it++) {
            result.push_back(it->value());
        }

        return result;
    }

private:
    // geolib3::Point2 - позиция объекта в меркаторе
    // ObjectPassageIndx - {primaryId, passageIndx}
    geolib3::StaticGeometrySearcher<geolib3::Point2, ObjectPassageIndx> searcher_;
};

} // namespace

std::vector<std::pair<ObjectPassageIndx, ObjectPassageIndx>>
generateMergeCandidates(
    const std::vector<ObjectsInPassage>& objectsInPassages,
    const MergeCandidatesParams& params)
{
    ObjectSearcher searcher(objectsInPassages);

    std::vector<std::pair<ObjectPassageIndx, ObjectPassageIndx>> mergeCandidates;

    for (size_t passageIndx1 = 0; passageIndx1 < objectsInPassages.size(); passageIndx1++) {
        for (const auto& [primaryId1, object1] : objectsInPassages[passageIndx1].objectByPrimaryId) {
            const auto& location1 = objectsInPassages[passageIndx1].locationByPrimaryId.at(primaryId1);
            const geolib3::Point2& position1 = location1.mercatorPosition;
            const auto [heading1, _, __] = decomposeRotation(location1.rotation);
            const double distance = geolib3::toMercatorUnits(params.distanceMeters, position1);
            const AnyObjectAttrs attrs1 = getObjectAttrs(object1);

            for (const auto& [primaryId2, passageIndx2] : searcher.find(position1, distance)) {
                if (passageIndx1 >= passageIndx2) {
                    continue;
                }

                const auto& object2 = objectsInPassages[passageIndx2].objectByPrimaryId.at(primaryId2);

                if (object1.type() != object2.type()) {
                    continue;
                }

                if (!matchAttrs(attrs1, object2)) {
                    continue;
                }

                const auto& location2 = objectsInPassages[passageIndx2].locationByPrimaryId.at(primaryId2);
                const geolib3::Point2 position2 = location2.mercatorPosition;
                const auto [heading2, _, __] = decomposeRotation(location2.rotation);

                if (geolib3::distance(position1, position2) < distance &&
                    geolib3::angleBetween(heading1, heading2) < params.angleEpsilon)
                {
                    mergeCandidates.emplace_back(
                        ObjectPassageIndx{primaryId1, passageIndx1},
                        ObjectPassageIndx{primaryId2, passageIndx2}
                    );
                }
            }
        }
    }

    return mergeCandidates;
}

} // namespace maps::mrc::eye
