using TeleWave.Domain.Programming;

namespace TeleWave.Application.Programming.Templates;

/// <summary>
/// Покрытие вещательных суток слотами. Общая часть проверок сетки и автогенерации: считать дыры
/// они обязаны одинаково, иначе генератор «закроет всё», а проверки продолжат ругаться на то же
/// самое место.
///
/// Всё меряется смещением от начала вещательных суток канала, а не часами на стене: сутки канала
/// начинаются в <c>DayStartTime</c>, и ночной блок с 00:00 до 06:00 относится к предыдущему дню.
/// </summary>
public static class GridCoverage
{
    public const int MinutesInDay = 24 * 60;

    /// <summary>Дни недели в порядке показа: неделя начинается с понедельника, воскресенье последнее.</summary>
    public static readonly int[] Week = [1, 2, 3, 4, 5, 6, 0];

    /// <summary>Смещение времени от начала вещательных суток, минуты.</summary>
    public static int OffsetInDay(TimeOnly time, TimeOnly dayStart)
    {
        var diff = (int)(time.ToTimeSpan() - dayStart.ToTimeSpan()).TotalMinutes;
        return diff >= 0 ? diff : diff + MinutesInDay;
    }

    /// <summary>Время в сутках канала по смещению от начала вещательных суток.</summary>
    public static TimeOnly AtOffset(int offsetMinutes, TimeOnly dayStart)
    {
        var minutes = ((int)dayStart.ToTimeSpan().TotalMinutes + offsetMinutes) % MinutesInDay;
        return new TimeOnly(minutes / 60, minutes % 60);
    }

    /// <summary>Слот действует в указанный день: без дня недели — каждый день.</summary>
    public static bool AppliesTo(Slot slot, int weekday) =>
        slot.Weekday is null || slot.Weekday == weekday;

    /// <summary>
    /// Интервалы суток указанного дня, не покрытые ни одним из слотов. Границы — смещения от начала
    /// вещательных суток; интервал, вылезающий за сутки, обрезается по ним.
    /// </summary>
    public static List<(int From, int To)> Gaps(
        IEnumerable<Slot> slots,
        int weekday,
        TimeOnly dayStart
    )
    {
        var intervals = slots
            .Where(s => AppliesTo(s, weekday))
            .Select(s =>
            {
                var from = OffsetInDay(s.TargetStart, dayStart);
                return (From: from, To: from + s.TargetDurationMinutes);
            })
            .OrderBy(i => i.From)
            .ToList();

        var gaps = new List<(int From, int To)>();
        var cursor = 0;
        foreach (var interval in intervals)
        {
            if (interval.From > cursor)
                gaps.Add((cursor, interval.From));
            cursor = Math.Max(cursor, Math.Min(interval.To, MinutesInDay));
        }

        if (cursor < MinutesInDay)
            gaps.Add((cursor, MinutesInDay));

        return gaps;
    }

    /// <summary>Интервалы, свободные во всех наборах сразу.</summary>
    public static List<(int From, int To)> Intersect(
        IEnumerable<IReadOnlyList<(int From, int To)>> sets
    )
    {
        var mask = new bool[MinutesInDay];
        Array.Fill(mask, true);

        foreach (var set in sets)
        {
            var current = ToMask(set);
            for (var minute = 0; minute < MinutesInDay; minute++)
                mask[minute] &= current[minute];
        }

        return FromMask(mask);
    }

    /// <summary>Что останется от интервалов, если вычесть из них другие.</summary>
    public static List<(int From, int To)> Subtract(
        IReadOnlyList<(int From, int To)> from,
        IReadOnlyList<(int From, int To)> minus
    )
    {
        var mask = ToMask(from);
        var cut = ToMask(minus);

        for (var minute = 0; minute < MinutesInDay; minute++)
            mask[minute] &= !cut[minute];

        return FromMask(mask);
    }

    /// <summary>
    /// Интервалы считаются поминутной маской: суток всего 1440 минут, а очевидная правильность
    /// здесь дороже экономии — на этих операциях держится то, какие куски сетки застраиваются.
    /// </summary>
    private static bool[] ToMask(IReadOnlyList<(int From, int To)> intervals)
    {
        var mask = new bool[MinutesInDay];

        foreach (var (from, to) in intervals)
            for (var minute = Math.Max(0, from); minute < Math.Min(to, MinutesInDay); minute++)
                mask[minute] = true;

        return mask;
    }

    private static List<(int From, int To)> FromMask(bool[] mask)
    {
        var result = new List<(int From, int To)>();
        var start = -1;

        for (var minute = 0; minute <= MinutesInDay; minute++)
        {
            var covered = minute < MinutesInDay && mask[minute];
            if (covered && start < 0)
                start = minute;
            else if (!covered && start >= 0)
            {
                result.Add((start, minute));
                start = -1;
            }
        }

        return result;
    }
}
