#include "module.h"

#include "../utils/geom.h"
#include "../utils/object_elements_within_aoi.h"

#include <yandex/maps/wiki/validator/check.h>
#include <yandex/maps/wiki/validator/categories.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/closest_point.h>
#include <maps/libs/ymapsdf/include/ft.h>

using FtType = maps::ymapsdf::ft::Type;
using maps::wiki::validator::categories::URBAN_AREAL;
using maps::wiki::validator::categories::URBAN_ROADNET_PARKING_CONTROLLED_ZONE;
using maps::wiki::validator::categories::URBAN_ROADNET_PARKING_LOT;
using maps::wiki::validator::categories::URBAN_ROADNET_PARKING_LOT_LINEAR;
using maps::wiki::validator::utils::filterObjectsOutsideOfAoiWithWarnings;

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


VALIDATOR_CHECK_PART(urban_roadnet_parking, residential_parking_cant_be_toll,
    URBAN_ROADNET_PARKING_LOT)
{
    context->objects<URBAN_ROADNET_PARKING_LOT>()
        .visit([&](const ParkingLot* parkingLot)
    {
        if (parkingLot->isToll() && parkingLot->isResidential()) {
            context->error("parking-lot-cant-be-residential-and-toll-simultaneously",
                           parkingLot->geom(), {parkingLot->id()});
        }
    });
}


VALIDATOR_CHECK_PART(urban_roadnet_parking, zones_without_parkings,
    URBAN_ROADNET_PARKING_CONTROLLED_ZONE)
{
    context->objects<URBAN_ROADNET_PARKING_CONTROLLED_ZONE>().visit(
        [&](const ParkingControlledZone* zone)
        {
            if (zone->parkingLots().size() == 0 and
                zone->linearParkingLots().size() == 0)
                {
                    context->critical("parking-zone-without-parkings", zone->geom(), {zone->id()});
                }
        });
}


VALIDATOR_CHECK_PART(urban_roadnet_parking, assigned_parkings_types,
    URBAN_ROADNET_PARKING_CONTROLLED_ZONE, URBAN_ROADNET_PARKING_LOT, URBAN_ROADNET_PARKING_LOT_LINEAR)
{
    context->objects<URBAN_ROADNET_PARKING_CONTROLLED_ZONE>().visit(
        [&](const ParkingControlledZone* zone)
        {
            // Parking lots
            auto parkingLotIds =
                filterObjectsOutsideOfAoiWithWarnings<URBAN_ROADNET_PARKING_LOT>(
                   context, zone->parkingLots(), zone->geom()
                );
            for (auto parkingLotId: parkingLotIds) {
                const auto* parkingLot =
                    context->objects<URBAN_ROADNET_PARKING_LOT>().byId(parkingLotId);

                if (!parkingLot->isToll()) {
                    context->critical("parking-lot-free-is-assigned-to-a-zone",
                                      parkingLot->geom(), {zone->id(), parkingLotId});
                }
            }

            // Linear parking lots
            auto parkingLotLinearIds =
                filterObjectsOutsideOfAoiWithWarnings<URBAN_ROADNET_PARKING_LOT_LINEAR>(
                   context, zone->linearParkingLots(), zone->geom()
                );
            for (auto parkingLotLinearId: parkingLotLinearIds) {
                const auto* parkingLotLinear =
                    context->objects<URBAN_ROADNET_PARKING_LOT_LINEAR>().byId(parkingLotLinearId);

                switch (FtType(parkingLotLinear->featureType())) {
                    case FtType::UrbanRoadnetParkingFree:
                        context->critical("parking-lot-linear-free-is-assigned-to-a-zone",
                                          parkingLotLinear->geom().boundingBox().center(),
                                          {zone->id(), parkingLotLinearId});
                        break;
                    case FtType::UrbanRoadnetParkingToll:
                        break;
                    case FtType::UrbanRoadnetParkingRestricted:
                        context->critical("parking-lot-linear-restricted-is-assigned-to-a-zone",
                                          parkingLotLinear->geom().boundingBox().center(),
                                          {zone->id(), parkingLotLinearId});
                        break;
                    case FtType::UrbanRoadnetParkingProhibited:
                        context->critical("parking-lot-linear-prohibited-is-assigned-to-a-zone",
                                          parkingLotLinear->geom().boundingBox().center(),
                                          {zone->id(), parkingLotLinearId});
                        break;
                    default:
                        context->critical("parking-lot-linear-has-unsupported-ft-type",
                                          parkingLotLinear->geom().boundingBox().center(),
                                          {parkingLotLinearId});
                }
            }
        });
}


