// DualSense adaptive-trigger effect parsing: the host forwards the raw 11-byte trigger
// parameter block a game wrote to its virtual DualSense (output report 0x02, bytes 11–21
// for L2 / 22–32 for R2: one mode byte + 10 parameter bytes). The mode values and layouts
// follow the community-established conventions (Nielk1's TriggerEffectGenerator, ds5w,
// inputtino) — Sony has never documented them. Parsing is TOTAL: any unknown or short
// block degrades to `.off`, never traps, so a game using an exotic raw mode can't break
// the session.
//
// `parse` is a pure function (unit-tested without a controller); `apply(to:)` maps the
// result onto Apple's GCDualSenseAdaptiveTrigger — exact for the 10-zone positional modes
// (0x21/0x26 → the positional resistiveStrengths/amplitudes APIs), best-effort for the
// composite ones (bow, galloping) that have no GC equivalent.

import Foundation
import GameController

/// A parsed DualSense trigger effect. Positions and strengths are normalized 0...1
/// (GCDualSenseAdaptiveTrigger's domain); `positional*` carry one value per trigger zone
/// (10 zones across the travel).
public enum DualSenseTriggerEffect: Equatable, Sendable {
    case off
    /// Constant resistance from `start` to the end of travel.
    case feedback(start: Float, strength: Float)
    /// Resistance from `start` that releases past `end` (trigger-break / weapon feel).
    case weapon(start: Float, end: Float, strength: Float)
    /// Vibration once the trigger passes `start`.
    case vibration(start: Float, amplitude: Float, frequency: Float)
    /// Per-zone resistance (10 zones).
    case positionalFeedback(strengths: [Float])
    /// Per-zone vibration amplitudes (10 zones) at `frequency`.
    case positionalVibration(amplitudes: [Float], frequency: Float)
    /// Resistance ramping `startStrength` → `endStrength` between `start` and `end`
    /// (the closest GC rendering of the bow effect).
    case slope(start: Float, end: Float, startStrength: Float, endStrength: Float)

