import AppKit
import SwiftUI

/// The system's **visual accommodations**, as one named surface — `Motion`'s sibling
/// (10-accessibility.md ▸ Text scaling & visual accommodations). Motion owns Reduce Motion; this owns
/// the other two settings the design commits to, plus the one that decides whether a focus ring is
/// furniture or a lifeline:
///
/// - **Increase Contrast** — "strengthens borders and the selection indicator";
/// - **Reduce Transparency** — "glass underlays go solid, wherever they appear";
/// - **Full Keyboard Access** — "the board is one tab stop with arrow-key navigation within".
///
/// The rule this type exists to enforce is `Motion`'s, transplanted: **no call site anywhere decides
/// for itself what an accommodation means.** A view that draws a border asks for a border width by
/// meaning and passes in what the environment reports; what "increased" does to that width is
/// decided once, here, so a new bordered surface inherits the answer instead of inventing one.
///
/// ### Why the decisions are pure functions of an environment value
///
/// Same reason `Motion.reloadAnimates` is: so a test can hold them still. Nothing below renders, and
/// the claims 10-accessibility.md actually makes — a heavier ring under Increase Contrast, a solid
/// underlay under Reduce Transparency — are assertable only if the decision is separable from the
/// drawing. The two `AnyShapeStyle`-producing families therefore go through small `Equatable` enums
/// (`Underlay`, `Wash`) exactly as the transitions go through `Motion.Appearance`, because
/// `AnyShapeStyle` is opaque and a claim about it would be untestable.
///
/// ### Where the values come from
///
/// Views read `@Environment(\.colorSchemeContrast)` and `@Environment(\.accessibilityReduceTransparency)`
/// and pass them in. Code with no environment to read asks AppKit the same questions
/// (`prefersIncreasedContrast`, `prefersReducedTransparency`) — `Motion.prefersReducedMotion`'s
/// pattern, for its reason.
enum Accommodations {

    // MARK: - Increase Contrast

    /// A stroke's width: `base` normally, **one point heavier** when the user has asked for stronger
    /// borders (10-accessibility.md: "Increase Contrast strengthens borders and the selection
    /// indicator").
    ///
    /// One point rather than a multiplier, deliberately. The board's strokes span 1pt (a well's
    /// separator hairline) to 3pt (the template chooser's selection frame), and a factor that made
    /// the hairline legible would turn the chooser's frame into a slab. A flat point is what the
    /// system's own controls do under the setting, and it is monotone: a heavier stroke stays
    /// heavier than a lighter one, so the visual hierarchy the widths encode survives the setting.
    ///
    /// It is *not* scaled by the text size, and that is a ruling rather than an oversight: a border
    /// is a hairline against a background, not a glyph — AppKit's own controls keep their stroke
    /// weights across text sizes, and a 3pt selection ring at a large text size would read as a
    /// fill.
    static func borderWidth(_ base: CGFloat, contrast: ColorSchemeContrast) -> CGFloat {
        contrast == .increased ? base + 1 : base
    }

    /// Whether a plate that normally floats on its fill alone draws an **outline** at all.
    ///
    /// This is the other half of "strengthens borders", and the half that is easy to miss: a card
    /// face and a lane plate carry no resting border — they are a fill against the board background,
    /// which is exactly the distinction Increase Contrast exists to rescue for a user who cannot see
    /// it. So under the setting they gain a hairline in the separator colour, and the selection ring
    /// above stays what it always was: the *accent*-coloured one, still unambiguous against it.
    ///
    /// Chrome, never information: nothing about the board's meaning changes, so nothing has to be
    /// said differently to VoiceOver when this flips.
    static func drawsRestingBorder(contrast: ColorSchemeContrast) -> Bool {
        contrast == .increased
    }

    /// A decorative accent drawn at reduced alpha — the marquee band's border, the drag shadow's
    /// dashes, the new-card editor's well — taken to **full strength** under Increase Contrast.
    ///
    /// Alpha is the other way a border can be weak, and a width bump alone would leave a 55%-alpha
    /// dashed outline just as hard to see two points wider.
    static func accentOpacity(_ base: Double, contrast: ColorSchemeContrast) -> Double {
        contrast == .increased ? 1 : base
    }

    /// Whether a selected lane draws its **halo** — the soft accent glow bleeding outward from the
    /// plate's hairline edge (`LaneView.selectionStroke`), the system focus ring's vocabulary at
    /// selection strength.
    ///
    /// Under Increase Contrast the answer is no, and that is the setting's own logic rather than a
    /// retreat: "strengthens borders and the selection indicator" means *crisper*, and a blur is
    /// the one thing a border cannot become. The edge instead goes full-alpha and heavier
    /// (`accentOpacity`, `borderWidth` at the solid ring's base), so the setting sees the same
    /// unambiguous solid ring it always has — the halo is the resting treatment's softness, not
    /// the indicator itself.
    static func drawsSelectionHalo(contrast: ColorSchemeContrast) -> Bool {
        contrast != .increased
    }

    /// Increase Contrast, asked of AppKit rather than of the SwiftUI environment — for callers built
    /// outside a rendered hierarchy, where the environment's accessibility values are not reliably
    /// populated (`Motion.prefersReducedMotion`'s constituency).
    @MainActor
    static var prefersIncreasedContrast: Bool {
        NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast
    }

    // MARK: - Reduce Transparency

