#!/usr/bin/env bash
# lesavka‑core.sh - one‑shot USB‑gadget bring‑up (Pi‑5 / Arch‑ARM)
# Presents:  • Boot‑protocol keyboard (hidg0)
#            • Boot‑protocol mouse    (hidg1)
#            • Stereo UAC2 speaker + microphone
set -euo pipefail

log() { printf '[lesavka-core] %s\n' "$*"; }

G=/sys/kernel/config/usb_gadget/lesavka

load_uvc_env_defaults() {
    local env_file=/etc/lesavka/uvc.env
    [[ -r $env_file ]] || return 0
    local line key value
    while IFS= read -r line || [[ -n $line ]]; do
        [[ $line =~ ^[[:space:]]*# || -z $line ]] && continue
        [[ $line == *=* ]] || continue
        key=${line%%=*}
        value=${line#*=}
        [[ $key =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
        [[ -z ${!key+x} ]] || continue
        export "$key=$value"
    done <"$env_file"
}

load_uvc_env_defaults

if [[ ${LESAVKA_CORE_ONESHOT:-0} == 1 ]]; then
    # Dev calibration needs a complete descriptor refresh without rebooting the Pi.
    export LESAVKA_ALLOW_GADGET_RESET=${LESAVKA_ALLOW_GADGET_RESET:-1}
    export LESAVKA_FORCE_GADGET_REBUILD=${LESAVKA_FORCE_GADGET_REBUILD:-1}
    export LESAVKA_ATTACH_WRITE_UDC=${LESAVKA_ATTACH_WRITE_UDC:-1}
    export LESAVKA_DETACH_CLEAR_UDC=${LESAVKA_DETACH_CLEAR_UDC:-1}
    export LESAVKA_UVC_FALLBACK=${LESAVKA_UVC_FALLBACK:-0}
fi

find_udc() {
    ls /sys/class/udc 2>/dev/null | head -n1 || true
}

udc_state() {
    local udc="$1"
    if [[ -z $udc ]]; then
        echo "unknown"
        return 0
    fi
    cat "/sys/class/udc/$udc/state" 2>/dev/null || echo "unknown"
}

udc_driver() {
    local udc="$1"
    local driver=""
    driver="$(readlink -f "/sys/bus/platform/devices/$udc/driver" 2>/dev/null || true)"
    [[ -n $driver ]] || return 0
    basename "$driver"
}

udc_supports_soft_connect() {
    local udc="$1"
    [[ -n $udc ]] || return 1
    [[ -w /sys/class/udc/$udc/soft_connect ]] || return 1
    [[ ${LESAVKA_FORCE_SOFT_CONNECT:-0} == 1 ]] && return 0
    local driver=""
    driver="$(udc_driver "$udc")"
    [[ $driver == "dwc2" ]] && return 1
    return 0
}

is_attached_state() {
    case "$1" in
        configured|addressed|default|suspended)
            return 0
            ;;
    esac
    return 1
}

detach_gadget() {
    local udc=""
    udc="$(find_udc)"
    local state
    state="$(udc_state "$udc")"
    case "$state" in
        ""|unknown|not\ attached)
            log "detach skipped (state=$state)"
            return 0
            ;;
    esac
    if udc_supports_soft_connect "$udc"; then
        echo 0 >"/sys/class/udc/$udc/soft_connect" 2>/dev/null || true
    fi
    if [[ -n ${LESAVKA_DETACH_CLEAR_UDC:-} && -e $G/UDC ]]; then
        echo "" >"$G/UDC" 2>/dev/null || true
    fi
    if [[ -n $udc ]]; then
        log "detached (state=${state:-unknown})"
    else
        log "detached (no UDC)"
    fi
}

attach_gadget() {
    if [[ ! -d $G ]]; then
        log "gadget path missing; need full setup"
        return 1
    fi
    local udc=""
    udc="$(find_udc)"
    if [[ -z $udc ]]; then
        log "UDC not found; need full setup"
        return 1
    fi
    if udc_supports_soft_connect "$udc"; then
        echo 1 >"/sys/class/udc/$udc/soft_connect" 2>/dev/null || true
    fi
    if [[ -n ${LESAVKA_ATTACH_WRITE_UDC:-} && -e $G/UDC ]]; then
        echo "$udc" >"$G/UDC" 2>/dev/null || true
    fi
    log "attached to $udc"
    return 0
}

gadget_has_expected_functions() {
    [[ -d $G/functions/hid.usb0 ]] || return 1
    [[ -d $G/functions/hid.usb1 ]] || return 1
    if [[ -z ${DISABLE_UAC:-} ]]; then
        [[ -d $G/functions/uac2.usb0 ]] || return 1
    fi
    if [[ -z ${DISABLE_UVC:-} ]]; then
        [[ -d $G/functions/uvc.usb0 ]] || return 1
    fi
    return 0
}

case "${1:-}" in
    --detach)
        detach_gadget
        exit 0
        ;;
    --detach-hard)
        LESAVKA_DETACH_CLEAR_UDC=1 detach_gadget
        exit 0
        ;;
    --attach)
        if attach_gadget; then
            exit 0
        fi
        ;;
    --help|-h)
        echo "Usage: $0 [--attach|--detach|--detach-hard]"
        exit 0
        ;;
