using SkiaSharp;
using Jiaowu.Api.Infrastructure.Pdf;

namespace Jiaowu.Api.Infrastructure.Timetables;

public static class TimetablePdfExporter
{
    private const float PageWidth = 842;
    private const float PageHeight = 595;
    private const float Margin = 24;
    private static readonly SKColor Ink = new(27, 53, 74);
    private static readonly SKColor Muted = new(92, 111, 126);
    private static readonly SKColor Accent = new(32, 105, 99);
    private static readonly SKColor Rule = new(206, 220, 226);
    private static readonly SKColor Pale = new(241, 247, 247);

    public static byte[] Create(TimetableData timetable, string? fontPath = null) => Create([timetable], fontPath);

    public static byte[] Create(IReadOnlyCollection<TimetableData> timetables, string? fontPath = null)
    {
        if (timetables.Count == 0)
            throw new ArgumentException("至少需要一张课表。", nameof(timetables));

        using var typeface = PdfTypefaceResolver.ResolveChineseTypeface(fontPath);
        using var stream = new MemoryStream();
        using var document = SKDocument.CreatePdf(stream);
        var page = 0;
        foreach (var timetable in timetables)
        {
            var periods = Periods(timetable);
            foreach (var periodPage in periods.Chunk(9))
                DrawPage(document, typeface, timetable, periodPage, ++page);
            foreach (var coursePage in timetable.FlexibleCourses.Chunk(9))
                DrawFlexibleCoursesPage(document, typeface, timetable, coursePage, ++page);
        }
        document.Close();
        return stream.ToArray();
    }

