// Finger touches → host mouse, for the touchscreen devices: a port of the Android client's
// touch gesture model (clients/android .../TouchInput.kt) so the two touch clients feel
// identical. Two mouse modes share one gesture vocabulary — tap = left click · two-finger
// tap = right click · two-finger drag = scroll · tap-then-press-and-drag = held left drag
// (text selection / window moves) · three-finger tap = cycles the stats overlay tiers
// (off → compact → normal → detailed, matching Android) · three-finger swipe up/down =
// summon/dismiss the local soft keyboard for typing on the host (`onKeyboardGesture`):
//
//  * trackpad (default): the cursor STAYS PUT on touch-down and moves by the finger's
//    relative delta with mild acceleration — swipe to nudge, lift and re-swipe to walk it
//    across, tap to click where it is. This is what makes the cursor reachable on a small
//    screen.
//  * pointer: the cursor jumps to the finger and follows it (absolute moves through the
//    aspect-fit letterbox) — direct pointing for desktop-style use.
//
// The third `TouchInputMode` (`touch`) never reaches this type: `StreamLayerUIView` forwards
// those fingers as REAL wire touches (multi-touch passthrough) instead.

#if os(iOS)
import Foundation
import PunktfunkCore
import PunktfunkShared
import UIKit

/// How touchscreen fingers drive the host — persisted under `DefaultsKey.touchMode`, latched
/// per gesture by `StreamLayerUIView` (a Settings change applies from the NEXT touch, and a
/// gesture never splits across models). `trackpad` is the default: a cursor is the
/// universally workable model; passthrough only helps hosts/apps that actually speak touch.
public enum TouchInputMode: String, CaseIterable, Sendable {
    case trackpad
    case pointer
    case touch

    /// The persisted setting, defaulting to trackpad when unset/unknown.
    public static var current: TouchInputMode {
        TouchInputMode(
            rawValue: SessionSettings.current.touchMode
        ) ?? .trackpad
    }
}

/// The gesture state machine behind the two mouse modes. One instance per stream view, fed
/// only the DIRECT touches (fingers/Pencil — indirect pointers have their own path). Runs
/// entirely on the main thread (UIKit touch delivery). Touches are tracked by identity key
/// with positions cached per event — `UITouch` objects are never retained.
final class TouchMouse {
    /// Gesture/ballistics tuning. Distances are in points where they gate gestures; the
    /// relative ballistics work in PHYSICAL pixels (point deltas × screen scale) so the
    /// acceleration curve matches the Android client's pixel-based constants 1:1.
    enum Tuning {
        /// Movement under this (pt) still counts as a tap, not a drag.
        static let tapSlop: CGFloat = 8
        /// A new touch this soon (s) after a tap, near it, starts a held left-button drag.
        static let tapDragWindow: TimeInterval = 0.25
        /// Two-finger pan distance (pt) per 120-unit wheel notch — matches the feel of the
        /// indirect-trackpad scroll path in StreamViewIOS (~10 pt per notch).
        static let scrollNotchPt: CGFloat = 10
        /// Base finger-px → host-px gain (~1:1, never twitchy). The acceleration below lets a
        /// flick cross the screen while a slow drag stays precise.
        static let pointerSens: CGFloat = 1.3
        /// Above `accelSpeedFloor` px/ms the gain ramps by `accelGain` per px/ms, capped at
        /// `accelMax` (so a fast swipe can't fling the cursor uncontrollably).
        static let accelGain: CGFloat = 0.6
        static let accelSpeedFloor: CGFloat = 0.3
        static let accelMax: CGFloat = 3.0
        /// Three-finger vertical swipe: the fraction of the view height the centroid must
        /// travel to summon (up) / dismiss (down) the local soft keyboard.
        static let keyboardSwipeFraction: CGFloat = 0.10

        /// Acceleration multiplier for a finger speed in physical px per ms.
        static func accel(forSpeed speed: CGFloat) -> CGFloat {
            min(1 + accelGain * max(speed - accelSpeedFloor, 0), accelMax)
        }
    }

