#!/usr/bin/env bash
# Build and install the POG422 fork of the Exar xr_usb_serial driver on Raspberry Pi OS.
# Run as topbox (not root); sudo is used only for install/modprobe steps.
set -euo pipefail

# Self-heal CRLF when synced from Windows/Dropbox (same as fixture-timing/install.sh).
if grep -q $'\r' "${BASH_SOURCE[0]}" 2>/dev/null; then
  sed -i 's/\r$//' "${BASH_SOURCE[0]}"
  exec bash "${BASH_SOURCE[0]}" "$@"
fi

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"

# Kernel 6.12+ Pi headers reject M= paths with spaces ("4-Board Test Fixture").
setup_build_link() {
  local cache_dir="${HOME:?HOME not set}/.cache"
  local link="${cache_dir}/pog422-xr-usb-serial-build"
  mkdir -p "${cache_dir}"
  rm -rf "${link}"
  ln -sfn "${ROOT}" "${link}"
  if [[ ! -f "${link}/Makefile" ]]; then
    echo "FATAL: build symlink missing Makefile: ${link} -> ${ROOT}" >&2
    exit 1
  fi
  echo "${link}"
}

stop_fixture_uart_users() {
  echo "=== Stopping fixture processes on ttyXRUSB (required before module reload) ==="
  pkill -f pog422_uartd 2>/dev/null || true
  pkill -f pog422_fixture_bridge 2>/dev/null || true
  pkill -f pog422_fixture_listen 2>/dev/null || true
  sleep 0.5
  if command -v fuser >/dev/null 2>&1; then
    for dev in /dev/ttyXRUSB*; do
      [[ -e "$dev" ]] || continue
      sudo fuser -k "$dev" 2>/dev/null || true
    done
  fi
  sleep 0.5
}

POG422_BUILD_M="$(setup_build_link)"
export POG422_BUILD_M

# Prior `sudo bash install_pi.sh` (or sudo make) leaves root-owned .o/.mod/.ko in
# the source tree; non-root rebuild then fails with "Permission denied" on .mod.
ensure_source_writable() {
  local me grp
  me="$(id -un)"
  grp="$(id -gn)"
  if [[ ! -w "${ROOT}" ]] \
    || find "${ROOT}" -maxdepth 2 ! -user "${me}" 2>/dev/null | grep -q .; then
    echo "=== Fixing ownership under ${ROOT} (was not fully owned by ${me}) ==="
    sudo chown -R "${me}:${grp}" "${ROOT}"
  fi
  if [[ ! -w "${ROOT}" ]]; then
    echo "FATAL: ${ROOT} still not writable by ${me}" >&2
    echo "  Fix: sudo chown -R ${me}:${grp} ${ROOT}" >&2
    exit 1
  fi
  if ! touch "${ROOT}/.pog422_write_test" 2>/dev/null; then
    echo "FATAL: cannot create files in ${ROOT}" >&2
    echo "  Fix: sudo chown -R ${me}:${grp} ${ROOT}" >&2
    exit 1
  fi
  rm -f "${ROOT}/.pog422_write_test"
}

KVER="$(uname -r)"
KBUILD="/lib/modules/${KVER}/build"
MOD_NAME="xr_usb_serial_common"
MOD_DIR="/lib/modules/${KVER}/updates"
MOD_DIR_LEGACY="/lib/modules/${KVER}/extra"
MODPROBE_DROPIN="/etc/modprobe.d/xr-usb-serial-pog422.conf"
LEGACY_BLACKLIST="/etc/modprobe.d/blacklist-cdc-acm-xr21.conf"
EXPECTED_VER="2.2.2.10"
# modinfo version is authoritative; strings are a secondary sanity check.
RS485_STRING_MARKERS=("2.2.2.10" "2.2.2.9" "2.2.2.8" "2.2.2.7" "2.2.2.6" "2.2.2.5" "2.2.2.4" "2.2.2.3" "2.2.2.2" "2.2.2.1" "2.2.1.17" "2.2.1.16" "2.2.1.15" "2.2.1.14" "2.2.1.13" "2.2.1.12" "2.2.1.11" "2.2.1.10" "2.2.1.9" "2.2.1.8" "2.2.1.7" "2.2.1.6" "2.2.1.5" "2.2.1.4" "2.2.1.3" "2.2.1.2" "2.2.1.1" "2.2.1.0" "EG 2.2" "2.1.0.0" "EG 2.1" "1W-pog422-tx-fifo-errno-ch" "POG422 1W" "1V-pog422-tx-fifo-dma-buf" "POG422 1V" "1U-pog422-tx-fifo-uart-block" "POG422 1U" "1T-pog422-tx-fifo-get-reg-fix" "POG422 1T" "1S-pog422-inject-tx-fifo-diag" "POG422 1S" "1R-pog422-preciseflags-ioctl-fix" "POG422 1R" "1Q-pog422-wide-preciseflags-only" "POG422 1Q" "1P-pog422-inject-wide-teardown-wait" "POG422 1P" "1O-pog422-inject-manual-de-hold" "POG422 1O" "1N-pog422-manual-gpio7-inject" "POG422 1N" "manual GPIO7" "1N mixed" "1M-pog422-wide-off-no-disable" "1L-pog422-inject-tx-wire-drain" "1K-pog422-inject-tx-drain" "1J-pog422-wide-inject-tx" "1I-pog422-dttty-rx-ring" "1H-pog422-rx-extra-byte-fix" "1G-pog422-rs485-mark-space-hold" "1F-pog422-rs485-auto" "GPIO7 RS485 auto" "POG422 RS485 auto")

