#!/bin/bash

# VGS decorative screensaver runner. Launches a fullscreen ghostty window per
# monitor (window class org.vgs.screensaver) that animates ASCII/Unicode art with
# `tte` (Terminal Text Effects). VGS-owned; ScreensaverService drives it via
# `vshell screensaver launch|stop`. The per-window animation loop is
# `vshell screensaver run`; `vshell screensaver transcode` renders an image to art.
#
# Idle gating (inhibitors, media, fullscreen, the enabled/off setting) is owned by
# the shell's IdleService + ScreensaverService, so this script does not re-check it.

set -euo pipefail

script_path="$(readlink -f -- "${BASH_SOURCE[0]}")"
script_dir="$(cd -- "$(dirname -- "$script_path")" && pwd)"

CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
CLASS="org.vgs.screensaver"
BRANDING="$CONFIG_HOME/vshell/branding/screensaver.txt"
# Package-owned default art (the VGS logo, pre-rendered). Read-only: it is the
# fallback when the user has no art of their own, so the saver works on a fresh
# install with no ImageMagick and no first-run transcode. `$script_dir/..` is the
# repo root in a checkout and /usr/lib/vshell in a package, and both carry
# config/vshell/. Regenerate after changing the logo with:
#   vshell screensaver transcode quickshell/vshell/assets/vgslogo.svg \
#     config/vshell/branding/screensaver.txt --width 100 --height 40
BUNDLED_BRANDING="${VSHELL_ROOT:-$(cd -- "$script_dir/.." && pwd)}/config/vshell/branding/screensaver.txt"
SETTINGS="$CONFIG_HOME/vshell/settings.json"
GHOSTTY_CONF="$CONFIG_HOME/vshell/ghostty-screensaver.conf"
IS_NIRI=0
if [[ -n ${NIRI_SOCKET:-} ]] && command -v niri >/dev/null 2>&1 && niri msg version >/dev/null 2>&1; then
  IS_NIRI=1
fi

ensure_ghostty_conf() {
  [[ -f "$GHOSTTY_CONF" ]] && return 0
  mkdir -p "$(dirname "$GHOSTTY_CONF")"
  cat >"$GHOSTTY_CONF" <<'CONF'
font-family = "JetBrainsMono Nerd Font Mono"
font-size = 18
font-style = Normal
font-synthetic-style = true
adjust-cell-height = 0%

background = #000000
foreground = #ffffff
window-padding-x = 0
window-padding-y = 0
window-padding-balance = false
window-decoration = false
resize-overlay = never
quit-after-last-window-closed = true
confirm-close-surface = false
scrollback-limit = 1000
CONF
}

default_text() {
  printf '%s\n' "$(hostname 2>/dev/null || echo vshell)"
  printf '%s\n' "$(date '+%A %B %-d, %Y')"
}

# Is a source picture selected in settings? $BRANDING is generated output, not a
# hand-authored file, so it only outranks the bundled logo while the picture it
# was generated from is still selected. Without this, clearing the picture would
# leave the old art in place forever, and the hostname/date card an older VGS
# wrote once into $BRANDING would outrank the logo for every existing user.
# Anything unreadable answers "yes", which keeps the previous behaviour rather
# than discarding art on a bad settings read.
picture_configured() {
  [[ -r "$SETTINGS" ]] || return 0
  command -v jq >/dev/null 2>&1 || return 0
  local configured
  configured="$(jq -r '.screensaverAsciiImagePath // ""' "$SETTINGS" 2>/dev/null)" || return 0
  [[ -n "$configured" ]]
}

