#!/usr/bin/env bash
# Start a disposable nested X11 server plus Lime's safe live-development
# runtime. Run this from the normal graphical session, never from inside the
# Xephyr session it creates.
#
# Usage:
#   run-xephyr-dev [--config PATH | --no-config] [--display :99]
#                  [--screen 1600x1000] [--repl-port 4005]

set -euo pipefail

script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_dir="$(cd -- "$script_dir/.." && pwd)"
config_home="${XDG_CONFIG_HOME:-${HOME}/.config}"
default_config_file="$config_home/lime/config.lisp"
display_name=":99"
screen_size="1600x1000"
repl_port="4005"
config_file=""
session_mode="default"
session_file=""

if [[ -f "$default_config_file" ]]; then
  config_file="$default_config_file"
fi

usage() {
  cat <<EOF
Usage: $(basename "$0") [options]

Start Lime in a disposable Xephyr session from the current desktop.

  -c, --config PATH   Load this Lime configuration file.
      --no-config     Use Lime's starter bindings instead of a config file.
  -d, --display NAME  Nested display name (default: :99).
  -s, --screen SIZE   Xephyr size (default: 1600x1000).
  -p, --repl-port N   Loopback Slynk port (default: 4005).
      --session-file PATH  Use a specific saved desk (separate from configuration).
      --no-session     Disable session restore/autosave for a disposable test.
  -h, --help          Show this help.

Without an explicit config choice, the runner loads
  $default_config_file
when it exists; otherwise it uses the starter bindings.
Session data defaults to Lime's XDG state directory, separated by DISPLAY.
EOF
}

while (($#)); do
  case "$1" in
    -c|--config)
      [[ $# -ge 2 ]] || { printf '%s needs a path.\n' "$1" >&2; exit 2; }
      config_file="$2"
      shift 2
      ;;
    --no-config)
      config_file=""
      shift
      ;;
    --session-file)
      [[ $# -ge 2 ]] || { printf '%s needs a path.\n' "$1" >&2; exit 2; }
      session_mode="file"
      session_file="$2"
      [[ "$session_file" = /* ]] || session_file="$PWD/$session_file"
      shift 2
      ;;
    --no-session)
      session_mode="disabled"
      shift
      ;;
    -d|--display)
      [[ $# -ge 2 ]] || { printf '%s needs a display name.\n' "$1" >&2; exit 2; }
      display_name="$2"
      shift 2
      ;;
    -s|--screen)
      [[ $# -ge 2 ]] || { printf '%s needs a size.\n' "$1" >&2; exit 2; }
      screen_size="$2"
      shift 2
      ;;
    -p|--repl-port)
      [[ $# -ge 2 ]] || { printf '%s needs a port number.\n' "$1" >&2; exit 2; }
      repl_port="$2"
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      printf 'Unknown option: %s\n\n' "$1" >&2
      usage >&2
      exit 2
      ;;
  esac
done

if ! [[ "$display_name" =~ ^:[0-9]+$ ]]; then
  printf '%s\n' "--display must look like :99, got $display_name" >&2
  exit 2
fi

display_number="${display_name#:}"
socket_path="/tmp/.X11-unix/X${display_number}"
if [[ -S "$socket_path" ]]; then
  printf 'Display %s is already in use (%s). Choose another with --display.\n' \
         "$display_name" "$socket_path" >&2
  exit 2
fi

if ! [[ "$repl_port" =~ ^[1-9][0-9]*$ ]] || ((repl_port > 65535)); then
  printf 'Slynk port must be an integer from 1 through 65535, got %s\n' "$repl_port" >&2
  exit 2
fi

if [[ -n "$config_file" ]]; then
  if [[ ! -f "$config_file" ]]; then
    printf 'Lime config does not exist: %s\n' "$config_file" >&2
    exit 2
  fi
  # Resolve before changing into the repository so an explicit relative path
  # still means the path relative to the shell that launched this runner.
  config_file="$(cd -- "$(dirname -- "$config_file")" && pwd)/$(basename -- "$config_file")"
fi

command -v Xephyr >/dev/null || {
  printf 'Xephyr is required but was not found in PATH.\n' >&2
  exit 127
}
command -v sbcl >/dev/null || {
  printf 'SBCL is required but was not found in PATH.\n' >&2
  exit 127
}

cleanup() {
  if kill -0 "$xephyr_pid" 2>/dev/null; then
    kill -TERM "$xephyr_pid" 2>/dev/null || true
    wait "$xephyr_pid" 2>/dev/null || true
  fi
}

Xephyr "$display_name" -ac -screen "$screen_size" -br -nolisten tcp &
xephyr_pid=$!
trap cleanup EXIT INT TERM

for attempt in {1..50}; do
  if [[ -S "$socket_path" ]]; then
    break
  fi
  if ! kill -0 "$xephyr_pid" 2>/dev/null; then
    wait "$xephyr_pid"
  fi
  sleep 0.1
done

if [[ ! -S "$socket_path" ]]; then
  printf 'Xephyr did not make display %s ready.\n' "$display_name" >&2
  exit 1
fi

# DISPLAY is exported only to this script and its children.  Your normal dwm
# terminal/session keeps its original display and its existing applications.
export DISPLAY="$display_name"
export LIME_XEPHYR_INTERNAL_CONFIG_FILE="$config_file"
export LIME_XEPHYR_INTERNAL_REPL_PORT="$repl_port"
export LIME_XEPHYR_INTERNAL_SESSION_MODE="$session_mode"
export LIME_XEPHYR_INTERNAL_SESSION_FILE="$session_file"

printf 'Lime test session: Xephyr %s (%s), Slynk 127.0.0.1:%s\n' \
       "$display_name" "$screen_size" "$repl_port"
if [[ -n "$config_file" ]]; then
  printf 'Configuration: %s\n' "$config_file"
else
  printf 'Configuration: starter bindings (no config file)\n'
fi
printf 'Close Lime or press Ctrl-C here to tear down the nested session.\n'

cd "$repo_dir"
sbcl --noinform --non-interactive \
     --eval '(require :asdf)' \
     --eval '(asdf:load-asd (merge-pathnames "lime.asd" (uiop:getcwd)))' \
     --eval '(asdf:load-system "lime/dev")' \
     --eval '(lime.dev:run-development-session
                :display-name (uiop:getenv "DISPLAY")
                :session-file (let ((mode (uiop:getenv "LIME_XEPHYR_INTERNAL_SESSION_MODE")))
                                (cond ((string= mode "disabled") nil)
                                      ((string= mode "file") (uiop:getenv "LIME_XEPHYR_INTERNAL_SESSION_FILE"))
                                      (t :default)))
                :config-file (let ((path (uiop:getenv "LIME_XEPHYR_INTERNAL_CONFIG_FILE")))
                               (and (plusp (length path)) path))
                :repl-port (parse-integer (uiop:getenv "LIME_XEPHYR_INTERNAL_REPL_PORT")))'
