import AppKit
import SwiftUI

/// **The colour rectangle** — a plain clickable swatch (`ColorSwatchControl`, `PickerRect.swift`'s
/// shared chrome) that opens a SwiftUI popover holding the background palette grid: **None**, the
/// role's own sixteen wells, and an **Other…** row onto the system Colors panel.
///
/// ### The owner's reversal (2026-08-10, Pipeline card 5004c540, verbatim)
///
/// "...lets do same for color, just a 4:6 ratio rectangle for color, no trigger, a grid of colors, and
/// a choice for 'Other' or 'More' to pull up generic color picker."
///
/// This retires `ColorComboView` and its `NSMenu` dropdown outright — the swatch-faced two-zone combo
/// this file used to hold, whose trigger popped **None**, the role's own entries, an off-palette
/// "current" row and **Other…** as menu items. The dropdown's whole pure model (`ColorComboItem`,
/// `ColorComboMatch`, `ColorComboMenu`, `ColorComboModel.match`/`.menu`/`.displayName`/
/// `.normalizedHex`) went with it: nothing left calls a menu into being, so nothing here builds one.
///
/// `ColorSwatchRole` is the one surviving piece of the old pure model — which of the two palettes a
/// swatch offers. The new grid's wells are `role.palette`, matched by **literal name**, exactly the
/// rule `StyleEditorView.backgroundWells` already applies to the Style… popover's own background grid
/// — reusing that grid's own layout and matching convention rather than the retired dropdown's hex-
/// normalizing cleverness, so the app's two background grids agree about what "selected" means. An
/// off-palette hex that happens to equal a palette colour numerically no longer highlights that well;
/// it did in the old dropdown and does not in the Style… popover's grid either.
///
/// ### The "Other…" row and the debounce
///
/// Opens `SharedColorPanelSession` — the same shared, pre-debounced (~400ms) Colors-panel takeover
/// `StyleEditorView.openBackgroundPanel` and `SymbolPicker.openTintPanel` already use, because this
/// swatch's popover is transient exactly like theirs: opening the panel dismisses the popover, so a
/// per-view-owned panel session (what `ColorComboView`'s coordinator held) would have nothing left to
/// talk to. The write always goes through **`onPanelChange`, never `onChange`'s funnel** — "no
/// StyleRecents feed on drag ticks" (`StyleCommand.apply`'s recents feed is keyed to `background`
/// specifically, and a Colors-panel pick must never enter it, even the one settled value the debounce
/// finally delivers) — `StyleEditorView.openBackgroundPanel`'s own rule, restated here since this door
/// opens the identical session.

// MARK: - Role

/// Which of the two palettes a swatch offers — `Palette.backgrounds` for `background`, the only role
/// wired to a caller today; `Palette.foregrounds` stays for symmetry with `ColorSwatchRole`'s own
/// pre-2026-08-10 shape, unused but free.
enum ColorSwatchRole: Sendable, Equatable {
    case background
    case foreground

    var palette: [PaletteColor] {
        switch self {
        case .background: Palette.backgrounds
        case .foreground: Palette.foregrounds
        }
    }
}

// MARK: - Geometry

/// The popover's own grid geometry — `SymbolPickerLayout`'s pattern, restated for a plain colour
/// grid: the well side and spacing read off `StyleEditorLayout`'s statics, scaled up by the same
/// `gridScale` the symbol popover's grid uses, and **seven columns** — `StyleEditorLayout.popover`'s
/// own settled fit for None + sixteen wells (7 + 7 + 3), reused here rather than re-derived, since it
/// is exactly this count of wells again.
struct ColorSwatchPopoverLayout: Equatable {

    static let columns = 7
    /// The grid's enlargement over the style editor's well size — `SymbolPickerLayout.gridScale`'s
    /// own figure, so the two new-since-2026-08-10 popovers read at the same scale.
    static let gridScale: CGFloat = 1.3

    var wellSide: CGFloat
    var wellSpacing: CGFloat
    /// The popover's own inset, on all four sides, and the gap between the grid and the **Other…**
    /// row below it.
    var contentPadding: CGFloat
    var gridWidth: CGFloat
    /// The grid's width plus its padding on both sides — the popover's fixed width.
    var popoverWidth: CGFloat

