import Foundation

// MARK: - PrintBlock

/// One element of a printed document, in reading order — **the whole vocabulary of what can appear on
/// paper**, and the only thing `PrintDocumentBuilder` produces.
///
/// ### Why a block list rather than an attributed string
///
/// The same argument `BodyMarkup` makes for existing: the decisions and the drawing are different
/// jobs, and a decision buried in a pile of font attributes is a decision nobody can test. Every rule
/// this feature has — which components appear, in what order, where a page break falls, which end of a
/// thread comes first — is settled here, in a value a test can compare against a literal; the renderer
/// beneath it (`PrintDocumentRenderer`) is then only ever wrong about *typography*.
///
/// `.pageBreak` is the clearest case. It is a **marker in the list**, not a paragraph of whitespace and
/// not a hint: `PrintDocumentView` splits the document at these markers and paginates each side
/// independently, so "between lanes" really starts a sheet. A test can hold the marker; nobody can hold
/// a promise about spacing.
///
/// Bodies stay **strings** here rather than parsed `BodyMarkup`. The parse belongs to the render pass
/// (which reuses `BodyMarkup.parse` and `BodyMarkupRenderer` wholesale rather than reading Markdown a
/// second way), and keeping the string means a test of the *document's structure* compares words rather
/// than block trees. What the builder does ask of the Markdown layer is the one question that is
/// structural: `BodyMarkup.isEmpty`, which decides whether there is a body to print at all.
public enum PrintBlock: Sendable, Equatable {

    /// A real sheet boundary. Never the first or last block, and never doubled — see
    /// `PrintDocumentBuilder`.
    case pageBreak

    /// The document's title, for a board print: the board's name, once, at the top. A card print has
    /// none — its own title line is the card's, and the board is named in the running head.
    case boardHeading(String)

    /// A lane's name, opening its run of cards. Board prints only.
    case laneHeading(String)

    case cardTitle(String)

    /// The icon-and-labels line. One block for the pair because it is one line on paper
    /// (`PrintOptions.includesLabels`); emitted only when at least one half has something to say.
    case cardMeta(icon: String?, labels: [String])

    /// The card's Markdown, unparsed — see the type's note.
    case cardBody(String)

    /// The thread's own small heading, carrying its count so the reader knows what follows and how
    /// much of it there is.
    case commentsHeading(count: Int)

    case comment(author: String?, created: Date?, body: String)
}

// MARK: - PrintDocumentBuilder

/// **`PrintSource` + `PrintOptions` → `[PrintBlock]`**: every rule about what a printed board or card
/// contains, in one pure function.
///
/// ### The order within a card is fixed
///
/// Title, then the icon-and-labels line, then the body, then the comments. It is not configurable and
/// the card that specifies this feature does not ask for it to be: the list is a document's natural
/// order (what is this, how is it tagged, what does it say, what was said about it), and an option that
/// let a user put the body above the title would be a page-layout program.
///
/// ### Empty is empty, all the way up
///
/// The build is bottom-up and drops what has nothing in it:
///
/// - a **card** that would emit no blocks (an untitled, unlabelled card with an empty body, or every
///   component toggled off) contributes nothing — and, crucially, does not consume a page break;
/// - a **lane** whose every card dropped out has no heading either;
/// - an **empty lane** is omitted entirely (see below).
///
/// This is what keeps `.betweenCards` from printing blank sheets for cards that had nothing on them,
/// which is the one way a page-break option can be actively harmful. It is also why the checks are here
/// rather than in the renderer: "would this print anything" is a question about content, and the
/// renderer has no business asking it.
///
/// **Empty lanes are omitted** (decision, 2026-08-09 — flagged for review on the card): an empty "Done"
/// column is real information on screen, but on paper it is a heading with nothing under it, and under
/// `.betweenLanes` it is a heading with nothing under it *on its own sheet*. A print is a document of
/// content. The alternative — heading with no break — was declined for making the page-break rule
/// conditional on a lane's contents, which is exactly the kind of clever nobody can predict.
///
/// ### Page breaks are emitted lazily, before content that follows content
///
/// Never leading (a document does not start with a sheet boundary), never trailing, never doubled. The
/// mechanism is one flag — has anything been emitted yet — consulted at each lane and each card, which
/// is what makes the three modes compose without a case analysis per pair.
public enum PrintDocumentBuilder {