esac

cleanup() {
    if [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]]; then
        LESAVKA_DETACH_CLEAR_UDC=1 detach_gadget
    else
        detach_gadget
    fi
}

DISABLE_UAC=${LESAVKA_DISABLE_UAC:-}
DISABLE_UVC=${LESAVKA_DISABLE_UVC:-}
ALLOW_RESET=${LESAVKA_ALLOW_GADGET_RESET:-}
UVC_FALLBACK=${LESAVKA_UVC_FALLBACK:-1}
UVC_STREAMING_INTERVAL=${LESAVKA_UVC_STREAMING_INTERVAL:-1}
UVC_MAXPACKET=${LESAVKA_UVC_MAXPACKET:-1024}
UVC_MAXBURST=${LESAVKA_UVC_MAXBURST:-1}
UVC_INTERVAL=${LESAVKA_UVC_INTERVAL:-}
UVC_WIDTH=${LESAVKA_UVC_WIDTH:-1280}
UVC_HEIGHT=${LESAVKA_UVC_HEIGHT:-720}
UVC_FPS=${LESAVKA_UVC_FPS:-20}
UVC_ADVERTISE_EXTRA_MODES=${LESAVKA_UVC_ADVERTISE_EXTRA_MODES:-0}
UVC_DISABLE_IRQ=${LESAVKA_UVC_DISABLE_IRQ:-}
flag_enabled() {
    case "${1,,}" in
        1|true|yes|on) return 0 ;;
        0|false|no|off|"") return 1 ;;
        *) return 0 ;;
    esac
}
if flag_enabled "${LESAVKA_UVC_BULK:-1}"; then
    UVC_BULK_REQUESTED=1
    UVC_BULK=1
else
    UVC_BULK_REQUESTED=
    UVC_BULK=
fi
UVC_CODEC=${LESAVKA_UVC_CODEC:-mjpeg}
case "${UVC_CODEC,,}" in
    mjpeg|mjpg|jpeg)
        UVC_CODEC=mjpeg
        ;;
    *)
        log "UVC codec '$UVC_CODEC' is not supported by the MJPEG UVC helper; using mjpeg"
        UVC_CODEC=mjpeg
        ;;
esac

uvc_fifo_min() {
    local path="$1"
    local raw=""
    raw="$(cat "$path" 2>/dev/null || true)"
    if [[ -z $raw ]]; then
        return 0
    fi
    echo "$raw" | tr ', ' '\n' | awk 'NF{print $1}' | awk '
        $1 > 0 { if (min == "" || $1 < min) min = $1 }
        END { if (min != "") print min }'
}

uvc_fifo_min_debugfs() {
    local path="$1"
    awk -F': ' '/^g_tx_fifo_size\[/{print $2}' "$path" 2>/dev/null | awk '
        $1 > 0 { if (min == "" || $1 < min) min = $1 }
        END { if (min != "") print min }' || true
}

uvc_fifo_np_debugfs() {
    local path="$1"
    awk -F': ' '/^g_np_tx_fifo_size/{print $2; exit}' "$path" 2>/dev/null || true
}

