#include <yandex/maps/wiki/common/schedule.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <maps/libs/common/include/exception.h>

#include <algorithm>
#include <iterator>
#include <sstream>
#include <string>
#include <utility>

namespace maps::wiki::common {

namespace {

int
minutes(const Time& time)
{
    return
        static_cast<int>(time.hour()) * MINUTES_PER_HOUR +
        static_cast<int>(time.minute());
}

WeekdayFlags
intersection(WeekdayFlags lhs, WeekdayFlags rhs)
{
    return lhs & rhs;
}

WeekdayFlags
difference(WeekdayFlags lhs, WeekdayFlags rhs)
{
    return lhs & ~rhs;
}

std::optional<IntRange>
intersection(const IntRange& lhs, const IntRange& rhs)
{
    auto left = std::max(lhs.first, rhs.first);
    auto right = std::min(lhs.second, rhs.second);
    return left > right ? std::nullopt : std::optional<IntRange>({left, right});
}

IntRanges
difference(const IntRange& lhs, const IntRange& rhs)
{
    auto inter = intersection(lhs, rhs);
    if (!inter) {
        return {lhs};
    } else {
        IntRanges result;
        if (lhs.first < inter->first) {
            result.emplace_back(lhs.first, inter->first - 1);
        }
        if (inter->second < lhs.second) {
            result.emplace_back(inter->second + 1, lhs.second);
        }
        return result;
    }
}

IntRanges
intersection(const IntRanges& lhs, const IntRanges& rhs)
{
    IntRanges result;
    for (const auto& range1 : lhs) {
        for (const auto& range2 : rhs) {
            auto inter = intersection(range1, range2);
            if (inter) {
                result.push_back(*inter);
            }
        }
    }
    return result;
}

IntRanges
difference(const IntRanges& lhs, const IntRanges& rhs)
{
    if (rhs.empty()) {
        return lhs;
    }
    IntRanges result;
    for (const auto& range1 : lhs) {
        // Every range from RHS is being sequentially
        // subtracted from every range in LHS
        IntRanges curDiff = {range1};
        for (const auto& range2 : rhs) {
            IntRanges temp;
            for (const auto& rangeDiff : curDiff) {
                auto diff = difference(rangeDiff, range2);
                temp.insert(temp.end(), diff.begin(), diff.end());
            }
            curDiff = temp;
        }
        result.insert(result.end(), curDiff.begin(), curDiff.end());
    }
    return result;
}

// Returns true, if schedules intersect in the same day of week
bool
directlyIntersects(const Schedule& lhs, const Schedule& rhs)
{
    if (WeekdayFlags::None == (lhs.weekdays() & rhs.weekdays())) {
        return false;
    }
    if (intersection(lhs.dayRanges(), rhs.dayRanges()).empty()) {
        return false;
    }
    const auto timeIntersections = intersection(lhs.minuteRanges(), rhs.minuteRanges());
    if (timeIntersections.empty()) {
        return false;
    }
    if (!lhs.departureTimes().empty() || !rhs.departureTimes().empty()) {
        return true;
    }
    for (const auto& [start, end] : timeIntersections) {
        if (start != end) {
            return true;
        }
    }
    return false;
};

// Returns true, if schedules intersect in adjacent days of week
// because left schedule continues after midnight
// and right schedule starts to operate in that time
bool
overnightIntersects(const Schedule& lhs, const Schedule& rhs)
{
    // If there are departure times - it is not a frequency schedule
    if (!lhs.departureTimes().empty()) {
        return false;
    }
    // If there is a single minute range - there is no overnight
    if (lhs.minuteRanges().size() < 2) {
        return false;
    }

    // Circular binary shift by 1 bit for 7-bit value
    // https://en.wikipedia.org/wiki/Circular_shift#Implementing_circular_shifts
    auto lhsNextWeekdays = static_cast<int>(lhs.weekdays());
    lhsNextWeekdays = (lhsNextWeekdays << 1 | lhsNextWeekdays >> (7 - 1)) & 127;

    if (WeekdayFlags::None == (static_cast<WeekdayFlags>(lhsNextWeekdays) & rhs.weekdays())) {
        return false;
    }
    if (intersection(lhs.dayRanges(), rhs.dayRanges()).empty() &&
        lhs.dayRanges().back().second + 1 != rhs.dayRanges().front().first) {
            return false;
    }

    // minuteRanges() for overnight frequency schedule contains
    // time interval before midnight (i. e. in the same day) at .front() and
    // time interval after midnight (i. e. in the next day) at .back()
    const auto rhsMinuteRanges = rhs.minuteRanges();
    const auto timeIntersections = intersection(
        {lhs.minuteRanges().back()},
        !rhsMinuteRanges.empty() && rhs.departureTimes().empty()
            ? IntRanges{rhsMinuteRanges.front()}
            : rhsMinuteRanges);
    if (timeIntersections.empty()) {
        return false;
    }
    for (const auto& [start, end] : timeIntersections) {
        if (start < lhs.minuteRanges().back().second) {
            return true;
        }
    }
    return false;
};

} // namespace

Schedule::Schedule(
    const std::string& startDate, const std::string& endDate,
    WeekdayFlags weekdays,
    const std::string& startTime, const std::string& endTime,
    size_t frequency)
    : startDate_(startDate, YEAR_START_DATE)
    , endDate_(endDate, YEAR_END_DATE)
    , weekdays_(weekdays)
    , frequency_(frequency)
{
    if (!startTime.empty()) {
        startTime_ = Time(startTime);
    }
    if (!endTime.empty()) {
        endTime_ = Time(endTime);
    }
}

Schedule::Schedule(
    const std::string& startDate, const std::string& endDate,
    WeekdayFlags weekdays,
    const std::vector<std::string>& departureTimes)
    : startDate_(startDate, YEAR_START_DATE)
    , endDate_(endDate, YEAR_END_DATE)
    , weekdays_(weekdays)
    , frequency_(0)
{
    for (const auto& depTime : departureTimes) {
        departureTimes_.emplace_back(depTime);
    }
}

Schedule::Schedule(
    const Date& startDate, const Date& endDate,
    WeekdayFlags weekdays,
    const Time& startTime, const Time& endTime,
    size_t frequency)
    : startDate_(startDate)
    , endDate_(endDate)
    , weekdays_(weekdays)
    , startTime_(startTime)
    , endTime_(endTime)
    , frequency_(frequency)
{
}

Schedule::Schedule(
    const Date& startDate, const Date& endDate,
    WeekdayFlags weekdays,
    std::vector<Time> departureTimes)
    : startDate_(startDate)
    , endDate_(endDate)
    , weekdays_(weekdays)
    , frequency_(0)
    , departureTimes_(std::move(departureTimes))
{
}

IntRanges
Schedule::dayRanges() const
{
    auto start = startDate_.dayOfYear();
    auto end = endDate_.dayOfYear();
    if (start <= end) {
        return {{start, end}};
    } else {
        return {{start, MAX_DAYS_PER_YEAR}, {1, end}};
    }
}

IntRanges
Schedule::minuteRanges() const
{
    IntRanges ranges;
    if (startTime_ && endTime_) {
        auto start = minutes(*startTime_);
        auto end = minutes(*endTime_);
        if (start <= end) {
            ranges.push_back({start, end});
        } else {
            ranges.push_back({start, HOURS_PER_DAY * MINUTES_PER_HOUR});
            ranges.push_back({0, end});
        }
    }
    if (!departureTimes_.empty()) {
        for (const auto& depTime : departureTimes_) {
            ranges.push_back({minutes(depTime), minutes(depTime)});
        }
    }
    return ranges;
}

bool
Schedule::intersects(const Schedule& other) const
{
    return directlyIntersects(*this, other) ||
           overnightIntersects(*this, other) ||
           overnightIntersects(other, *this);
}

std::ostream& operator<<(std::ostream& os, const Schedule& schedule)
{
    os << "Start date: " << schedule.startDate()
       << ", end date: " << schedule.endDate();

    os << ", weekdays:";
    for (int dayOfWeek = 1; dayOfWeek <= DAYS_PER_WEEK; ++dayOfWeek) {
        if ((schedule.weekdays() & weekdayFromOrdinal(dayOfWeek)) != WeekdayFlags::None)
        {
            os << " " << std::to_string(dayOfWeek);
        }
    }

    if (schedule.startTime() && schedule.endTime()) {
        os << ", start time: " << *schedule.startTime()
           << ", end time: " << *schedule.endTime();
    }

    if (schedule.frequency() != 0) {
        os << ", frequency: " << schedule.frequency();
    }

    auto printTime = [](const Time& time) {
        std::ostringstream ss;
        ss << time;
        return ss.str();
    };

    if (!schedule.departureTimes().empty()) {
        os << ", departure times: "
           << join(schedule.departureTimes(), printTime, ",");
    }

    return os;
}

namespace {

enum class MergeMode
{
    Union,
    Replace
};

struct MergedSchedules
{
    Schedules left;
    Schedules right;
    Schedules leftRight;
};

void addSchedule(
    Schedules& result,
    const IntRange& dayRange,
    WeekdayFlags weekdays,
    const Schedule& baseSchedule)
{
    if (baseSchedule.departureTimes().empty()) {
        result.emplace_back(
            Date(dayRange.first), Date(dayRange.second), weekdays,
            *baseSchedule.startTime(), *baseSchedule.endTime(), baseSchedule.frequency());
    } else {
        result.emplace_back(
            Date(dayRange.first), Date(dayRange.second), weekdays,
            baseSchedule.departureTimes());
    }
};

MergedSchedules
mergeSchedules(
    const Schedule& lhs,
    const Schedule& rhs,
    MergeMode mode)
{
    if (mode == MergeMode::Union &&
        (lhs.departureTimes().empty() || rhs.departureTimes().empty()))
    {
        REQUIRE(!lhs.intersects(rhs),
            "Union with intersecting interval schedule is not supported");
        return {{lhs}, {rhs}, {}};
    }

    auto daysLeftRight = intersection(lhs.dayRanges(), rhs.dayRanges());
    auto weekdaysLeftRight = intersection(lhs.weekdays(), rhs.weekdays());

    if (daysLeftRight.empty() || !weekdaysLeftRight) {
        return {{lhs}, {rhs}, {}};
    }

    auto daysLeft = difference(lhs.dayRanges(), daysLeftRight);
    auto daysRight = difference(rhs.dayRanges(), daysLeftRight);

    auto weekdaysLeft = difference(lhs.weekdays(), weekdaysLeftRight);
    auto weekdaysRight = difference(rhs.weekdays(), weekdaysLeftRight);

    MergedSchedules result;

    // Left only dates & weekdays : preserve left schedule
    for (const auto& dayRange : daysLeft) {
        addSchedule(result.left, dayRange, lhs.weekdays(), lhs);
    }
    // Right only dates & weekdays : preserve right schedule
    for (const auto& dayRange : daysRight) {
        addSchedule(result.right, dayRange, rhs.weekdays(), rhs);
    }

    for (const auto& dayRange : daysLeftRight) {
        // Shared dates & left only weekdays : preserve left schedule
        if (!!weekdaysLeft) {
            addSchedule(result.leftRight, dayRange, weekdaysLeft, lhs);
        }
        // Shared dates & right only weekdays : preserve right schedule
        if (!!weekdaysRight) {
            addSchedule(result.leftRight, dayRange, weekdaysRight, rhs);
        }

        // Shared days & weekdays : merge departure times / replace by right schedule.
        if (mode == MergeMode::Replace) {
            addSchedule(result.leftRight, dayRange, weekdaysLeftRight, rhs);
        } else {
            std::vector<Time> mergedDepartureTimes;
            std::set_union(
                lhs.departureTimes().begin(), lhs.departureTimes().end(),
                rhs.departureTimes().begin(), rhs.departureTimes().end(),
                std::back_inserter(mergedDepartureTimes));

            result.leftRight.emplace_back(
                Date(dayRange.first), Date(dayRange.second), weekdaysLeftRight,
                mergedDepartureTimes);
        }
    }
    return result;
}

} // namespace

Schedules
unionSchedules(const Schedules& lhs, const Schedules& rhs)
{
    Schedules result = lhs;
    Schedules right = rhs;

    for (auto it1 = result.begin(); it1 != result.end(); ++it1) {
        for (auto it2 = right.begin(); it2 != right.end();) {
            auto curMergedSchedule = mergeSchedules(*it1, *it2, MergeMode::Union);
            if (curMergedSchedule.leftRight.empty()) {
                ++it2;
                continue;
            }
            it1 = result.insert(
                result.erase(it1),
                curMergedSchedule.left.begin(),
                curMergedSchedule.left.end());
            it1 = result.insert(
                it1,
                curMergedSchedule.leftRight.begin(),
                curMergedSchedule.leftRight.end());
            it2 = right.insert(
                right.erase(it2),
                curMergedSchedule.right.begin(),
                curMergedSchedule.right.end());
        }
    }
    result.insert(result.end(), right.begin(), right.end());

    std::sort(result.begin(), result.end());
    return result;
}

Schedules
replaceSchedules(const Schedules& lhs, const Schedules& rhs)
{
    Schedules result = lhs;
    Schedules right = rhs;

    for (auto it2 = right.begin(); it2 != right.end(); ++it2) {
        for (auto it1 = result.begin(); it1 != result.end();) {
            auto curMergedSchedule = mergeSchedules(*it1, *it2, MergeMode::Replace);
            if (curMergedSchedule.leftRight.empty()) {
                ++it1;
                continue;
            }
            it1 = result.insert(
                result.erase(it1),
                curMergedSchedule.left.begin(),
                curMergedSchedule.left.end());
        }
    }
    result.insert(result.end(), right.begin(), right.end());

    std::sort(result.begin(), result.end());
    return result;
}

} // namespace maps::wiki::common
