#include <maps/wikimap/mapspro/services/mrc/eye/lib/generate_lane_hypothesis/include/generator.h>

#include <maps/wikimap/mapspro/services/mrc/eye/lib/generate_hypothesis/include/load.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/generate_hypothesis/include/graph.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/generate_hypothesis/include/common.h>

#include <maps/wikimap/mapspro/services/mrc/eye/lib/location/include/rotation.h>

#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/misc.h>

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/hypothesis_gateway.h>

#include <boost/algorithm/cxx11/any_of.hpp>

namespace maps::mrc::eye {

namespace {

bool isLaneRoadMarking(const db::eye::SignAttrs& attrs) {
    static const std::vector<traffic_signs::TrafficSign> ROAD_MARKING = {
        traffic_signs::TrafficSign::RoadMarkingLaneDirectionF,
        traffic_signs::TrafficSign::RoadMarkingLaneDirectionR,
        traffic_signs::TrafficSign::RoadMarkingLaneDirectionL,
        traffic_signs::TrafficSign::RoadMarkingLaneDirectionFR,
        traffic_signs::TrafficSign::RoadMarkingLaneDirectionFL,
        traffic_signs::TrafficSign::RoadMarkingLaneDirectionRL,
    };

    return wiki::common::isIn(attrs.type, ROAD_MARKING);
}

bool isLaneSign(const db::eye::SignAttrs& attrs) {
    static const std::vector<traffic_signs::TrafficSign> SIGNS = {
        traffic_signs::TrafficSign::PrescriptionLaneDirectionF,
        traffic_signs::TrafficSign::PrescriptionLaneDirectionFr,
        traffic_signs::TrafficSign::PrescriptionLaneDirectionR,
        traffic_signs::TrafficSign::PrescriptionLaneDirectionFR,
        traffic_signs::TrafficSign::PrescriptionLaneDirectionFl,
        traffic_signs::TrafficSign::PrescriptionLaneDirectionL,
        traffic_signs::TrafficSign::PrescriptionLaneDirectionFL,
        traffic_signs::TrafficSign::PrescriptionLanesDirection,
    };

    return wiki::common::isIn(attrs.type, SIGNS);
}

db::eye::Hypothesis generateLaneHypothesis(
    const db::eye::ObjectLocation& location,
    const object::RoadElement& element)
{
    db::eye::LaneHypothesisAttrs attrs{element.revisionId()};

    return db::eye::Hypothesis(location.mercatorPos(), attrs);
}

bool matchToLaneRoadMarking(
    const wiki::common::Lane& lane,
    const db::eye::SignAttrs& attrs)
{
    REQUIRE(isLaneRoadMarking(attrs), "Object is not lane road marking!");

    static const auto LEFT_DIRECTIONS = {
        wiki::common::LaneDirection::Left45,
        wiki::common::LaneDirection::Left90,
        wiki::common::LaneDirection::Left135,
        wiki::common::LaneDirection::Left180
    };

    static const auto RIGHT_DIRECTIONS = {
        wiki::common::LaneDirection::Right45,
        wiki::common::LaneDirection::Right90,
        wiki::common::LaneDirection::Right135,
        wiki::common::LaneDirection::Right180
    };

    auto contains = [&](auto direction) {
        return lane.contains(direction);
    };

    const bool forward = contains(wiki::common::LaneDirection::StraightAhead);
    const bool left = boost::algorithm::any_of(LEFT_DIRECTIONS, contains);
    const bool right = boost::algorithm::any_of(RIGHT_DIRECTIONS, contains);

    switch (attrs.type) {
        case traffic_signs::TrafficSign::RoadMarkingLaneDirectionF: // 1.18.1
            return !left && !right && forward;
        case traffic_signs::TrafficSign::RoadMarkingLaneDirectionR: // 1.18.2
            return !left && right && !forward;
        case traffic_signs::TrafficSign::RoadMarkingLaneDirectionL: // 1.18.3
            return left && !right && !forward;
        case traffic_signs::TrafficSign::RoadMarkingLaneDirectionFR: // 1.18.4
            return !left && right && forward;
        case traffic_signs::TrafficSign::RoadMarkingLaneDirectionFL: // 1.18.5
            return left && !right && forward;
        case traffic_signs::TrafficSign::RoadMarkingLaneDirectionRL: // 1.18.6
            return left && right && !forward;
        default:
            return false;
    }
}

template<typename... Args>
bool match(const wiki::common::Lane& lane, Args ...directions)
{
    if (lane.kind() != wiki::common::LaneKind::Auto) {
        return false;
    }
    const size_t directionsId = (0 | ... | static_cast<size_t>(directions));
    return lane.directionsId() == directionsId;
}

bool matchToLaneSign(
    const object::RoadElement& element,
    const wiki::common::Lane& lane,
    const db::eye::SignAttrs& attrs)
{
    switch (attrs.type) {
        case traffic_signs::TrafficSign::PrescriptionLaneDirectionF: // 5.15.2_forward
            return match(lane, wiki::common::LaneDirection::StraightAhead);
        case traffic_signs::TrafficSign::PrescriptionLaneDirectionFr: // 5.15.2__right_45
            return match(lane, wiki::common::LaneDirection::Right45);
        case traffic_signs::TrafficSign::PrescriptionLaneDirectionR: // 5.15.2_right
            return match(lane, wiki::common::LaneDirection::Right90);
        case traffic_signs::TrafficSign::PrescriptionLaneDirectionFR: // 5.15.2_forward_right
            return match(lane, wiki::common::LaneDirection::StraightAhead, wiki::common::LaneDirection::Right90);
        case traffic_signs::TrafficSign::PrescriptionLaneDirectionFl: // 5.15.2_left_45
            return match(lane, wiki::common::LaneDirection::Left45) or (
                element.fow() == object::RoadElement::FormOfWay::TwoWayRoad
                    and match(lane, wiki::common::LaneDirection::Left45, wiki::common::LaneDirection::Left180)
            );
        case traffic_signs::TrafficSign::PrescriptionLaneDirectionL: // 5.15.2_left
            return match(lane, wiki::common::LaneDirection::Left90) or (
                element.fow() == object::RoadElement::FormOfWay::TwoWayRoad
                    and match(lane, wiki::common::LaneDirection::Left90, wiki::common::LaneDirection::Left180)
            );
        case traffic_signs::TrafficSign::PrescriptionLaneDirectionFL: // 5.15.2_forward_left
            return match(lane, wiki::common::LaneDirection::StraightAhead, wiki::common::LaneDirection::Left90) or (
                element.fow() == object::RoadElement::FormOfWay::TwoWayRoad and match(lane,
                    wiki::common::LaneDirection::StraightAhead,
                    wiki::common::LaneDirection::Left90,
                    wiki::common::LaneDirection::Left180
                )
            );
        case traffic_signs::TrafficSign::PrescriptionLanesDirection: // 5.15.1
            return lane.kind() == wiki::common::LaneKind::Auto; // Any lane is okey
        default:
            return false;
    }
}

bool roadElementFilter(const object::RoadElement& element) {
  return not element.underConstruction()
        && element.fc() <= object::RoadElement::FunctionalClass::MinorRoad;
}

std::unordered_multimap<traffic_signs::TrafficSign, traffic_signs::TrafficSign> generateLaneOrRoadMarkingMap()
{
    const std::unordered_map<traffic_signs::TrafficSign, traffic_signs::TrafficSign> signToRoadMark {
        {traffic_signs::TrafficSign::PrescriptionLaneDirectionF, traffic_signs::TrafficSign::RoadMarkingLaneDirectionF},
        {traffic_signs::TrafficSign::PrescriptionLaneDirectionFr, traffic_signs::TrafficSign::RoadMarkingLaneDirectionR},
        {traffic_signs::TrafficSign::PrescriptionLaneDirectionR, traffic_signs::TrafficSign::RoadMarkingLaneDirectionR},
        {traffic_signs::TrafficSign::PrescriptionLaneDirectionFR, traffic_signs::TrafficSign::RoadMarkingLaneDirectionFR},
        {traffic_signs::TrafficSign::PrescriptionLaneDirectionFl, traffic_signs::TrafficSign::RoadMarkingLaneDirectionL},
        {traffic_signs::TrafficSign::PrescriptionLaneDirectionL, traffic_signs::TrafficSign::RoadMarkingLaneDirectionL},
        {traffic_signs::TrafficSign::PrescriptionLaneDirectionFL, traffic_signs::TrafficSign::RoadMarkingLaneDirectionFL},
    };

    std::unordered_multimap<traffic_signs::TrafficSign, traffic_signs::TrafficSign> result;
    for (const auto& [sign, roadMark]: signToRoadMark) {
        result.emplace(sign, roadMark);
        result.emplace(roadMark, sign);
    }

    return result;
}

bool areEqualLaneSignOrRoadMarkingTypes(
    traffic_signs::TrafficSign lhs,
    traffic_signs::TrafficSign rhs)
{
    static const auto mapping = generateLaneOrRoadMarkingMap();

    if (lhs == rhs) {
        return true;
    }

    const auto range = mapping.equal_range(lhs);
    return boost::algorithm::any_of(
        range.first, range.second,
        [&](const auto& pair) {
            return pair.second == rhs;
        }
    );
}

} // namespace

bool LaneHypothesisGeneratorImpl::appliesToObject(const db::eye::Object& object)
{
    db::eye::SignAttrs attrs = object.attrs<db::eye::SignAttrs>();
    return !attrs.temporary && (isLaneSign(attrs) || isLaneRoadMarking(attrs));
}

bool LaneHypothesisGeneratorImpl::hasDuplicate(
    pqxx::transaction_base& txn,
    const db::eye::Hypothesis& hypothesis,
    db::TId objectId)
{
    constexpr double LANE_SEARCH_RADIUS = 100;

    const geolib3::BoundingBox searchBox = makeSearchBox(hypothesis.mercatorPos(), LANE_SEARCH_RADIUS);

    db::eye::Hypotheses hypotheses = db::eye::HypothesisGateway(txn).load(
        db::eye::table::Hypothesis::position.intersects(searchBox) &&
        db::eye::table::Hypothesis::type.equals(hypothesis.type())
    );

    db::eye::LaneHypothesisAttrs attrs = hypothesis.attrs<db::eye::LaneHypothesisAttrs>();
    hypotheses.erase(
        std::remove_if(hypotheses.begin(), hypotheses.end(),
            [&](const db::eye::Hypothesis& dbHypothesis) {
                db::eye::LaneHypothesisAttrs dbAttrs = dbHypothesis.attrs<db::eye::LaneHypothesisAttrs>();
                return attrs.roadElementRevisionId != dbAttrs.roadElementRevisionId;
            }
        ),
        hypotheses.end()
    );

    if (hypotheses.empty()) {
        return false;
    }

    db::eye::Object object = db::eye::ObjectGateway(txn).loadById(objectId);

    db::TIds hypothesisIds;
    for (const auto& hypothesis: hypotheses) {
        hypothesisIds.push_back(hypothesis.id());
    }

    const db::eye::HypothesisObjects hypothesisObjects
        = db::eye::HypothesisObjectGateway(txn).load(
            db::eye::table::HypothesisObject::hypothesisId.in(hypothesisIds)
        );

    db::TIds objectIds;
    for (const auto& hypothesisObject : hypothesisObjects) {
        objectIds.push_back(hypothesisObject.objectId());
    }

    const db::eye::Objects objects = db::eye::ObjectGateway(txn).loadByIds(objectIds);

    if (objects.empty()) {
        return false;
    }

    traffic_signs::TrafficSign signType = object.attrs<db::eye::SignAttrs>().type;
    return boost::algorithm::any_of(objects,
        [&](const db::eye::Object& other) {
            db::eye::SignAttrs attrs = other.attrs<db::eye::SignAttrs>();
            return areEqualLaneSignOrRoadMarkingTypes(signType, attrs.type);
        }
    );
}

db::eye::Hypotheses LaneHypothesisGeneratorImpl::validate(
    const db::eye::Object& object,
    const db::eye::ObjectLocation& location,
    const db::eye::Objects& /*slaveObjects*/,
    object::Loader& loader)
{
    const db::eye::SignAttrs attrs = object.attrs<db::eye::SignAttrs>();

