#!/usr/bin/env bash
# Spotlight launcher backend for eww.
#   query <text>    build results, push to eww (spot-results), cache for activate
#   activate <idx>  run the cached result's action, close the launcher
#   open [clip]      open + focus the launcher (optionally in clipboard mode)
#   close
#   reindex          rebuild the .desktop app cache
#
# Sigils inside the query:
#   =expr  calc      ?text  web      >cmd  shell      :name  emoji
#   /path  ~path     file   ;text    clipboard
# No sigil → fuzzy apps (plus an inline calc row if the text is an expression).
set -u

CACHE="$HOME/.cache/eww"; mkdir -p "$CACHE"
RES="$CACHE/spot-results.json"
APPTSV="$CACHE/spot-apps.tsv"
EMOJI="$(cd "$(dirname "$0")/.." && pwd)/data/emoji.tsv"
MAX=12

lc() { printf '%s' "$1" | tr 'A-Z' 'a-z'; }

# On Sway the launcher runs inside a one-key "launcher" mode (only Escape bound) so
# Escape handling is done inside eww (the spotlight window's onkey); nothing to
# reset at the compositor level on niri.
nav_reset() { :; }

resolve_icon() {   # icon name -> file path (Papirus svg preferred)
  local name="$1" base sz ext f
  [ -z "$name" ] && name="application-x-executable"
  case "$name" in /*) [ -f "$name" ] && { printf '%s' "$name"; return; };; esac
  for base in /usr/share/icons/Papirus-Dark /usr/share/icons/Papirus; do
    for sz in 64x64 48x48 32x32 24x24; do
      for ext in svg png; do
        [ -f "$base/$sz/apps/$name.$ext" ] && { printf '%s' "$base/$sz/apps/$name.$ext"; return; }
      done
    done
  done
  f=$(find /usr/share/icons/hicolor -type f \( -name "$name.svg" -o -name "$name.png" \) 2>/dev/null \
        | sort -t/ -k6 -rn | head -1)
  [ -n "$f" ] && { printf '%s' "$f"; return; }
  for ext in svg png xpm; do
    [ -f "/usr/share/pixmaps/$name.$ext" ] && { printf '%s' "/usr/share/pixmaps/$name.$ext"; return; }
  done
  printf '/usr/share/icons/Papirus-Dark/64x64/apps/application-x-executable.svg'
}

# ---- app index (cached TSV: id \t name \t iconpath \t haystack) -------------
reindex() {
  local dirs=(/usr/share/applications "$HOME/.local/share/applications" \
              /var/lib/flatpak/exports/share/applications \
              "$HOME/.local/share/flatpak/exports/share/applications")
  : > "$APPTSV.tmp"
  local d f
  for d in "${dirs[@]}"; do
    [ -d "$d" ] || continue
    for f in "$d"/*.desktop; do
      [ -f "$f" ] || continue
      awk -F= '
        /^\[/ { grp = ($0=="[Desktop Entry]"); next }
        grp && /^Name=/        && n==""  { n=substr($0,6) }
        grp && /^Icon=/        && ic=="" { ic=substr($0,6) }
        grp && /^NoDisplay=/   { nd=substr($0,11) }
        grp && /^Hidden=/      { hd=substr($0,8) }
        grp && /^Type=/        { ty=substr($0,6) }
        grp && /^Comment=/     && cm=="" { cm=substr($0,9) }
        grp && /^GenericName=/ && gn=="" { gn=substr($0,13) }
        grp && /^Keywords=/    && kw=="" { kw=substr($0,10) }
        END {
          if (n=="" || ty!="Application" || nd=="true" || hd=="true") exit
          printf "%s\t%s\t%s\t%s\n", n, ic, gn" "cm" "kw, ""
        }' "$f" | while IFS=$'\t' read -r name icon extra _; do
        [ -n "$name" ] || continue
        local id; id=$(basename "$f" .desktop)
        printf '%s\t%s\t%s\t%s\n' "$id" "$name" "$(resolve_icon "$icon")" "$(lc "$name $extra")" >> "$APPTSV.tmp"
      done
    done
  done
  sort -u -t$'\t' -k2 "$APPTSV.tmp" > "$APPTSV"; rm -f "$APPTSV.tmp"
}
ensure_index() {
  [ -s "$APPTSV" ] || reindex
  # rebuild if any app dir is newer than the cache
  local newer
  newer=$(find /usr/share/applications "$HOME/.local/share/applications" \
               -name '*.desktop' -newer "$APPTSV" -print -quit 2>/dev/null)
  [ -n "$newer" ] && reindex
}

# ---- result assembly --------------------------------------------------------
items=()
add() {  # type iconOrPath title sub act
  local img="" gly="$2"
  case "$2" in /*) img="$2"; gly="";; esac
  items+=("$(jq -nc --arg t "$1" --arg g "$gly" --arg im "$img" --arg ti "$3" --arg s "$4" --arg a "$5" \
    '{type:$t,icon:$g,img:$im,title:$ti,sub:$s,act:$a}')")
}
emit() {
  if [ ${#items[@]} -eq 0 ]; then printf '[]' > "$RES"
  else printf '%s\n' "${items[@]}" | jq -s 'to_entries|map(.value + {idx:.key})' > "$RES"; fi
  eww update spot-results="$(cat "$RES")" 2>/dev/null
}

# ---- modes ------------------------------------------------------------------
m_calc() {  # inline=1 marks the auto row added before apps
  local expr="$1" out
  out=$(qalc -t -- "$expr" 2>/dev/null)
  [ -z "$out" ] && return
  case "$out" in *error*|*Error*) return;; esac
  add calc "󰪚" "$out" "= $expr  ·  Enter kopiert" "$out"
}
m_web() {
  local q="$1" url
  [ -n "$q" ] || return
  url="https://duckduckgo.com/?q=$(jq -rn --arg q "$q" '$q|@uri')"
  add web "󰖟" "$q" "Im Web suchen · DuckDuckGo" "$url"
}
m_shell() {
  local c="$1"; [ -n "$c" ] || return
  add shell "" "$c" "Shell-Befehl ausführen" "$c"
}
m_emoji() {
  local q; q=$(lc "$1")
  [ -f "$EMOJI" ] || return
  local line
  if [ -z "$q" ]; then line=$(head -n $MAX "$EMOJI")
  else line=$(grep -i -- "$q" "$EMOJI" 2>/dev/null | head -n $MAX); fi
  while IFS=$'\t' read -r ch name; do
    [ -n "$ch" ] && add emoji "$ch" "$name" "Enter kopiert" "$ch"
  done <<< "$line"
}
m_files() {
  local q="$1" base="$HOME"
  command -v fd >/dev/null 2>&1 || { add note "" "fd nicht installiert" "sudo pacman -S fd" ""; return; }
  case "$q" in /*) base=/; ;; "~"*) q="${q#\~}"; q="${q#/}";; esac
  [ -n "$q" ] || return
  while IFS= read -r p; do
    [ -n "$p" ] || continue
    add file "󰈔" "$(basename "$p")" "${p/#$HOME/~}" "$p"
  done < <(fd -H -E .git --max-results $MAX -- "$q" "$base" 2>/dev/null)
}
m_clip() {
  local q="$1"
  command -v cliphist >/dev/null 2>&1 || { add note "" "cliphist nicht installiert" "sudo pacman -S cliphist" ""; return; }
  local line
  if [ -z "$q" ]; then line=$(cliphist list 2>/dev/null | head -n $MAX)
  else line=$(cliphist list 2>/dev/null | grep -i -- "$q" | head -n $MAX); fi
  local id text
  while IFS= read -r row; do
    [ -n "$row" ] || continue
    id=${row%%$'\t'*}; text=${row#*$'\t'}
    add clip "󰅍" "$text" "Clipboard · Enter kopiert" "$id"
  done <<< "$line"
}
m_apps() {
  [ -z "$1" ] && return            # empty query → no app rows (clean start)
  local q; q=$(lc "$1")
  ensure_index
  local rows
  rows=$(awk -F'\t' -v q="$q" '
    function score(s,  i,p) { p=index(s,q); return p }
    {
      hay=$4; name=tolower($2)
      ok=1; n=split(q,toks," ")
      for(i=1;i<=n;i++) if(index(hay,toks[i])==0){ok=0;break}
      if(!ok) next
      sc = (index(name,q)==1)?0 : (index(name,q)>0?1 : 2)
      printf "%d\t%s\t%s\t%s\n", sc, $1, $2, $3
    }' "$APPTSV" | sort -n -k1 | head -n $MAX)
  local id name icon _sc
  while IFS=$'\t' read -r _sc id name icon; do
    [ -n "$id" ] && add app "$icon" "$name" "Anwendung" "$id"
  done <<< "$rows"
}

# ---- dispatch ---------------------------------------------------------------
_build() {   # sigil-aware build (apps mode / default); fills items[]
  local raw="$*"
  items=()
  case "$raw" in
    "="*)  m_calc "${raw#=}" ;;
    "?"*)  m_web "${raw#\?}" ;;
    ">"*)  m_shell "${raw#>}" ;;
    ":"*)  m_emoji "${raw#:}" ;;
    ";"*)  m_clip "${raw#;}" ;;
    "/"*|"~"*) m_files "$raw" ;;
    "")    m_apps "" ;;
    *)
      if printf '%s' "$raw" | grep -qE '^[0-9.(][0-9.+*/^%() -]*$' \
         && printf '%s' "$raw" | grep -qE '[-+*/^%]'; then m_calc "$raw"; fi
      m_apps "$raw"
      ;;
  esac
}

