//! Host→client gamepad feedback pulls (Option B): blocking JNI shims that forward to the connector's
//! rumble (0xCA) / HID-output (0xCD) planes and return one decoded event. Kotlin owns the poll
//! threads + the Android Vibrator/Lights rendering (see `GamepadFeedback.kt`) — no JNI upcalls, no
//! `JavaVM` attach, no cached method ids. Mirrors the audio plane's one-thread-per-plane contract,
//! except the thread lives in Kotlin and we just expose the blocking pull.
//!
//! Not android-gated: `next_rumble`/`next_hidout` are pure-Rust on the `quic` feature, so these
//! compile on the host build too (parity with the input shims in [`crate::session`]).

use crate::session::{jni_guard, SessionHandle};
use jni::objects::{JByteBuffer, JObject};
use jni::sys::{jint, jlong};
use jni::JNIEnv;
use punktfunk_core::quic::HidOutput;
use std::time::Duration;

/// Short blocking timeout: long enough not to busy-spin, short enough that the Kotlin poll thread
/// observes its `running=false` flag promptly on teardown.
const PULL_TIMEOUT: Duration = Duration::from_millis(100);

// HID-output kind tags written into the returned ByteBuffer (Kotlin reads them back).
const TAG_LED: u8 = 0x01;
const TAG_PLAYER_LEDS: u8 = 0x02;
const TAG_TRIGGER: u8 = 0x03;

/// `NativeBridge.nativeNextRumble(handle): Long` — block up to ~100 ms for the next rumble update.
/// Returns `(low << 16) | high` (each 0..=0xFFFF; `0` = stop), or `-1` on timeout / session closed.
/// Pad index is dropped (single-pad model). Run from a dedicated Kotlin poll thread.
#[no_mangle]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeNextRumble(
    _env: JNIEnv,
    _this: JObject,
    handle: jlong,
) -> jlong {
    // Runs on a Kotlin poll thread, so a panic here would abort the process; guard the boundary.
    jni_guard(-1, || {
        if handle == 0 {
            return -1;
        }
        // SAFETY: live handle per the nativeConnect/nativeClose contract; next_rumble is &self on the
        // Sync connector — safe alongside the decode/audio/input threads. Kotlin stops these poll
        // threads (and joins them — unbounded) before nativeClose frees the handle.
        let h = unsafe { &*(handle as *const SessionHandle) };
        match h.client.next_rumble(PULL_TIMEOUT) {
            Ok((_pad, low, high)) => (jlong::from(low) << 16) | jlong::from(high),
            Err(_) => -1, // NoFrame (timeout) or Closed — Kotlin loops on its running flag
        }
    })
}

/// `NativeBridge.nativeNextHidout(handle, buf): Int` — block up to ~100 ms for the next DualSense
/// HID-output event, written into the caller's direct ByteBuffer as `[kind][fields…]`:
///   Led        → `[0x01][r][g][b]`         (len 4)
///   PlayerLeds → `[0x02][bits]`            (len 2)
///   Trigger    → `[0x03][which][effect…]`  (len 2 + effect.len())
/// Returns the byte count written, or `-1` on timeout / session closed / buffer too small.
#[no_mangle]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeNextHidout(
    env: JNIEnv,
    _this: JObject,
    handle: jlong,
    buf: JByteBuffer,
) -> jint {
    // Runs on a Kotlin poll thread, so a panic here would abort the process; guard the boundary.
    jni_guard(-1, || {
        if handle == 0 {
            return -1;
        }
        // SAFETY: live handle per the contract; next_hidout is &self on the Sync connector.
        let h = unsafe { &*(handle as *const SessionHandle) };
        let ev = match h.client.next_hidout(PULL_TIMEOUT) {
            Ok(ev) => ev,
            Err(_) => return -1, // timeout or closed — Kotlin loops
        };

        // The caller passes a direct ByteBuffer (allocateDirect) so we write its backing store directly.
        let cap = match env.get_direct_buffer_capacity(&buf) {
            Ok(c) => c,
            Err(_) => return -1,
        };
        let ptr = match env.get_direct_buffer_address(&buf) {
            Ok(p) if !p.is_null() => p,
            _ => return -1,
        };
        // SAFETY: `ptr`/`cap` describe the direct ByteBuffer's backing store, valid for this call.
        let out = unsafe { std::slice::from_raw_parts_mut(ptr, cap) };

        let n = match ev {
            HidOutput::Led { r, g, b, .. } => {
                if cap < 4 {
                    return -1;
                }
                out[0] = TAG_LED;
                out[1] = r;
                out[2] = g;
                out[3] = b;
                4
            }
            HidOutput::PlayerLeds { bits, .. } => {
                if cap < 2 {
                    return -1;
                }
                out[0] = TAG_PLAYER_LEDS;
                out[1] = bits;
                2
            }
            HidOutput::Trigger { which, effect, .. } => {
                let n = 2 + effect.len();
                if cap < n {
                    return -1; // the raw DS5 trigger block is ~11 bytes; Kotlin allocates 64
                }
                out[0] = TAG_TRIGGER;
                out[1] = which;
                out[2..n].copy_from_slice(&effect);
                n
            }
            HidOutput::TrackpadHaptic { .. } => {
                // Steam Controller trackpad-coil haptics — no Android equivalent; drop it (motor
                // rumble already rides the universal 0xCA plane).
                return -1;
            }
        };
        n as jint
    })
}
