#!/usr/bin/env bash
# deck-it - the launcher
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

# Usage: deck-it                start the dashboard + extended display over USB
#        deck-it stop           tear it all down
#        deck-it env            print MON/PORT/VNC_PORT for this config
#        deck-it pair           pair the tablet for Wi-Fi by scanning a QR code
#        deck-it wifi           start over Wi-Fi
#        deck-it install        print the exec-once line for hypr-theme's autostart
#        deck-it uninstall      forget the generated trigger
#        deck-it watch-usb      start on plug-in, tear down on unplug (run from exec-once)
set -u
DIR="$(dirname "$(readlink -f "$0")")"
SELF="$DIR/deck-it"
CONF="${XDG_CONFIG_HOME:-$HOME/.config}/deck-it"
VNC="$CONF/vnc"
RUN="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/deck-it"

if [[ "${1:-}" == "env" ]]; then
    (cd "$DIR" && exec python3 -m deckit env)
fi

MON=TAB PORT=8765 VNC_PORT=5900
env_out=$(cd "$DIR" && python3 -m deckit env 2>&1) && eval "$env_out" || echo "Config problem: $env_out" >&2

teardown() {
    pkill -f "^bash $SELF watch" 2>/dev/null
    pkill -x wayvnc
    pkill -f '[p]ython3 -m deckit' 2>/dev/null
    pkill -f "^[^ ]*swaybg -o $MON( |$)" 2>/dev/null
    hyprctl output remove "$MON" >/dev/null 2>&1
    local s
    for s in $(adb devices | awk 'NR > 1 && $2 == "device" {print $1}'); do
        timeout 5 adb -s "$s" reverse --remove-all 2>/dev/null
    done
    rm -f "$RUN/serial"
}

tunnel() {
    adb -s "$1" reverse "tcp:$VNC_PORT" "localfilesystem:$RUN/vnc.sock" >/dev/null &&
        adb -s "$1" reverse "tcp:$PORT" "localfilesystem:$RUN/http.sock" >/dev/null
}

# The display stream needs a password over TLS; both live in a 0700 dir and AVNC gets them in the link.
vnc_setup() {
    mkdir -p -m 700 "$VNC"
    (
        umask 077
        [[ -s "$VNC/password" ]] || tr -dc A-Za-z0-9 </dev/urandom | head -c 24 > "$VNC/password"
        [[ -s "$VNC/tls_cert.pem" ]] || openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
            -days 3650 -subj /CN=deck-it -keyout "$VNC/tls_key.pem" -out "$VNC/tls_cert.pem" 2>/dev/null
        printf 'enable_auth=true\nusername=deck-it\npassword=%s\nprivate_key_file=%s\ncertificate_file=%s\n' \
            "$(cat "$VNC/password")" "$VNC/tls_key.pem" "$VNC/tls_cert.pem" > "$VNC/config"
    )
}

open_avnc() {
    adb -s "$1" shell am start -a android.intent.action.VIEW \
        -d "'vnc://127.0.0.1:$VNC_PORT?VncUsername=deck-it&VncPassword=$(cat "$VNC/password")'" com.gaurav.avnc >/dev/null 2>&1
}

# A dropped Wi-Fi link can still report "device", and adb calls on it hang, so probe with a timeout.
alive() {
    timeout 6 adb -s "$1" shell true >/dev/null 2>&1
}

wifi_connect() {
    local addr
    # An explicit address, or the last one used. Never "any TCP device": a TV
    # or another device with ADB on the network would be picked instead.
    for addr in ${WIFI_ADDR:-} $(cat "$RUN/serial" 2>/dev/null); do
        [ -n "$addr" ] || continue
        timeout 5 adb connect "$addr" 2>/dev/null | grep -q '^connected\|already connected' || true
        alive "$addr" && { echo "$addr"; return 0; }
    done
    for addr in $(adb mdns services 2>/dev/null | awk '/_adb-tls-connect/ {print $NF}' | sort -u); do
        timeout 5 adb connect "$addr" 2>/dev/null | grep -q '^connected\|already connected' &&
            alive "$addr" && { echo "$addr"; return 0; }
    done
    return 1
}

usb_present() {
    [[ "$(timeout 3 adb -d get-state 2>/dev/null)" == device ]]
}

case "${1:-}" in
stop)
    teardown
    echo "Stopped"
    exit 0
    ;;
teardown)
    teardown
    exit 0
    ;;
pair)
    if ! adb mdns services >/dev/null 2>&1; then
        echo "This adb has no mDNS discovery. Pair from the tablet instead:"
        echo "  Developer options > Wireless debugging > Pair device with pairing code"
        echo "  adb pair <ip>:<pair-port> <code>"
        echo "  adb connect <ip>:<connect-port>"
        exit 1
    fi
    command -v qrencode >/dev/null || { echo "Install qrencode first"; exit 1; }
    name="deck-it-$(tr -dc a-z0-9 </dev/urandom | head -c 8)"
    pass=$(tr -dc 0-9 </dev/urandom | head -c 8)
    echo "On the tablet: Developer options > Wireless debugging > Pair device with QR code, and scan this:"
    echo
    qrencode -t ansiutf8 -m 2 "WIFI:T:ADB;S:$name;P:$pass;;"
    echo "Waiting for the scan (2 minutes)..."
    for _ in {1..120}; do
        addr=$(adb mdns services | awk -v n="$name" '$1 == n && /_adb-tls-pairing/ {print $NF; exit}')
        if [[ -n "$addr" ]]; then
            adb pair "$addr" "$pass" | grep -q 'Successfully paired' || { echo "Pairing failed"; exit 1; }
            echo "Paired. Connecting..."
            for _ in {1..10}; do addr=$(wifi_connect) && break; sleep 2; done
            [[ -n "$addr" ]] && echo "Connected to $addr. Start with: deck-it wifi" || echo "Paired, but couldn't connect yet. Try: deck-it wifi"
            exit 0
        fi
        sleep 1
    done
    echo "No scan detected. Is the tablet on the same Wi-Fi network as this PC?"
    exit 1
    ;;
