// SPDX-FileCopyrightText: 2026 Brent Daignault
// SPDX-License-Identifier: GPL-3.0-or-later

import CoreGraphics
import Foundation

// ── Page numbering ───────────────────────────────────────────────────────────
//
// Fixed pages (cover, committee, callers) are logical pages 1–3 in both
// outputs.  Members and New Dancers occupy as many pages as their layout's
// rows × columns allow.  The schedule is always forced onto the very last
// page: for portrait that's simply the page right after New Dancers (no
// padding needed); for booklet it's the final padded page, which may leave
// genuinely blank pages between New Dancers and the schedule.

struct PageMap {
    let pCover = 1
    let pCommittee = 2
    let pCallers = 3
    let pMembersStart = 4
    let memberPages: Int
    let pNewDancersStart: Int
    let newDancerPages: Int
    let pSchedule: Int
    let contentPageCount: Int
    let totalPageCount: Int

    init(layout: Layout, memberCount: Int, newDancerCount: Int, mode: RenderMode) {
        let perPage = layout.rows * layout.nCols

        memberPages = memberCount == 0
            ? 1 : Int(ceil(CGFloat(memberCount) / CGFloat(perPage)))
        pNewDancersStart = pMembersStart + memberPages

        newDancerPages = newDancerCount == 0
            ? 1 : Int(ceil(CGFloat(newDancerCount) / CGFloat(perPage)))
        let afterNewDancers = pNewDancersStart + newDancerPages
        contentPageCount = afterNewDancers

        switch mode {
        case .portrait:
            pSchedule = afterNewDancers
            totalPageCount = afterNewDancers
        case .booklet:
            totalPageCount = Imposition.paddedPageCount(contentPages: afterNewDancers)
            pSchedule = totalPageCount
        }
    }
}

// ── RosterDocument ───────────────────────────────────────────────────────────
//
// Holds all input data and knows how to draw any single logical page's
// content into a context, given the page map and layout for whichever
// output (booklet or portrait) is being produced.  This is the layer shared
// between both renderers — it has no knowledge of physical sheets, sides,
// or imposition at all.

struct RosterDocument {
    let cfg: Config
    let callers: [Member]
    let cuers: [Member]
    let members: [Member]
    let newDancers: [Member]

    func draw(logicalPage lp: Int, map: PageMap, layout: Layout, into ctx: CGContext) {
        switch lp {
        case map.pCover:
            PDFDrawing.coverPage(ctx, pageW: layout.pageW, pageH: layout.pageH,
                                mx: layout.mx, my: layout.my, cfg: cfg)

        case map.pCommittee:
            PDFDrawing.imagePage(ctx, path: cfg.committeeImage,
                                 placeholder: "[committee image not found]",
                                 pageW: layout.pageW, pageH: layout.pageH,
                                 lmargin: layout.mx, tmargin: layout.my)

        case map.pCallers:
            drawCallersAndCuers(into: ctx, layout: layout)

        case map.pSchedule:
            PDFDrawing.imagePage(ctx, path: cfg.scheduleImage,
                                 placeholder: "[schedule image not found]",
                                 pageW: layout.pageW, pageH: layout.pageH,
                                 lmargin: layout.mx, tmargin: layout.my)

        case map.pMembersStart..<(map.pMembersStart + map.memberPages):
            drawList(title: "Members", list: members,
                    pageIndex: lp - map.pMembersStart, into: ctx, layout: layout)

        case map.pNewDancersStart..<(map.pNewDancersStart + map.newDancerPages):
            drawList(title: "New Dancers", list: newDancers,
                    pageIndex: lp - map.pNewDancersStart, into: ctx, layout: layout)

        default:
            break   // blank pad page — nothing to draw
        }
    }

    // MARK: - Page 3: Caller then Cuer, stacked vertically

    private func drawCallersAndCuers(into ctx: CGContext, layout: Layout) {
        var colX = [CGFloat](repeating: 0, count: layout.nCols)
        for c in 0..<layout.nCols {
            colX[c] = layout.mx + CGFloat(c) * (layout.colW + Metrics.cgap)
        }
        var y = layout.pageH - layout.my

        if !callers.isEmpty {
            y = PDFDrawing.sectionHeader(ctx, pageW: layout.pageW, mx: layout.mx, y: y,
                                         title: "Caller")
            var col = 0
            var colY = [CGFloat](repeating: y, count: layout.nCols)
            for m in callers {
                PDFDrawing.label(ctx, colX: colX[col], colW: layout.colW, y: colY[col], member: m)
                colY[col] -= Metrics.labelH + Metrics.labelGap
                col = (col + 1) % layout.nCols
            }
            let used = colY.map { y - $0 }.max() ?? 0
            y -= used + 18.0
        }

        if !cuers.isEmpty {
            y = PDFDrawing.sectionHeader(ctx, pageW: layout.pageW, mx: layout.mx, y: y,
                                         title: "Cuer")
            var col = 0
            var colY = [CGFloat](repeating: y, count: layout.nCols)
            for m in cuers {
                PDFDrawing.label(ctx, colX: colX[col], colW: layout.colW, y: colY[col], member: m)
                colY[col] -= Metrics.labelH + Metrics.labelGap
                col = (col + 1) % layout.nCols
            }
        }
    }

    // MARK: - Members / New Dancers list pages
    //
    //   rowOrder    — labels alternate columns left-to-right across each row
    //   columnOrder — each column fills completely before the next

    private func drawList(title: String, list: [Member], pageIndex: Int,
                          into ctx: CGContext, layout: Layout) {
        let perPage = layout.rows * layout.nCols
        var colX = [CGFloat](repeating: 0, count: layout.nCols)
        for c in 0..<layout.nCols {
            colX[c] = layout.mx + CGFloat(c) * (layout.colW + Metrics.cgap)
        }

        var y = layout.pageH - layout.my
        if pageIndex == 0 {
            y = PDFDrawing.sectionHeader(ctx, pageW: layout.pageW, mx: layout.mx, y: y,
                                         title: title)
        } else {
            y -= 4.0
        }

        let start = pageIndex * perPage
        let end = min(start + perPage, list.count)
        guard start < end else { return }

        switch cfg.labelSequence {
        case .columnOrder:
            var idx = start
            for c in 0..<layout.nCols where idx < end {
                var cy = y
                var r = 0
                while r < layout.rows, idx < end {
                    PDFDrawing.label(ctx, colX: colX[c], colW: layout.colW, y: cy, member: list[idx])
                    cy -= Metrics.labelH + Metrics.labelGap
                    idx += 1
                    r += 1
                }
            }

        case .rowOrder:
            var colY = [CGFloat](repeating: y, count: layout.nCols)
            var col = 0
            for i in start..<end {
                PDFDrawing.label(ctx, colX: colX[col], colW: layout.colW, y: colY[col], member: list[i])
                colY[col] -= Metrics.labelH + Metrics.labelGap
                col = (col + 1) % layout.nCols
            }
        }
    }
}
