import AppKit
import SwiftUI

/// The colour vocabulary the `background` and `iconColor` fields are written in
/// (03-board-ui.md § Styling ▸ Capabilities): **a kebab-case palette name, or a `#RRGGBB[AA]`
/// hex**. This file is the single source for the name→hex mapping — the pathfinder's twelve icon
/// tints and twelve backgrounds, carried over verbatim as the starting point ("The pathfinder's
/// palettes (12 icon tints, 12 backgrounds) carry over"), **grown to sixteen each on 2026-08-09**.
///
/// ### The two tables are one structure
///
/// Each is **four neutrals plus a twelve-stop hue ring**, and the two rings are paired stop for
/// stop — `carnation`↔`light-cayenne`, `fern`↔`smokey-fern`, `light-teal`↔`dark-teal`,
/// `pale-violet`↔`smokey-rich-eggplant`. The tint ring is light and the background ring is deep;
/// otherwise they name the same hues in the same order. Anything added to one belongs in the other
/// at the same position, which is why the 2026-08-09 additions come in pairs.
///
/// ### How the four new stops were chosen
///
/// Not by taste — by the holes. The original ring sat at 352°, 19°, 38°, 110°, 180°, 205° and 268°
/// (plus one desaturated slate at 218° that is a neutral in all but name), and its four widest gaps
/// were 38→110, 268→352, 110→205 and 205→268. So: **yellow-green at 64°, jade at 158°, indigo at
/// 238°, magenta at 312°**, each inserted at its hue position rather than appended, so the ring
/// stays a ring. Saturation and brightness were set to each table's own existing statistics rather
/// than picked by eye, which is what keeps the new swatches *inside* the family: the tints land at
/// 2.18–4.67 contrast on white against the old set's 2.18–4.94, and the backgrounds at 6.6–14.7
/// against the old 5.7–16.6. `rich-lime` is deliberately darkened to `smokey-tangerine`'s exact
/// 2.18 — an untamed yellow-green would have been the brightest thing in the table by a wide
/// margin.
///
/// The design's AA promise is not a claim this file makes; it is `ContrastMathTests`' two computed
/// assertions over `backgrounds`, which is precisely what made growing the table a safe thing to do.
///
/// ### Lenient, never an error
///
/// A stored value is *cosmetic*, so an unrecognized one is not a load failure, not a warning, and
/// not a placeholder colour: resolution simply yields `nil` and the call site falls back to its
/// own default — no stripe for a card `background`, the standard secondary tint for an
/// `iconColor` (03 § Card face). The bytes on disk are left exactly as written until the author
/// changes them, which is what makes "custom hex is not pickable in-app but stays fully honored
/// from disk" (03 § Controls) true in both directions: curated in-app, unlimited on disk.
///
/// Names are matched **exactly** — kebab-case as the tables below spell them. `Background` is not
/// `background`, and a near-miss degrades like any other unknown value rather than guessing at
/// what the author meant.
struct PaletteColor: Identifiable, Sendable {
    /// Kebab-case, exactly as written to frontmatter.
    let name: String
    let hex: String
    var id: String { name }
}

enum Palette {

    /// Icon-tint palette (`iconColor`) — **four neutrals, then the hue ring, then the one muted
    /// slate**, in that order. See `Palette`'s own doc comment for the ring and how the four
    /// 2026-08-09 additions were placed in it.
    static let foregrounds: [PaletteColor] = [
        PaletteColor(name: "obsidian", hex: "#000000"),
        PaletteColor(name: "aluminum", hex: "#9B9B9B"),
        PaletteColor(name: "soapstone", hex: "#D5D5D5"),
        PaletteColor(name: "chalk", hex: "#FFFFFF"),
        PaletteColor(name: "carnation", hex: "#FF576C"),
        PaletteColor(name: "rich-grapefruit", hex: "#FF864C"),
        PaletteColor(name: "smokey-tangerine", hex: "#E5A334"),
        PaletteColor(name: "rich-lime", hex: "#ADB812"),
        PaletteColor(name: "fern", hex: "#50B23D"),
        PaletteColor(name: "light-jade", hex: "#21B880"),
        PaletteColor(name: "light-teal", hex: "#00B7B7"),
        PaletteColor(name: "deep-sky-blue", hex: "#0084E5"),
        PaletteColor(name: "rich-indigo", hex: "#6065E6"),
        PaletteColor(name: "pale-violet", hex: "#8C59C5"),
        PaletteColor(name: "rich-magenta", hex: "#D952BE"),
        PaletteColor(name: "deep-cool-granite", hex: "#597199"),
    ]