    static func metrics(bodyPointSize: CGFloat) -> ColorSwatchPopoverLayout {
        let side = (StyleEditorLayout.wellSide(bodyPointSize: bodyPointSize) * gridScale).rounded()
        let spacing = StyleEditorLayout.wellSpacing(bodyPointSize: bodyPointSize)
        let padding = StyleEditorLayout.sectionSpacing(bodyPointSize: bodyPointSize)
        let gridWidth = (side * CGFloat(columns) + spacing * CGFloat(columns - 1)).rounded()
        return ColorSwatchPopoverLayout(
            wellSide: side,
            wellSpacing: spacing,
            contentPadding: padding,
            gridWidth: gridWidth,
            popoverWidth: (gridWidth + padding * 2).rounded()
        )
    }
}

// MARK: - The picker

/// The public surface: a colour rectangle plus its popover, `SymbolPicker`'s own shape one dimension
/// over. No store, no undo stack, no target — `onChange`/`onPanelChange` are the whole contract, the
/// same split `ColorComboView`'s `onChange`/`onPanelChange` offered, so `CardStyleSection` keeps its
/// two-closure wiring across the control swap.
struct ColorSwatchPicker: View {

    let role: ColorSwatchRole
    /// The raw stored value — a palette name or a hand-written hex, exactly as the frontmatter field
    /// carries it.
    let value: String?
    var isEnabled: Bool = true
    /// A discrete pick — a grid well or **None**. Fired once, synchronously; the caller commits it
    /// immediately and decides for itself whether it feeds `StyleRecents`.
    let onChange: (String?) -> Void
    /// A value settled from the Colors panel's **Other…** door — always raw, never through the
    /// recents funnel (this file's own header).
    let onPanelChange: (String?) -> Void

    /// The popover's presented flag — view-local `@State`, `SymbolPicker.isPopoverPresented`'s own
    /// reasoning: a plain SwiftUI `.popover` nests correctly inside another one (the board popover
    /// mounts a symbol rectangle the identical way), where a hand-driven `NSPopover` would not.
    @State private var isPopoverPresented = false

    var body: some View {
        ColorSwatchView(value: value, isEnabled: isEnabled, onClick: { isPopoverPresented = true })
            .help(helpText)
            .accessibilityLabel(helpText)
            .popover(isPresented: $isPopoverPresented, arrowEdge: .bottom) {
                ColorSwatchPopoverContent(
                    current: value,
                    palette: role.palette,
                    layout: .metrics(bodyPointSize: CardWindowMetrics.bodyPointSize),
                    onSelect: { picked in
                        isPopoverPresented = false
                        onChange(picked)
                    },
                    onPickOther: {
                        isPopoverPresented = false
                        openColorPanel()
                    }
                )
            }
    }

    private var helpText: String {
        role == .background ? "Background" : "Color"
    }

    /// **Other…** — the shared, debounced Colors panel, for the same outlives-the-popover reason
    /// `SymbolPicker.openTintPanel` opens it: dismissing this popover would take a view-owned
    /// coordinator with it before the panel finished streaming.
    private func openColorPanel() {
        SharedColorPanelSession.present(
            seed: value.flatMap(Palette.nsColor(for:)),
            matching: role.palette
        ) { picked in
            onPanelChange(picked)
        }
    }
}

// MARK: - The AppKit bridge

/// `ColorSwatchPicker`'s face: a `ColorSwatchControl` with its one zone wired to the caller's
/// closure. Deliberately dumb, `SymbolPicker`'s own `SymbolGlyphView` split: every decision about
/// what a click means lives in SwiftUI, where the popover's presented state already does.
private struct ColorSwatchView: NSViewRepresentable {

    let value: String?
    let isEnabled: Bool
    let onClick: () -> Void

    func makeNSView(context: Context) -> ColorSwatchControl {
        ColorSwatchControl(frame: .zero)
    }

    func updateNSView(_ control: ColorSwatchControl, context: Context) {
        control.metrics = .current
        control.isEnabled = isEnabled
        control.swatchValue = value
        control.accessibilityValueText = value ?? "None"
        control.onClick = onClick
    }