compute_uvc_payload_cap() {
    UVC_PAYLOAD_CAP=""
    UVC_PAYLOAD_SRC=""
    UVC_PAYLOAD_PCT=""
    UVC_FIFO_PERIODIC="$(uvc_fifo_min /sys/module/dwc2/parameters/g_tx_fifo_size)"
    UVC_FIFO_NP="$(uvc_fifo_min /sys/module/dwc2/parameters/g_np_tx_fifo_size)"
    UVC_FIFO_SRC_PERIODIC=""
    UVC_FIFO_SRC_NP=""
    if [[ -n $UVC_FIFO_PERIODIC ]]; then
        UVC_FIFO_SRC_PERIODIC="dwc2.params"
    fi
    if [[ -n $UVC_FIFO_NP ]]; then
        UVC_FIFO_SRC_NP="dwc2.params"
    fi
    UVC_UDC="$(ls /sys/class/udc 2>/dev/null | head -n1 || true)"
    if [[ -n $UVC_UDC ]]; then
        local params="/sys/kernel/debug/usb/$UVC_UDC/params"
        if [[ -r $params ]]; then
            if [[ -z $UVC_FIFO_PERIODIC ]]; then
                UVC_FIFO_PERIODIC="$(uvc_fifo_min_debugfs "$params")"
                if [[ -n $UVC_FIFO_PERIODIC ]]; then
                    UVC_FIFO_SRC_PERIODIC="debugfs.params"
                fi
            fi
            if [[ -z $UVC_FIFO_NP ]]; then
                UVC_FIFO_NP="$(uvc_fifo_np_debugfs "$params")"
                if [[ -n $UVC_FIFO_NP ]]; then
                    UVC_FIFO_SRC_NP="debugfs.params"
                fi
            fi
        fi
    fi

    if [[ -n ${LESAVKA_UVC_MAXPAYLOAD_LIMIT:-} ]]; then
        UVC_PAYLOAD_CAP="${LESAVKA_UVC_MAXPAYLOAD_LIMIT}"
        UVC_PAYLOAD_SRC="env"
        UVC_PAYLOAD_PCT=100
        return
    fi

    local chosen=""
    if [[ -n $UVC_BULK ]]; then
        if [[ -n $UVC_FIFO_NP ]]; then
            chosen="$UVC_FIFO_NP"
            UVC_PAYLOAD_SRC="${UVC_FIFO_SRC_NP:-dwc2.g_np_tx_fifo_size}"
        elif [[ -n $UVC_FIFO_PERIODIC ]]; then
            chosen="$UVC_FIFO_PERIODIC"
            UVC_PAYLOAD_SRC="${UVC_FIFO_SRC_PERIODIC:-dwc2.g_tx_fifo_size}"
        fi
    else
        if [[ -n $UVC_FIFO_PERIODIC ]]; then
            chosen="$UVC_FIFO_PERIODIC"
            UVC_PAYLOAD_SRC="${UVC_FIFO_SRC_PERIODIC:-dwc2.g_tx_fifo_size}"
        elif [[ -n $UVC_FIFO_NP ]]; then
            chosen="$UVC_FIFO_NP"
            UVC_PAYLOAD_SRC="${UVC_FIFO_SRC_NP:-dwc2.g_np_tx_fifo_size}"
        fi
    fi

    if [[ -z $chosen ]]; then
        return
    fi

    local pct=${LESAVKA_UVC_LIMIT_PCT:-95}
    if ((pct < 1)); then
        pct=1
    elif ((pct > 100)); then
        pct=100
    fi
    UVC_PAYLOAD_PCT=$pct
    local bytes=$((chosen * 4))
    UVC_PAYLOAD_CAP=$((bytes * pct / 100))
}

apply_uvc_payload_limits() {
    compute_uvc_payload_cap
    if [[ -n $UVC_PAYLOAD_CAP && $UVC_PAYLOAD_CAP -gt 0 ]]; then
        log "UVC fifo periodic=${UVC_FIFO_PERIODIC:-?} np=${UVC_FIFO_NP:-?} cap=${UVC_PAYLOAD_CAP}B pct=${UVC_PAYLOAD_PCT:-?} src=${UVC_PAYLOAD_SRC:-?} udc=${UVC_UDC:-?}"
        if ((UVC_MAXPACKET > UVC_PAYLOAD_CAP)); then
            log "clamping UVC maxpacket $UVC_MAXPACKET -> $UVC_PAYLOAD_CAP"
            UVC_MAXPACKET=$UVC_PAYLOAD_CAP
        fi
    fi
    if [[ -n $UVC_BULK && $UVC_MAXPACKET -gt 512 ]]; then
        log "clamping UVC maxpacket $UVC_MAXPACKET -> 512 (bulk)"
        UVC_MAXPACKET=512
    fi
}
if [[ -n ${LESAVKA_UVC_MJPEG:-} ]]; then
    UVC_CODEC=mjpeg
fi
MAX_SPEED=${LESAVKA_MAX_SPEED:-high-speed}

if [[ -z $UVC_INTERVAL ]]; then
    UVC_INTERVAL=$((10000000 / UVC_FPS))
fi
UVC_FRAME_SIZE=
UVC_INTERVAL_30=${LESAVKA_UVC_INTERVAL_30:-333333}
UVC_INTERVAL_20=${LESAVKA_UVC_INTERVAL_20:-500000}
UVC_MJPEG_BUDGET_BYTES_PER_SEC=${LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC:-4500000}
UVC_ISOCHRONOUS_LIMIT_PCT=${LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT:-85}

uvc_isochronous_budget_bytes_per_sec() {
    local maxpacket=${UVC_MAXPACKET:-1024}
    local pct=${UVC_ISOCHRONOUS_LIMIT_PCT:-85}
    if ((pct < 1)); then
        pct=1
    elif ((pct > 100)); then
        pct=100
    fi
    echo $((maxpacket * 8000 * pct / 100))
}