    /// Background palette (`background`) — the wells the style editor offers, "every pair
    /// AA-verified at design time" (03-board-ui.md § Styling ▸ Controls). The verification is
    /// `ContrastMathTests`' two computed assertions over this table, which is what makes growing it
    /// safe: a new well with no readable ink fails there rather than shipping.
    ///
    /// Same shape as `foregrounds` and paired to it hue for hue — see `Palette`'s doc comment.
    static let backgrounds: [PaletteColor] = [
        PaletteColor(name: "obsidian", hex: "#000000"),
        PaletteColor(name: "shale", hex: "#5B5B5B"),
        PaletteColor(name: "aluminum", hex: "#9B9B9B"),
        PaletteColor(name: "chalk", hex: "#FFFFFF"),
        PaletteColor(name: "light-cayenne", hex: "#B6071E"),
        PaletteColor(name: "light-mocha", hex: "#B73C14"),
        PaletteColor(name: "smokey-mocha", hex: "#674611"),
        PaletteColor(name: "smokey-lime", hex: "#5B610E"),
        PaletteColor(name: "smokey-fern", hex: "#145312"),
        PaletteColor(name: "dark-jade", hex: "#035437"),
        PaletteColor(name: "dark-teal", hex: "#005152"),
        PaletteColor(name: "smokey-ocean", hex: "#003168"),
        PaletteColor(name: "smokey-indigo", hex: "#14177A"),
        PaletteColor(name: "smokey-rich-eggplant", hex: "#290659"),
        PaletteColor(name: "smokey-magenta", hex: "#5C074B"),
        PaletteColor(name: "intense-cool-shale", hex: "#1F2E45"),
    ]

    /// The tint names the icon-colour pickers offer: `foregrounds` **minus the four neutrals and
    /// minus `deep-cool-granite`**.
    ///
    /// A symbol's *tint* wants colour — "no tint" is the None well's job, not a grey's — and the
    /// mutedest entry of the ring is the one that reads least like a deliberate choice at glyph
    /// size. Eleven remain, which is exactly what a leading None needs to fill a 4×3 grid
    /// (`SymbolPickerLayout`), and that is not a coincidence: the ring was grown to sixteen partly
    /// so this row could gain its third row (2026-08-09).
    ///
    /// Derived rather than hand-listed, so a palette addition joins the tint row automatically and
    /// a rename can never leave a dead name behind — the failure mode the old hand-written constant
    /// had.
    static let tints: [PaletteColor] = {
        let neutrals: Set<String> = ["obsidian", "aluminum", "soapstone", "chalk", "deep-cool-granite"]
        return foregrounds.filter { !neutrals.contains($0.name) }
    }()
}

// The pathfinder's panel round-trip helpers, ported below (`NSColor.paletteHexString`,
// `Palette.name(forHex:in:)`): the reusable colour-picker rectangle (`ColorSwatchPicker`,
// ColorSwatchPicker.swift) is the surface that finally needs them — a colour the *system picker*
// returns has to become a stored value the same way a palette pick already does: the palette NAME
// when the colour lands exactly on one of the twelve, the hex otherwise.

// MARK: - Menu swatches

/// A colour value drawn as a picture, for the one surface that cannot take a SwiftUI shape: **menu
/// items**. AppKit renders a menu row from its label's image and text, so the quick-style recents row
/// (03-board-ui.md § Styling ▸ Controls) needs an `NSImage` per dot where the editor's own wells are
/// ordinary views.
///
/// **`rectImage`, the wide sibling this enum used to carry, is retired (2026-08-10).** It built the
/// `NSMenuItem` images for `ColorComboView`'s dropdown rows and its collapsed face — both gone with
/// that control (`ColorSwatchPicker.swift`'s own header). `circleImage` below is the one caller left:
/// the quick-style recents row is still a menu, and a menu can still only render `Image`/`Text`.
enum PaletteSwatch {

    /// A filled dot for `value` (a palette name or `#RRGGBB[AA]` hex), hairline-bordered.
    ///
    /// The border is not decoration: the background palette contains `chalk` (`#FFFFFF`), and an
    /// unbordered white dot on a light menu is an invisible menu item — the same reason the editor's
    /// wells are stroked (10-accessibility.md's contrast stance applied to the app's own chrome).
    ///
    /// A value that resolves to nothing draws the border alone rather than a guessed colour, matching
    /// every other lenient rendering here: there is no colour, so show none.
    static func circleImage(for value: String, diameter: CGFloat = 14) -> NSImage {
        let color = Palette.nsColor(for: value)
        return NSImage(size: NSSize(width: diameter, height: diameter), flipped: false) { rect in
            let inset = rect.insetBy(dx: 0.5, dy: 0.5)
            let path = NSBezierPath(ovalIn: inset)
            // Under a translucent colour the menu's own backdrop would show through unevenly across
            // appearances; filling the standard control backdrop first makes the dot composite the
            // same way in light and dark. An opaque colour covers it completely.
            NSColor.textBackgroundColor.setFill()
            path.fill()
            color?.setFill()
            path.fill()
            NSColor.separatorColor.setStroke()
            path.lineWidth = 1
            path.stroke()
            return true
        }
    }
}

