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

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

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

using categories::TRANSPORT_METRO_LINE;
using categories::TRANSPORT_METRO_THREAD;
using categories::TRANSPORT_METRO_STATION;
using categories::TRANSPORT_OPERATOR;

bool needValidation(CheckContext* context, TId operatorId)
{
    if (operatorId == 0) {
        return false;
    }
    if (!context->objects<TRANSPORT_OPERATOR>().loaded(operatorId)) {
        context->critical(
            "transport-operator-is-outside-of-validation-area",
            boost::none,
            {operatorId});
        return false;
    }
    auto transportOperator = context->objects<TRANSPORT_OPERATOR>().byId(operatorId);
    return !(transportOperator && transportOperator->skipValidation());
}


bool
isDurationValid(TSeconds duration, TSeconds minDuration, TSeconds maxDuration)
{
    return duration >= minDuration && duration < maxDuration;
}


bool
isBoundToOperatorToValidate(CheckContext* context, const TransportMetroStation* station)
{
    context->checkLoaded<TRANSPORT_METRO_LINE>();
    if (!context->objects<TRANSPORT_METRO_LINE>().loaded(station->line())) {
        return false;
    }
    const auto* line =
        context->objects<TRANSPORT_METRO_LINE>().byId(
            station->line());
    return needValidation(context, line->transportOperator());
}

bool
isBoundToOperatorToValidate(CheckContext* context, const TransportElement* element)
{
    context->checkLoaded<TRANSPORT_METRO_LINE>();
    context->checkLoaded<TRANSPORT_METRO_THREAD>();
    for (auto lineId : element->lines()) {
        if (!context->objects<TRANSPORT_METRO_LINE>().loaded(lineId)) {
            continue;
        }
        const auto* line =
            context->objects<TRANSPORT_METRO_LINE>().byId(lineId);
        if (needValidation(context, line->transportOperator())) {
            return true;
        }
    }
    for (auto threadId : element->threads()) {
        if (!context->objects<TRANSPORT_METRO_THREAD>().loaded(threadId)) {
            continue;
        }
        const auto* thread =
            context->objects<TRANSPORT_METRO_THREAD>().byId(threadId);
        if (isBoundToOperatorToValidate<TRANSPORT_METRO_LINE>(context, thread)) {
            return true;
        }
    }
    return false;
}

bool
isBoundToOperatorToValidate(CheckContext* context, const TransportTransition* transition)
{
    context->checkLoaded<TRANSPORT_METRO_STATION>();
    if (!context->objects<TRANSPORT_METRO_STATION>().loaded(transition->stationA())
        ||
        !context->objects<TRANSPORT_METRO_STATION>().loaded(transition->stationB())) {
        return false;
    }
    const auto* stationA =
        context->objects<TRANSPORT_METRO_STATION>().byId(transition->stationA());
    const auto* stationB =
        context->objects<TRANSPORT_METRO_STATION>().byId(transition->stationB());
    return isBoundToOperatorToValidate(context, stationA) &&
        isBoundToOperatorToValidate(context, stationB);
}

bool
isBoundToOperatorToValidate(CheckContext* context, const TransportPassageway* passageway)
{
    context->checkLoaded<TRANSPORT_METRO_STATION>();
    if (!context->objects<TRANSPORT_METRO_STATION>().loaded(passageway->station())) {
        return false;
    }
    const auto* station =
        context->objects<TRANSPORT_METRO_STATION>().byId(passageway->station());
    return isBoundToOperatorToValidate(context, station);
}

void
restoreThreadStopSequence(std::vector<const TransportThreadStop*>& unsorted)
{
    size_t tail = 0;
    size_t tailPrev = 0;
    TId previousId = 0;
    if (unsorted.size() < 2) {
        return;
    }
    while (tail < unsorted.size() - 1) {
        for (size_t j = tail; j < unsorted.size(); ++j) {
            if (unsorted[j]->previous() != previousId) {
                continue;
            }
            if (j != tail ) {
                std::swap(unsorted[j], unsorted[tail]);
            }
            previousId = unsorted[tail]->id();
            ++tail;
            break;
        }
        if (tailPrev == tail) {
            return;
        }
        tailPrev = tail;
    }
}

boost::optional<geolib3::Polygon2>
bufferedConvexHull(
    const std::vector<boost::optional<geolib3::Point2>>& optPoints,
    double bufferWidth)
{
    geolib3::PointsVector points;
    for (const auto& optPoint : optPoints) {
        if (optPoint) {
            points.push_back(*optPoint);
        }
    }
    if (!points.empty()) {
        return geolib3::bufferedConvexHull(points, bufferWidth);
    }
    return boost::none;
}

bool
checkThreadStopSequenceHasFatalErrors(
    CheckContext* context,
    const std::vector<const TransportThreadStop*>& threadStops,
    TId threadId)
{
    std::unordered_map<TId, std::unordered_set<TId>> previousToThreadStopId;
    for (const auto& threadStop : threadStops) {
        previousToThreadStopId[threadStop->previous()].insert(threadStop->id());
    }
    std::vector<TId> branchingErrors;
    std::vector<TId> beginErrors;
    for (const auto& kv : previousToThreadStopId) {
        const auto& nextStopsIds = kv.second;
        const auto stopId = kv.first;
        if (nextStopsIds.size() < 2) {
            continue;
        }
        if (stopId) {
            branchingErrors.insert(branchingErrors.end(),
                nextStopsIds.begin(), nextStopsIds.end());
        } else {
            beginErrors.insert(beginErrors.end(),
                nextStopsIds.begin(), nextStopsIds.end());
        }
    }
    if (!beginErrors.empty()) {
        beginErrors.push_back(threadId);
        context->fatal(
                "transport-thread-multiple-stops-without-previous",
                boost::none,
                {beginErrors});
    }
    if (!branchingErrors.empty()) {
        branchingErrors.push_back(threadId);
        context->fatal(
                "transport-thread-stops-with-not-unique-previous",
                boost::none,
                {branchingErrors});
    }
    return !branchingErrors.empty() || !beginErrors.empty();
}

common::Schedule createSchedule(const Schedule* schedule)
{
    return
        schedule->departureTime().empty()
            ? common::Schedule(
                schedule->dateStart(),
                schedule->dateEnd(),
                schedule->weekdays(),
                schedule->timeStart(),
                schedule->timeEnd(),
                schedule->frequency())
            : common::Schedule(
                schedule->dateStart(),
                schedule->dateEnd(),
                schedule->weekdays(),
                {schedule->departureTime()});
}

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