uvc_mjpeg_frame_size_for_fps() {
    local fps=$1
    if ((fps < 1)); then
        fps=1
    fi
    local budget=$UVC_MJPEG_BUDGET_BYTES_PER_SEC
    if [[ -z $UVC_BULK ]]; then
        local isoch_budget
        isoch_budget="$(uvc_isochronous_budget_bytes_per_sec)"
        if ((isoch_budget > 0 && budget > isoch_budget)); then
            budget=$isoch_budget
        fi
    fi
    local per_frame=$((budget / fps))
    if ((per_frame < 1)); then
        per_frame=1
    elif ((per_frame > 8388608)); then
        per_frame=8388608
    fi
    echo "$per_frame"
}

uvc_fps_for_interval() {
    local interval=$1
    if ((interval <= 0)); then
        echo "$UVC_FPS"
    else
        echo $((10000000 / interval))
    fi
}

UVC_DERIVED_FRAME_SIZE="$(uvc_mjpeg_frame_size_for_fps "$UVC_FPS")"
if [[ -n ${LESAVKA_UVC_FRAME_SIZE:-} ]]; then
    UVC_FRAME_SIZE=$LESAVKA_UVC_FRAME_SIZE
    if ((UVC_FRAME_SIZE > UVC_DERIVED_FRAME_SIZE)); then
        log "clamping requested UVC frame size $UVC_FRAME_SIZE -> $UVC_DERIVED_FRAME_SIZE (wire budget)"
        UVC_FRAME_SIZE=$UVC_DERIVED_FRAME_SIZE
    fi
else
    UVC_FRAME_SIZE=$UVC_DERIVED_FRAME_SIZE
fi

uvc_selected_frame_index() {
    if ! flag_enabled "$UVC_ADVERTISE_EXTRA_MODES"; then
        echo 1
        return
    fi
    case "${UVC_WIDTH}x${UVC_HEIGHT}" in
        1920x1080) echo 1 ;;
        1280x720) echo 2 ;;
        *) echo 2 ;;
    esac
}

write_active_mjpeg_frame_descriptor() {
    case "${UVC_WIDTH}x${UVC_HEIGHT}" in
        1920x1080)
            write_mjpeg_frame_descriptor 1080p 1920 1080
            ;;
        1280x720)
            write_mjpeg_frame_descriptor 720p 1280 720
            ;;
        *)
            log "unsupported MJPEG UVC profile ${UVC_WIDTH}x${UVC_HEIGHT}; advertising safe 720p"
            write_mjpeg_frame_descriptor 720p 1280 720
            ;;
    esac
}

uvc_frame_size_for() {
    local width=$1 height=$2
    local derived
    derived="$(uvc_mjpeg_frame_size_for_fps "$(uvc_fps_for_interval "$(uvc_default_interval_for "$width" "$height")")")"
    if [[ $width == "$UVC_WIDTH" && $height == "$UVC_HEIGHT" && -n ${LESAVKA_UVC_FRAME_SIZE:-} ]]; then
        if ((LESAVKA_UVC_FRAME_SIZE < derived)); then
            echo "$LESAVKA_UVC_FRAME_SIZE"
        else
            echo "$derived"
        fi
    else
        echo "$derived"
    fi
}

uvc_default_interval_for() {
    local width=$1 height=$2
    if [[ $width == "$UVC_WIDTH" && $height == "$UVC_HEIGHT" ]]; then
        echo "$UVC_INTERVAL"
    else
        echo "$UVC_INTERVAL_30"
    fi
}

write_mjpeg_frame_descriptor() {
    local name=$1 width=$2 height=$3
    local frame="$F/streaming/mjpeg/m/$name"
    local default_interval
    default_interval="$(uvc_default_interval_for "$width" "$height")"
    mkdir -p "$frame"
    echo 0 >"$frame/bmCapabilities"
    echo "$width" >"$frame/wWidth"
    echo "$height" >"$frame/wHeight"
    echo "$(uvc_frame_size_for "$width" "$height")" >"$frame/dwMaxVideoFrameBufferSize"
    echo "$default_interval" >"$frame/dwDefaultFrameInterval"
    if flag_enabled "$UVC_ADVERTISE_EXTRA_MODES"; then
        cat <<EOF >"$frame/dwFrameInterval"
${UVC_INTERVAL_30}
${UVC_INTERVAL_20}
EOF
    else
        echo "$default_interval" >"$frame/dwFrameInterval"
    fi
}

wait_for_enum() {
    local tries=${1:-50}   # 50 x 100ms = 5s
    UDC_STATE="unknown"
    UDC_SPEED="unknown"
    for ((i=0; i<tries; i++)); do
        UDC_STATE=$(cat "/sys/class/udc/$UDC/state" 2>/dev/null || echo "unknown")
        UDC_SPEED=$(cat "/sys/class/udc/$UDC/current_speed" 2>/dev/null || echo "unknown")
        if [[ "$UDC_STATE" != "not attached" && "$UDC_STATE" != "unknown" ]]; then
            return 0
        fi
        sleep 0.1
    done
    return 1
}

