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 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`).
                    // Both are spelled out rather than defaulted, because `select`'s sole-member
                    // default would otherwise pick one up the moment a band happened to sweep
                    // exactly one card.
                    store.select(ids, in: session.container, anchor: nil, head: nil)
                }
            }
            .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) }
    }
}