extension Palette {

    /// Resolves a stored value — a palette name, or a hand-written `#RRGGBB`/`#RRGGBBAA` hex —
    /// searching the icon tints first, then the backgrounds. **Both tables answer either field**:
    /// the split is what each *picker* offers, not a namespace, so a hand-written
    /// `background: carnation` resolves rather than reading as garbage.
    ///
    /// `nil` means unrecognized, which is a rendering instruction ("use your default"), never an
    /// error — see this file's leading comment.
    static func nsColor(for value: String) -> NSColor? {
        if value.hasPrefix("#") { return NSColor(paletteHex: value) }
        guard let hex = (foregrounds + backgrounds).first(where: { $0.name == value })?.hex else { return nil }
        return NSColor(paletteHex: hex)
    }

    /// SwiftUI variant of `nsColor(for:)` — what the views actually call.
    static func color(named name: String) -> Color? {
        nsColor(for: name).map { Color(nsColor: $0) }
    }

    /// The lenient read of a whole frontmatter field: a missing or malformed `background` /
    /// `iconColor` resolves exactly like an unrecognized one — there is no colour, so use the
    /// default.
    ///
    /// Folding all three `FieldValue` shapes into one `nil` mirrors `ItemSymbol.name(_:fallback:)`
    /// and keeps every call site free of the distinction, which no renderer has a use for.
    static func color(for field: FieldValue<String>) -> Color? {
        guard let value = field.value else { return nil }
        return color(named: value)
    }

    /// The name of `palette`'s entry whose hex matches `hex`, case-insensitively — the pathfinder's
    /// round trip, ported for `SystemColorPanel`'s own panel handoff: a colour the system picker
    /// returns comes back as `NSColor.paletteHexString`'s canonical `#RRGGBB[AA]`, and this is what
    /// turns that back into "the user picked Light Cayenne" instead of leaving it as an anonymous
    /// hex. `nil` when nothing in `palette` matches, which the caller reads as "store the hex instead."
    static func name(forHex hex: String, in palette: [PaletteColor]) -> String? {
        palette.first { $0.hex.caseInsensitiveCompare(hex) == .orderedSame }?.name
    }
}

// MARK: - Hex ↔ colour

extension NSColor {
    /// `#RRGGBB` or `#RRGGBBAA` → `NSColor` in **sRGB** — the colour space the hex digits name,
    /// so a value hand-written from a screenshot or a design tool renders as the same colour the
    /// author sampled. Returns `nil` for anything else: a missing `#`, a short or long digit run,
    /// or a non-hex character.
    convenience init?(paletteHex hex: String) {
        var string = hex
        if string.hasPrefix("#") { string.removeFirst() }
        var alpha: CGFloat = 1
        if string.count == 8 {
            guard let alphaByte = UInt32(string.suffix(2), radix: 16) else { return nil }
            alpha = CGFloat(alphaByte) / 255
            string.removeLast(2)
        }
        guard string.count == 6, let value = UInt32(string, radix: 16) else { return nil }
        self.init(
            srgbRed: CGFloat((value >> 16) & 0xFF) / 255,
            green: CGFloat((value >> 8) & 0xFF) / 255,
            blue: CGFloat(value & 0xFF) / 255,
            alpha: alpha
        )
    }

    /// The reverse of `init?(paletteHex:)`: `#RRGGBB`, or `#RRGGBBAA` when the colour is
    /// translucent — full opacity collapses to six digits rather than a trailing `FF`, so a colour
    /// that round-trips through the panel without the user touching the opacity slider is written
    /// exactly as a curated palette entry would be. `nil` only for a colour space **sRGB** cannot
    /// convert into, which no picker swatch or palette entry here ever is.
    var paletteHexString: String? {
        guard let srgb = usingColorSpace(.sRGB) else { return nil }
        let red = Int((srgb.redComponent * 255).rounded())
        let green = Int((srgb.greenComponent * 255).rounded())
        let blue = Int((srgb.blueComponent * 255).rounded())
        let alpha = Int((srgb.alphaComponent * 255).rounded())
        return alpha >= 255
            ? String(format: "#%02X%02X%02X", red, green, blue)
            : String(format: "#%02X%02X%02X%02X", red, green, blue, alpha)
    }
}