    /// Obeys an explicit finite proposal when SwiftUI hands one over, else falls back to the
    /// control's own intrinsic width — `SymbolGlyphView.sizeThatFits`'s rule, restated so the two
    /// rectangles answer a proposal identically.
    func sizeThatFits(_ proposal: ProposedViewSize, nsView: ColorSwatchControl, context: Context) -> CGSize? {
        let width: CGFloat
        if let proposed = proposal.width, proposed.isFinite {
            width = proposed
        } else {
            width = nsView.intrinsicContentSize.width
        }
        return CGSize(width: width, height: nsView.intrinsicContentSize.height)
    }
}

// MARK: - The control

/// `PickerRectControl` with a **swatch** filling the whole face — the colour half of the shared
/// chrome, and the whole of what is specific to colour about it.
final class ColorSwatchControl: PickerRectControl {

    /// The value the face currently draws — a palette name or a hand-written hex, handed over on
    /// every SwiftUI update.
    var swatchValue: String? {
        didSet {
            guard swatchValue != oldValue else { return }
            needsDisplay = true
        }
    }

    /// The colour rect: a `textBackgroundColor` underlay so a translucent stored colour composites
    /// the same way in light and dark, the resolved colour on top, a `separatorColor` hairline
    /// stroke last. `nil`/unresolvable value → underlay + stroke only, the same "there is no colour,
    /// so show none" rule the retired combo's face drew — straight into the control's own graphics
    /// context, exactly as before, rather than through `PaletteSwatch.rectImage`'s intermediate
    /// `NSImage` (that function had exactly one caller, the retired dropdown's menu rows, and is
    /// gone with it).
    override func drawFace(in rect: NSRect) {
        guard rect.width > 0, rect.height > 0 else { return }
        let path = NSBezierPath(roundedRect: rect, xRadius: metrics.cornerRadius, yRadius: metrics.cornerRadius)
        NSColor.textBackgroundColor.setFill()
        path.fill()
        if let swatchValue, let color = Palette.nsColor(for: swatchValue) {
            color.setFill()
            path.fill()
        }
        NSColor.separatorColor.setStroke()
        path.lineWidth = 1
        path.stroke()
    }
}

// MARK: - The popover's content

/// The popover's body: the palette grid, then **Other…** — `SymbolPickerPopoverContent`'s shape, one
/// grid narrower (no search, no second grid): this popover has exactly one dimension to offer.
private struct ColorSwatchPopoverContent: View {

    let current: String?
    let palette: [PaletteColor]
    let layout: ColorSwatchPopoverLayout
    let onSelect: (String?) -> Void
    let onPickOther: () -> Void

    var body: some View {
        VStack(alignment: .leading, spacing: layout.wellSpacing) {
            ColorSwatchWellGrid(current: current, palette: palette, layout: layout, onSelect: onSelect)
            // Trailing-aligned link row onto the standalone panel — `SymbolPickerPopoverContent.
            // deeperRow`'s own shape, restated: a well that is not a value is a well that has to be
            // learned, and the row already spells this door as a titled affordance.
            HStack(spacing: 0) {
                Spacer(minLength: 0)
                Button("Other…", action: onPickOther)
                    .buttonStyle(.link)
                    .font(.caption)
            }
        }
        .padding(layout.contentPadding)
        .frame(width: layout.popoverWidth)
    }
}

// MARK: - Wells

/// One well in the grid: a palette name, or `nil` for the leading **None**.
private struct ColorSwatchWell: Identifiable {
    let id: Int
    let name: String?
    let label: String
    let isSelected: Bool
}

/// The palette grid: the leading **None** well and `palette`'s own entries, seven wide —
/// `StyleWellGrid`'s pattern (StyleEditor.swift), mirrored rather than shared for the reason
/// `SymbolWellGrid`/`SymbolColorGrid` (SymbolPicker.swift) already mirror it instead of widening that
/// file's `private` access for one caller outside it.
private struct ColorSwatchWellGrid: View {

    let current: String?
    let palette: [PaletteColor]
    let layout: ColorSwatchPopoverLayout
    let onSelect: (String?) -> Void

    @FocusState private var focused: Int?
    @Environment(\.colorSchemeContrast) private var contrast