    /// What an untitled card or lane prints as. The word is a **rendering**, exactly as it is
    /// everywhere else in the app (03-board-ui.md § Card face; `CardWindowHost.subtitle`), which is why
    /// it lives here and not in `PrintCard.title`.
    public static let untitled = "Untitled"

    /// The document.
    ///
    /// Options are read through `normalized` so the render's reading is the one that decides — a blank
    /// custom line does not print an empty footer line, whatever the toggle says (`PrintOptions.normalized`).
    public static func blocks(from source: PrintSource, options rawOptions: PrintOptions) -> [PrintBlock] {
        let options = rawOptions.normalized
        var blocks: [PrintBlock] = []

        /// Whether a lane has already been laid down, and therefore whether a sheet boundary is owed
        /// before the next one — the lazy-break mechanism the type's note describes. `false` until a
        /// lane has actually been emitted, which is what makes a leading break impossible rather than
        /// merely unlikely.
        var hasEmittedLane = false

        for lane in source.lanes {
            // Built before anything about the lane is emitted, so a lane whose cards all dropped out
            // takes its heading and its page break with it.
            let cardRuns = lane.cards.map { cardBlocks($0, options: options) }.filter { !$0.isEmpty }
            guard !cardRuns.isEmpty else { continue }

            if options.pageBreaks != .flow, hasEmittedLane {
                blocks.append(.pageBreak)
            }

            // A card print carries its lane for context (`PrintSource.card`), but the document is the
            // card: a lane heading over a single card would be a document about the wrong thing.
            if source.scope == .board {
                blocks.append(.laneHeading(lane.title ?? untitled))
            }
            hasEmittedLane = true

            for (offset, run) in cardRuns.enumerated() {
                if options.pageBreaks == .betweenCards, offset > 0 {
                    blocks.append(.pageBreak)
                }
                blocks.append(contentsOf: run)
            }
        }

        // **A document with no content has no heading either** — the "empty is empty, all the way up" rule
        // taken to the top: with every component switched off, or on a board whose lanes are all empty, the
        // answer is *nothing*, not a board title over blank paper (which is what `PrintCoordinator` refuses
        // to spend a sheet on).
        guard !blocks.isEmpty else { return [] }

        // The board's name goes on last so it can be conditional on there being something to name — and it
        // is deliberately outside the page-break bookkeeping above. It is the first lane's running-in title,
        // not a title page: a break counted from the heading would put the board's name alone on sheet one
        // of every print with breaks switched on, which nobody asked for and nobody would keep.
        if source.scope == .board, !source.boardTitle.isEmpty {
            blocks.insert(.boardHeading(source.boardTitle), at: 0)
        }

        return blocks
    }

    /// One card's blocks, or `[]` when the options and the card between them have nothing to print.
    ///
    /// The four component toggles are read here and nowhere else, so "which components appear" is one
    /// function rather than a condition spread across a walk.
    private static func cardBlocks(_ card: PrintCard, options: PrintOptions) -> [PrintBlock] {
        var blocks: [PrintBlock] = []

        if options.includesTitle {
            blocks.append(.cardTitle(card.title ?? untitled))
        }

        if options.includesLabels {
            // Nothing to say is nothing printed: a card with no chosen icon and no labels would
            // otherwise contribute a blank line, and a blank line is the shape a reader reads as a
            // missing value.
            let labels = card.labels.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
            if card.icon != nil || !labels.isEmpty {
                blocks.append(.cardMeta(icon: card.icon, labels: labels))
            }
        }

        // `BodyMarkup.isEmpty` rather than `body.isEmpty`, which is the same question the card window
        // asks to decide whether a body is worth previewing (05-card-window.md ▸ Mode grammar): a body
        // of one newline previews as a blank page, and prints as one too unless something says
        // otherwise.
        if options.includesBody, !BodyMarkup.isEmpty(card.body) {
            blocks.append(.cardBody(card.body))
        }

        if options.includesComments, !card.comments.isEmpty {
            let ordered = options.commentSort == .newestFirst ? Array(card.comments.reversed()) : card.comments
            blocks.append(.commentsHeading(count: ordered.count))
            for comment in ordered {
                blocks.append(.comment(author: comment.author, created: comment.created, body: comment.body))
            }
        }

        // A card whose every block dropped out prints nothing at all — not a title, not a break. See
        // the type's "Empty is empty" note; the caller relies on this being `[]` and not `[.cardTitle]`.
        return blocks
    }
}
