#include <maps/wikimap/mapspro/services/mrc/eye/lib/generate_wrong_parking/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/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/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/direction.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/units_literals.h>

#include <algorithm>

using namespace maps::geolib3::literals;

namespace maps::mrc::eye {

namespace {

constexpr int NOT_FOUND = -1;

bool isParkingSign(const db::eye::SignAttrs& attrs) {
    static const std::vector<traffic_signs::TrafficSign> PARKING_SIGN_TYPES{
        traffic_signs::TrafficSign::InformationParking,
        traffic_signs::TrafficSign::ProhibitoryNoParking,
        traffic_signs::TrafficSign::ProhibitoryNoParkingOrStopping,
    };

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

bool isPaidServices(const db::eye::Objects& signs) {
    for (size_t i = 0; i < signs.size(); i++) {
        if (traffic_signs::TrafficSign::InformationPaidServices ==
            signs[i].attrs<db::eye::SignAttrs>().type)
        {
            return true;
        }
    }
    return false;
}

db::eye::Hypothesis generateAbsentParkingHypothesis(
    const db::eye::ObjectLocation& location,
    bool paidParking)
{
    db::eye::AbsentParkingAttrs attrs;
    attrs.isToll = paidParking;
    return db::eye::Hypothesis(location.mercatorPos(), attrs);
}

// db::eye::Hypothesis generateTollParkingHypothesis(
//     const db::eye::ObjectLocation& location,
//     const object::RevisionID& parkingObjectId)
// {
//     db::eye::AbsentParkingAttrs attrs{parkingObjectId, true};
//     return db::eye::Hypothesis(location.mercatorPos(), attrs);
// }

// db::eye::Hypothesis generateFreeParkingHypothesis(
//     const db::eye::ObjectLocation& location,
//     const object::RevisionID& parkingObjectId)
// {
//     db::eye::AbsentParkingAttrs attrs{parkingObjectId, false};
//     return db::eye::Hypothesis(location.mercatorPos(), attrs);
// }

db::eye::Hypothesis generateWrongParkingFtTypeHypothesis(
    const db::eye::ObjectLocation& location,
    const object::LinearParkingLot& lot)
{
    db::eye::WrongParkingFtTypeAttrs attrs{
        lot.revisionId(),
        ymapsdf::ft::Type::UrbanRoadnetParkingProhibited, lot.type()
    };

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

bool isCodirectional(const DirectedPoint& point, const object::LinearParkingLot& lot)
{
    constexpr auto MAX_ANGLE = geolib3::toRadians(45_deg);
    const auto direction = geolib3::Direction2(point.heading());
    const geolib3::Polyline2& geom = lot.geom();
    const size_t index = geom.closestPointSegmentIndex(point.mercator());
    const geolib3::Segment2 segment = geom.segmentAt(index);
    const geolib3::Direction2 segmentDirection(segment);
    // Linear parking lots are bidirectional in our data model
    return geolib3::angleBetween(segmentDirection, direction) < MAX_ANGLE
        || geolib3::angleBetween(segmentDirection, -direction) < MAX_ANGLE;
}

object::LinearParkingLots loadLinearParkingLots(
    const db::eye::ObjectLocation& location,
    object::Loader& loader)
{
    constexpr double LINEAR_PARKING_LOT_RADIUS_METERS = 30.0;

    const double maxDistance = geolib3::toMercatorUnits(
        LINEAR_PARKING_LOT_RADIUS_METERS,
        location.mercatorPos()
    );
    return load<object::LinearParkingLot>(
        loader,
        inArea(location.mercatorPos(), LINEAR_PARKING_LOT_RADIUS_METERS),
        [&](const auto& lot) {
            return isCodirectional(location, lot)
                && geolib3::distance(lot.geom(), location.mercatorPos()) < maxDistance;
        }
    );
}

object::ParkingLots loadParkingLots(
    const db::eye::ObjectLocation& location,
    object::Loader& loader)
{
    constexpr double PARKING_LOT_RADIUS_METERS = 70.0;
    const double maxDistance = geolib3::toMercatorUnits(
        PARKING_LOT_RADIUS_METERS,
        location.mercatorPos()
    );
    return load<object::ParkingLot>(
        loader,
        inArea(location.mercatorPos(), PARKING_LOT_RADIUS_METERS),
        [&](const auto& lot) {
            return geolib3::distance(lot.geom(), location.mercatorPos()) < maxDistance;
        }
    );
}

int findNearestLot(
    const db::eye::ObjectLocation& location,
    const object::ParkingLots& lots)
{
    int nearestLotIdx = NOT_FOUND;
    double minDistance = DBL_MAX;
    for (size_t i = 0; i < lots.size(); i++) {
        const double distance = geolib3::distance(lots[i].geom(), location.mercatorPos());
        if (distance < minDistance) {
            nearestLotIdx = (int)i;
            minDistance = distance;
        }
    }
    return nearestLotIdx;
}

int findNearestLot(
    const db::eye::ObjectLocation& location,
    const object::LinearParkingLots& lots)
{
    int nearestLotIdx = NOT_FOUND;
    double minDistance = DBL_MAX;
    for (size_t i = 0; i < lots.size(); i++) {
        const object::LinearParkingLot& lot = lots[i];
        if (lot.type() != ymapsdf::ft::Type::UrbanRoadnetParkingFree &&
            lot.type() != ymapsdf::ft::Type::UrbanRoadnetParkingToll) {
            continue;
        }
        const double distance = geolib3::distance(lot.geom(), location.mercatorPos());
        if (distance < minDistance) {
            nearestLotIdx = (int)i;
            minDistance = distance;
        }
    }
    return nearestLotIdx;
}

// db::eye::Hypotheses validateInformationParkingSign(
//     const db::eye::ObjectLocation& location,
//     const object::ParkingLot& lot,
//     bool paidParking)
// {
//     if (lot.isToll()) {
//         if (paidParking) {
//             return {};
//         }
//         return {generateFreeParkingHypothesis(location, lot.revisionId())};
//     }
//     if (paidParking) {
//         return {generateTollParkingHypothesis(location, lot.revisionId())};
//     }
//     return {};
// }

// db::eye::Hypotheses validateInformationParkingSign(
//     const db::eye::ObjectLocation& location,
//     const object::LinearParkingLot& lot,
//     bool paidParking)
// {
//     if (lot.type() == ymapsdf::ft::Type::UrbanRoadnetParkingToll) {
//         if (paidParking) {
//             return {};
//         }
//         return {generateFreeParkingHypothesis(location, lot.revisionId())};
//     }
//     if (paidParking) {
//         return {generateTollParkingHypothesis(location, lot.revisionId())};
//     }
//     return {};
// }

db::eye::Hypotheses validateInformationParkingSign(
    const db::eye::ObjectLocation& location,
    const object::ParkingLots& lots,
    const object::LinearParkingLots& linearLots,
    bool paidParking)
{
    /*
        Находим ближайшии парковки (линейную и нелинейную) к месту расположения знака
        если перковки не найденны шлем гипотезу об отсутствии
        иначе выбираем ближайшую к знаку и проверяем, что её платность совпадает с
        платностью знака
    */
    const int nearestLotIdx = findNearestLot(location, lots);
    const int nearestLinearLotIdx = findNearestLot(location, linearLots);

    /// Temporarily disable hupotheses about paid parkings because of high rate of
    /// false-positives https://st.yandex-team.ru/MAPSMRC-3732#6193668dc760336378fd549b
    /// The cause will be fixed in MAPSMRC-3739

    if (!paidParking &&(NOT_FOUND == nearestLotIdx) && (NOT_FOUND == nearestLinearLotIdx)) {
        return {generateAbsentParkingHypothesis(location, paidParking)};
    }
    return {};
}

db::eye::Hypotheses validateNoParkingSign(
        const db::eye::ObjectLocation& location,
        const object::LinearParkingLots& lots,
        double epsilonMeters)
{
    double minBadDistance = std::numeric_limits<double>::max();
    double minGoodDistance = std::numeric_limits<double>::max();
    const object::LinearParkingLot* badLot = nullptr;
    const object::LinearParkingLot* goodLot = nullptr;
    for (const auto& lot: lots) {
        const double distance = geolib3::distance(location.mercatorPos(), lot.geom());
        if (lot.type() == ymapsdf::ft::Type::UrbanRoadnetParkingProhibited) {
            if (distance < minGoodDistance) {
                minGoodDistance = distance;
                goodLot = std::addressof(lot);
            }
        } else {
            if (distance < minBadDistance) {
                minBadDistance = distance;
                badLot = std::addressof(lot);
            }
        }
    }
    if (!badLot) {
        return {};
    }

    const double epsilon = geolib3::toMercatorUnits(epsilonMeters, location.mercatorPos());
    if (goodLot && minGoodDistance < minBadDistance + epsilon) {
        return {};
    }

    return {generateWrongParkingFtTypeHypothesis(location, *badLot)};
}

} // namespace

bool WrongParkingGeneratorImpl::appliesToObject(const db::eye::Object& object)
{
    db::eye::SignAttrs attrs = object.attrs<db::eye::SignAttrs>();
    return ! attrs.temporary && isParkingSign(attrs);
}

bool WrongParkingGeneratorImpl::hasDuplicate(
    pqxx::transaction_base& txn,
    const db::eye::Hypothesis& hypothesis,
    db::TId /*objectId*/)
{
    constexpr double SEARCH_RADIUS_METERS = 30.;
    return hasDuplicateDefaultCheck(txn, hypothesis, SEARCH_RADIUS_METERS);
}

db::eye::Hypotheses WrongParkingGeneratorImpl::validate(
    const db::eye::Object& object,
    const db::eye::ObjectLocation& location,
    const db::eye::Objects& slaveObjects,
    object::Loader& loader)
{
    constexpr double LINEAR_PARKING_LOT_EPSILON_METERS = 15.0;

    if (object.attrs<db::eye::SignAttrs>().type == traffic_signs::TrafficSign::InformationParking) {
        return validateInformationParkingSign(
            location,
            loadParkingLots(location, loader),
            loadLinearParkingLots(location, loader),
            isPaidServices(slaveObjects)
        );
    }
    return validateNoParkingSign(location, loadLinearParkingLots(location, loader), LINEAR_PARKING_LOT_EPSILON_METERS);
}

} // namespace maps::mrc::eye
