#pragma once
#include "common.h"
#include <yandex/maps/wiki/validator/check.h>

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

using categories::RD_EL;
using categories::RD_JC;

namespace {

template <typename CondObject>
common::AccessId
getCondAccessId(const CondObject* condition)
{
    return condition->accessId();
}

} // namespace

template <typename CondCategory>
void
conditionPassabilityCheck(
    CheckContext* context,
    const typename CondCategory::TObject* condition,
    size_t maxElements,
    Severity tooManyToElementsSeverity,
    Severity passabilitySeverity = Severity::Fatal)
{
    const auto& roadElements = context->objects<RD_EL>();
    const auto& junctions = context->objects<RD_JC>();

    if (!junctions.loaded(condition->viaJunction())) {
        // Condition out of AOI
        return;
    }

    int currentZlevel = 0;
    if (roadElements.loaded(condition->fromRoadElement())) {
        auto fromRoadElement =
                roadElements.byId(condition->fromRoadElement());
        if (!canPassRoadElementFrom(
                fromRoadElement,
                utils::oppositeJunctionId(fromRoadElement,
                                   condition->viaJunction()),
                getCondAccessId<typename CondCategory::TObject>(condition)))
        {
            context->report(
                            passabilitySeverity,
                            "unpassable-from-element",
                           utils::geomForReport(fromRoadElement),
                           { condition->id(), fromRoadElement->id() });
        }

        currentZlevel =
            (condition->viaJunction() == fromRoadElement->startJunction()
             ? fromRoadElement->fromZlevel()
             : fromRoadElement->toZlevel());
    }

    if (maxElements && condition->toRoadElements().size() > maxElements) {
        context->report(tooManyToElementsSeverity,
                        "too-many-to-elements",
                        junctions.byId(condition->viaJunction())->geom(),
                        { condition->id() });
    }

    TId currentJunctionId = condition->viaJunction();

    for (const auto& toRoadElementPair : condition->toRoadElements()) {
        TId toRoadElementId = toRoadElementPair.second;
        if (!roadElements.loaded(toRoadElementId)) {
            break;
        }

        auto toRoadElement = roadElements.byId(toRoadElementId);

        auto toRoadElementZlevels =
            std::make_pair(toRoadElement->fromZlevel(), toRoadElement->toZlevel());
        if (currentJunctionId == toRoadElement->endJunction()) {
            std::swap(toRoadElementZlevels.first,
                      toRoadElementZlevels.second);
        }

        if (toRoadElementZlevels.first != currentZlevel) {
            context->report(
                passabilitySeverity,
                "to-element-zlevel-mismatch",
                utils::geomForReport(toRoadElement),
                {
                    condition->id(),
                     currentJunctionId,
                     toRoadElementId
                 });
        }

        if (!canPassRoadElementFrom(
            toRoadElement,
            currentJunctionId,
            getCondAccessId<typename CondCategory::TObject>(condition)))
        {
            context->report(
                passabilitySeverity,
                "unpassable-to-element",
                utils::geomForReport(toRoadElement),
                { condition->id(), toRoadElementId });
            break;
        }

        currentJunctionId = utils::oppositeJunctionId(toRoadElement, currentJunctionId);
        if (currentJunctionId == 0) {
            // to elements are not continuous.
            break;
        }

        currentZlevel = toRoadElementZlevels.second;
    }
}

template <typename CondCategory>
void conditionAccessIdCheck(CheckContext* context, const typename CondCategory::TObject* condition,
    const std::string& conditionName, std::vector<TId>& idsToReport) {
    const auto& roadElements = context->objects<RD_EL>();
    const auto& junctions = context->objects<RD_JC>();

    if (!junctions.loaded(condition->viaJunction())) {
        // Condition out of AOI
        return;
    }

    idsToReport = { condition->id() };

    const auto condAccessId = condition->accessId();
    auto effectiveAccessId = condAccessId;
    if (!effectiveAccessId) {
        context->fatal(conditionName + "-without-access",
            junctions.byId(condition->viaJunction())->geom(),
            idsToReport);
        return;
    }

    if (roadElements.loaded(condition->fromRoadElement())) {
        auto fromRoadElement =
                roadElements.byId(condition->fromRoadElement());
        auto elemAccessId = fromRoadElement->accessId();

        effectiveAccessId = effectiveAccessId & elemAccessId;
        if (condAccessId != (condAccessId & elemAccessId)) {
            idsToReport.push_back(fromRoadElement->id());
        }
    }

    for (const auto& toRoadElementPair : condition->toRoadElements()) {
        TId toRoadElementId = toRoadElementPair.second;
        if (!roadElements.loaded(toRoadElementId)) {
            continue;
        }
        auto toRoadElement = roadElements.byId(toRoadElementId);
        auto elemAccessId = toRoadElement->accessId();

        effectiveAccessId = effectiveAccessId & elemAccessId;
        if (condAccessId != (condAccessId & elemAccessId)) {
            idsToReport.push_back(toRoadElement->id());
        }
    }

    if (!effectiveAccessId) {
        context->error(conditionName + "-access-not-applicable",
            junctions.byId(condition->viaJunction())->geom(),
            idsToReport);
    }
}
} // namespace checks
} // namespace validator
} // namespace wiki
} // namespace maps
