#pragma once

#include "module.h"
#include <yandex/maps/wiki/validator/check.h>
#include <yandex/maps/wiki/validator/categories.h>

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

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

#include <maps/libs/geolib/include/point.h>

#include <algorithm>
#include <functional>
#include <unordered_set>

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

namespace gl = maps::geolib3;

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

template <typename CondCategory>
void
conditionTopoCheck(
    CheckContext* context,
    const typename CondCategory::TObject* condition,
    const std::string& category)
{
    const RD_EL::TObjectsView& roadElements = context->objects<RD_EL>();
    const RD_JC::TObjectsView& junctions = context->objects<RD_JC>();

    if (!junctions.loaded(condition->viaJunction())) {
        // Condition out of AOI
        return;
    }
    const gl::Point2 anchor = junctions.byId(condition->viaJunction())->geom();

    // Maneuvers must have 'to' elements
    if (CondCategory::id() == categories::COND::id() &&
        condition->toRoadElements().empty()) {
            context->fatal("no-to-elements", anchor, { condition->id() });
    }

    if (!roadElements.loaded(condition->fromRoadElement())) {
        if (condition->fromRoadElement()) {
            context->fatal("from-element-disconnected",
                           anchor,
                           { condition->id(),
                             condition->viaJunction(),
                             condition->fromRoadElement() });
        }
    } else {
        auto fromElement = roadElements.byId(condition->fromRoadElement());
        if (!common::isIn(condition->viaJunction(), { fromElement->startJunction(),
                                              fromElement->endJunction() })) {
            context->fatal("from-element-disconnected",
                           anchor,
                           { condition->id(),
                             condition->viaJunction(),
                             fromElement->id() });
        }
    }

    TId currentJunctionId = condition->viaJunction();
    std::unordered_set<TId> junctionsPassed({ currentJunctionId });

    for (TSeqNum seqNum = 0;
         seqNum < condition->toRoadElements().size();
         ++seqNum) {
        if (!junctions.loaded(currentJunctionId)) {
            break;
        }

        auto currentJunctionGeom = junctions.byId(currentJunctionId)->geom();

        TId toElementId =
                condition->toRoadElements()[seqNum].second;
        if (!roadElements.loaded(toElementId)) {
            context->fatal(
                    "to-element-disconnected",
                    currentJunctionGeom,
                    { condition->id(),
                      currentJunctionId,
                      toElementId });
            break;
        }

        auto toElement = roadElements.byId(toElementId);

        if (condition->toRoadElements()[seqNum].first != seqNum) {
            context->fatal("wrong-seq-num",
                           utils::geomForReport(toElement),
                           { condition->id(), toElementId });
            break;
        }

        auto nextJunctionId = utils::oppositeJunctionId(toElement, currentJunctionId);
        if (!nextJunctionId) {
            context->fatal("to-element-disconnected",
                           currentJunctionGeom,
                           { condition->id(),
                             currentJunctionId,
                             toElementId });
            break;
        }

        currentJunctionId = nextJunctionId;
        if (!junctionsPassed.insert(currentJunctionId).second) {
            context->fatal("looped-" + category,
                           junctions.byId(currentJunctionId)->geom(),
                           { condition->id(), currentJunctionId});
            break;
        }
    }
}

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