#include "module.h"
#include "../utils/misc.h"
#include "../utils/road_utils.h"

#include <yandex/maps/wiki/common/misc.h>
#include <yandex/maps/wiki/validator/check.h>
#include <yandex/maps/wiki/validator/categories.h>

namespace maps {
namespace wiki {
namespace validator {
namespace checks {

using categories::RD_EL;
using categories::VEHICLE_RESTRICTION;

namespace {

const size_t VISIT_BATCH_SIZE = 10000;

const int MIN_SPEED_LIMIT = 5;
const int MAX_SPEED_LIMIT = 150;
const int MAX_SPEED_LIMIT_FC8 = 50;

const std::string RESIDENTIAL_UNIVERSAL_ID = "residential";

std::string fowSuffix(common::FOW fow) {
    return
        "-fow" +
        std::to_string(static_cast<std::underlying_type<common::FOW>::type>(fow));
}

struct RdElByVehicleRestrictionClass
{
    std::unordered_set<TId> residentialRdElIds;
    std::unordered_set<TId> restrictedEcoClassRdElIds;
    std::unordered_set<TId> restrictedRdElIds;
};

RdElByVehicleRestrictionClass
collectVehicleRestrictions(CheckContext* context)
{
    RdElByVehicleRestrictionClass result;
    context->objects<VEHICLE_RESTRICTION>().visit(
            [&](const VehicleRestriction* vr)
    {
        const auto& restrictedElements = vr->restrictsRoadElements();
        if (vr->universalId() == RESIDENTIAL_UNIVERSAL_ID) {
            result.residentialRdElIds.insert(restrictedElements.begin(), restrictedElements.end());
        } else if (
            vr->vehicleRestrictionParameters() &&
            vr->vehicleRestrictionParameters()->minEcoClass)
        {
            result.restrictedEcoClassRdElIds.insert(restrictedElements.begin(), restrictedElements.end());
        } else {
            result.restrictedRdElIds.insert(restrictedElements.begin(), restrictedElements.end());
        }
    });
    return result;
}

} // namespace

VALIDATOR_SIMPLE_CHECK( rd_el_ecoclass_and_restricted, RD_EL, VEHICLE_RESTRICTION )
{
    const auto restrictionClasses = collectVehicleRestrictions(context);
    context->objects<RD_EL>().batchVisit(
            [&](const RoadElement* element)
    {
        const auto fc = element->fc();
        const auto accessId = element->accessId();
        const bool restrictedForTrucks = restrictionClasses.restrictedRdElIds.count(element->id());
        const bool restrictedEcoClass = restrictionClasses.restrictedEcoClassRdElIds.count(element->id());

        if (!!(accessId & common::AccessId::Truck) &&
            common::isIn(fc, {1, 2, 3, 4, 5, 6, 7}) &&
            !(restrictedEcoClass && restrictedForTrucks))
        {
            context->critical(
                "rd-high-fc-with-trucks-access-missing-restrictions",
                utils::geomForReport(element),
                {element->id()}
            );
        }
    }, VISIT_BATCH_SIZE);
}

namespace {
void checkPavement(const RoadElement* element, CheckContext* context)
{
    if (element->paved()) {
        return;
    }
    if (element->structType() == common::StructType::Tunnel) {
        context->error("wrong-paved-tunnel",
            utils::geomForReport(element), {element->id()});
    }
    const auto fow = element->fow();
    const auto fc = element->fc();
    std::optional<Severity> severity;
    if (common::isIn(fow, {
        common::FOW::TwoCarriageway,
        common::FOW::Roundabout,
        common::FOW::AlternateRoad
        }))
    {
        severity = Severity::Error;
    } else if (fow == common::FOW::None && fc < 5) {
        severity = Severity::Critical;
    } else if (fow == common::FOW::None && fc < 7) {
        severity = Severity::Error;
    }
    if (!severity) {
        return;
    }
    context->report(
        *severity,
        "wrong-paved" + fowSuffix(element->fow()),
        utils::geomForReport(element), {element->id()});
}
} // namespace

VALIDATOR_SIMPLE_CHECK( linked_attributes_err, RD_EL, VEHICLE_RESTRICTION )
{
    const auto restrictionClasses = collectVehicleRestrictions(context);
    context->objects<RD_EL>().batchVisit(
            [&](const RoadElement* element)
    {
        const auto fc = element->fc();
        const auto accessId = element->accessId();
        const auto direction = element->direction();
        const auto fow = element->fow();
        const auto structType = element->structType();
        const auto fZLevel = element->fromZlevel();
        const auto tZLevel = element->toZlevel();
        const bool residential = element->residential() || restrictionClasses.residentialRdElIds.count(element->id());
        const bool restrictedForTrucks = restrictionClasses.restrictedRdElIds.count(element->id());
        const auto backBus = element->backBus();
        const auto backTaxi = element->backTaxi();
        const auto backBicycle = element->backBicycle();

        if (common::isIn(fow, {common::FOW::TwoCarriageway, common::FOW::Roundabout}) &&
            direction == RoadElement::Direction::Both)
        {
            context->error(
                "wrong-oneway" + fowSuffix(element->fow()),
                utils::geomForReport(element), {element->id()});
        }

        if (element->dr() && direction != RoadElement::Direction::Both) {
            context->error("wrong-oneway-dr",
                utils::geomForReport(element), {element->id()});
        }

        if (common::isIn(fow, {
                common::FOW::Roundabout,
                common::FOW::Ramp,
                common::FOW::AlternateRoad,
                common::FOW::Turnabout
            }) && structType != common::StructType::None) {
                context->error("wrong-struct-type",
                    utils::geomForReport(element), {element->id()});
        }

        if (structType == common::StructType::Bridge && (fZLevel <= 0 || tZLevel <= 0)) {
            context->error("wrong-zlev-bridge",
                utils::geomForReport(element), {element->id()});
        }

        if (structType == common::StructType::Tunnel && (fZLevel >= 0 || tZLevel >= 0)) {
            context->error("wrong-zlev-tunnel",
                utils::geomForReport(element), {element->id()});
        }

        checkPavement(element, context);

        if (element->underConstruction() && accessId != common::AccessId::None) {
            context->critical("wrong-access-id-uc",
                utils::geomForReport(element), {element->id()});
        }

        if (structType == common::StructType::Stairs
            && !common::isIn(accessId, {common::AccessId::None, common::AccessId::Pedestrian})) {
                context->error("wrong-access-id-stairs",
                    utils::geomForReport(element), {element->id()});
        }

        if (structType == common::StructType::Tunnel &&
            !!(accessId & common::AccessId::Pedestrian) &&
            fc < 8) {
                context->error("wrong-access-id-tunnel",
                    utils::geomForReport(element), {element->id()});
        }

        if (fc <= 6 && common::isIn(accessId, {common::AccessId::None, common::AccessId::Pedestrian})) {
            context->critical("wrong-access-id-fc",
                utils::geomForReport(element), {element->id()});
        }

        if (fow == common::FOW::PedestrianCrossing && fc != 10) {
            context->error("wrong-fc-fow18", utils::geomForReport(element), {element->id()});
        }

        if (common::isIn(fow, {
                common::FOW::TwoCarriageway,
                common::FOW::Roundabout,
                common::FOW::Ramp,
                common::FOW::AlternateRoad,
                common::FOW::Turnabout
            }) && fc > 7) {
                context->error("wrong-low-fc-fow", utils::geomForReport(element), {element->id()});
        }

        if (fc == 10 && (accessId | utils::notVehicle) != utils::notVehicle) {
                context->critical(
                    "rd-fc10-with-vehicle-access-id",
                    utils::geomForReport(element),
                    {element->id()}
                );
        }

        if (fc <=5 &&
            common::AccessId::None != (accessId & (common::AccessId::Pedestrian | common::AccessId::Bicycle))) {
            context->error("rd-high-fc-with-bicycle-access",
                    utils::geomForReport(element), {element->id()});
        }

        if (element->toll()) {
            if (fc >= 5 && !(fow == common::FOW::Ramp && common::isIn(fc, {5, 6, 7}))) {
                context->error("wrong-fc-toll",
                    utils::geomForReport(element), {element->id()});
            }

            if (direction == RoadElement::Direction::Both) {
                context->error("wrong-oneway-toll",
                    utils::geomForReport(element), {element->id()});
            }

            if (!!(accessId & (common::AccessId::Pedestrian | common::AccessId::Bicycle))) {
                context->error("wrong-access-id-toll",
                    utils::geomForReport(element), {element->id()});
            }
        }

        if (fow == common::FOW::Driveway) {
            if (structType != common::StructType::Bridge) {
                context->error("fow-driveway-without-struct-bridge",
                    utils::geomForReport(element), {element->id()});
            }

            if (fc > 8) {
                context->error("fow-driveway-with-low-fc",
                    utils::geomForReport(element), {element->id()});
            }
        }

        auto checkSpeedLimitRange = [&](const std::optional<int>& speedLimit, const std::string& name) {
            if (!speedLimit) {
                return;
            }
            if (*speedLimit > MAX_SPEED_LIMIT || *speedLimit < MIN_SPEED_LIMIT) {
                context->fatal(
                    name + "-out-of-range",
                    utils::geomForReport(element),
                    {element->id()});
            }

            if (fc == 8 && *speedLimit > MAX_SPEED_LIMIT_FC8) {
                context->error(
                    "too-high-" + name,
                    utils::geomForReport(element),
                    {element->id()});
            }
        };
        checkSpeedLimitRange(element->speedLimit(), "speed-limit");
        checkSpeedLimitRange(element->speedLimitT(), "speed-limit-t");
        checkSpeedLimitRange(element->speedLimitTruck(), "speed-limit-truck");
        checkSpeedLimitRange(element->speedLimitTruckT(), "speed-limit-truck-t");

        auto checkSpeedLimitTruckAndGeneral = [&](const std::optional<int>& speedLimit, const std::optional<int>& speedLimitTruck, const std::string& name) {
            if (!speedLimitTruck) {
                return;
            }
            if (!(accessId & common::AccessId::Truck)) {
                context->error(
                    name + "-no-access",
                    utils::geomForReport(element),
                    {element->id()});
            }
            if (speedLimit) {
                if (*speedLimit < *speedLimitTruck) {
                    context->error(
                        name + "-greater",
                        utils::geomForReport(element),
                        {element->id()});
                }
            } else {
                context->error(
                    name + "-only",
                    utils::geomForReport(element),
                    {element->id()});
            }
        };

        checkSpeedLimitTruckAndGeneral(element->speedLimit(), element->speedLimitTruck(), "speed-limit-truck");
        checkSpeedLimitTruckAndGeneral(element->speedLimitT(), element->speedLimitTruckT(), "speed-limit-truck-t");

        if (element->speedLimitT() && !element->speedLimit()) {
            context->error(
                    "speed-limit-backward-only",
                    utils::geomForReport(element),
                    {element->id()});
        }

        if (element->speedLimitTruckT() && !element->speedLimitTruck()) {
            context->error(
                    "speed-limit-truck-backward-only",
                    utils::geomForReport(element),
                    {element->id()});
        }

        if (backBus && direction == RoadElement::Direction::Both) {
            context->critical(
                "back-bus-with-both-directions",
                utils::geomForReport(element),
                {element->id()}
            );
        }

        if (backTaxi && direction == RoadElement::Direction::Both) {
            context->critical(
                "back-taxi-with-both-directions",
                utils::geomForReport(element),
                {element->id()}
            );
        }

        if (backBus && common::isSet(accessId, utils::notVehicle)) {
            context->critical(
                "back-bus-with-not-vehicle-access-id",
                utils::geomForReport(element),
                {element->id()}
            );
        }

        if (backTaxi && common::isSet(accessId, utils::notVehicle)) {
            context->critical(
                "back-taxi-with-not-vehicle-access-id",
                utils::geomForReport(element),
                {element->id()}
            );
        }

        if (backBus && accessId == common::AccessId::Bus) {
            context->critical(
                "back-bus-with-bus-only-access-id",
                utils::geomForReport(element),
                {element->id()}
            );
        }

        if (backTaxi && accessId == common::AccessId::Taxi) {
            context->critical(
                "back-taxi-with-taxi-only-access-id",
                utils::geomForReport(element),
                {element->id()}
            );
        }

        if (backBus && backTaxi &&
            accessId == (common::AccessId::Bus | common::AccessId::Taxi)) {
                context->critical(
                    "back-bus-taxi-with-bus-taxi-only-access-id",
                    utils::geomForReport(element),
                    {element->id()}
                );
        }

        if (residential && !common::isIn(fc, {6, 7})) {
            context->critical(
                "wrong-residential-fc",
                utils::geomForReport(element),
                {element->id()}
            );
        }

        if (restrictedForTrucks && common::isIn(fc, {8, 9, 10})) {
            context->critical(
                "wrong-restricted-for-trucks-fc",
                utils::geomForReport(element),
                {element->id()}
            );
        }
        if (restrictedForTrucks && !(accessId & common::AccessId::Truck) && !element->underConstruction()) {
            context->critical(
                "wrong-access-id-restricted-for-trucks",
                utils::geomForReport(element),
                {element->id()}
            );
        }

        if (backBicycle && direction == RoadElement::Direction::Both) {
            context->critical(
                "back-bicycle-with-both-directions",
                utils::geomForReport(element),
                {element->id()}
            );
        }

        if ((fc < 3 || fc > 7) && backBicycle) {
            context->error("rd-wrong-fc-with-back-bicycle",
                utils::geomForReport(element), {element->id()});
        }

        if (backBicycle && accessId == common::AccessId::Bicycle) {
            context->critical(
                "back-bicycle-with-only-bicycle-access-id",
                utils::geomForReport(element),
                {element->id()}
            );
        }

    }, VISIT_BATCH_SIZE);
}

VALIDATOR_SIMPLE_CHECK( linked_attributes_warn, RD_EL )
{
    context->objects<RD_EL>().visit(
            [&](const RoadElement* element)
    {
        const auto accessId = element->accessId();
        const auto fow = element->fow();
        const auto fZLevel = element->fromZlevel();
        const auto tZLevel = element->toZlevel();

        if (common::isIn(fow, {common::FOW::Ramp, common::FOW::AlternateRoad}) &&
            element->direction() == RoadElement::Direction::Both)
        {
            context->warning(
                "wrong-oneway" + fowSuffix(element->fow()),
                utils::geomForReport(element), {element->id()});
        }

        if (abs(fZLevel - tZLevel) > 1) {
            context->warning("wrong-zlev-diff",
                utils::geomForReport(element), {element->id()});
        }

        if (fow == common::FOW::TwoCarriageway &&
            common::isIn(accessId, {common::AccessId::None, common::AccessId::Pedestrian}))
        {
            context->warning(
                "wrong-access-id" + fowSuffix(element->fow()),
                utils::geomForReport(element), {element->id()});
        }
    });
}

VALIDATOR_SIMPLE_CHECK( speed_limits_multiple, RD_EL )
{
    context->objects<RD_EL>().visit(
            [&](const RoadElement* element)
    {
        auto checkSpeedLimitIsMultipleOf5 =
            [&](const std::optional<int>& speedLimit, const std::string& name)
            {
                if (speedLimit && (*speedLimit % 5)) {
                    context->error(
                        name + "-not-multiple-of-5",
                        utils::geomForReport(element),
                        {element->id()}
                    );
                }
            };

        checkSpeedLimitIsMultipleOf5(element->speedLimit(), "speed-limit");
        checkSpeedLimitIsMultipleOf5(element->speedLimitT(), "speed-limit-t");
        checkSpeedLimitIsMultipleOf5(element->speedLimitTruck(), "speed-limit-truck");
        checkSpeedLimitIsMultipleOf5(element->speedLimitTruckT(), "speed-limit-truck-t");
    });
}

} // namespace checks
} // namespace validator
} // namespace wiki
} // namespace maps
