import AppKit

// MARK: - Identifiers

extension NSToolbarItem.Identifier {
    static let boardSearch = Self("board.search")
    static let boardNewCard = Self("board.newCard")
    static let boardNewLane = Self("board.newLane")
    static let boardUndo = Self("board.undo")
    static let boardRedo = Self("board.redo")
    static let boardShowTrash = Self("board.showTrash")
}

// MARK: - The board window's toolbar

/// The board window's toolbar (03-board-ui.md ▸ Toolbar).
///
/// ### The default is one item, and the catalog is five more
///
/// "**Board window default: the search field, nothing else** — trailing, the one default item; the
/// titlebar stays clean." The flexible space ahead of it is what "trailing" means to `NSToolbar`.
///
/// "**Catalog** (available via Customize): New Card, New Lane, Undo, Redo …, Show Trash (toggle
/// state matching the View menu checkmark)." Every one of them is the *same command* as its menu row
/// — the predicates below are the rows' own (`BoardStore.newCardTarget`, `acceptsBoardMutations`),
/// and the two actions with consequences call the rows' own functions (`beginNewCard`,
/// `setTrashVisible`) rather than restating them. That is what makes "toolbars are pure enhancement"
/// true of the code: removing every item removes nothing but a shortcut to a menu row.
///
/// "The board popover deliberately has **no toolbar item** — the window-title widget is its
/// committed home" — so there is no Board Info entry here, and its absence is pinned by a test.
///
/// ### Undo and Redo are the responder chain's, exactly as the menu's are
///
/// The app ships no Undo/Redo rows of its own: those are the standard Edit-menu items, nil-target
/// `undo:`/`redo:` resolved up the responder chain (`KanbanApp.menuCommands`). The toolbar items
/// carry the same actions with the same nil target, so "matching their menu items" (03) is not a
/// predicate written here — it is literally the same validation. Both reach the board window, whose
/// `windowWillReturnUndoManager` hands back the session's `BoardUndoManager`, and both therefore
/// enable exactly when that board has a step to cross and no read-only lock stands
/// (13-native-undo.md ▸ Rules). **Every board has undo in every tier**, so there is no tier-shaped
/// disablement to write: 03's parenthetical about boards without undo is 06's *git* substrate, which
/// only a Pro subscription binds.
///
/// Their labels are the design's one exception to the menu-title rule: `NSUndoManager` rewrites the
/// *menu* titles as the stack changes ("Undo Move Card"), which a toolbar label does not track, so
/// these two are built from static labels (`ToolbarItemSpec.staticLabel`).
@MainActor
enum BoardToolbar {

    /// Shared by every board window, which is what makes the user's arrangement the *app's* rather
    /// than one window's — Finder's behaviour, and the reason the identifier is a constant.
    static let identifier = "dev.rzen.indie.Kanban.board"

    /// "The search field, nothing else — trailing, the one default item."
    static let defaultItems: [NSToolbarItem.Identifier] = [.flexibleSpace, .boardSearch]

    static func specs(store: BoardStore, search: BoardSearchPresentation) -> [ToolbarItemSpec] {
        [
            .mirroring(
                menuTitle: "New Card",
                identifier: .boardNewCard,
                symbol: "doc.badge.plus",
                behavior: .button(
                    isEnabled: { [weak store] in store?.newCardTarget != nil },
                    perform: { [weak store] in store?.beginNewCard() }
                )
            ),
            .mirroring(
                menuTitle: "New Lane",
                identifier: .boardNewLane,
                symbol: "rectangle.stack.badge.plus",
                behavior: .button(
                    isEnabled: { [weak store] in store?.acceptsBoardMutations == true },
                    perform: { [weak store] in store?.createLane() }
                )
            ),
            .staticLabel(
                "Undo",
                identifier: .boardUndo,
                symbol: "arrow.uturn.backward",
                behavior: .responderAction(NSSelectorFromString("undo:"))
            ),
            .staticLabel(
                "Redo",
                identifier: .boardRedo,
                symbol: "arrow.uturn.forward",
                behavior: .responderAction(NSSelectorFromString("redo:"))
            ),
            .mirroring(
                menuTitle: "Show Trash",
                identifier: .boardShowTrash,
                symbol: "trash",
                behavior: .toggle(
                    isEnabled: { [weak store] in store != nil },
                    isOn: { [weak store] in store?.transient.isTrashVisible == true },
                    setOn: { [weak store] shown in store?.setTrashVisible(shown) }
                )
            ),
            .staticLabel(
                "Search",
                identifier: .boardSearch,
                symbol: nil,
                // The field's width in *characters* rather than points (10-accessibility.md ▸ Text
                // scaling: "no fixed point sizes") — the same 17 ems the transient bar's field takes
                // (`BoardSearchBar`), so ⌘F's two homes are one width whichever the user is in.
                behavior: .control(width: BoardMetrics.em(17, bodyPointSize: BoardMetrics.bodyPointSize)) {
                    [weak store] willBeInserted in
                    guard willBeInserted, let store else {
                        return BoardSearchFieldController.makePaletteField()
                    }
                    return BoardSearchFieldController.makeField(
                        store: store,
                        presentation: search,
                        home: .toolbar
                    )
                }
            ),
        ]
    }

    /// The window's toolbar, wired to tell the search presentation where its field currently lives —
    /// which is the whole input to ⌘F's transient fallback (03: "with the field removed from the
    /// toolbar, invoking it surfaces the field transiently until the search clears").
    static func controller(store: BoardStore, search: BoardSearchPresentation) -> WindowToolbarController {
        let controller = WindowToolbarController(
            identifier: identifier,
            specs: specs(store: store, search: search),
            defaults: defaultItems
        )
        controller.onInstalledItemsChanged = { [weak search] identifiers in
            search?.isInstalledInToolbar = identifiers.contains(.boardSearch)
        }
        return controller
    }
}
