import AppKit
import SwiftUI

// MARK: - The modifier a click carried

extension ClickModifier {

    /// The modifier the keyboard is holding **right now**, reduced to the grammar's three cases.
    ///
    /// Read from `NSEvent.modifierFlags` rather than from the gesture value, because SwiftUI's
    /// `TapGesture` hands its handler nothing about the event — and `EventModifiers` on a
    /// `.modifiers(_:)`-qualified gesture would need one recogniser per modifier, three of which
    /// would then race to consume the same click.
    ///
    /// **⌘ wins over ⇧** when both are down: 04-interactions.md gives the two no combined meaning
    /// ("⌘-click toggles; ⇧-click range-extends"), so the reduction happens once, here, and no call
    /// site re-decides it.
    @MainActor
    static var current: ClickModifier {
        let flags = NSEvent.modifierFlags
        if flags.contains(.command) { return .command }
        if flags.contains(.shift) { return .shift }
        return .plain
    }
}

// MARK: - The click a handler is riding

/// Which click of a multi-click run the current gesture handler is riding — `NSEvent.clickCount`
/// off the event being dispatched, read the way `ClickModifier.current` reads the keyboard:
/// SwiftUI's `TapGesture` hands its handler nothing about the event.
///
/// **This is how a surface without a drag source gets a double-click meaning** (the 2026-08-06
/// click-latency fix). A second tap recogniser is never the way: a *multi-click* recogniser on a
/// dragless subtree — a sequential `.onTapGesture(count: 2)` or even a simultaneous two-tap —
/// makes macOS hold every primary click on that subtree pending disambiguation for the system
/// double-click interval. (A real `.onDrag` forces immediate delivery, which is why the card
/// faces, the lane header and the trash rows — `CardFaceView`'s simultaneous arrangement — can
/// carry one and stay instant. A lone count-1 tap needs no such help: measured on real events
/// 2026-08-07, `LaneView`'s dragless empty-space layer fires in ~1–3 ms — there is nothing to
/// disambiguate. And an `.onDrag` must never be added there *for* delivery: even an
/// empty-provider drag source claims drags outright and kills the rubber band's simultaneous
/// `DragGesture`.) A single `.onTapGesture` fires once per click of a run, so branching on this
/// count expresses "first click selects, second creates" — Finder's cadence — with exactly one
/// recogniser and nothing to disambiguate.
enum PointerClick {

    /// The `clickCount` of the click being handled: 1 for a lone click or a run's first, 2 for
    /// the second click of a double, and so on.
    ///
    /// `NSApp.currentEvent` rather than a stored flag: the event being dispatched *is* the click,
    /// and AppKit's `clickCount` already embodies the system double-click interval and the
    /// spatial-proximity rule, so no timer here could disagree with the event stream's own
    /// pairing. A current event that is not a mouse click (or is absent — a synthetic call) reads
    /// as a first click, which fails toward the single-click action: selection stays reachable.
    @MainActor
    static var count: Int {
        guard let event = NSApp.currentEvent else { return 1 }
        switch event.type {
        case .leftMouseDown, .leftMouseUp: return max(1, event.clickCount)
        default: return 1
        }
    }
}

// MARK: - The rubber band's gesture

/// What a board window lends its empty surfaces so each can be a rubber band: the one session, the
/// one target registry, and the store the band selects into.
///
/// `BoardDropContext`'s sibling in role — the strip owning state that a leaf gesture needs — but a
/// value rather than a pair of closures, because all three surfaces (lane empty space, the board
/// backdrop, the trash column) want the *same* gesture rather than three variations threaded with
/// different geometry. Only the side differs, and that is the parameter.
@MainActor
struct MarqueeControl {

    let session: MarqueeSession
    let registry: MarqueeTargetRegistry
    let store: BoardStore

    /// Whether two of these lend the same band — the whole of what this value contributes to
    /// `LaneView.==` and `CardFaceView.==` (`BoardDropContext.isEquivalent(to:)` is its twin).
    ///
    /// All three members are window-lived objects, so identity is the comparison: this struct holds
    /// no geometry and no closures of its own, and the strip rebuilds it on every body pass.
    nonisolated func isEquivalent(to other: MarqueeControl) -> Bool {
        session === other.session
            && registry === other.registry
            && store === other.store
    }