    /// Wire events out (the owner gates them on its capture state).
    var send: ((PunktfunkInputEvent) -> Void)?
    /// View-space point → host-mode pixels through the letterbox (pointer mode's moves).
    var hostPoint: ((CGPoint) -> StreamLayerUIView.HostPoint?)?
    /// Three-finger vertical swipe crossed the threshold: `true` = show the local soft
    /// keyboard (swipe up), `false` = dismiss it (swipe down). Fires at most once per gesture.
    var onKeyboardGesture: ((Bool) -> Void)?

    /// No gesture in flight (all fingers up) — the view uses this to release its mode latch.
    var isIdle: Bool { !sessionActive && lastPos.isEmpty }

    private var trackpad = true
    /// Last known position per active finger (identity key) — kept because moved events only
    /// carry the CHANGED touches while the scroll centroid needs every finger.
    private var lastPos: [ObjectIdentifier: CGPoint] = [:]
    private var sessionActive = false
    private var startPoint = CGPoint.zero
    private var maxFingers = 0
    private var moved = false
    private var scrolling = false
    private var dragHeld = false
    // Trackpad relative-motion state: the tracked finger, its last position/time, and the
    // sub-pixel remainder so a slow drag isn't lost to integer truncation.
    private var trackKey: ObjectIdentifier?
    private var prevPoint = CGPoint.zero
    private var prevTime: TimeInterval = 0
    private var carryX: CGFloat = 0
    private var carryY: CGFloat = 0
    /// Scroll anchor (centroid) — re-anchored every time a notch fires.
    private var scrollAnchor = CGPoint.zero
    // Keyboard-swipe state: the 3+-finger centroid anchor (per finger count, like the scroll
    // anchor) and a once-per-gesture latch.
    private var kbCount = 0
    private var kbAnchor = CGPoint.zero
    private var kbFired = false
    // Tap-drag arming: a quick tap leaves a window in which the next nearby touch drags.
    private var lastTapUp: TimeInterval = 0
    private var lastTapPoint = CGPoint.zero

    /// GameStream mouse button ids.
    private enum Button { static let left: UInt32 = 1; static let right: UInt32 = 3 }

    func began(_ touches: Set<UITouch>, in view: UIView, trackpad: Bool) {
        let starting = lastPos.isEmpty
        for touch in touches {
            lastPos[ObjectIdentifier(touch)] = touch.location(in: view)
        }
        if starting, let first = touches.first {
            self.trackpad = trackpad
            sessionActive = true
            startPoint = first.location(in: view)
            maxFingers = 0
            moved = false
            scrolling = false
            kbCount = 0
            kbFired = false
            // A touch landing just after a quick tap nearby = tap-and-drag: hold the left
            // button for this whole gesture (laptop-trackpad convention).
            dragHeld = first.timestamp - lastTapUp < Tuning.tapDragWindow
                && abs(startPoint.x - lastTapPoint.x) < Tuning.tapSlop
                && abs(startPoint.y - lastTapPoint.y) < Tuning.tapSlop
            lastTapUp = 0 // consume the arming either way
            // Pointer mode jumps the cursor to the finger; trackpad leaves it put (the whole
            // point — you nudge it with swipes instead).
            if !trackpad, let h = hostPoint?(startPoint) {
                send?(.mouseMoveAbs(x: h.x, y: h.y, surfaceWidth: h.w, surfaceHeight: h.h))
            }
            if dragHeld { send?(.mouseButton(Button.left, down: true)) }
            trackKey = ObjectIdentifier(first)
            prevPoint = startPoint
            prevTime = first.timestamp
            carryX = 0
            carryY = 0
        }
        maxFingers = max(maxFingers, lastPos.count)
    }