# Print the art file tte should read, most specific first:
#   1. the user's own art — what `vshell screensaver transcode` writes — while a
#      picture is still selected
#   2. the bundled VGS logo, used read-only so a fresh install needs no write
#      into ~/.config and no ImageMagick
#   3. a generated hostname/date card, only if the user path is writable
# Returns non-zero when no art can be produced at all, so the caller refuses to
# launch rather than putting up an empty fullscreen window.
resolve_branding() {
  if [[ -s "$BRANDING" ]] && picture_configured; then
    printf '%s\n' "$BRANDING"
    return 0
  fi
  if [[ -s "$BUNDLED_BRANDING" ]]; then
    printf '%s\n' "$BUNDLED_BRANDING"
    return 0
  fi
  if mkdir -p "$(dirname "$BRANDING")" 2>/dev/null && default_text >"$BRANDING" 2>/dev/null && [[ -s "$BRANDING" ]]; then
    printf '%s\n' "$BRANDING"
    return 0
  fi
  echo "no screensaver art available (looked in $BRANDING and $BUNDLED_BRANDING)" >&2
  return 1
}

cmd_stop() {
  if ((IS_NIRI)); then
    niri msg -j windows |
      jq -r --arg c "$CLASS" '.[] | select(.app_id == $c) | .id' |
      while IFS= read -r id; do
        [[ -n $id ]] || continue
        niri msg action close-window --id "$id" >/dev/null 2>&1 || true
      done
    return
  fi
  hyprctl clients -j |
    jq -r --arg c "$CLASS" '.[] | select(.class == $c) | .pid' |
    while IFS= read -r pid; do
      [[ -n $pid ]] || continue
      kill "$pid" 2>/dev/null || true
    done
}

# The uwsm systemd scope is optional here for the same reason it is in the
# capture scripts, and honours the same VSHELL_NO_APP_SCOPE opt-out.
_use_app_scope() {
  [[ -z ${VSHELL_NO_APP_SCOPE:-} ]] && command -v uwsm >/dev/null 2>&1
}

cmd_launch() {
  command -v tte >/dev/null 2>&1 || exit 1
  command -v ghostty >/dev/null 2>&1 || exit 1
  ensure_ghostty_conf
  # Refuse up front rather than covering every monitor with an empty terminal.
  resolve_branding >/dev/null || exit 1

  local monitor_count screensaver_count
  if ((IS_NIRI)); then
    monitor_count="$(niri msg -j outputs | jq 'length')"
    screensaver_count="$(niri msg -j windows | jq --arg c "$CLASS" '[.[] | select(.app_id == $c)] | length')"
  else
    monitor_count="$(hyprctl monitors -j | jq '[.[] | select(.disabled == false)] | length')"
    screensaver_count="$(hyprctl clients -j | jq --arg c "$CLASS" '[.[] | select(.class == $c)] | length')"
  fi

  # Already covering every monitor -> nothing to do.
  (( screensaver_count >= monitor_count )) && return 0
  # Partial coverage (monitor hotplug etc.) -> clear and relaunch cleanly.
  if (( screensaver_count > 0 )); then
    cmd_stop
    sleep 0.3
  fi

  local focused_monitor launch_cmd
  if ((IS_NIRI)); then
    local -a niri_launch
    if _use_app_scope; then
      niri_launch=(uwsm app -- ghostty)
    else
      niri_launch=(ghostty)
    fi
    focused_monitor="$(niri msg -j focused-output | jq -r '.name // empty')"
    niri msg -j outputs |
      jq -r 'keys[]' |
      while IFS= read -r monitor; do
        [[ -n $monitor ]] || continue
        niri msg action focus-monitor "$monitor" >/dev/null 2>&1 || true
        niri msg action spawn -- "${niri_launch[@]}" \
          "--class=$CLASS" --title=Screensaver "--config-file=$GHOSTTY_CONF" \
          --font-size=18 -e vshell screensaver run >/dev/null
        for _ in {1..40}; do
          id="$(niri msg -j windows | jq -r --arg c "$CLASS" '[.[] | select(.app_id == $c)] | last | .id // empty')"
          [[ -n $id ]] && break
          sleep 0.05
        done
        [[ -n ${id:-} ]] && niri msg action fullscreen-window --id "$id" >/dev/null 2>&1 || true
        sleep 0.25
      done
    [[ -n $focused_monitor ]] && niri msg action focus-monitor "$focused_monitor" >/dev/null 2>&1 || true
    return 0
  fi
  focused_monitor="$(hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name' | head -n1)"
  local launch_prefix=""
  if _use_app_scope; then
    launch_prefix="uwsm app -- "
  fi
  launch_cmd="${launch_prefix}ghostty --class=$CLASS --title=Screensaver --config-file=$GHOSTTY_CONF --font-size=18 -e vshell screensaver run"

  hyprctl monitors -j |
    jq -r '.[] | select(.disabled == false) | .name' |
    while IFS= read -r monitor; do
      [[ -n $monitor ]] || continue
      hyprctl dispatch "hl.dsp.focus({monitor=\"$monitor\"})" >/dev/null 2>&1 || true
      hyprctl dispatch "hl.dsp.exec_cmd('$launch_cmd')" >/dev/null 2>&1 || true
      sleep 0.25
    done

  [[ -n $focused_monitor ]] && hyprctl dispatch "hl.dsp.focus({monitor=\"$focused_monitor\"})" >/dev/null 2>&1 || true
  return 0
}