    /// The band, as one gesture attached with `simultaneousGesture` wherever empty space is.
    ///
    /// - **The begin guard is geometric**: a drag whose start lands inside a registered frame is
    ///   somebody else's (a card drag, a drag out of the trash), so no band begins and the sample
    ///   loop simply keeps declining for the rest of that drag. Deciding this by frames rather than
    ///   by gesture priority is what keeps the two from fighting, and it stays correct as the
    ///   masonry reflows.
    /// - **The container is fixed at the origin** — 04-interactions.md ▸ The trash's rule ("the
    ///   rubber band stays on the side it started on"), stored in the session so a band dragged
    ///   across the boundary keeps its meaning.
    /// - **Live-updating, not commit-on-release**: each sample recomputes the whole set from the
    ///   band, so the selection follows the cursor both ways. An empty band clears rather than
    ///   leaving the last non-empty one standing.
    /// - **Alive under the read-only lock**: selection is not a mutation (02-architecture.md § The
    ///   lock's scope), and no `isEditingInline` guard either — a click-away mid-rename already
    ///   commits through the field's own focus loss.
    func gesture(in container: ItemContainer) -> some Gesture {
        DragGesture(minimumDistance: MarqueeSession.minimumDistance, coordinateSpace: .named(BoardView.stripSpace))
            .onChanged { value in
                if !session.isActive {
                    guard !registry.contains(value.startLocation) else { return }
                    session.begin(at: value.startLocation, in: container)
                }
                session.update(to: value.location)
                guard let rect = session.rect else { return }
                let ids = MarqueeMath.selection(rect: rect, targets: registry.all, in: session.container)
                if ids.isEmpty {
                    store.clearSelection()
                } else {
                    // Neither cursor: a band names no click to range from and no item to arrow from,
                    // so a ⇧-click after one acts plain and an arrow re-derives a position from the
                    // set's last member (`TransientBoardState.selectionAnchor`, `selectionHead`).
                    //
                    // **`defaultsSoleMember: false` is what says that, and the explicit `nil`s never
                    // did.** `nil` *is* the default, so a band that swept exactly one card used to
                    // pick up both cursors anyway — harmless for the anchor, not for the head:
                    // `LaneView.cardStack` watches `selectionHead` and scrolls the lane to it, which
                    // means a one-card band could scroll the board out from under the drag that was
                    // drawing it. The flag is the only way to spell "no gesture named these".
                    store.select(
                        ids, in: session.container, anchor: nil, head: nil, defaultsSoleMember: false
                    )
                }
            }
            .onEnded { _ in session.end() }
    }
}

// MARK: - Registering a sweepable frame

extension View {

    /// Keeps this item's drawn frame in the window's marquee registry, and takes it out again when
    /// the view goes away.
    ///
    /// The frame is measured in `BoardView.stripSpace`, the one space every marquee coordinate lives
    /// in — the band's own points come from a drag gesture in the same space, so no conversion
    /// happens anywhere.
    ///
    /// **This is also how the search filter reaches the band and the arrows** (04-interactions.md
    /// § Search, "marquee, … arrow nav … all read it"): a card the filter hides is never built, so
    /// it registers nothing, and the two surfaces that navigate by drawn frames narrow with the
    /// masonry rather than re-running the predicate.
    ///
    /// **A leaving card stays input-reachable for its out-transition** (04-interactions.md § Search,
    /// settled): "marquee and arrow targets deregister when the ~0.28 s animate-out ends, so a card
    /// mid-departure is briefly reachable while already out of the selection — accepted: it is
    /// literally on screen for that span, and closing the window would teach three input sites a
    /// predicate the layout already applied". That is this modifier's construction rather than a rule
    /// it implements: `onDisappear` fires when SwiftUI really removes the view — at the end of the
    /// card transition `Motion.contentReflow` is timing — not when the query changed, so the
    /// registration outlives the filter by exactly the length of the animation and not a frame more.
    /// The card has already left the selection by then (`TransientBoardState.constrainToSearch(in:)`
    /// runs at the keystroke), which is what makes the window visible rather than phantom.
    @MainActor
    func marqueeTarget(
        _ id: ItemID,
        kind: SelectionKind,
        container: ItemContainer,
        in registry: MarqueeTargetRegistry
    ) -> some View {
        // The space name is read here, on the main actor, rather than inside the measuring closure:
        // `BoardView` is main-actor-isolated by its `View` conformance, and the closure is not.
        let space = BoardView.stripSpace
        return onGeometryChange(for: CGRect.self) { proxy in
            proxy.frame(in: .named(space))
        } action: { frame in
            registry.update(MarqueeTarget(id: id, kind: kind, container: container, frame: frame))
        }
        .onDisappear { registry.remove(id) }
    }
}