watch)
    # Runs in the background in Wi-Fi mode: reconnect when the tablet drops, then restore the tunnels.
    while sleep 5; do
        serial=$(cat "$RUN/serial" 2>/dev/null) || exit 0
        alive "$serial" && timeout 6 adb -s "$serial" reverse --list 2>/dev/null | grep -q "tcp:$PORT" && continue
        timeout 5 adb disconnect "$serial" >/dev/null 2>&1
        new=$(wifi_connect) || continue
        tunnel "$new" && echo "$new" > "$RUN/serial" && open_avnc "$new"
    done
    ;;
install)
    echo "Add this line to hypr-theme's autostart so the tablet is picked up at login and when plugged in:"
    echo "    exec-once = $SELF watch-usb"
    echo "On a new tablet, run 'deck-it' once by hand to accept the USB debugging prompt."
    exit 0
    ;;
uninstall)
    rm -f "$CONF/trigger"
    echo "Remove the 'exec-once = $SELF watch-usb' line from hypr-theme's autostart."
    exit 0
    ;;
setup-display)
    (cd "$DIR" && exec python3 -m deckit setup-display)
    ;;
watch-usb)
    # A user-space watcher, so it can reach the session: udev RUN= cannot.
    # Start on plug-in, tear down on unplug.
    known=""
    while sleep 5; do
        now=$(timeout 4 adb -d get-serialno 2>/dev/null)
        if [[ -n "$now" && "$now" != "$known" ]]; then
            "$SELF" start >/dev/null 2>&1 &
            known=$now
        elif [[ -z "$now" && -n "$known" ]]; then
            teardown
            known=""
        fi
    done
    ;;
wifi)
    if [[ "${2:-}" == *:* ]]; then WIFI_ADDR="$2"; fi
    echo "Looking for the tablet on Wi-Fi..."
    while :; do
        [[ "${2:-}" == --wait ]] && usb_present && { echo "Tablet is on USB, leaving it to the USB start"; exit 0; }
        ANDROID_SERIAL=$(wifi_connect) && break
        [[ "${2:-}" == --wait ]] || { echo "Not found. Turn on Wireless debugging on the tablet, or pair first with: deck-it pair"; exit 1; }
        sleep 10
    done
    ;;
"")
    echo "Waiting for tablet on USB (plug it in, allow USB debugging)..."
    timeout 60 adb wait-for-usb-device || { echo "No tablet found"; exit 1; }
    ANDROID_SERIAL=$(adb -d get-serialno)
    ;;
*)
    sed -n '2,9p' "$SELF" | sed 's/^# \{0,1\}//'
    exit 1
    ;;
esac
export ANDROID_SERIAL

for _ in {1..30}; do hyprctl monitors >/dev/null 2>&1 && break; sleep 1; done

(cd "$DIR" && python3 -m deckit setup-display) || exit 1

# Both servers listen on sockets in a 0700 dir, so only this user and its adb tunnel can reach them.
mkdir -p -m 700 "$RUN"
vnc_setup
# wayvnc leaves its socket behind when killed and then refuses to bind, so clear it first.
pgrep -x wayvnc >/dev/null || { rm -f "$RUN/vnc.sock"; setsid wayvnc -C "$VNC/config" -u -o "$MON" "$RUN/vnc.sock" >/dev/null 2>&1 & }
pgrep -f '[p]ython3 -m deckit' >/dev/null || (cd "$DIR" && setsid python3 -m deckit >server.log 2>&1 &)
sleep 1

tunnel "$ANDROID_SERIAL" || { echo "Could not set up the tunnel to the tablet"; exit 1; }
# Opening the page with the secret stores it as a cookie in the tablet's browser, which unlocks the API.
token="$CONF/token"
for _ in {1..10}; do [[ -s "$token" ]] && break; sleep 0.5; done
open_avnc "$ANDROID_SERIAL"
sleep 1
# The dashboard last, so it is the foreground app; AVNC is one switch away.
adb shell am start -a android.intent.action.VIEW -d "http://127.0.0.1:$PORT/?t=$(cat "$token")" >/dev/null 2>&1

if [[ "${1:-}" == wifi ]]; then
    echo "$ANDROID_SERIAL" > "$RUN/serial"
    pgrep -f "^bash $SELF watch" >/dev/null || setsid bash "$SELF" watch >/dev/null 2>&1 &
fi

echo "Ready. Display is in AVNC, dashboard at http://127.0.0.1:$PORT"
