#!/usr/bin/env bash
# midi-autoconnect — enforce a fixed ALSA-seq MIDI wiring.
#
# Rules (re-applied every $INTERVAL seconds, idempotent):
#   1. Launch Control XL :0  ->  Midi Through :0   (controls out)
#   2. ACTIVELY CUT  Midi Through :0 -> Launch Control XL :0
#
# aconnect matches clients by name, so this survives replug / renumbering.
# "Connection is already subscribed" errors are expected and ignored.
#
# ---------------------------------------------------------------------------
# 2026-07-27 — why rule 2 INVERTED from "connect" to "disconnect".
#
# The old rule 2 connected Midi Through BACK to the LCXL, labelled "LED/visual
# feedback". It is in fact a MIDI FEEDBACK LOOP and the direct cause of the
# long-standing "my button colours snap to yellow the moment I press them" bug:
#
#   press button -> LCXL emits Note On velocity 127
#                -> LCXL:0 -> Midi Through:0 -> (rule 2) -> LCXL:0
#                -> the LCXL reads an INBOUND note velocity as an LED COLOUR
#                -> 127 = 0x7F = full red + full green = YELLOW
#
# So every press repainted itself yellow, wiping whatever the HUD had set.
# `aconnect -d 14:0 24:0` fixed it by hand — and then this service silently
# recreated the loop within 2 seconds, which is why the fix "never stuck"
# across a Tidal reboot. The reconcile loop was fighting the operator.
#
# The LCXL's LEDs do NOT need this link: the HUD drives them directly with
# SysEx via `aseqsend` to the LCXL's own port (lib/lcxl-leds.js). Nothing was
# ever gained by the return leg. LCXL -> Midi Through -> Ardour still works, so
# Ardour's click-to-map fader learn is unaffected; Ardour simply stops talking
# back to the surface, which is what we want when the HUD owns the LEDs.
#
# We DISCONNECT rather than merely "not connect", because Ardour's control
# surface and SuperCollider's `MIDIIn.connectAll` can both recreate the return
# leg on their own. Same shape as every other binding on this rig: a
# connection that must be re-asserted, never assumed.
#
# The old rule 3 (Midi Through -> every aseqdump) is gone too: aseqdump
# clients already subscribe to the LCXL directly, so the extra hop delivered
# every CC TWICE to the HUD — pure duplicated work on a renderer we are
# actively trying to keep off the audio thread's back.
# ---------------------------------------------------------------------------

set -u

# The source is RESOLVED, not hardcoded, because the device name changed with
# the hardware: v2 advertises "Launch Control XL", v3 advertises "LCXL3 1".
# The old hardcoded name matched neither of the v3 ports, and because connect()
# sends aconnect's complaints to /dev/null, the failure was perfectly silent —
# Ardour simply stopped hearing the surface on 2026-08-28 with every check green.
#
# Preference order matters:
#   1. "ParVagues LCXL3" — the lcxl3-driver's virtual port. In DAW mode the v3
#      reports on its DAW interface with a FIXED CC map that disagrees with the
#      corpus, so the driver's translated, v2-numbered stream is the one Ardour
#      learned its 12 CCs from. Prefer it whenever it exists.
#   2. "LCXL3"           — the raw v3, for when the driver is not running.
#   3. "Launch Control XL" — v2, still correct if the old surface is plugged in.
LCXL_CANDIDATES=("ParVagues LCXL3" "LCXL3" "Launch Control XL")
THRU="Midi Through"
LCXL_PORT=0          # "first channel"
THRU_PORT=0
INTERVAL="${MIDI_AUTOCONNECT_INTERVAL:-2}"
LAST_SRC=""

connect() {  # $1 = source, $2 = dest ; silent unless it's a real failure
    aconnect "$1" "$2" 2>/dev/null
}

disconnect() {  # $1 = source, $2 = dest ; silent when there was nothing to cut
    aconnect -d "$1" "$2" 2>/dev/null
}

# Echo the resolved source ONCE per change, never every INTERVAL: a line every
# 2s is not a log, it is a way to make a real change invisible.
resolve_lcxl() {
    local names; names="$(aconnect -i 2>/dev/null; aconnect -o 2>/dev/null)"
    local cand
    for cand in "${LCXL_CANDIDATES[@]}"; do
        if printf '%s' "$names" | grep -qF "$cand"; then
            printf '%s' "$cand"
            return 0
        fi
    done
    return 1
}

reconcile() {
    local src
    if ! src="$(resolve_lcxl)"; then
        if [ "$LAST_SRC" != "__none__" ]; then
            echo "midi-autoconnect: no surface present (looked for: ${LCXL_CANDIDATES[*]})"
            LAST_SRC="__none__"
        fi
        return 0
    fi
    if [ "$src" != "$LAST_SRC" ]; then
        echo "midi-autoconnect: surface = '$src'"
        LAST_SRC="$src"
    fi
    connect    "$src:$LCXL_PORT" "$THRU:$THRU_PORT"   # rule 1: controls out
    disconnect "$THRU:$THRU_PORT" "$src:$LCXL_PORT"   # rule 2: kill the loop
}

echo "midi-autoconnect: reconciling every ${INTERVAL}s"
while :; do
    reconcile
    sleep "$INTERVAL"
done