    const bool laneRoadMarking = isLaneRoadMarking(attrs);
    const bool laneSign = isLaneSign(attrs);

    if (!laneRoadMarking && !laneSign) {
        return {};
    }

    std::optional<object::RoadElement> badElement = std::nullopt;
    double minDistance = 0;
    Graph graph = loadGraph(loader, location.mercatorPos(), roadElementFilter, 120);

    for (const auto& startNodeId: graph.getClosestCodirectional(location)) {
        Path path{startNodeId};
        for (OptionalNodeId nodeId = startNodeId; nodeId; ) {
            const RoadCross roadCross = graph.getRoadCross(path.back());
            if (roadCross.isSimple()) {
                nodeId = tryPushForwardMove(path, roadCross);
                continue;
            }
            const graph::DirectedId directedId = graph.getDirectedId(*nodeId);
            const auto& element = graph.elementByDirectedId(directedId);
            const bool match = boost::algorithm::any_of(
                element.lanes(directedId.direction()),
                [&](const auto& lane) {
                    if (laneRoadMarking) {
                        return matchToLaneRoadMarking(lane, attrs);
                    }
                    return matchToLaneSign(element, lane, attrs);
                }
            );
            // Generate only for important roads
            const auto& start = graph.elementByNodeId(path.front());
            if (not match and start.fc() <= object::RoadElement::FunctionalClass::LocalRoad) {
                const double distance = geolib3::distance(location.mercatorPos(), start.geom());
                if (not badElement or distance < minDistance) {
                    badElement = element;
                    minDistance = distance;
                }
                break;
            }
            return {};
        }
    }

    if (badElement) {
        return {generateLaneHypothesis(location, *badElement)};
    }

    return {};
}

} // namespace maps::mrc::eye