    func moved(_ touches: Set<UITouch>, in view: UIView) {
        guard sessionActive else { return }
        for touch in touches where lastPos[ObjectIdentifier(touch)] != nil {
            lastPos[ObjectIdentifier(touch)] = touch.location(in: view)
        }
        // Dropping below three fingers forgets the keyboard-swipe anchor, so a 3→2→3 bounce
        // re-anchors instead of reading the count change as swipe travel.
        if lastPos.count < 3 { kbCount = 0 }
        if lastPos.count == 2 {
            scrollByCentroid()
        } else if lastPos.count >= 3 {
            keyboardSwipe(in: view)
        } else if !scrolling, let touch = touches.first(where: {
            lastPos[ObjectIdentifier($0)] != nil
        }) {
            singleFinger(touch, in: view)
        }
    }

    func ended(_ touches: Set<UITouch>, in view: UIView) {
        guard sessionActive || !lastPos.isEmpty else { return }
        var upTime: TimeInterval = 0
        for touch in touches {
            lastPos.removeValue(forKey: ObjectIdentifier(touch))
            if trackKey == ObjectIdentifier(touch) { trackKey = nil }
            upTime = max(upTime, touch.timestamp)
        }
        guard lastPos.isEmpty, sessionActive else { return }
        sessionActive = false
        if dragHeld {
            dragHeld = false
            send?(.mouseButton(Button.left, down: false)) // end the drag
        } else if !moved {
            switch maxFingers {
            case 3...:
                Self.cycleStats() // in-stream stats-tier cycle, same as Android
            case 2: // two-finger tap → right click
                send?(.mouseButton(Button.right, down: true))
                send?(.mouseButton(Button.right, down: false))
            default: // tap → left click (at the cursor's current spot), arm tap-drag
                send?(.mouseButton(Button.left, down: true))
                send?(.mouseButton(Button.left, down: false))
                lastTapUp = upTime
                lastTapPoint = startPoint
            }
        }
    }

    /// System-cancelled touches (incoming call, gesture takeover): release anything held but
    /// never synthesize a click out of a cancellation.
    func cancelled(_ touches: Set<UITouch>) {
        for touch in touches {
            lastPos.removeValue(forKey: ObjectIdentifier(touch))
            if trackKey == ObjectIdentifier(touch) { trackKey = nil }
        }
        if lastPos.isEmpty { abortSession() }
    }

    /// Session teardown: release anything held on the wire and forget all gesture state.
    func reset() {
        lastPos.removeAll()
        trackKey = nil
        abortSession()
        lastTapUp = 0
    }

    private func abortSession() {
        if dragHeld {
            dragHeld = false
            send?(.mouseButton(Button.left, down: false))
        }
        sessionActive = false
        scrolling = false
        moved = false
    }

    // MARK: - Per-event work

    /// Two fingers → scroll by the centroid delta; never move the cursor. Fires a notch per
    /// `scrollNotchPt` of pan and re-anchors on fire; finger up scrolls up, finger right
    /// scrolls right (the host WHEEL(120) convention).
    private func scrollByCentroid() {
        let n = CGFloat(lastPos.count)
        let cx = lastPos.values.reduce(0) { $0 + $1.x } / n
        let cy = lastPos.values.reduce(0) { $0 + $1.y } / n
        if !scrolling {
            scrolling = true
            scrollAnchor = CGPoint(x: cx, y: cy)
        }
        let notchesY = Int32((scrollAnchor.y - cy) / Tuning.scrollNotchPt)
        let notchesX = Int32((cx - scrollAnchor.x) / Tuning.scrollNotchPt)
        if notchesY != 0 {
            send?(.scroll(notchesY * 120))
            scrollAnchor.y = cy
            moved = true
        }
        if notchesX != 0 {
            send?(.scroll(notchesX * 120, horizontal: true))
            scrollAnchor.x = cx
            moved = true
        }
    }