query() { _build "$*"; emit; }

qmode() {   # explicit category (from a pill); text is the raw search, no sigil
  local mode="$1"; shift
  items=()
  case "$mode" in
    clip)  m_clip  "$*" ;;
    emoji) m_emoji "$*" ;;
    calc)  m_calc  "$*" ;;
    web)   m_web   "$*" ;;
    files) m_files "$*" ;;
    apps|*) _build "$*" ;;
  esac
  emit
}

activate() {
  local idx="$1"
  [ -s "$RES" ] || exit 0
  local type act
  type=$(jq -r --argjson i "$idx" '.[$i].type // empty' "$RES" 2>/dev/null)
  act=$(jq -r --argjson i "$idx" '.[$i].act // empty' "$RES" 2>/dev/null)
  [ -n "$type" ] || { spot_close; exit 0; }
  case "$type" in
    app)   setsid -f gtk-launch "$act" >/dev/null 2>&1 ;;
    web)   setsid -f xdg-open "$act" >/dev/null 2>&1 ;;
    shell) setsid -f sh -c "$act" >/dev/null 2>&1 ;;
    file)  setsid -f xdg-open "$act" >/dev/null 2>&1 ;;
    calc|emoji) printf '%s' "$act" | wl-copy ;;
    clip)  cliphist decode "$act" 2>/dev/null | wl-copy ;;
  esac
  spot_close
}

