#include "masstransit.h"
#include "magic_string.h"
#include "utils.h"

#include <yandex/maps/wiki/common/schedule.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/diffalert/object.h>
#include <yandex/maps/wiki/diffalert/snapshot.h>
#include <maps/libs/log8/include/log8.h>

#include <array>
#include <algorithm>
#include <ctype.h>

namespace maps {
namespace wiki {
namespace diffalert {
namespace {
const double STOP_DISTANCE_THRESHOLD_METERS = 300.0;
const std::string STR_DECREASED = "decreased";
const std::string STR_INCREASED = "increased";
const unsigned int DEFAULT_WEEK_EVERY_DAY = 127;

const double THREAD_LENGTH_CHANGE_ALERT_THRESHOLD = 10.0;
std::map<double, Priority> LENGTH_CHANGE_TO_PRIORITY {
    {1000.0, {3,2}},
    {5000.0, {2,2}},
    {10000.0, {1,2}},
};

const std::set<std::string> ROUTE_CATEGORIES {
    cat::TRANSPORT_METRO_LINE,
    cat::TRANSPORT_BUS_ROUTE,
    cat::TRANSPORT_TRAM_ROUTE,
    cat::TRANSPORT_WATERWAY_ROUTE
};

const std::set<std::string> OPERATOR_CATEGORIES {
    cat::TRANSPORT_OPERATOR,
    cat::TRANSPORT_METRO_OPERATOR
};

const std::set<std::string> THREAD_CATEGORIES {
    cat::TRANSPORT_METRO_THREAD,
    cat::TRANSPORT_BUS_THREAD,
    cat::TRANSPORT_TRAM_THREAD,
    cat::TRANSPORT_WATERWAY_THREAD
};

const std::set<std::string> STOPS {
    cat::TRANSPORT_WATERWAY_STOP,
    cat::TRANSPORT_METRO_STATION,
    cat::TRANSPORT_STOP
};

const std::map<std::string, std::string> THREAD_TO_ELEMENT_ROLE {
    {cat::TRANSPORT_METRO_THREAD, role::METRO_PART},
    {cat::TRANSPORT_BUS_THREAD, role::BUS_PART},
    {cat::TRANSPORT_TRAM_THREAD, role::TRAM_PART},
    {cat::TRANSPORT_WATERWAY_THREAD, role::WATERWAY_PART},
};

bool isRoute(const Object& obj)
{
    return ROUTE_CATEGORIES.count(obj.categoryId());
}

bool isOperator(const Object& obj)
{
    return OPERATOR_CATEGORIES.count(obj.categoryId());
}

bool isThread(const Object& obj)
{
    return THREAD_CATEGORIES.count(obj.categoryId());
}

bool isStop(const Object& obj)
{
    return STOPS.count(obj.categoryId());
}

bool isMasstransit(const Object& obj)
{
    return
        isRoute(obj) || isOperator(obj) || isThread(obj);
}

bool isDrasticFreqChange(size_t freq1, size_t freq2)
{
    return freq1 && freq2 && ((freq1 * 2 < freq2) || (freq2 * 2 < freq1));
}

}//namespace

void checkMasstransitOperatorCreatedOrDeleted(const DiffContext& diffContext, MessageReporter& messages)
{
    if (!isAny(diffContext, isOperator)) {
        return;
    }
    if (!diffContext.oldObject()) {
        messages.report({0, 0}, "masstransit-operator-created");
    } else if (!diffContext.newObject()) {
        messages.report({0, 0}, "masstransit-operator-deleted");
    }
}

void checkMasstransitNotOperatingChange(const DiffContext& diffContext, MessageReporter& messages)
{
    if (!isAny(diffContext, isMasstransit) ||
        !diffContext.newObject() ||
        !diffContext.oldObject()) {
        return;
    }
    if (diffContext.oldObject()->attr(attr::SYS_NOT_OPERATING) !=
        diffContext.newObject()->attr(attr::SYS_NOT_OPERATING)) {
        uint32_t minorPriority = isOperator(*diffContext.oldObject()) ? 0 : 2;
        messages.report({0, minorPriority}, "masstransit-not-operating-attribute-changed");
    }
}

void checkMasstransitRouteOfficialNameChange(const DiffContext& diffContext, MessageReporter& messages)
{
    if (!isAny(diffContext, isRoute) ||
        !diffContext.newObject() ||
        !diffContext.oldObject()) {
        return;
    }
    if (officialNamesChanged(diffContext)) {
        messages.report({0, 2}, "masstransit-route-official-name-changed");
    }
}

namespace {
common::Schedule
createSchedule(const Object& freqDtObject)
{
    const std::string weekdaysStr = freqDtObject.attr(attr::FREQ_DT_DAY).value();
    const std::string departureTime = freqDtObject.attr(attr::FREQ_DT_DEPARTURE_TIME);
    return departureTime.empty()
        ? common::Schedule(
            (std::string)freqDtObject.attr(attr::FREQ_DT_DATE_START),
            (std::string)freqDtObject.attr(attr::FREQ_DT_DATE_END),
            static_cast<common::WeekdayFlags>(
                weekdaysStr.empty()
                ? DEFAULT_WEEK_EVERY_DAY
                : std::stoll(weekdaysStr)),
            (std::string)freqDtObject.attr(attr::FREQ_DT_TIME_START),
            (std::string)freqDtObject.attr(attr::FREQ_DT_TIME_END),
            freqDtObject.attr(attr::FREQ_DT_FREQ))
        : common::Schedule(
            freqDtObject.attr(attr::FREQ_DT_DATE_START),
            freqDtObject.attr(attr::FREQ_DT_DATE_END),
            static_cast<common::WeekdayFlags>(
                weekdaysStr.empty()
                ? DEFAULT_WEEK_EVERY_DAY
                : std::stoll(weekdaysStr)),
            {departureTime});
}

size_t
scheduleTripsPerDay(const common::Schedule& schedule, size_t freq)
{
    ASSERT(freq);
    size_t trips = 0;
    for (const auto& timeInterval : schedule.minuteRanges()) {
        trips += (timeInterval.second - timeInterval.first) / freq;
    }
    return trips;
}

struct RouteTripCounts
{
    RouteTripCounts():empty(true) { dayTrips.fill(0); }
    std::array<size_t, common::MAX_DAYS_PER_YEAR> dayTrips;
    bool empty;
};

RouteTripCounts
routeTripCounts(Object& routeObject, Snapshot& snapshot)
{
    RouteTripCounts counts;
    TIds threadIds = slaveRelativesIds(routeObject, {role::ASSIGNED_THREAD});
    if (threadIds.empty()) {
        return counts;
    }
    const auto threads = snapshot.objectsByIds(threadIds);
    TIds routeFreqIds;
    std::map<TId, TId> freqToThreads;
    for (const auto& thread : threads) {
        auto freqIds = slaveRelativesIds(*thread, {role::APPLIED_TO});
        routeFreqIds.insert(freqIds.begin(), freqIds.end());
        for (const auto& freqId : freqIds) {
            freqToThreads.emplace(freqId, thread->id());
        }
    }
    if (routeFreqIds.empty()) {
        return counts;
    }
    const auto freqs = snapshot.objectsByIds(routeFreqIds);
    for (const auto& freqObject : freqs) {
        const std::string freqVal = freqObject->attr(attr::FREQ_DT_FREQ).value();
        if (freqVal.empty() ||
            !std::all_of(freqVal.begin(), freqVal.end(), isdigit)) {
            continue;
        }
        const auto freqValInt = std::stoll(freqVal);
        if (!freqValInt) {
            continue;
        }
        const auto schedule = createSchedule(*freqObject);
        auto tripsPerDay = scheduleTripsPerDay(schedule, freqValInt);
        auto weekdaysMask = static_cast<size_t>(schedule.weekdays());
        for (const auto& days : schedule.dayRanges()) {
            if (days.first < 1 || days.second < 1) {
                ERROR() << "Thread: " << freqToThreads.at(freqObject->id())
                    << " freq: " << freqObject->id() << " invalid date.";
                continue;
            }
            for (int day = days.first - 1; day + 1 < days.second; ++day) {
                if ((1 << (day % 7)) &  weekdaysMask) {
                    counts.dayTrips[day] += tripsPerDay;
                    counts.empty = false;
                }
            }
        }
    }
    return counts;
}

TIds
relativesIds(TId oid, const Relations& relations, const std::set<std::string>& roles)
{
    TIds ids;
    for (const auto& relation: relations) {
        if ((relation.slaveId == oid || relation.masterId == oid)
            && (roles.empty() || roles.count(relation.role))) {
            ids.insert(relation.slaveId == oid
                ? relation.masterId
                : relation.slaveId);
        }
    }
    return ids;
}

Priority
lengthToMessagePriority(double lengthChange)
{
    for (const auto& pair : LENGTH_CHANGE_TO_PRIORITY) {
        const auto& lengthChangeThreshold = pair.first;
        const auto& priority = pair.second;
        if (lengthChangeThreshold > lengthChange) {
            return {priority.major, priority.minor, -lengthChange};
        }
    }
    return {0, 2, -lengthChange};
}

std::string
threadElemenRole(const std::string& threadCategory)
{
    auto it = THREAD_TO_ELEMENT_ROLE.find(threadCategory);
    REQUIRE(it != THREAD_TO_ELEMENT_ROLE.end(),
        "Can't determine thread element role for category:" << threadCategory);
    return it->second;
}

}//namespace


void checkMasstransitRouteSchedule(const DiffContext& diffContext, MessageReporter& messages)
{
    if (!isAny(diffContext, isRoute) ||
        !diffContext.newObject() ||
        !diffContext.oldObject()) {
        return;
    }
    const auto oldTripCounts = routeTripCounts(*diffContext.oldObject(), diffContext.oldSnapshot());
    if (oldTripCounts.empty) {
        return;
    }
    const auto newTripCounts = routeTripCounts(*diffContext.newObject(), diffContext.newSnapshot());
    if (newTripCounts.empty) {
        return;
    }
    for (size_t day = 0; day < common::MAX_DAYS_PER_YEAR; ++day) {
        if (isDrasticFreqChange(oldTripCounts.dayTrips[day], newTripCounts.dayTrips[day])) {
            messages.report({0, 2}, "masstransit-route-dayly-trips-drastic-change");
            break;
        }
    }
    for (size_t day = 0; day < common::MAX_DAYS_PER_YEAR; day += 7) {
        size_t oldWeekTrips = 0;
        size_t newWeekTrips = 0;
        for (size_t weekday = day; weekday < day + 7 && weekday < common::MAX_DAYS_PER_YEAR; ++weekday) {
            oldWeekTrips += oldTripCounts.dayTrips[weekday];
            newWeekTrips += newTripCounts.dayTrips[weekday];
        }
        if (isDrasticFreqChange(oldWeekTrips, newWeekTrips)) {
            messages.report({0, 2}, "masstransit-route-weekly-trips-drastic-change");
            break;
        }
    }
}

void checkMasstransitRouteWithThreadsModified(const DiffContext& diffContext, MessageReporter& messages)
{
    if (!isAny(diffContext, isRoute) ||
        !diffContext.newObject() ||
        !diffContext.oldObject()) {
        return;
    }
    if (slaveRelativesIds(*diffContext.oldObject(), {role::ASSIGNED_THREAD}).empty() &&
        slaveRelativesIds(*diffContext.newObject(), {role::ASSIGNED_THREAD}).empty())
    {
        return;
    }
    messages.report({1, 1}, "masstransit-route-with-threads-modified");
}

void checkMasstransitStopSignificantMove(const DiffContext& diffContext, MessageReporter& messages)
{
    if (!isAny(diffContext, isStop) ||
        !diffContext.newObject() ||
        !diffContext.oldObject()) {
        return;
    }
    geos::geom::Coordinate oldPosition;
    geos::geom::Coordinate newPosition;
    ASSERT(diffContext.oldObject()->geom()->getCentroid(oldPosition));
    ASSERT(diffContext.newObject()->geom()->getCentroid(newPosition));

    double ratio = mercatorDistanceRatio((oldPosition.y + newPosition.y) * 0.5);

    if (oldPosition.distance(newPosition) * ratio > STOP_DISTANCE_THRESHOLD_METERS) {
        messages.report({1, 2}, "masstransit-stop-significant-postion-change");
    }
}

void checkMasstransitRouteThreadsCount(const DiffContext& diffContext, MessageReporter& messages)
{
    if (!isAny(diffContext, isRoute) ||
        !diffContext.newObject() ||
        !diffContext.oldObject()) {
        return;
    }
    const auto oldCount = slaveRelativesIds(*diffContext.oldObject(), {role::ASSIGNED_THREAD}).size();
    const auto newCount = slaveRelativesIds(*diffContext.newObject(), {role::ASSIGNED_THREAD}).size();
    if (oldCount == newCount) {
        return;
    }
    messages.report({1, 2}, "masstransit-route-threads-count-" +
        (oldCount < newCount ? STR_INCREASED : STR_DECREASED));
}

void checkMasstransitOperatorRoutesCount(const DiffContext& diffContext, MessageReporter& messages)
{
    if (!isAny(diffContext, isOperator) ||
        !diffContext.newObject() ||
        !diffContext.oldObject()) {
        return;
    }
    const auto oldCount = slaveRelativesIds(*diffContext.oldObject(), {}).size();
    const auto newCount = slaveRelativesIds(*diffContext.newObject(), {}).size();
    if (oldCount == newCount) {
        return;
    }
    messages.report({1, 1}, "masstransit-operator-routes-count-" +
        (oldCount < newCount ? STR_INCREASED : STR_DECREASED));
}

void checkMasstransitTransportStopWaiypointFlagChange(const DiffContext& diffContext, MessageReporter& messages)
{
    if (!isAny(diffContext, isStop) ||
        !diffContext.newObject() ||
        !diffContext.oldObject()) {
        return;
    }
    if (diffContext.newObject()->attr(attr::TRANSPORT_STOP_WAYPOINT).as<bool>() !=
        diffContext.oldObject()->attr(attr::TRANSPORT_STOP_WAYPOINT).as<bool>()) {
        messages.report({1, 3}, "masstransit-stop-waypoint-flag-change");
    }
}

void checkMasstransitThreadCompositionChange(const DiffContext& diffContext, MessageReporter& messages)
{
    if (!isAny(diffContext, isThread) ||
        !diffContext.newObject() ||
        !diffContext.oldObject()) {
        return;
    }
    const auto threadCategoryId = diffContext.newObject()->categoryId();
    const auto elementRole = threadElemenRole(threadCategoryId);
    const auto addedElements = relativesIds(diffContext.objectId(), diffContext.relationsAdded(), {elementRole});
    const auto removedElements = relativesIds(diffContext.objectId(), diffContext.relationsDeleted(), {elementRole});
    if (addedElements.empty() && removedElements.empty()) {
        return;
    }
    auto measuredDifference = fabs(elementsLength(diffContext.newSnapshot(), addedElements) +
        elementsLength(diffContext.oldSnapshot(), removedElements));
    if (measuredDifference < THREAD_LENGTH_CHANGE_ALERT_THRESHOLD) {
        return;
    }
    messages.report(lengthToMessagePriority(measuredDifference),
        "masstransit-thread-composition-change");
}
} // namespace diffalert
} // namespace wiki
} // namespace maps