exec 2> >(tee -a /tmp/lesavka-core.debug.$(date +%s).log)
set -x
echo "[lesavka-core] running: $0 (sha1sum=$(sha1sum "$0" | cut -d' ' -f1))"

#──────────────────────────────────────────────────
# 1.  Ensure overlay + kernel modules
#──────────────────────────────────────────────────
CFG=/boot/config.txt
grep -q 'dtoverlay=dwc2,dr_mode=peripheral' "$CFG" || echo 'dtoverlay=dwc2,dr_mode=peripheral' >> "$CFG"

modprobe dwc2 || { echo "dwc2 not in kernel; abort" >&2; exit 1; }
modprobe libcomposite || { echo "libcomposite not in kernel; abort" >&2; exit 1; }

if [[ -n ${LESAVKA_RELOAD_UVCVIDEO:-} ]]; then
    modprobe -r uvcvideo 2>/dev/null || true
fi
modprobe uvcvideo || { echo "uvcvideo not in kernel; abort" >&2; exit 1; }

udevadm control --reload || log "⚠️  udevadm control --reload failed or timed out"
udevadm trigger --subsystem-match=video4linux || log "⚠️  udevadm video4linux trigger failed"
udevadm settle --timeout=5 || log "⚠️  udevadm settle timed out"

#──────────────────────────────────────────────────
# 2.  Wait for UDC device to appear (max 10 s)
#──────────────────────────────────────────────────
log "⏳ waiting for UDC to register ..."
UDC=""
for _ in {1..100}; do                       # 100 × 100ms = 10s
    UDC=$(ls /sys/class/udc 2>/dev/null | head -n1) && [[ -n $UDC ]] && break
    sleep 0.1
done

if [[ -z $UDC ]]; then
    log "⚠️  UDC still absent - trying manual bind"
    for drv in dwc2 dwc3; do
        drv_root="/sys/bus/platform/drivers/$drv"
        [[ -d $drv_root ]] || continue
        for node in /sys/bus/platform/devices/*usb*; do
            node=${node##*/}               # strip path
            echo "$node" >"$drv_root/bind" 2>/dev/null || continue
        done
    done
    # re-check for another 5 s
    for i in {1..50}; do
        UDC=$(ls /sys/class/udc 2>/dev/null | head -n1) && [[ -n $UDC ]] && break
        sleep 0.1
    done
fi

[[ -n $UDC ]] || { log "❌ UDC not present after manual bind"; exit 1; }
log "✅ UDC detected: $UDC"

# If a gadget is already configured, avoid tearing it down unless forced.
GADGET_NEEDS_REBUILD=0
if [[ -d $G && -z $ALLOW_RESET ]]; then
    if [[ -s $G/UDC || -d $G/configs/c.1 ]]; then
        if gadget_has_expected_functions; then
            log "🔒 gadget already configured; skipping reset."
            log "    Set LESAVKA_ALLOW_GADGET_RESET=1 to force rebuild."
            attach_gadget || true
            exit 0
        fi
        log "⚠️  gadget is configured but missing expected functions; continuing toward rebuild."
        GADGET_NEEDS_REBUILD=1
    fi
fi

# Guard against lockups: if the gadget is already bound, don't reset unless forced.
BOUND_UDC=""
if [[ -r $G/UDC ]]; then
    BOUND_UDC=$(cat "$G/UDC" 2>/dev/null || true)
fi
if [[ -n $BOUND_UDC && -z $ALLOW_RESET && "$GADGET_NEEDS_REBUILD" != "1" ]]; then
    log "🔒 gadget already bound to '$BOUND_UDC' - refusing reset."
    log "    Set LESAVKA_ALLOW_GADGET_RESET=1 to force."
    exit 0
fi

# Guard against lockups: don't reset gadget while host is attached unless forced.
UDC_STATE="$(udc_state "$UDC")"
if [[ -z $ALLOW_RESET ]] && is_attached_state "$UDC_STATE" && [[ "$GADGET_NEEDS_REBUILD" != "1" ]]; then
    log "🔒 UDC state is '$UDC_STATE' - refusing gadget reset while host attached."
    log "    Set LESAVKA_ALLOW_GADGET_RESET=1 to force."
    exit 0
fi
if [[ "$GADGET_NEEDS_REBUILD" == "1" ]]; then
    log "♻️  incomplete gadget detected; rebuilding even though the old gadget is still bound."
fi

#──────────────────────────────────────────────────
# 3.  (Re‑)create gadget
#──────────────────────────────────────────────────
mountpoint -q /sys/kernel/config || mount -t configfs none /sys/kernel/config