# --- per-window animation loop (runs inside the fullscreen ghostty) ------------

_screensaver_in_focus() {
  if ((IS_NIRI)); then
    niri msg -j focused-window | jq -e --arg c "$CLASS" '.app_id == $c' >/dev/null 2>&1
    return
  fi
  hyprctl activewindow -j | jq -e --arg c "$CLASS" '.class == $c' >/dev/null 2>&1
}

_cursor_position() {
  ((IS_NIRI)) && return 0
  hyprctl cursorpos -j 2>/dev/null | jq -r '[.x, .y] | @tsv' 2>/dev/null || true
}

_run_cleanup() {
  printf '\033[0m\033[?25h' || true
  pkill -x tte 2>/dev/null || true
}

_run_exit() {
  trap - INT TERM HUP QUIT
  _run_cleanup
  "$script_dir/vshell-screensaver" stop >/dev/null 2>&1 || true
  exit 0
}

cmd_run() {
  command -v tte >/dev/null 2>&1 || exit 1
  local art
  art="$(resolve_branding)" || exit 1

  trap _run_exit INT TERM HUP QUIT

  local tty_name startup_grace_until initial_cursor current_cursor
  tty_name="$(tty 2>/dev/null || true)"
  startup_grace_until=$(( $(date +%s) + 3 ))
  initial_cursor=""

  # Black background, clear screen, hide cursor.
  printf '\033]11;rgb:00/00/00\007'
  printf '\033[2J\033[H\033[?25l'

  while true; do
    # Re-resolve each cycle: picking a picture in settings writes the user art
    # while the saver is up, and it should take over on the next effect rather
    # than staying on the bundled logo until relaunch.
    art="$(resolve_branding)" || _run_exit
    tte \
      --input-file "$art" \
      --random-effect \
      --frame-rate 120 \
      --canvas-width 0 \
      --canvas-height 0 \
      --reuse-canvas \
      --anchor-canvas c \
      --anchor-text c \
      --no-eol \
      --no-restore-cursor &

    if [[ -n $tty_name ]]; then
      while pgrep -t "${tty_name#/dev/}" -x tte >/dev/null; do
        if (( $(date +%s) < startup_grace_until )); then
          sleep 0.2
          continue
        fi
        [[ -z $initial_cursor ]] && initial_cursor="$(_cursor_position)"
        current_cursor="$(_cursor_position)"
        # Dismiss on any keypress, focus loss, or real cursor movement.
        if read -r -n1 -t 1 ||
          ! _screensaver_in_focus ||
          { [[ -n $initial_cursor && -n $current_cursor ]] && [[ $current_cursor != "$initial_cursor" ]]; }; then
          _run_exit
        fi
      done
    else
      wait $! || true
    fi
  done
}

case "${1:-}" in
launch)
  shift || true
  cmd_launch "$@"
  ;;
stop)
  cmd_stop
  ;;
run)
  cmd_run
  ;;
transcode)
  shift || true
  exec "$script_dir/vshell-transcode-ascii" "$@"
  ;;
*)
  echo "Usage: vshell screensaver launch|stop|run|transcode [...]" >&2
  exit 2
  ;;
esac