VALIDATOR_CHECK_PART(urban_roadnet_parking, distance_to_parkings,
    URBAN_ROADNET_PARKING_CONTROLLED_ZONE, URBAN_ROADNET_PARKING_LOT, URBAN_ROADNET_PARKING_LOT_LINEAR)
{
    using geolib3::distance;

    const auto MAX_DISTANCE_IN_METERS = 4000.0;

    context->objects<URBAN_ROADNET_PARKING_CONTROLLED_ZONE>().visit(
        [&](const ParkingControlledZone* zone)
        {
            const double merDistRatio = utils::mercatorDistanceRatio(zone->geom());

            // Parking lots
            auto parkingLotIds =
                filterObjectsOutsideOfAoiWithWarnings<URBAN_ROADNET_PARKING_LOT>(
                   context, zone->parkingLots(), zone->geom()
                );
            for (auto parkingLotId: parkingLotIds) {
                const auto* parkingLot =
                    context->objects<URBAN_ROADNET_PARKING_LOT>().byId(parkingLotId);

                if (distance(parkingLot->geom(), zone->geom()) * merDistRatio > MAX_DISTANCE_IN_METERS) {
                    context->warning("parking-lot-is-located-too-far-from-the-zone",
                                     parkingLot->geom(), {zone->id(), parkingLotId});
                }
            }

            // Linear parking lots
            auto parkingLotLinearIds =
                filterObjectsOutsideOfAoiWithWarnings<URBAN_ROADNET_PARKING_LOT_LINEAR>(
                   context, zone->linearParkingLots(), zone->geom()
                );
            for (auto parkingLotLinearId: parkingLotLinearIds) {
                const auto* parkingLotLinear =
                    context->objects<URBAN_ROADNET_PARKING_LOT_LINEAR>().byId(parkingLotLinearId);

                const auto closestPoint = geolib3::closestPoint(parkingLotLinear->geom(), zone->geom());

                if (distance(closestPoint, zone->geom()) * merDistRatio > MAX_DISTANCE_IN_METERS) {
                    context->warning("parking-lot-linear-is-located-too-far-from-the-zone",
                                     parkingLotLinear->geom().boundingBox().center(),
                                     {zone->id(), parkingLotLinearId});
                }
            }
        });
}


VALIDATOR_CHECK_PART(urban_roadnet_parking, connection_between_parking_territory_and_parking_lot,
    URBAN_AREAL, URBAN_ROADNET_PARKING_LOT)
{
    context->objects<URBAN_AREAL>().visit(
        [&](const PolygonFeature* area)
        {
            if (FtType(area->featureType()) != FtType::UrbanRoadnetParkingLot) {
                if (context->objects<URBAN_ROADNET_PARKING_LOT>().loaded(area->parent())) {
                    context->fatal("parking-lot-has-area-of-wrong-type",
                        area->geom(), {area->id(), area->parent()});
                }
                return;
            }

            if (area->parent() == 0) {
                context->critical("parking-territory-is-not-connected-to-parking-lot",
                                  area->geom(), {area->id()});
                return;
            }

            if (!context->objects<URBAN_ROADNET_PARKING_LOT>().loaded(area->parent())) {
                context->critical("parking-territory-is-connected-to-wrong-object",
                                  area->geom(), {area->id(), area->parent()});
                return;
            }
        });
}


VALIDATOR_CHECK_PART(urban_roadnet_parking, unassigned_parking_lot_linears,
    URBAN_ROADNET_PARKING_LOT_LINEAR)
{
    context->objects<URBAN_ROADNET_PARKING_LOT_LINEAR>().visit(
        [&](const URBAN_ROADNET_PARKING_LOT_LINEAR::TObject* parkingLotLinear) {
            if (FtType(parkingLotLinear->featureType()) == FtType::UrbanRoadnetParkingToll and
                !parkingLotLinear->parent())
            {
                context->critical("parking-lot-linear-toll-is-not-assigned-to-a-zone",
                                  parkingLotLinear->geom().boundingBox().center(), {parkingLotLinear->id()});
            }
        });
}


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