if [[ -d $G ]]; then
  echo '' >"$G/UDC" 2>/dev/null || true
  sleep 0.2
  # configfs doesn't allow unlinking attribute files; remove links then rmdir.
  find "$G" -type l -delete 2>/dev/null || true
  for dir in "$G/functions" "$G/configs" "$G/strings" "$G/os_desc" "$G/webusb"; do
    [[ -d $dir ]] || continue
    find "$dir" -mindepth 1 -depth -type d -exec rmdir {} \; 2>/dev/null || true
  done
fi

mkdir -p "$G"
echo 0x1d6b >"$G/idVendor"       # Linux Foundation
echo 0x0104 >"$G/idProduct"      # Multifunction Composite Gadget
echo 0x0200 >"$G/bcdUSB"
echo "$MAX_SPEED" >"$G/max_speed"

mkdir -p "$G/strings/0x409"
echo "$(cat /proc/sys/kernel/random/uuid)" >"$G/strings/0x409/serialnumber"
echo "Lesavka"                               >"$G/strings/0x409/manufacturer"
echo "Lesavka Composite"                     >"$G/strings/0x409/product"

# ----------------------- HID keyboard (usb0) -----------------------
mkdir -p "$G/functions/hid.usb0"
echo 1  >"$G/functions/hid.usb0/protocol"
echo 1  >"$G/functions/hid.usb0/subclass"
echo 8  >"$G/functions/hid.usb0/report_length"
printf '\x05\x01\x09\x06\xa1\x01\x05\x07\x19\xe0\x29\xe7\x15\x00\x25\x01'\
'\x75\x01\x95\x08\x81\x02\x95\x01\x75\x08\x81\x01\x95\x05\x75\x01\x05'\
'\x08\x19\x01\x29\x05\x91\x02\x95\x01\x75\x03\x91\x01\x95\x06\x75\x08'\
'\x15\x00\x25\x65\x05\x07\x19\x00\x29\x65\x81\x00\xc0' \
  >"$G/functions/hid.usb0/report_desc"

# ----------------------- HID mouse (usb1) --------------------------
mkdir -p "$G/functions/hid.usb1"
echo 2 > "$G/functions/hid.usb1/protocol"   # Boot mouse
echo 1 > "$G/functions/hid.usb1/subclass"
echo 4 > "$G/functions/hid.usb1/report_length"
printf '\x05\x01\x09\x02\xa1\x01\x09\x01\xa1\x00'\
'\x05\x09\x19\x01\x29\x03\x15\x00\x25\x01\x95\x03\x75\x01\x81\x02'\
'\x95\x01\x75\x05\x81\x03'\
'\x05\x01\x09\x30\x09\x31\x09\x38\x15\x81\x25\x7f\x75\x08\x95\x03\x81\x06'\
'\xc0\xc0' >"$G/functions/hid.usb1/report_desc"

if [[ -z $DISABLE_UAC ]]; then
  # ---------- UAC2 function - speaker + mic, 2×48 kHz stereo ---------
  mkdir -p "$G/functions/uac2.usb0"
  U="$G/functions/uac2.usb0"
  # Playback (speaker)
  echo 0x3     >"$U/p_chmask"   # L+R
  echo 48000   >"$U/p_srate"
  echo 2       >"$U/p_ssize"    # 16 bit
  # Capture (microphone)
  echo 0x3     >"$U/c_chmask"
  echo 48000   >"$U/c_srate"
  echo 2       >"$U/c_ssize"
  # Keep host-visible USB mixer controls out of the descriptor by default.
  # PipeWire/WirePlumber can persist empty channel maps for gadget hardware
  # volume controls, leaving the Lesavka sink routed but stuck at digital zero.
  if [[ ${LESAVKA_UAC_HOST_MIXER_CONTROLS:-0} == 1 ]]; then
    echo 1 >"$U/p_volume_present" 2>/dev/null || true
    echo 1 >"$U/p_mute_present" 2>/dev/null || true
    echo 1 >"$U/c_volume_present" 2>/dev/null || true
    echo 1 >"$U/c_mute_present" 2>/dev/null || true
  else
    echo 0 >"$U/p_volume_present" 2>/dev/null || true
    echo 0 >"$U/p_mute_present" 2>/dev/null || true
    echo 0 >"$U/c_volume_present" 2>/dev/null || true
    echo 0 >"$U/c_mute_present" 2>/dev/null || true
  fi
  # The dwc2-backed gadget path behaves more reliably with adaptive capture
  # than with the default async+feedback mode for the host-facing mic stream.
  echo adaptive >"$U/c_sync"    2>/dev/null || true
  # Optional: allocate a few extra request buffers
  echo 32      >"$U/req_number" 2>/dev/null || true
  # Friendly host-side labels when this kernel exposes UAC2 descriptor names.
  # Browsers still choose their own wording, but these make Lesavka easier to
  # recognize than the generic "Multifunction Composite Gadget" fallback.
  echo "Lesavka Audio Bridge" >"$U/function_name" 2>/dev/null || true
  echo "Lesavka Audio Control" >"$U/if_ctrl_name" 2>/dev/null || true
  echo "Lesavka Speaker Playback" >"$U/p_it_name" 2>/dev/null || true
  echo "Lesavka Host Speaker" >"$U/p_ot_name" 2>/dev/null || true
  echo "Lesavka Remote Mic" >"$U/c_it_name" 2>/dev/null || true
  echo "Lesavka Microphone" >"$U/c_ot_name" 2>/dev/null || true
