#!/bin/bash

# omarchy:summary=Cycle through the windows of an app, or launch it if none are open
# omarchy:args=<app|browser|terminal|ai|webapp|webapp-NAME|kitty-CLASS>

set -uo pipefail

APP="${1:-}"

if [[ -z $APP ]]; then
  echo "Usage: omarchy-cycle <app|browser|terminal|ai|webapp|webapp-NAME|kitty-CLASS>"
  exit 1
fi

# Per-class launch commands for kitty-* (class -> command run via `kitty -e`).
# Unmapped classes just open a plain kitty with that class.
declare -A KITTY_CMDS=(
  # [kitty-lazygit]="lazygit"
  # [kitty-btop]="btop"
)

# --- Hyprland helpers --------------------------------------------------------

# Print the addresses of all windows whose class or initialClass matches the
# regex in $1. An optional regex in $2 excludes matches again — used to keep
# scratch windows out of a cycle. Windows are sorted by workspace, then by
# address, so the cycle order stays the same between calls.
addresses_matching() {
  hyprctl clients -j | jq -r --arg re "$1" --arg not "${2:-}" '
    [ .[]
      | select(((.class // "") | test($re; "i")) or ((.initialClass // "") | test($re; "i")))
      | select($not == "" or (
          (((.class // "") | test($not; "i")) or ((.initialClass // "") | test($not; "i"))) | not
        ))
    ]
    | sort_by(.workspace.id, .address)
    | .[].address'
}

# Same, but drops site-specific app (SSA) windows, so real browsers stay apart
# from web apps that share a class prefix.
addresses_matching_no_ssa() {
  hyprctl clients -j | jq -r --arg re "$1" '
    [ .[]
      | select(((.class // "") | test($re; "i")) or ((.initialClass // "") | test($re; "i")))
      | select(((.class // "") | test("^(chrome-|webapp-)"; "i")) | not)
    ]
    | sort_by(.workspace.id, .address)
    | .[].address'
}

focused_address() {
  hyprctl activewindow -j 2>/dev/null | jq -r '.address // empty'
}

focus_window() {
  hyprctl dispatch "hl.dsp.focus({ window = \"address:$1\" })" >/dev/null 2>&1 ||
    hyprctl dispatch focuswindow "address:$1" >/dev/null
}

spawn() {
  exec setsid uwsm-app -- $1
}

# --- Find the windows --------------------------------------------------------

declare -a INSTANCES

if [[ $APP == "webapp" ]]; then
  # Cycle all web apps: Firefox SSAs (WebApp-*) and Chromium/Brave SSAs (chrome-*).
  # Real browsers are excluded, because their class has no SSA prefix.
  INSTANCES=($(addresses_matching '^(webapp-|chrome-)'))
  FALLBACK_APP="webapp-youtube"

elif [[ $APP == webapp-* ]]; then
  # Extract the web app name (e.g. "webapp-claude" -> "claude").
  WEBAPP_NAME="${APP#webapp-}"
  # Firefox SSAs use the class WebApp-<Name>. Chromium SSAs use
  # chrome-<host>__-Default, so the name is matched anywhere in the class.
  INSTANCES=($(addresses_matching "webapp-${WEBAPP_NAME}|^chrome-.*${WEBAPP_NAME}"))

  if ((${#INSTANCES[@]} == 0)); then
    # No window is open. Launch from the desktop entry.
    DESKTOP_FILE=$(find ~/.local/share/applications -iname "WebApp-${WEBAPP_NAME}*.desktop" -print -quit 2>/dev/null)
    [[ -n $DESKTOP_FILE ]] ||
      DESKTOP_FILE=$(find ~/.local/share/applications -iname "*${WEBAPP_NAME}*.desktop" -print -quit 2>/dev/null)

    if [[ -n $DESKTOP_FILE ]]; then
      exec setsid gtk-launch "$(basename "$DESKTOP_FILE")"
    else
      echo "No desktop file found for web app: ${WEBAPP_NAME}"
      exit 1
    fi
  fi

elif [[ $APP == "kitty" ]]; then
  # Cycle all kitty windows, whatever the class (kitty, kitty-nvim, ...).
  INSTANCES=($(addresses_matching '^kitty'))
  DEFAULT_SPAWN="kitty"

elif [[ $APP == kitty-* ]]; then
  # One kitty class, e.g. kitty-nvim -> class "kitty-nvim".
  INSTANCES=($(addresses_matching "^${APP}$"))

  if ((${#INSTANCES[@]} == 0)); then
    cmd="${KITTY_CMDS[$APP]:-}"
    if [[ -n $cmd ]]; then
      spawn "kitty --class $APP -e $cmd"
    else
      spawn "kitty --class $APP"
    fi
  fi

elif [[ $APP == "ghostty" ]]; then
  # Cycle Ghostty terminals (class com.mitchellh.ghostty).
  INSTANCES=($(addresses_matching 'ghostty'))
  DEFAULT_SPAWN="ghostty"

elif [[ $APP == "browser" ]]; then
  # Cycle all browsers. Web apps are left out.
  INSTANCES=($(addresses_matching_no_ssa '^(zen|brave|chromium|google-chrome|firefox|librewolf|vivaldi|microsoft-edge|helium)'))
  DEFAULT_LAUNCHER="omarchy-launch-browser"

elif [[ $APP == "brave" ]]; then
  # Cycle the Brave Origin browser and all of its web apps. Both the
  # real browser (class brave-origin) and the SSAs (class brave-<host>)
  # share the brave- prefix, so one regex catches them all.
  INSTANCES=($(addresses_matching '^brave'))
  DEFAULT_LAUNCHER="omarchy-launch-browser"

elif [[ $APP == "terminal" ]]; then
  # Cycle all terminals. Tern (so.stencil.tern) is the SUPER + A scratch
  # terminal and the pattern below already leaves it out.
  INSTANCES=($(addresses_matching '(kitty|alacritty|ghostty|foot|wezterm)'))
  DEFAULT_LAUNCHER="omarchy-launch-terminal"

elif [[ $APP == "ai" ]]; then
  # Cycle the AI desktop apps (Claude/Hermes).
  INSTANCES=($(addresses_matching '(claude|hermes)'))
  DEFAULT_SPAWN="hermes desktop"

else
  # Any other app, matched on the full class.
  INSTANCES=($(addresses_matching "^${APP}$"))
fi

# --- Launch if nothing is open -----------------------------------------------

if ((${#INSTANCES[@]} == 0)); then
  if [[ -n ${DEFAULT_LAUNCHER:-} ]]; then
    exec setsid "$DEFAULT_LAUNCHER"
  fi

  if [[ -n ${DEFAULT_SPAWN:-} ]]; then
    spawn "$DEFAULT_SPAWN"
  fi

  if [[ -n ${FALLBACK_APP:-} ]]; then
    exec "$0" "$FALLBACK_APP"
  fi

  if [[ $APP == "webapp" ]]; then
    echo "No ${APP} windows running"
    exit 0
  fi

  spawn "$APP"
fi

# --- Focus, or cycle to the next window --------------------------------------

if ((${#INSTANCES[@]} == 1)); then
  focus_window "${INSTANCES[0]}"
  exit 0
fi

FOCUSED=$(focused_address)

# If the focused window is one of ours, go to the next one. If it is not,
# go to the first one.
next_index=0
for i in "${!INSTANCES[@]}"; do
  if [[ -n $FOCUSED && ${INSTANCES[$i]} == "$FOCUSED" ]]; then
    next_index=$(((i + 1) % ${#INSTANCES[@]}))
    break
  fi
done

focus_window "${INSTANCES[$next_index]}"