    /// Three+ fingers → the keyboard swipe, never scroll (the documented vocabulary is
    /// TWO-finger scroll; 3+ only fell into the scroll path as an accident of its old `>= 2`
    /// bound). The centroid is anchored per finger count — real fingers never land or lift in
    /// the same event, so a count change must re-anchor rather than read as travel — and the
    /// gesture fires at most once, when the vertical travel crosses the threshold: up = show
    /// the local soft keyboard, down = dismiss it.
    private func keyboardSwipe(in view: UIView) {
        let n = CGFloat(lastPos.count)
        let cx = lastPos.values.reduce(0) { $0 + $1.x } / n
        let cy = lastPos.values.reduce(0) { $0 + $1.y } / n
        if lastPos.count != kbCount {
            kbCount = lastPos.count
            kbAnchor = CGPoint(x: cx, y: cy)
        } else {
            let dy = cy - kbAnchor.y
            // Real centroid travel disqualifies the tap classification in `ended` (else a
            // sub-threshold swipe would still fire the three-finger stats tap).
            if abs(dy) > Tuning.tapSlop || abs(cx - kbAnchor.x) > Tuning.tapSlop { moved = true }
            if !kbFired, abs(dy) >= view.bounds.height * Tuning.keyboardSwipeFraction {
                kbFired = true
                onKeyboardGesture?(dy < 0) // finger up → show, finger down → dismiss
            }
        }
        // Leaving the scroll state stale would read the 3→2 centroid jump as a wheel notch;
        // clearing it makes a return to two fingers re-anchor fresh. Same for the trackpad's
        // tracked finger: its prev position froze while 3+ fingers were down, so dropping
        // straight back to one finger must re-anchor (zero delta), not replay the whole
        // 3-finger phase as one cursor jump.
        scrolling = false
        trackKey = nil
    }

    /// One finger (and the gesture never became a scroll — dropping back from two fingers to
    /// one must not jerk the cursor).
    private func singleFinger(_ touch: UITouch, in view: UIView) {
        let loc = touch.location(in: view)
        if abs(loc.x - startPoint.x) > Tuning.tapSlop || abs(loc.y - startPoint.y) > Tuning.tapSlop {
            moved = true
        }
        guard trackpad else {
            if let h = hostPoint?(loc) { // pointer mode: the cursor follows the finger
                send?(.mouseMoveAbs(x: h.x, y: h.y, surfaceWidth: h.w, surfaceHeight: h.h))
            }
            return
        }
        // Relative: move by the finger delta × (sensitivity × acceleration), carrying the
        // sub-pixel remainder. Re-anchor (zero delta this frame) if the tracked finger
        // changed, so lifting one of several fingers never jumps the cursor.
        let key = ObjectIdentifier(touch)
        if key != trackKey {
            trackKey = key
            prevPoint = loc
            prevTime = touch.timestamp
            return
        }
        // Ballistics in physical pixels so the curve matches the Android tuning exactly.
        let scale = view.window?.screen.scale ?? view.traitCollection.displayScale
        let dx = (loc.x - prevPoint.x) * scale
        let dy = (loc.y - prevPoint.y) * scale
        let dtMs = max((touch.timestamp - prevTime) * 1000, 1)
        prevPoint = loc
        prevTime = touch.timestamp
        let gain = Tuning.pointerSens * Tuning.accel(forSpeed: hypot(dx, dy) / dtMs)
        carryX += dx * gain
        carryY += dy * gain
        let outX = Int32(carryX) // truncates toward zero → remainder kept with its sign
        let outY = Int32(carryY)
        if outX != 0 || outY != 0 {
            send?(.mouseMove(dx: outX, dy: outY))
            carryX -= CGFloat(outX)
            carryY -= CGFloat(outY)
        }
    }

    /// Three-finger tap cycles the stats overlay tiers (off → compact → normal → detailed) —
    /// through the shared `statsVerbosity` default, which the app's HUD views observe via
    /// @AppStorage (so this needs no wiring to them). Same cycle as Android's triple-tap.
    private static func cycleStats() {
        StatsVerbosity.cycle()
    }
}
#endif