else
  log "🔇 UAC2 disabled (LESAVKA_DISABLE_UAC set)"
fi

if [[ -z $DISABLE_UVC ]]; then
  # ----------------------- UVC function (usb‑video) ------------------
  mkdir -p "$G/functions/uvc.usb0"
  F="$G/functions/uvc.usb0"
  if [[ -n $UVC_BULK_REQUESTED ]]; then
    if [[ -e "$F/streaming_bulk" ]]; then
      if echo 1 >"$F/streaming_bulk" 2>/dev/null && [[ $(cat "$F/streaming_bulk" 2>/dev/null) == 1 ]]; then
        UVC_BULK=1
      else
        log "UVC bulk request did not stick; using isochronous descriptors"
        UVC_BULK=
        UVC_MAXPACKET=${LESAVKA_UVC_MAXPACKET:-1024}
      fi
    else
      # Some kernels do not expose the patched bulk-transfer knob. Falling
      # back to isochronous must also avoid the 512-byte bulk packet clamp;
      # otherwise MJPEG frames are much more likely to arrive truncated.
      log "UVC bulk requested but this kernel lacks streaming_bulk; using isochronous descriptors"
      UVC_BULK=
      UVC_MAXPACKET=${LESAVKA_UVC_MAXPACKET:-1024}
    fi
  elif [[ -e "$F/streaming_bulk" ]]; then
    if ! echo 0 >"$F/streaming_bulk" 2>/dev/null || [[ $(cat "$F/streaming_bulk" 2>/dev/null) != 0 ]]; then
      log "UVC streaming_bulk could not be disabled; refusing to infer transport from attribute existence"
      UVC_BULK=
    fi
  fi
  apply_uvc_payload_limits
  echo "$UVC_STREAMING_INTERVAL" >"$F/streaming_interval"
  echo "$UVC_MAXPACKET" >"$F/streaming_maxpacket"
  echo "$UVC_MAXBURST" >"$F/streaming_maxburst"
  if [[ -n $UVC_BULK && $(cat "$F/streaming_bulk" 2>/dev/null) != 1 ]]; then
    log "UVC bulk readback diverged after descriptor setup; falling back to isochronous sizing"
    UVC_BULK=
    UVC_MAXPACKET=${LESAVKA_UVC_MAXPACKET:-1024}
    apply_uvc_payload_limits
    echo "$UVC_MAXPACKET" >"$F/streaming_maxpacket"
  fi

# ── 1.  FORMAT DESCRIPTOR ──────────────────────────────────────────
  if [[ "$UVC_CODEC" == "mjpeg" ]]; then
    mkdir -p "$F/streaming/mjpeg/m"
    echo "$(uvc_selected_frame_index)" >"$F/streaming/mjpeg/m/bDefaultFrameIndex" 2>/dev/null || true
    echo 0 >"$F/streaming/mjpeg/m/bmaControls" 2>/dev/null || true

    if flag_enabled "$UVC_ADVERTISE_EXTRA_MODES"; then
      write_mjpeg_frame_descriptor 1080p 1920 1080
      write_mjpeg_frame_descriptor 720p 1280 720
    else
      write_active_mjpeg_frame_descriptor
    fi
  else
    # uncompressed YUY2, 16 bpp
    mkdir -p "$F/streaming/uncompressed/yuyv"
    # GUID = {59555932-0000-0010-8000-00aa00389b71} (“YUY2”) little-endian
    printf '\x59\x55\x59\x32\x00\x00\x10\x00\x80\x00\x00\xaa\x00\x38\x9b\x71' \
        >"$F/streaming/uncompressed/yuyv/guidFormat"
    echo 16  >"$F/streaming/uncompressed/yuyv/bBitsPerPixel"

    mkdir -p "$F/streaming/uncompressed/yuyv/480p"
    echo "$UVC_WIDTH"  >"$F/streaming/uncompressed/yuyv/480p/wWidth"
    echo "$UVC_HEIGHT" >"$F/streaming/uncompressed/yuyv/480p/wHeight"
    echo "$UVC_FRAME_SIZE" >"$F/streaming/uncompressed/yuyv/480p/dwMaxVideoFrameBufferSize"
    echo "$UVC_INTERVAL" >"$F/streaming/uncompressed/yuyv/480p/dwDefaultFrameInterval"
    cat <<EOF >"$F/streaming/uncompressed/yuyv/480p/dwFrameInterval"