    var body: some View {
        LazyVGrid(
            columns: Array(
                repeating: GridItem(.flexible(minimum: layout.wellSide), spacing: layout.wellSpacing),
                count: ColorSwatchPopoverLayout.columns
            ),
            spacing: layout.wellSpacing
        ) {
            ForEach(wells) { well in
                Button {
                    onSelect(well.name)
                } label: {
                    swatch(well.name.flatMap(Palette.color(named:)))
                        .overlay(selectionRing(well.isSelected))
                        .contentShape(Rectangle())
                }
                .buttonStyle(.plain)
                .focusable()
                .focused($focused, equals: well.id)
                .help(well.label)
                .accessibilityLabel(well.label)
                .accessibilityAddTraits(well.isSelected ? [.isSelected] : [])
            }
        }
        .onKeyPress(keys: [.leftArrow, .rightArrow, .upArrow, .downArrow], phases: .down) { press in
            move(press.key)
        }
    }

    /// **None** leads, selected whenever `current` is absent — literal-name matching only
    /// (`ColorSwatchPicker`'s own header), so an off-palette hex leaves every well unselected exactly
    /// as `StyleEditorView.backgroundWells` already does.
    private var wells: [ColorSwatchWell] {
        var wells = [ColorSwatchWell(id: 0, name: nil, label: "None", isSelected: current == nil)]
        for (index, color) in palette.enumerated() {
            wells.append(ColorSwatchWell(
                id: index + 1,
                name: color.name,
                label: color.name,
                isSelected: current == color.name
            ))
        }
        return wells
    }

    /// A colour swatch, always stroked (`chalk` is `#FFFFFF`; an unbordered white swatch is an
    /// invisible control), with the corner-to-corner slash standing in for "no colour" on the None
    /// well — `StyleWellFace`'s own vocabulary, restated.
    private func swatch(_ color: Color?) -> some View {
        RoundedRectangle(cornerRadius: cornerRadius)
            .fill(color ?? Color(nsColor: .textBackgroundColor))
            .overlay {
                if color == nil {
                    ColorSwatchNoneStrike(inset: max(1, (layout.wellSide * 0.15).rounded()))
                        .stroke(.secondary, lineWidth: Accommodations.borderWidth(1, contrast: contrast))
                }
            }
            .overlay(
                RoundedRectangle(cornerRadius: cornerRadius)
                    .strokeBorder(.separator, lineWidth: Accommodations.borderWidth(1, contrast: contrast))
            )
            .frame(width: layout.wellSide, height: layout.wellSide)
    }

    private var cornerRadius: CGFloat { max(1, (layout.wellSide * 0.2).rounded()) }

    private func selectionRing(_ isSelected: Bool) -> some View {
        RoundedRectangle(cornerRadius: cornerRadius)
            .strokeBorder(
                isSelected ? AnyShapeStyle(Color.accentColor) : AnyShapeStyle(.clear),
                lineWidth: Accommodations.borderWidth(2, contrast: contrast)
            )
            .padding(-Accommodations.borderWidth(2, contrast: contrast) / 2)
    }

    /// `SymbolWellGrid.move(_:)`'s rule, at this grid's own seven columns.
    private func move(_ key: KeyEquivalent) -> KeyPress.Result {
        let delta: Int
        switch key {
        case .leftArrow: delta = -1
        case .rightArrow: delta = 1
        case .upArrow: delta = -ColorSwatchPopoverLayout.columns
        case .downArrow: delta = ColorSwatchPopoverLayout.columns
        default: return .ignored
        }
        let current = focused ?? 0
        let next = min(max(0, current + delta), wells.count - 1)
        focused = next
        return .handled
    }
}

/// The None well's corner-to-corner slash — `StyleEditor.swift`'s `NoValueStrike`/`SymbolPicker.
/// swift`'s `ColorNoneStrike`, restated as a third sibling for the same reason those two are already
/// siblings rather than one shared exported shape.
private struct ColorSwatchNoneStrike: Shape {
    let inset: CGFloat

    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.minX + inset, y: rect.maxY - inset))
        path.addLine(to: CGPoint(x: rect.maxX - inset, y: rect.minY + inset))
        return path
    }
}