modinfo_resolved_ko() {
  modinfo "${MOD_NAME}" 2>/dev/null | awk -F':[ \t]*' '/^filename:/ { print $2; exit }'
}

remove_stale_xr_modules() {
  local f base
  echo "=== Removing stale ${MOD_NAME} copies (updates/ wins over extra/) ==="
  for base in "${MOD_DIR}" "${MOD_DIR_LEGACY}"; do
    [[ -d "${base}" ]] || continue
    for f in \
      "${base}/${MOD_NAME}.ko.xz" \
      "${base}/${MOD_NAME}.ko"; do
      if [[ -f "${f}" ]]; then
        echo "  remove ${f}"
        sudo rm -f "${f}"
      fi
    done
  done
}

ko_has_rs485_strings() {
  local ko="$1"
  local m
  for m in "${RS485_STRING_MARKERS[@]}"; do
    if strings "${ko}" | grep -qF "${m}"; then
      return 0
    fi
  done
  return 1
}

ko_built_version() {
  modinfo "$1" 2>/dev/null | awk -F':[ \t]*' '/^version:/ { print $2; exit }'
}

echo "=== POG422 xr-usb-serial driver install (${EXPECTED_VER}) ==="
echo "kernel: ${KVER}"
echo "source: ${ROOT}"

if [[ ! -d "$KBUILD" ]]; then
  echo "FATAL: kernel headers missing at ${KBUILD}" >&2
  echo "Install with: sudo apt-get update && sudo apt-get install -y raspberrypi-kernel-headers" >&2
  echo "or: sudo apt-get install -y linux-headers-${KVER}" >&2
  exit 1
fi

if ! command -v make >/dev/null 2>&1; then
  echo "Installing build tools..."
  sudo apt-get update
  sudo apt-get install -y build-essential
fi

echo "=== Building (forced clean — avoids stale .ko from Dropbox sync) ==="
echo "build link: ${POG422_BUILD_M} -> ${ROOT}"
ensure_source_writable
rm -f "${ROOT}/${MOD_NAME}.ko" "${ROOT}/${MOD_NAME}.o" "${ROOT}/${MOD_NAME}.mod" \
  "${ROOT}/${MOD_NAME}.mod.c" "${ROOT}/."*.cmd "${ROOT}/Module.symvers" \
  "${ROOT}/modules.order" 2>/dev/null || true
rm -rf "${ROOT}/.tmp_versions" 2>/dev/null || true
make -C "${KBUILD}" M="${POG422_BUILD_M}" HW_DEBUG="${HW_DEBUG:-0}" clean || true
touch "${ROOT}/xr_usb_serial_common.c"
BUILD_LOG="$(mktemp)"
ERRORS_TXT="${ROOT}/../ERRORS.txt"
if ! make -C "${KBUILD}" M="${POG422_BUILD_M}" HW_DEBUG="${HW_DEBUG:-0}" -j"$(nproc)" modules 2>&1 | tee "${BUILD_LOG}"; then
  cp "${BUILD_LOG}" "${ERRORS_TXT}"
  echo "FATAL: kernel module build failed" >&2
  echo "Full build log saved to: ${ERRORS_TXT}" >&2
  if grep -q 'Permission denied' "${BUILD_LOG}"; then
    echo "HINT: Permission denied usually means a prior sudo build left root-owned files." >&2
    echo "  Fix: sudo chown -R $(id -un):$(id -gn) ${ROOT}" >&2
    echo "  Then re-run install (no sudo on the script itself)." >&2
  fi
  rm -f "${BUILD_LOG}"
  exit 1
fi
if ! grep -qE 'CC.*xr_usb_serial_common|LD.*xr_usb_serial_common' "${BUILD_LOG}"; then
  echo "WARN: build log missing CC/LD for xr_usb_serial_common — checking .ko anyway" >&2
fi
rm -f "${BUILD_LOG}"

if [[ ! -f "${ROOT}/${MOD_NAME}.ko" ]]; then
  echo "FATAL: build did not produce ${MOD_NAME}.ko" >&2
  exit 1