    private static void DrawPage(
        SKDocument document,
        SKTypeface typeface,
        TimetableData timetable,
        IReadOnlyList<int> periods,
        int page)
    {
        using var canvas = document.BeginPage(PageWidth, PageHeight);
        canvas.Clear(SKColors.White);
        using var frame = new SKPaint { Color = Accent, Style = SKPaintStyle.Stroke, StrokeWidth = 1.1f };
        canvas.DrawRect(14, 14, PageWidth - 28, PageHeight - 28, frame);

        DrawText(canvas, typeface, timetable.Subject.Name, Margin, 53, 19, Ink, bold: true);
        DrawText(canvas, typeface, "课程表", PageWidth - Margin, 53, 11, Accent, SKTextAlign.Right, true);
        DrawText(canvas, typeface,
            $"{timetable.Subject.Code} · {timetable.Term.Name}" +
            (timetable.Plan is null ? "" : $" · {timetable.Plan.Version} · {PlanStatus(timetable.Plan.Status)}"),
            Margin, 74, 9, Muted);
        DrawText(canvas, typeface,
            $"导出时间：{DateTime.Now:yyyy-MM-dd HH:mm}",
            PageWidth - Margin, 74, 8, Muted, SKTextAlign.Right);
        using var titleRule = new SKPaint { Color = Rule, StrokeWidth = .8f };
        canvas.DrawLine(Margin, 86, PageWidth - Margin, 86, titleRule);

        var widths = new[] { 84f, 101f, 101f, 101f, 101f, 101f, 101f, 101f };
        var headers = new[] { "节次 / 时间", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日" };
        const float headerHeight = 24;
        const float rowHeight = 46;
        var y = 103f;
        using var header = new SKPaint { Color = Accent, Style = SKPaintStyle.Fill };
        using var grid = new SKPaint { Color = Rule, Style = SKPaintStyle.Stroke, StrokeWidth = .55f };
        canvas.DrawRect(Margin, y, widths.Sum(), headerHeight, header);
        var x = Margin;
        for (var column = 0; column < headers.Length; column++)
        {
            DrawText(canvas, typeface, headers[column], x + widths[column] / 2, y + 16, 8.5f,
                SKColors.White, SKTextAlign.Center, true);
            x += widths[column];
        }

        var entries = timetable.Entries.Concat(timetable.ExamEntries).Concat(timetable.ExperimentEntries).ToArray();
        for (var rowIndex = 0; rowIndex < periods.Count; rowIndex++)
        {
            var period = periods[rowIndex];
            var rowY = y + headerHeight + rowIndex * rowHeight;
            using var leftFill = new SKPaint { Color = Pale, Style = SKPaintStyle.Fill };
            canvas.DrawRect(Margin, rowY, widths[0], rowHeight, leftFill);
            var slot = timetable.Slots.FirstOrDefault(item => item.PeriodNumber == period);
            DrawText(canvas, typeface, $"第 {period} 节", Margin + widths[0] / 2, rowY + 18, 8.5f,
                Ink, SKTextAlign.Center, true);
            if (slot is not null)
                DrawText(canvas, typeface, $"{slot.StartsAt:HH\\:mm}-{slot.EndsAt:HH\\:mm}",
                    Margin + widths[0] / 2, rowY + 32, 7, Muted, SKTextAlign.Center);

            x = Margin + widths[0];
            for (var day = 1; day <= 7; day++)
            {
                var cellEntries = entries.Where(item =>
                    item.DayOfWeek == day &&
                    item.StartPeriod <= period &&
                    item.StartPeriod + item.PeriodCount - 1 >= period).ToArray();
                if (cellEntries.Length > 0)
                {
                    using var fill = new SKPaint
                    {
                        Color = cellEntries.Any(item => item.IsExam)
                            ? new SKColor(255, 243, 230)
                            : cellEntries.Any(item => item.IsExperiment)
                                ? new SKColor(232, 246, 241)
                                : new SKColor(234, 245, 244),
                        Style = SKPaintStyle.Fill
                    };
                    canvas.DrawRect(x, rowY, widths[day], rowHeight, fill);
                    var startingEntries = cellEntries
                        .Where(item => item.StartPeriod == period)
                        .ToArray();
                    if (startingEntries.Length > 0)
                    {
                        var spanRows = ConsecutiveSpan(periods, rowIndex, startingEntries);
                        DrawCellText(canvas, typeface, startingEntries, x, rowY, widths[day],
                            rowHeight * spanRows);
                    }
                }
                x += widths[day];
            }
        }

        var tableHeight = headerHeight + periods.Count * rowHeight;
        canvas.DrawRect(Margin, y, widths.Sum(), tableHeight, grid);
        x = Margin;
        for (var column = 0; column < widths.Length - 1; column++)
        {
            x += widths[column];
            canvas.DrawLine(x, y, x, y + tableHeight, grid);
        }
        for (var row = 0; row < periods.Count; row++)
        {
            var lineY = y + headerHeight + row * rowHeight;
            canvas.DrawLine(Margin, lineY, Margin + widths[0], lineY, grid);
            x = Margin + widths[0];
            for (var day = 1; day <= 7; day++)
            {
                var crossesBoundary = row > 0 && periods[row] == periods[row - 1] + 1 &&
                    entries.Any(item => item.DayOfWeek == day &&
                        item.StartPeriod < periods[row] &&
                        item.StartPeriod + item.PeriodCount - 1 >= periods[row]);
                if (!crossesBoundary)
                    canvas.DrawLine(x, lineY, x + widths[day], lineY, grid);
                x += widths[day];
            }
        }

        DrawFooter(canvas, typeface, page, titleRule);
        document.EndPage();
    }

    private static void DrawCellText(
        SKCanvas canvas,
        SKTypeface typeface,
        IReadOnlyList<TimetableEntryDto> entries,
        float x,
        float y,
        float width,
        float height)
    {
        var text = string.Join("\n", entries.Select(EntryText));
        const float fontSize = 6.5f;
        const float lineHeight = 7.8f;
        var maxLines = Math.Max(1, (int)Math.Floor((height - 10) / lineHeight));
        var lines = WrapText(typeface, text, fontSize, width - 8).Take(maxLines).ToArray();
        for (var index = 0; index < lines.Length; index++)
            DrawText(canvas, typeface, lines[index], x + 4, y + 9 + index * lineHeight, fontSize, Ink);
    }

    private static void DrawFlexibleCoursesPage(
        SKDocument document,
        SKTypeface typeface,
        TimetableData timetable,
        IReadOnlyList<FlexibleCourseDto> courses,
        int page)
    {
        using var canvas = document.BeginPage(PageWidth, PageHeight);
        canvas.Clear(SKColors.White);
        using var frame = new SKPaint { Color = Accent, Style = SKPaintStyle.Stroke, StrokeWidth = 1.1f };
        using var titleRule = new SKPaint { Color = Rule, StrokeWidth = .8f };
        using var heading = new SKPaint { Color = new SKColor(128, 92, 39), Style = SKPaintStyle.Fill };
        canvas.DrawRect(14, 14, PageWidth - 28, PageHeight - 28, frame);
        DrawText(canvas, typeface, timetable.Subject.Name, Margin, 53, 19, Ink, bold: true);
        DrawText(canvas, typeface, "非排时课程", PageWidth - Margin, 53, 11, Accent, SKTextAlign.Right, true);
        DrawText(canvas, typeface, $"{timetable.Subject.Code} · {timetable.Term.Name}", Margin, 74, 9, Muted);
        canvas.DrawLine(Margin, 86, PageWidth - Margin, 86, titleRule);

        const float contentY = 107;
        const float rowHeight = 43;
        canvas.DrawRoundRect(new SKRect(Margin, contentY, PageWidth - Margin, contentY + 26), 3, 3, heading);
        DrawText(canvas, typeface, "非排时课程（不占正常上课时间与场地）", Margin + 12, contentY + 17, 9,
            SKColors.White, bold: true);
        for (var index = 0; index < courses.Count; index++)
        {
            var course = courses[index];
            var rowY = contentY + 26 + index * rowHeight;
            using var fill = new SKPaint
            {
                Color = index % 2 == 0 ? new SKColor(255, 248, 234) : SKColors.White,
                Style = SKPaintStyle.Fill
            };
            canvas.DrawRect(Margin, rowY, PageWidth - Margin * 2, rowHeight, fill);
            DrawText(canvas, typeface, $"{course.CourseCode} · {course.CourseName}", Margin + 12, rowY + 16,
                9, Ink, bold: true);
            DrawText(canvas, typeface,
                $"教师：{string.Join('、', course.TeacherNames)} · 第 {course.StartWeek}-{course.EndWeek} 周 · 每周 {course.WeeklyHours} 学时",
                Margin + 12, rowY + 31, 8, Muted);
            canvas.DrawLine(Margin, rowY + rowHeight, PageWidth - Margin, rowY + rowHeight, titleRule);
        }

        DrawFooter(canvas, typeface, page, titleRule);
        document.EndPage();
    }

    private static IReadOnlyList<int> Periods(TimetableData timetable)
    {
        var entries = timetable.Entries.Concat(timetable.ExamEntries).Concat(timetable.ExperimentEntries);
        var periods = timetable.Slots.Select(item => item.PeriodNumber)
            .Concat(entries.SelectMany(item => Enumerable.Range(item.StartPeriod, item.PeriodCount)))
            .Distinct().Order().ToArray();
        return periods.Length == 0 ? Enumerable.Range(1, 8).ToArray() : periods;
    }

    private static string EntryText(TimetableEntryDto entry) =>
        entry.IsExperiment
            ? $"【实验】{entry.ExperimentProjectName}\n" +
              $"{entry.CourseName} · {ArrangementMode(entry)}\n" +
              $"实验日：{entry.ExperimentDate:yyyy/M/d} · 第 {entry.StartPeriod}-{entry.StartPeriod + entry.PeriodCount - 1} 节\n" +
              $"{string.Join('、', entry.TeacherNames)} · {Location(entry)}"
            : entry.IsExam
            ? $"【考试】{entry.CourseName}\n" +
              $"考试日：{entry.ExamDate:yyyy/M/d} · 第 {entry.StartPeriod}-{entry.StartPeriod + entry.PeriodCount - 1} 节\n" +
              $"{string.Join('、', entry.TeacherNames)} · {Location(entry)}"
            : $"{(entry.Kind == Domain.Academic.ScheduleEntryKind.Experiment ? "【实验课】" : "")}{entry.CourseName}\n" +
              $"{string.Join('、', entry.TeacherNames)} · {Location(entry)}\n" +
              $"第 {entry.StartWeek}-{entry.EndWeek} 周";

    private static string ArrangementMode(TimetableEntryDto entry) => entry.ExperimentArrangementMode switch
    {
        Domain.Academic.ExperimentArrangementMode.Centralized => "集中安排",
        Domain.Academic.ExperimentArrangementMode.SelfScheduled => "自主预约",
        _ => "实验安排"
    };

    private static string Location(TimetableEntryDto entry) => string.Join(" · ", new[]
    {
        entry.CampusName,
        entry.BuildingName,
        entry.ClassroomName
    }.Where(item => !string.IsNullOrWhiteSpace(item)));

    private static IReadOnlyList<string> WrapText(SKTypeface typeface, string text, float size, float width)
    {
        using var font = new SKFont(typeface, size);
        var lines = new List<string>();
        foreach (var paragraph in text.Split('\n'))
        {
            var current = "";
            foreach (var character in paragraph)
            {
                var candidate = current + character;
                if (current.Length > 0 && font.MeasureText(candidate) > width)
                {
                    lines.Add(current);
                    current = character.ToString();
                }
                else current = candidate;
            }
            if (current.Length > 0) lines.Add(current);
        }
        return lines;
    }

    private static int ConsecutiveSpan(
        IReadOnlyList<int> periods,
        int startIndex,
        IReadOnlyCollection<TimetableEntryDto> entries)
    {
        var endPeriod = entries.Max(entry => entry.StartPeriod + entry.PeriodCount - 1);
        var count = 1;
        for (var index = startIndex + 1;
             index < periods.Count && periods[index] == periods[index - 1] + 1 && periods[index] <= endPeriod;
             index++)
            count++;
        return count;
    }

    private static void DrawText(SKCanvas canvas, SKTypeface typeface, string text, float x, float y,
        float size, SKColor color, SKTextAlign align = SKTextAlign.Left, bool bold = false)
    {
        using var font = new SKFont(typeface, size) { Embolden = bold };
        using var paint = new SKPaint { Color = color, IsAntialias = true };
        canvas.DrawText(text, x, y, align, font, paint);
    }

    private static void DrawFooter(SKCanvas canvas, SKTypeface typeface, int page, SKPaint titleRule)
    {
        var footerY = PageHeight - 31;
        canvas.DrawLine(Margin, footerY - 12, PageWidth - Margin, footerY - 12, titleRule);
        DrawText(canvas, typeface, "明序教务 · 课表导出", Margin, footerY, 7.5f, Muted);
        DrawText(canvas, typeface, $"第 {page} 页", PageWidth - Margin, footerY, 7.5f, Muted,
            SKTextAlign.Right);
    }

    private static string PlanStatus(Domain.Academic.SchedulePlanStatus status) => status switch
    {
        Domain.Academic.SchedulePlanStatus.Draft => "草稿",
        Domain.Academic.SchedulePlanStatus.Published => "已发布",
        _ => "已归档"
    };
}
