#pragma once

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

#include <boost/none.hpp>

#include <vector>

namespace maps {
namespace wiki {
namespace validator {

class TransportThread;
class TransportThreadStop;
class TransportStop;
class TransportElement;
class TransportTransition;
class TransportPassageway;
class TransportMetroStation;
class CheckContext;
class Edge;

namespace checks {

using categories::TRANSPORT_THREAD_STOP;
using categories::FREQ_DT;

bool needValidation(CheckContext* context, TId operatorId);

bool isDurationValid(TSeconds duration, TSeconds minDuration, TSeconds maxDuration);

bool isBoundToOperatorToValidate(CheckContext* context, const TransportMetroStation* station);
bool isBoundToOperatorToValidate(CheckContext* context, const TransportElement* element);
bool isBoundToOperatorToValidate(CheckContext* context, const TransportTransition* transition);
bool isBoundToOperatorToValidate(CheckContext* context, const TransportPassageway* passageway);

template <typename RouteCategory>
bool isBoundToOperatorToValidate(CheckContext* context, const TransportThread* thread)
{
    context->checkLoaded<RouteCategory>();
    if (!context->objects<RouteCategory>().loaded(thread->line())) {
        return false;
    }
    const auto* line =
        context->objects<RouteCategory>().byId(thread->line());
    return needValidation(context, line->transportOperator());
}

template <typename RouteCategory, typename ThreadCategory>
bool isBoundToOperatorToValidate(CheckContext* context, const TransportThreadStop* stop)
{
    context->checkLoaded<ThreadCategory>();
    return
        context->objects<ThreadCategory>().loaded(stop->thread()) &&
        isBoundToOperatorToValidate<RouteCategory>(
            context,
            context->objects<ThreadCategory>().byId(stop->thread()));
}

void restoreThreadStopSequence(std::vector<const TransportThreadStop*>& unsorted);

template <typename StopCategory>
const typename StopCategory::TObject*
stopByThreadStop(CheckContext* context, TId threadStopId)
{
    auto threadStop = context->objects<TRANSPORT_THREAD_STOP>().byId(threadStopId);
    if (!context->objects<StopCategory>().loaded(threadStop->station())) {
        context->error(
            "linked-object-is-outside-of-validation-area",
            boost::none,
            {threadStopId, threadStop->station()});
        return nullptr;
    }
    return context->objects<StopCategory>().byId(threadStop->station());
}

template<typename StopCategory>
boost::optional<typename StopCategory::TObject::TGeom>
stationGeomById(CheckContext* context, TId id)
{
    if (context->objects<StopCategory>().loaded(id)) {
        return context->objects<StopCategory>().byId(id)->geom();
    }
    return boost::none;
}

boost::optional<geolib3::Polygon2>
bufferedConvexHull(
    const std::vector<boost::optional<geolib3::Point2>>& optPoints,
    double bufferWidth);

enum class TravelTimeIsMandatory
{
    Yes,
    No
};

enum class WaitTimeIsMandatory
{
    Yes,
    No
};


template<typename StopCategory, typename RouteCagegory, typename ThreadCategory>
void
checkThreadStopsTiming(CheckContext* context,
    TravelTimeIsMandatory isTravelTimeMandatory,
    WaitTimeIsMandatory isWaitTimeMandatory,
    const TransportThreadStop* threadStop,
    TSeconds minTravelTime, TSeconds maxTravelTime,
    TSeconds minWaitTime, TSeconds maxWaitTime,
    Severity waitTimeSeverity)
{
    auto prevId = threadStop->previous();
    if (!prevId) {
        return;
    }
    if (!isBoundToOperatorToValidate<RouteCagegory, ThreadCategory>(context, threadStop)) {
        return;
    }
    if ((threadStop->travelTime() && !isDurationValid(
            *threadStop->travelTime(),
            minTravelTime,
            maxTravelTime)) ||
        (isTravelTimeMandatory == TravelTimeIsMandatory::Yes && !threadStop->travelTime())) {
        context->report(
            Severity::Error,
            "transport-thread-invalid-travel-time",
            stationGeomById<StopCategory>(context, threadStop->station()),
            {threadStop->id()});
    }
    if (!context->objects<TRANSPORT_THREAD_STOP>().loaded(prevId)) {
        return;
    }
    const TransportThreadStop* prevStop =
        context->objects<TRANSPORT_THREAD_STOP>().byId(prevId);
    auto prevStopGeom = stationGeomById<StopCategory>(context, prevStop->station());
    if (!prevStop->previous()) {
        if (prevStop->travelTime()) {
            context->report(
                Severity::Error,
                "transport-thread-first-stop-with-travel-time",
                prevStopGeom,
                {prevStop->id()});
        }
        if (prevStop->waitTime()) {
            context->report(
                Severity::Error,
                "transport-thread-first-stop-with-wait-time",
                prevStopGeom,
                {prevStop->id()});
        }
    } else if ((prevStop->waitTime() && !isDurationValid(
            *prevStop->waitTime(), minWaitTime, maxWaitTime)) ||
        (isWaitTimeMandatory == WaitTimeIsMandatory::Yes && !prevStop->waitTime())) {
        context->report(
            waitTimeSeverity,
            "transport-thread-invalid-wait-time",
            prevStopGeom,
            {prevStop->id()});
    }
}

template<typename Category>
bool performOperatorBindingCheck(CheckContext* context)
{
    context->objects<Category>().visit(
        [&](const typename Category::TObject* route)
        {
            if (!route->transportOperator()) {
                context->report(
                    route->threads().empty() ?  Severity::Warning : Severity::Error,
                    "transport-route-not-bound-to-operator",
                    boost::none, {route->id()});
            }
        });
    return true;
}

bool checkThreadStopSequenceHasFatalErrors(
    CheckContext* context,
    const std::vector<const TransportThreadStop*>& stops,
    TId threadId);

common::Schedule createSchedule(const Schedule* schedule);

template<typename ThreadCategory, typename RouteCategory>
bool performSchedulesCheck(CheckContext* context, size_t minFreq, size_t maxFreq)
{
    context->objects<ThreadCategory>().visit(
        [&](const TransportThread* thread)
        {
            if (!isBoundToOperatorToValidate<RouteCategory>(context, thread)) {
                return;
            }
            if (thread->schedules().empty()) {
                context->error("transport-thread-schedule-empty"
                    , boost::none, {thread->id()});
                return;
            }
            common::Schedules schedules;
            common::Schedules alternativeSchedules;
            schedules.reserve(schedules.size());
            alternativeSchedules.reserve(schedules.size());
            for (auto scheduleId : thread->schedules()) {
                auto schedule = context->objects<FREQ_DT>().byId(scheduleId);
                bool skip = false;

                if (!schedule->departureTime().empty()) {
                    if (!common::Time(schedule->departureTime()).isValid()) {
                        context->critical("transport-thread-schedule-departure-time"
                            , boost::none, {thread->id()});
                        skip = true;
                    }
                } else if (!schedule->timeStart().empty() || !schedule->timeEnd().empty()) {
                    if (!common::Time(schedule->timeStart()).isValid()) {
                        context->critical("transport-thread-schedule-bad-time-start"
                            , boost::none, {thread->id()});
                        skip = true;
                    }
                    if (!common::Time(schedule->timeEnd()).isValid()) {
                        context->critical("transport-thread-schedule-bad-time-end"
                            , boost::none, {thread->id()});
                        skip = true;
                    }
                    if (!skip && (schedule->timeStart() == schedule->timeEnd())) {
                        context->critical("transport-thread-schedule-time-start-equal-time-end"
                            , boost::none, {thread->id()});
                        skip = true;
                    }
                }

                if (!schedule->dateStart().empty() || !schedule->dateEnd().empty()) {
                    if (!common::Date(schedule->dateStart()).isValid()) {
                        context->critical("transport-thread-schedule-bad-date-start"
                            , boost::none, {thread->id()});
                        skip = true;
                    }
                    if (!common::Date(schedule->dateEnd()).isValid()) {
                        context->critical("transport-thread-schedule-bad-date-end"
                            , boost::none, {thread->id()});
                        skip = true;
                    }
                }

                const auto& frequency = schedule->frequency();
                if (frequency != 0 &&
                    !isDurationValid(frequency, minFreq, maxFreq))
                {
                    context->critical(
                        "transport-thread-schedule-invalid-frequency",
                        boost::none, {thread->id()});
                    skip = true;
                }

                if (skip) {
                    continue;
                }

                auto scheduleInfo = createSchedule(schedule);
                auto& checkAgainst = schedule->alternative()
                    ? alternativeSchedules
                    : schedules;
                for (const auto& otherSchedule : checkAgainst) {
                    if (otherSchedule.intersects(scheduleInfo)) {
                        context->fatal(
                            "transport-thread-schedule-intersections"
                            , boost::none, {thread->id()});
                        break;
                    }
                }
                checkAgainst.push_back(scheduleInfo);
            }
        });
    return true;
}


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