fi

built_ver="$(ko_built_version "${ROOT}/${MOD_NAME}.ko")"
if [[ -z "${built_ver}" ]]; then
  echo "FATAL: could not read version from ${MOD_NAME}.ko (modinfo)" >&2
  exit 1
fi
if [[ "${built_ver}" != "${EXPECTED_VER}" ]]; then
  echo "FATAL: built ${MOD_NAME}.ko version=${built_ver} want ${EXPECTED_VER}" >&2
  exit 1
fi
echo "OK: built ${MOD_NAME}.ko version=${built_ver}"
if ko_has_rs485_strings "${ROOT}/${MOD_NAME}.ko"; then
  echo "OK: RS485 marker string found in .ko"
else
  echo "WARN: RS485 marker strings not in strings(1) output — continuing (modinfo version OK)" >&2
fi

echo "=== Installing module to ${MOD_DIR} (modprobe resolves updates/ before extra/) ==="
remove_stale_xr_modules
sudo mkdir -p "$MOD_DIR"
sudo cp "${ROOT}/${MOD_NAME}.ko" "${MOD_DIR}/"
sudo depmod -a "$KVER"

resolved_ko="$(modinfo_resolved_ko)"
if [[ -z "${resolved_ko}" ]]; then
  echo "FATAL: modinfo ${MOD_NAME} has no filename after install" >&2
  exit 1
fi
resolved_ver="$(ko_built_version "${resolved_ko}")"
echo "modprobe resolves: ${resolved_ko}"
echo "resolved version: ${resolved_ver}"
if [[ "${resolved_ver}" != "${EXPECTED_VER}" ]]; then
  echo "FATAL: modprobe would load version=${resolved_ver} want ${EXPECTED_VER}" >&2
  echo "  Another ${MOD_NAME}.ko.xz may still shadow this install — check:" >&2
  echo "    find /lib/modules/${KVER} -name '${MOD_NAME}.ko*'" >&2
  exit 1
fi
if [[ "${resolved_ko}" == *".ko.xz" ]]; then
  echo "WARN: resolved path is .ko.xz — prefer plain .ko in updates/ (re-run depmod after removing .xz)" >&2
fi

if [[ -f "$LEGACY_BLACKLIST" ]]; then
  echo "=== Removing legacy cdc_acm blacklist (POG-CC / DyzineFrame needs cdc_acm) ==="
  sudo rm -f "$LEGACY_BLACKLIST"
fi

echo "=== Auto-load xr_usb_serial_common on boot ==="
sudo tee "$MODPROBE_DROPIN" >/dev/null <<EOF
# POG422 fixture driver (${MOD_NAME} ${EXPECTED_VER})
install ${MOD_NAME} /sbin/modprobe --ignore-install ${MOD_NAME}
softdep ${MOD_NAME} pre: usbserial
EOF

stop_fixture_uart_users

loaded_sysfs_ver=""
if [[ -r "/sys/module/${MOD_NAME}/version" ]]; then
  loaded_sysfs_ver="$(tr -d '\n' < "/sys/module/${MOD_NAME}/version")"
fi

echo "=== Reloading module ==="
echo "  NOTE: modinfo = on-disk .ko; cat /sys/module/${MOD_NAME}/version = what is RUNNING."
echo "  installed (modinfo): ${resolved_ver}"
echo "  loaded   (sysfs):    ${loaded_sysfs_ver:-not loaded}"
need_reload=1
if lsmod | grep -q "^${MOD_NAME}"; then
  # BUGFIX: never trust modinfo for "already loaded" — it reads the .ko on disk.
  if [[ -n "${loaded_sysfs_ver}" && "${loaded_sysfs_ver}" == "${EXPECTED_VER}" ]]; then
    echo "OK: ${MOD_NAME} already RUNNING at version=${loaded_sysfs_ver} — skip reload"
    need_reload=0
  else
    echo "Removing RUNNING ${MOD_NAME} (sysfs=${loaded_sysfs_ver:-unknown}, want ${EXPECTED_VER})..."
    if ! sudo modprobe -r "$MOD_NAME"; then
      echo "FATAL: could not unload ${MOD_NAME} (usbserial/tty still in use)." >&2
      echo "  Stop bridge/uartd, UNPLUG hat USB, then:" >&2
      echo "    sudo modprobe -r ${MOD_NAME} && sudo modprobe ${MOD_NAME}" >&2
      echo "  Then REPLUG hat. Or: sudo reboot" >&2
      exit 1
    fi
  fi
fi

if [[ "${need_reload}" -eq 1 ]]; then
  if ! sudo modprobe "$MOD_NAME"; then
    echo "FATAL: modprobe ${MOD_NAME} failed" >&2
    exit 1
  fi
fi