${UVC_INTERVAL}
$((UVC_INTERVAL * 2))
EOF
  fi

# ── 3.  REQUIRED HEADER LINKS (per UVC gadget docs) ────────────────
  mkdir -p "$F/streaming/header/h"
  pushd "$F/streaming/header/h" >/dev/null
  if [[ "$UVC_CODEC" == "mjpeg" ]]; then
      ln -s ../../mjpeg/m mjpeg
  else
      ln -s ../../uncompressed/yuyv yuyv
  fi
  popd >/dev/null

  for s in fs hs ss; do
      mkdir -p "$F/streaming/class/$s"
      pushd "$F/streaming/class/$s" >/dev/null
      ln -s ../../header/h h
      popd >/dev/null
  done

# ── 4.  Video‑Control interface ─────────────────────────────────────
  mkdir -p "$F/control/header/h"
  # The kernel UVC gadget docs require direct symlinks at control/class/fs and
  # control/class/ss. High-speed control descriptors are not exposed here.
  pushd "$F/control" >/dev/null
  ln -s header/h class/fs 2>/dev/null || true
  ln -s header/h class/ss 2>/dev/null || true
  popd >/dev/null

  if [[ -n $UVC_DISABLE_IRQ ]]; then
    echo 0 >"$F/control/enable_interrupt_ep" 2>/dev/null || true
  fi

# optional: hide unsupported controls
  echo 0 >"$F/control/terminal/camera/default/bmControls"    2>/dev/null || true
  echo 0 >"$F/control/processing/default/bmControls"         2>/dev/null || true

# friendly label
  mkdir -p "$F/control/header/h/strings/0x409" 2>/dev/null || true
  echo "Lesavka UVC" >"$F/control/header/h/strings/0x409/label" 2>/dev/null || true
else
  log "📷 UVC disabled (LESAVKA_DISABLE_UVC set)"
fi

# ----------------------- configuration -----------------------------
mkdir -p "$G/configs/c.1/strings/0x409"
echo 500 > "$G/configs/c.1/MaxPower"
# echo "Config 1" > "$G/configs/c.1/strings/0x409/configuration"
config_label="Config 1: HID"
if [[ -z $DISABLE_UAC ]]; then
  config_label+=" + UAC2"
fi
if [[ -z $DISABLE_UVC ]]; then
  config_label+=" + UVC"
fi
echo "$config_label" >"$G/configs/c.1/strings/0x409/configuration"

ln -s  $G/functions/hid.usb0  $G/configs/c.1/
ln -s  $G/functions/hid.usb1  $G/configs/c.1/
if [[ -z $DISABLE_UAC ]]; then
  ln -s  $U                   $G/configs/c.1/
fi
if [[ -z $DISABLE_UVC ]]; then
  ln -s  $G/functions/uvc.usb0  $G/configs/c.1/
fi

# mkdir -p $G/functions/hid.usb0/os_desc
# mkdir -p $G/functions/hid.usb1/os_desc
# mkdir -p $U/os_desc

# ---------- optional Microsoft OS descriptors ----------------------
# if [ -e "$G/os_desc/use" ]; then
#     echo 1        >"$G/os_desc/use"
#     echo 0xcd     >"$G/os_desc/b_vendor_code"
#     echo "MSFT100" >"$G/os_desc/qw_sign"

#     ln -s "$G/configs/c.1" "$G/os_desc"          # creates $G/os_desc/conf

#     echo "Lesavka Keyboard" >"$G/functions/hid.usb0/os_desc/interface"
#     echo "Lesavka Mouse"    >"$G/functions/hid.usb1/os_desc/interface"
#     echo "Lesavka Mic+Spkr" >"$U/os_desc/interface"
# fi

#──────────────────────────────────────────────────
# 4.  Bind gadget
#──────────────────────────────────────────────────
echo "$UDC" >"$G/UDC"
parts="hidg0,hidg1"
[[ -z $DISABLE_UAC ]] && parts+=",UAC2"
[[ -z $DISABLE_UVC ]] && parts+=",UVC"
log "🎉 gadget bound on $UDC ($parts)"

if wait_for_enum 50; then
    log "✅ UDC state is '$UDC_STATE' (speed=$UDC_SPEED)"
else
    log "⚠️  UDC state is '$UDC_STATE' (speed=$UDC_SPEED). Host not enumerated."
    if [[ -z $DISABLE_UVC && "$UVC_FALLBACK" != "0" ]]; then
        log "♻️  retrying without UVC (LESAVKA_UVC_FALLBACK=0 to disable)"
        exec env LESAVKA_DISABLE_UVC=1 LESAVKA_UVC_FALLBACK=0 "$0"
    fi
fi

exit 0