    /// What a **glass underlay** is made of — a real material, or the solid the setting replaces it
    /// with (10-accessibility.md: "Reduce Transparency: glass underlays go solid, wherever they
    /// appear").
    ///
    /// The design's own example (the card face carousel's page dots) died with the carousel
    /// (03-board-ui.md § Card face's no-carousel resettlement), so the rule's subjects on the board
    /// are the transient search bar's `.bar` material and the backdrop's title-bar frost — the
    /// "next material to arrive" this type was stated for, and it found the answer already written.
    enum Underlay: Equatable {
        /// `Material.bar` — the find-bar's own backdrop, translucent over the board beneath it.
        case glass
        /// `Material.thin` — the title-bar frost over a custom board backdrop
        /// (`BoardView.boardBackground`). Deliberately not `.bar` — the find-bar sits over lanes
        /// the board's own plates have already calmed, where the frost sits directly on an image
        /// the author may well have chosen *for* its busyness — and deliberately not the heavier
        /// notches either, tried and retired: `.ultraThick` read as a cloudy plate where a backdrop
        /// should still show through, and `.regular` still veiled it more than the chrome needs.
        /// The thin weight carries the chrome, and the dissolve below it (`BoardView.frostStrip`)
        /// is what keeps the strip from reading as a bar.
        case frost
        /// The window's own background colour, opaque.
        case solid

        var style: AnyShapeStyle {
            switch self {
            case .glass: AnyShapeStyle(.bar)
            case .frost: AnyShapeStyle(.thinMaterial)
            case .solid: AnyShapeStyle(Color(nsColor: .windowBackgroundColor))
            }
        }
    }

    static func underlay(reduceTransparency: Bool) -> Underlay {
        reduceTransparency ? .solid : .glass
    }

    /// The title-bar frost's own reading of the same rule — heavier glass, identical accommodation:
    /// under Reduce Transparency both underlays take the one solid.
    static func frost(reduceTransparency: Bool) -> Underlay {
        reduceTransparency ? .solid : .frost
    }

    /// A **translucent wash** — a tint laid over whatever happens to be behind it, and the shape
    /// every non-material translucency on the board takes: every lane's plate, the trash column's
    /// plate and hatched header, and the drag shadow's fill.
    ///
    /// These are not glass, and the distinction matters enough to keep two types: a material samples
    /// and blurs its backdrop, a wash simply composites at an alpha. But they fail the same way for
    /// the same user — the board's `background` is a colour the *user* chose (03-board-ui.md §
    /// Styling), so a 35%-alpha plate over a saturated board is exactly the "what is behind this"
    /// problem Reduce Transparency exists to remove. Under the setting each one takes the standard
    /// secondary background instead, which is opaque and appearance-aware.
    enum Wash: Equatable {
        /// `.quaternary` at `opacity`, over whatever is behind.
        case translucent(opacity: Double)
        /// The standard secondary background — opaque, so nothing shows through.
        case opaque

        var style: AnyShapeStyle {
            switch self {
            case let .translucent(opacity): AnyShapeStyle(HierarchicalShapeStyle.quaternary.opacity(opacity))
            case .opaque: AnyShapeStyle(.background.secondary)
            }
        }
    }

    /// Every lane's resting plate (03-board-ui.md § Styling ▸ Capabilities — the standard chrome's
    /// middle step, the pathfinder's lane surface carried over). Translucent so a board-chosen
    /// colour shows through it, which is what keeps `BoardTextInk`'s premise true for the header
    /// text sitting on it.
    static func lanePlateWash(reduceTransparency: Bool) -> Wash {
        reduceTransparency ? .opaque : .translucent(opacity: 0.35)
    }

    /// The trash column's plate — the same figure as an ordinary lane's, kept as its own knob
    /// because the trash answers to its own rendering spec (03-board-ui.md § Trash ▸ Rendering),
    /// and the quietest of its treatments: the hatched header above carries the column's identity.
    static func trashPlateWash(reduceTransparency: Bool) -> Wash {
        reduceTransparency ? .opaque : .translucent(opacity: 0.35)
    }

    /// The trash column's hatched header. Heavier than the plate, because it is the whole of "you
    /// are looking at the trash" (03-board-ui.md § Trash ▸ Rendering).
    static func trashHeaderWash(reduceTransparency: Bool) -> Wash {
        reduceTransparency ? .opaque : .translucent(opacity: 0.5)
    }

    /// A drop shadow's fill — the outline occupying an item's proposed landing spot (`DragShadow`).
    static func dragShadowWash(reduceTransparency: Bool) -> Wash {
        reduceTransparency ? .opaque : .translucent(opacity: 0.5)
    }

    /// Reduce Transparency, asked of AppKit — `prefersIncreasedContrast`'s twin, same constituency.
    @MainActor
    static var prefersReducedTransparency: Bool {
        NSWorkspace.shared.accessibilityDisplayShouldReduceTransparency
    }

    // MARK: - Full Keyboard Access

    /// Whether the system's **Full Keyboard Access** is on.
    ///
    /// It has one caller and one purpose: the board strip suppresses its focus ring, because "the
    /// strip is the window's content, not a control, and a rectangle around the whole board would
    /// read as an error state" (`BoardView`) — and that reasoning inverts completely under FKA,
    /// where 10-accessibility.md makes the board **one tab stop** and a tab stop nobody can see is
    /// not one. So the ring comes back exactly when Tab can land on it.
    ///
    /// There is no SwiftUI environment value for this and no change notification to observe, so it
    /// is read at body evaluation like any other system query here. That is honest for what it is: a
    /// setting a user turns on once (⌃F7, or System Settings ▸ Keyboard), not one that flips during
    /// a gesture — and a board window re-renders on nearly every interaction, so a flip is picked up
    /// almost immediately rather than never.
    @MainActor
    static var isFullKeyboardAccessEnabled: Bool {
        NSApp?.isFullKeyboardAccessEnabled ?? false
    }
}