# slide out, then unmap (niri doesn't animate layer map/unmap itself)
# Close spotlight with an animated slide-out, robustly: the full-screen
# spot-backdrop is closed FIRST and instantly (so a killed handler can never leave
# a click-eater → no lockout), then the body slides out (spot-open=false) and the
# window is unmapped from a DETACHED process eww can't kill.
spot_close() {
  eww close spot-backdrop 2>/dev/null        # catcher gone immediately (no lockout)
  eww update spot-open=false 2>/dev/null     # slide the body out
  setsid -f sh -c 'sleep 0.2; eww close spotlight' >/dev/null 2>&1
  nav_reset
}

open() {
  local mode="${1:-apps}"
  eww update spot-q="" spot-mode="$mode" 2>/dev/null
  if [ "$mode" = clip ]; then qmode clip ""; else items=(); emit; fi
  eww update spot-open=false 2>/dev/null   # start collapsed
  eww open spot-backdrop 2>/dev/null        # click-outside catcher (stacks below)
  eww open spotlight 2>/dev/null
  # reveal in a DETACHED process so an eww-killed handler can't leave spotlight
  # stuck collapsed under an open backdrop (lockout).
  setsid -f sh -c 'sleep 0.12; eww update spot-open=true' >/dev/null 2>&1
}

# called on every keystroke: render results for the active category.
# A leading sigil always wins, regardless of which pill is active.
type_query() {
  local text="$*"
  eww update spot-q="$text" 2>/dev/null
  case "$text" in
    "="*|"?"*|">"*|":"*|";"*|"/"*|"~"*) query "$text"; return ;;
  esac
  local mode; mode=$(eww get spot-mode 2>/dev/null); [ -z "$mode" ] && mode=apps
  qmode "$mode" "$text"
}

case "${1:-}" in
  query)    shift; query "$*" ;;
  qmode)    shift; mode="$1"; shift; qmode "$mode" "$*" ;;
  type)     shift; type_query "$*" ;;
  activate) activate "${2:-0}" ;;
  open)     open "${2:-}" ;;
  close)    spot_close ;;
  reindex)  reindex ;;
  *) echo "usage: spotlight.sh {query|qmode|type|cycle|activate|open|close|reindex}" >&2; exit 1 ;;
esac