    /// Parse a raw trigger parameter block (`[mode, p0...p9]`, ≤ 11 bytes — shorter blocks
    /// are zero-padded). Never fails: unknown modes are `.off`.
    public static func parse(_ block: [UInt8]) -> DualSenseTriggerEffect {
        guard let mode = block.first else { return .off }
        var p = [UInt8](block.dropFirst())
        if p.count < 10 { p.append(contentsOf: [UInt8](repeating: 0, count: 10 - p.count)) }

        // Helpers for the rich (0x2x) modes: a 10-bit active-zone mask in p0/p1 and 3-bit
        // per-zone values packed little-endian into the following 4 bytes.
        let zoneMask = UInt16(p[0]) | (UInt16(p[1]) << 8)
        let packed = UInt32(p[2]) | (UInt32(p[3]) << 8) | (UInt32(p[4]) << 16) | (UInt32(p[5]) << 24)
        func zoneValues() -> [UInt8] {
            (0..<10).map { i in
                zoneMask & (1 << i) != 0 ? UInt8((packed >> (3 * UInt32(i))) & 0x07) : 0
            }
        }
        // DualSense 3-bit strengths are 0...7 where an *active* zone's value v renders as
        // (v+1)/8 — a present-but-zero strength still resists slightly.
        func strength01(_ v: UInt8, active: Bool) -> Float {
            active ? Float(v + 1) / 8 : 0
        }
        func zone01(_ z: Int) -> Float { Float(z) / 9 }
        func firstActive() -> Int? { (0..<10).first { zoneMask & (1 << $0) != 0 } }
        func lastActive() -> Int? { (0..<10).last { zoneMask & (1 << $0) != 0 } }

        switch mode {
        case 0x00, 0x05, 0xF0...0xFF:
            // 0x00/0x05 are the documented off/reset modes; 0xFC/0xFB/0xFA show up from
            // calibration-adjacent writes — all render as off.
            return .off

        case 0x01:
            // Legacy continuous resistance: p0 = start position, p1 = force (both 0...255).
            return .feedback(start: Float(p[0]) / 255, strength: max(Float(p[1]) / 255, 1.0 / 8))

        case 0x02:
            // Legacy section: p0 = start, p1 = end (0...255); full-strength inside.
            let s = Float(p[0]) / 255
            let e = Float(p[1]) / 255
            return .weapon(start: min(s, e), end: max(max(s, e), min(s, e) + 0.01), strength: 1)

        case 0x06:
            // Legacy vibration ("automatic gun") — Nielk1's Simple_Vibration order:
            // p0 = frequency Hz, p1 = amplitude, p2 = start position.
            return .vibration(
                start: Float(p[2]) / 255,
                amplitude: max(Float(p[1]) / 255, 1.0 / 8),
                frequency: Float(p[0]) / 255)

        case 0x21:
            // Feedback: 10-bit zone mask + 3-bit strength per zone. A uniform suffix maps
            // exactly onto the simple feedback call; mixed strengths use the positional API.
            guard let first = firstActive() else { return .off }
            let values = zoneValues()
            let active = values.enumerated().filter { zoneMask & (1 << $0.offset) != 0 }
            if active.allSatisfy({ $0.element == active[0].element })
                && active.last?.offset == 9
                && active.map(\.offset) == Array(first...9)
            {
                return .feedback(
                    start: zone01(first), strength: strength01(active[0].element, active: true))
            }
            return .positionalFeedback(
                strengths: values.enumerated().map {
                    strength01($0.element, active: zoneMask & (1 << $0.offset) != 0)
                })

        case 0x22:
            // Bow (Nielk1 mode byte 0x22): start/end zones + draw strength and snap force
            // packed as a 3-bit pair in p2 (low bits draw, bits 3-5 snap; p3 is always 0).
            // No GC equivalent — render as a slope from draw resistance down to the snap.
            guard let s = firstActive(), let e = lastActive(), s < e else { return .off }
            let draw = strength01(p[2] & 0x07, active: true)
            let snap = strength01((p[2] >> 3) & 0x07, active: true)
            return .slope(start: zone01(s), end: zone01(e), startStrength: draw, endStrength: snap)

        case 0x25:
            // Weapon (Nielk1 mode byte 0x25): zone mask marks the start and end zones,
            // p2 = strength (3-bit, stored minus one).
            guard let s = firstActive(), let e = lastActive(), s < e else { return .off }
            return .weapon(start: zone01(s), end: zone01(e), strength: strength01(p[2] & 0x07, active: true))

        case 0x26:
            // Vibration: 10-bit zone mask + 3-bit amplitude per zone, p8 = frequency Hz.
            guard zoneMask != 0 else { return .off }
            let amplitudes = zoneValues().enumerated().map {
                strength01($0.element, active: zoneMask & (1 << $0.offset) != 0)
            }
            return .positionalVibration(amplitudes: amplitudes, frequency: Float(p[8]) / 255)

        case 0x23:
            // Galloping (Nielk1 mode byte 0x23): start/end zones, p2 = packed foot timing,
            // p3 = frequency Hz. The temporal hoofbeat pattern has no GC equivalent —
            // render as vibration across the active range.
            guard let s = firstActive(), let e = lastActive() else { return .off }
            var amplitudes = [Float](repeating: 0, count: 10)
            for z in s...e { amplitudes[z] = 0.5 }
            return .positionalVibration(amplitudes: amplitudes, frequency: Float(p[3]) / 255)

        case 0x27:
            // Machine (Nielk1 mode byte 0x27): start/end zones, p2 = two 3-bit amplitudes
            // (low bits A, bits 3-5 B — raw 0...7, no minus-one), p3 = frequency Hz. The
            // A/B alternation is temporal — render its stronger leg across the range.
            guard let s = firstActive(), let e = lastActive() else { return .off }
            let amp = Float(max(p[2] & 0x07, (p[2] >> 3) & 0x07)) / 7
            guard amp > 0 else { return .off }
            var amplitudes = [Float](repeating: 0, count: 10)
            for z in s...e { amplitudes[z] = amp }
            return .positionalVibration(amplitudes: amplitudes, frequency: Float(p[3]) / 255)

        default:
            return .off
        }
    }
}

extension DualSenseTriggerEffect {
    /// Replay this effect on a physical DualSense trigger. Main-thread only (GameController
    /// profile mutation). The GC `frequency` parameter is normalized 0...1 like ours.
    @MainActor
    public func apply(to trigger: GCDualSenseAdaptiveTrigger) {
        switch self {
        case .off:
            trigger.setModeOff()
        case let .feedback(start, strength):
            trigger.setModeFeedbackWithStartPosition(start, resistiveStrength: strength)
        case let .weapon(start, end, strength):
            trigger.setModeWeaponWithStartPosition(start, endPosition: end, resistiveStrength: strength)
        case let .vibration(start, amplitude, frequency):
            trigger.setModeVibrationWithStartPosition(start, amplitude: amplitude, frequency: frequency)
        case let .positionalFeedback(strengths):
            var s = GCDualSenseAdaptiveTrigger.PositionalResistiveStrengths(
                values: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
            withUnsafeMutableBytes(of: &s.values) { raw in
                let f = raw.bindMemory(to: Float.self)
                for (i, v) in strengths.prefix(10).enumerated() { f[i] = v }
            }
            trigger.setModeFeedback(resistiveStrengths: s)
        case let .positionalVibration(amplitudes, frequency):
            var a = GCDualSenseAdaptiveTrigger.PositionalAmplitudes(
                values: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
            withUnsafeMutableBytes(of: &a.values) { raw in
                let f = raw.bindMemory(to: Float.self)
                for (i, v) in amplitudes.prefix(10).enumerated() { f[i] = v }
            }
            trigger.setModeVibration(amplitudes: a, frequency: frequency)
        case let .slope(start, end, startStrength, endStrength):
            trigger.setModeSlopeFeedback(
                startPosition: start, endPosition: end,
                startStrength: startStrength, endStrength: endStrength)
        }
    }
}