# Re-read after modprobe — still may be stale until USB re-enumerates if unload failed silently.
if [[ -r "/sys/module/${MOD_NAME}/version" ]]; then
  loaded_sysfs_ver="$(tr -d '\n' < "/sys/module/${MOD_NAME}/version")"
fi
echo "After reload: loaded(sysfs)=${loaded_sysfs_ver:-?} installed(modinfo)=${resolved_ver}"
if [[ -n "${loaded_sysfs_ver}" && "${loaded_sysfs_ver}" != "${EXPECTED_VER}" ]]; then
  echo "FATAL: module still RUNNING as ${loaded_sysfs_ver}, not ${EXPECTED_VER}." >&2
  echo "  Unplug hat → sudo modprobe -r ${MOD_NAME} → sudo modprobe ${MOD_NAME} → replug." >&2
  exit 1
fi

echo "=== Install rebind helper (Exar hat: xr / cdc_acm → POG422 driver) ==="
sudo install -m 755 "${ROOT}/pog422_rebind_xr21.sh" /usr/local/sbin/pog422_rebind_xr21.sh
sudo install -m 755 "${ROOT}/verify_driver.sh" /usr/local/sbin/pog422_verify_driver.sh
sudo cp "${ROOT}/99-xr21-over-cdc-acm.rules" /etc/udev/rules.d/99-xr21-over-cdc-acm.rules
sudo cp "${ROOT}/xr-usb-serial-pog422.modules" /etc/modules-load.d/xr-usb-serial-pog422.conf
sudo udevadm control --reload-rules
sudo udevadm trigger

echo "=== Rebind hat if currently on /dev/ttyUSB* (in-kernel xr driver) ==="
if ls /dev/ttyUSB* >/dev/null 2>&1; then
  echo "  Close any process using ttyUSB*, then rebind runs."
fi
sudo /usr/local/sbin/pog422_rebind_xr21.sh || true

if ! ls /dev/ttyXRUSB* >/dev/null 2>&1; then
  sudo udevadm trigger --subsystem-match=tty 2>/dev/null || true
  sudo udevadm settle --timeout=5 2>/dev/null || true
  for _i in 1 2 3 4 5 6 7 8 9 10; do
    ls /dev/ttyXRUSB* >/dev/null 2>&1 && break
    sleep 0.5
  done
fi

echo "=== Verify (loaded=sysfs vs installed=modinfo) ==="
echo "installed (modinfo — on-disk .ko):"
modinfo "${MOD_NAME}" | grep -E '^(filename|version|description):' || true
echo "loaded (sysfs — what inject uses):"
if [[ -r "/sys/module/${MOD_NAME}/version" ]]; then
  echo "  /sys/module/${MOD_NAME}/version = $(tr -d '\n' < /sys/module/${MOD_NAME}/version)"
else
  echo "  (module not loaded)"
fi
lsmod | grep "$MOD_NAME" || true
echo
echo "Do NOT use plain 'modinfo … | grep version' alone to confirm a reload."
echo "  Use:  cat /sys/module/${MOD_NAME}/version"

if ls /dev/ttyXRUSB* >/dev/null 2>&1; then
  echo "OK: $(ls /dev/ttyXRUSB* | tr '\n' ' ')"
elif ls /dev/ttyUSB* >/dev/null 2>&1; then
  echo "WARN: hat still on in-kernel xr (/dev/ttyUSB*) — GPIO lines stay pulled up." >&2
  echo "  Close processes on ttyUSB*, then: sudo /usr/local/sbin/pog422_rebind_xr21.sh" >&2
  echo "  Or replug hat USB after rebind." >&2
else
  echo "NOTE: /dev/ttyXRUSB* not present yet."
  echo "  Unplug/replug the hat USB, or reboot, then check again."
fi

echo
echo "IMPORTANT: cdc_xr_usb_serial in dmesg is NORMAL for the POG422 fork."
echo "  Stock driver would show ttyUSB*, not ttyXRUSB*."
echo
echo "Probe + RS485 init run only when the hat USB re-enumerates:"
echo "  sudo reboot   # simplest"
echo "  sudo dmesg -T | grep -i POG422"
echo "  # want: POG422: GPIO7 RS485 auto on 2 B142x channel(s) at probe"
echo
echo "After reboot/replug, open a port once and check:"
echo "  sudo dmesg -T | grep 'GPIO Mode'"
echo "  # want: GPIO Mode = 339 (POG422 RS485 auto)"
echo "  # bad:  GPIO Mode = 300  (stale module still in RAM — reboot)"
echo
echo "Full check: bash ${ROOT}/verify_driver.sh"
echo
echo "Remove legacy gpio7low (manual GPIO7 — conflicts with RS485 auto):"
echo "  bash ${ROOT}/remove_gpio7low.sh"
echo
echo "Done."
