import AppKit

/// **The shared single-zone rectangle** — the chrome behind both pickers (`SymbolGlyphControl` in
/// SymbolPicker.swift, `ColorSwatchControl` in ColorSwatchPicker.swift): a bordered, filled rounded
/// rect with exactly **one** hit zone, the whole of it. A click anywhere opens the control's popover.
///
/// ### The owner's reversal (2026-08-10, Pipeline card 5004c540, verbatim)
///
/// "lets revert to previous look of the symbol picker, no combo trigger. just a rectangle (slightly
/// oversized) clickable to show a popover with grid of symbols and colors (same popover that shows up
/// on click of combo trigger). lets do same for color..."
///
/// This retires the two-zone combo chrome this file used to hold (`ComboFieldControl`/
/// `ComboFieldMetrics`, the 2026-08-09 "the two pickers should rhyme" pass): the face/trigger split,
/// the `NSPopUpButton`-style chevron square at the trailing edge, and the face zone's own door onto a
/// *standalone* picker independent of the popover. What survives, because it was never about the
/// trigger: the bordered rounded-rect field (`drawField`), the disabled treatment (a single
/// transparency layer under both), and — because a popover is still the one way into a standalone
/// picker (`SymbolBrowserPanel`'s **More Symbols…** row, the new colour popover's **Other…** row) —
/// the `.popUpButton` accessibility role and the value text a coordinator hands over.
///
/// ### Sizing (the same ruling): "50% taller... and about 4:6 ratio of height to width"
///
/// `height` is **2.1 em** — 50% over the two-zone chrome's last-shipped 1.4 em (18pt → 27pt at the
/// standard 13pt body). `width` is `height` **times 1.5** — the 4:6 ratio stated as a multiplier on
/// `height` rather than as its own independent em figure, so the ratio holds exactly (to rounding) at
/// every body size instead of two multiples that could drift apart the way `ComboFieldMetrics`'
/// `height` and `width` once did (that file's own retired header told that story). 10-accessibility.md's
/// full-relative-scaling rule, unchanged: every figure is a multiple of the body point size.
struct PickerRectMetrics: Equatable {

    /// The control's height — 2.1 em, 27pt at the standard 13pt body.
    var height: CGFloat
    /// The control's width — `height × 1.5`, so the 4:6 ratio is exact rather than approximated by
    /// two independently rounded em figures.
    var width: CGFloat
    /// The rectangle's own radius, and the field's — a point apart so the two rounded rects run
    /// concentric. Unchanged from the two-zone chrome's own figures; nothing about the trigger's
    /// removal touches these.
    var cornerRadius: CGFloat
    var fieldRadius: CGFloat

    static func metrics(bodyPointSize: CGFloat) -> PickerRectMetrics {
        func em(_ multiple: CGFloat) -> CGFloat { max(1, (bodyPointSize * multiple).rounded()) }
        let height = em(2.1)
        let width = max(1, (height * 1.5).rounded())
        return PickerRectMetrics(
            height: height,
            width: width,
            cornerRadius: em(0.23),
            fieldRadius: em(0.31)
        )
    }

    /// The live metrics — read from the same place every other font-derived geometry in the app
    /// reads it (`CardWindowMetrics.bodyPointSize`), so a text-size change moves both rectangles with
    /// everything else.
    @MainActor
    static var current: PickerRectMetrics { metrics(bodyPointSize: CardWindowMetrics.bodyPointSize) }
}

// MARK: - The control

/// The shared rectangle. Subclasses draw whatever fills it; nothing else here is theirs to change.
///
/// Plain internal rather than `private`: both subclasses live in other files, and each is an
/// `NSViewRepresentable`'s `NSViewType`, an associated-type witness the compiler requires to be at
/// least as visible as the representable itself.
class PickerRectControl: NSControl {

    /// The geometry every pass draws from, refreshed by the representable on each SwiftUI update so
    /// a text-size change lands without recreating the view.
    var metrics: PickerRectMetrics = .metrics(bodyPointSize: 13) {
        didSet {
            guard metrics != oldValue else { return }
            invalidateIntrinsicContentSize()
            needsDisplay = true
        }
    }

    /// The one click this control answers — anywhere in its bounds, since there is only the one zone
    /// now. Opens whichever popover the caller wired.
    ///
    /// Unannotated rather than `@MainActor`: this class is an `NSResponder` subclass and therefore
    /// already main-actor isolated, so every call site is on the main actor by construction.
    var onClick: (() -> Void)?

    /// What VoiceOver reads as this control's value: the resolved symbol's name, the swatch's stored
    /// value. Set by whichever coordinator owns the control.
    var accessibilityValueText: String?

    override var isEnabled: Bool {
        get { super.isEnabled }
        set {
            super.isEnabled = newValue
            needsDisplay = true
        }
    }

    override var intrinsicContentSize: NSSize {
        NSSize(width: metrics.width, height: metrics.height)
    }

    // MARK: Drawing

    override func draw(_ dirtyRect: NSRect) {
        guard let context = NSGraphicsContext.current?.cgContext else { return }
        context.saveGState()
        defer { context.restoreGState() }
        // A transparency layer, not a flat `setAlpha` around each shape: the face's underlay, fill
        // and stroke overlap, and drawing each at reduced alpha independently would let the stroke
        // double up over the fill beneath it.
        if !isEnabled {
            context.setAlpha(0.35)
            context.beginTransparencyLayer(auxiliaryInfo: nil)
        }
        drawField()
        drawFace(in: bounds)
        if !isEnabled {
            context.endTransparencyLayer()
        }
    }

    /// The subclass's half: whatever fills the whole rectangle. The base draws nothing. Handed the
    /// control's own `bounds` — there is no zone to carve out of it any more.
    func drawFace(in rect: NSRect) {}

    /// The control's own field: a bordered, filled rounded rect over the whole bounds, under the
    /// face — `controlColor` fill, `separatorColor` hairline, the half-point inset keeping the
    /// stroke on whole pixels. Unchanged from the two-zone chrome.
    private func drawField() {
        let path = NSBezierPath(
            roundedRect: bounds.insetBy(dx: 0.5, dy: 0.5),
            xRadius: metrics.fieldRadius,
            yRadius: metrics.fieldRadius
        )
        NSColor.controlColor.setFill()
        path.fill()
        NSColor.separatorColor.setStroke()
        path.lineWidth = 1
        path.stroke()
    }

    // MARK: Events

    /// Anywhere in the control, full stop — there is only the one zone now.
    override func mouseDown(with event: NSEvent) {
        guard isEnabled else { return }
        onClick?()
    }

    override var acceptsFirstResponder: Bool { isEnabled }

    /// Space and Return open the popover — the one keyboard path into this control, the same keys
    /// the two-zone chrome's trigger answered.
    override func keyDown(with event: NSEvent) {
        guard isEnabled else {
            super.keyDown(with: event)
            return
        }
        switch event.keyCode {
        case 49, 36, 76: // Space, Return, keypad Enter
            onClick?()
        default:
            super.keyDown(with: event)
        }
    }

    // MARK: Accessibility

    /// `.popUpButton` — unchanged from the two-zone chrome: this control still opens a set of
    /// choices, it just does so from one hit zone instead of two.
    override func accessibilityRole() -> NSAccessibility.Role? { .popUpButton }

    override func accessibilityValue() -> Any? { accessibilityValueText }

    override func accessibilityPerformPress() -> Bool {
        guard isEnabled else { return false }
        onClick?()
        return true
    }
}
