#!/usr/bin/env bash
# Dock backend (niri): pinned apps (real Papirus icons) + running
# apps, launch/focus, pin/unpin, new-window, close, and a hover-driven auto-hide.
#
#   apps                 JSON [{exec,class,icon,running,pinned}]  (pinned first)
#   launch <class> <exec...>   focus if running, else launch; then hide menus
#   new    <exec...>           always launch a fresh instance
#   close  <class>             close all windows of that class
#   pin    <class>             add a running app to the pinned list
#   unpin  <class>             remove an app from the pinned list
#   show                       open the dock now (cancels a pending hide)
#   hide                       hide the dock after HIDE_DELAY (debounced)
set -u

# ---- settings -------------------------------------------------------------
HIDE_DELAY=$(jq -r '.dock.hide_delay // 0.4' "${XDG_DATA_HOME:-$HOME/.local/share}/taninux/panel.json" 2>/dev/null)
case "$HIDE_DELAY" in ''|*[!0-9.]*) HIDE_DELAY="${DOCK_HIDE_DELAY:-0.4}";; esac
: "${HIDE_DELAY:=0.4}"                    # seconds the dock lingers after the
                                         # pointer leaves it (decimals allowed)
ANIM=0.2                                  # slide duration — matches the revealer
dlog() { :; }   # diagnostics off (flip to append-to-file if you need to trace)
CFG="$HOME/.config/eww"
PINS="$CFG/dock-pins"                    # one app per line:  exec|class|icon
HTOK="$HOME/.cache/eww/.dock-hide-token"
STAMP="$HOME/.cache/eww/.dock-menu-stamp"   # icon right-click marker (vs bg)
mkdir -p "$CFG" "$HOME/.cache/eww"

# seed the default pins on first run
if [ ! -f "$PINS" ]; then
  cat > "$PINS" <<'EOF'
librewolf|librewolf|librewolf
kitty|kitty|kitty
nautilus|org.gnome.Nautilus|org.gnome.Nautilus
code|Code|code-oss
obsidian|obsidian|obsidian
EOF
fi

# ---- niri helpers ---------------------------------------------------------
. "$(dirname "$0")/wm.sh"
clients_json() { wm_clients; }   # normalised [{id,pid,class,title,ws}]

# Hide is handled by the `watch` daemon (see bottom), NOT here — because eww
# kills an event-handler process the moment it lingers (a `sleep` for the
# slide-out gets killed → `eww close` never runs → the dock/backdrop sticks).
# So all the hover handlers do is write an instant intent to $HTOK; the daemon
# (a normal long-running process eww can't kill) does the timing + open/close.
#
# $HTOK states:  show  |  leave:<epoch_ms>  |  hidden
dock_dismiss() {                                   # instant: request hide + close menus
  eww close dock-menu dock-menu-backdrop dock-bg-menu 2>/dev/null
  printf 'hidden' > "$HTOK" 2>/dev/null
}
HIDE_MS=$(awk "BEGIN{print int($HIDE_DELAY*1000)}")   # debounce in ms for the daemon

# On-screen Y (logical px) of the TOP of dock icon <idx>, derived from the
# vertically-centred dock layout (niri has no cursor IPC, but we know the index).
# Used to place the right-click menu at the icon's height.
menu_y() {
  local idx="${1:-0}" H N isz itemH pitch content dtop y maxy
  H=$(niri msg --json outputs 2>/dev/null | jq -r 'first(.[] | .logical.height // empty) // 1152' 2>/dev/null)
  case "$H" in ''|*[!0-9]*) H=1152;; esac
  N=$(eww get dock-apps 2>/dev/null | jq 'length' 2>/dev/null); case "$N" in ''|*[!0-9]*) N=1;; esac
  isz=$(eww get cfg-icon-size 2>/dev/null); case "$isz" in ''|*[!0-9]*) isz=42;; esac
  [ "$isz" -lt 42 ] && isz=42                      # scss enforces a 42px min icon
  itemH=$((isz + 8)); pitch=$((itemH + 10))        # +padding, +inter-item spacing
  content=$((20 + N * itemH + (N - 1) * 10))       # +dock padding top/bottom
  dtop=$(( (H - content) / 2 )); [ "$dtop" -lt 0 ] && dtop=0
  y=$(( dtop + 10 + idx * pitch ))                 # top of icon <idx>
  maxy=$(( H - 280 )); [ "$maxy" -lt 8 ] && maxy=8 # keep an ~270px menu on screen
  [ "$y" -gt "$maxy" ] && y=$maxy
  [ "$y" -lt 8 ] && y=8
  printf '%s' "$y"
}

# ---- helpers --------------------------------------------------------------
resolve_icon() {   # icon name -> best file path (Papirus svg preferred)
  local name="$1" base sz ext f
  [ -z "$name" ] && name="application-x-executable"
  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
  # generic fallback so every tile has something
  resolve_generic
}
resolve_generic() {
  local p
  for p in /usr/share/icons/Papirus-Dark/64x64/apps/application-x-executable.svg \
           /usr/share/icons/Papirus/64x64/apps/application-x-executable.svg; do
    [ -f "$p" ] && { printf '%s' "$p"; return; }
  done
  printf ''
}
lc() { printf '%s' "$1" | tr 'A-Z' 'a-z'; }
running_classes() { clients_json | jq -r '.[].class' | tr 'A-Z' 'a-z'; }
is_running() { running_classes | grep -qx "$(lc "$1")"; }

# open windows of a class -> [{address, title, ws}]  (for the multi-instance menu)
win_list() {
  clients_json \
    | jq -c --arg c "$(lc "$1")" '
        [ .[] | select((.class|ascii_downcase)==$c)
              | { address: (.id|tostring),
                  title: ((.title // "") | if length>40 then .[0:40]+"…" else . end),
                  ws:    .ws } ]'
}

emit_row() {   # exec class iconname pinned
  local exec="$1" class="$2" icon="$3" pinned="$4" run=false
  is_running "$class" && run=true
  printf '%s\t%s\t%s\t%s\t%s\n' "$exec" "$class" "$(resolve_icon "$icon")" "$run" "$pinned"
}

# ---- commands -------------------------------------------------------------
case "${1:-apps}" in
  apps)
    {
      pinned_classes=""
      while IFS='|' read -r exec class icon; do
        [ -z "$exec" ] && continue
        emit_row "$exec" "$class" "$icon" true
        pinned_classes="$pinned_classes $(lc "$class")"
      done < "$PINS"
      # running apps that aren't pinned -> show them too (unpinned)
      clients_json \
        | jq -r '.[]|select(.class!="")|"\(.pid)\t\(.class)"' | sort -u \
        | while IFS=$'\t' read -r pid class; do
            case " $pinned_classes " in *" $(lc "$class") "*) continue;; esac
            exe=$(basename "$(readlink -f "/proc/$pid/exe" 2>/dev/null)" 2>/dev/null)
            [ -z "$exe" ] && exe="$class"
            emit_row "$exe" "$class" "$(lc "$class")" false
          done
    } | awk '!seen[$2]++' \
      | jq -R -s -c 'split("\n")|map(select(length>0))|map(split("\t"))
                     |map({exec:.[0],class:.[1],icon:.[2],running:(.[3]=="true"),pinned:(.[4]=="true")})
                     |to_entries|map(.value + {idx: .key})'
    ;;
  launch)
    class="${2:-}"; shift 2 2>/dev/null || shift $#
    # focus the running window (niri jumps to its workspace) else launch it
    id=$(clients_json \
           | jq -r --arg c "$(lc "$class")" '[.[]|select((.class|ascii_downcase)==$c)][0].id // empty')
    if [ -n "$id" ]; then
      wm_focus "$id"
    else
      setsid -f ${*} >/dev/null 2>&1
    fi
    dock_dismiss
    ;;
  menu-open)
    printf '%s' "$(date +%s%N)" > "$STAMP" 2>/dev/null   # mark: an icon was right-clicked
    class="${2:-}"; exe="${3:-}"; pinned="${4:-false}"; running="${5:-false}"; idx="${6:-}"
    eww update dock-sel-class="$class" dock-sel-exec="$exe" \
               dock-sel-pinned="$pinned" dock-sel-running="$running" 2>/dev/null
    eww update "dock-sel-windows=$(win_list "$class")" 2>/dev/null
    eww close dock-bg-menu 2>/dev/null   # icon menu wins over the background menu
    eww open dock-menu-backdrop 2>/dev/null
    eww close dock-menu 2>/dev/null      # re-open fresh so a switch re-renders
    # niri has no cursor Y, but we KNOW the clicked icon's index — so compute its
    # on-screen Y from the (vertically-centred) dock layout and open the menu
    # there with --pos, instead of always centring it.
    eww open dock-menu --anchor "top left" --pos "104x$(menu_y "${idx:-0}")" 2>/dev/null
    ;;
  bg-menu)
    # The icon's button onrightclick fires first (and stamps); this eventbox
    # onrightclick lands here right after. If an icon was just clicked, suppress
    # the background menu — only open it on a true empty-area right-click.
    now=$(date +%s%N); last=$(cat "$STAMP" 2>/dev/null || echo 0)
    [ "$(( (now - last) / 1000000 ))" -lt 250 ] && exit 0
    eww close dock-menu 2>/dev/null
    eww open dock-menu-backdrop 2>/dev/null
    eww close dock-bg-menu 2>/dev/null
    eww open dock-bg-menu 2>/dev/null
    ;;
  new)
    shift
    setsid -f ${*} >/dev/null 2>&1
    eww close dock-menu dock-menu-backdrop 2>/dev/null
    ;;
  focus-win)
    # focus one specific window by niri window id (jumps to its workspace)
    id="${2:-}"
    [ -n "$id" ] && wm_focus "$id"
    dock_dismiss
    ;;
  close)
    # kill by PID — robust, independent of the IPC focus API
    class="${2:-}"
    for pid in $(clients_json \
                   | jq -r --arg c "$(lc "$class")" '.[]|select((.class|ascii_downcase)==$c)|.pid'); do
      [ "$pid" -gt 0 ] 2>/dev/null && kill "$pid" 2>/dev/null
    done
    dock_dismiss
    ;;
  pin)
    class="${2:-}"
    grep -qiE "^[^|]*\|${class}\|" "$PINS" 2>/dev/null && { eww close dock-menu dock-menu-backdrop 2>/dev/null; exit 0; }
    pid=$(clients_json \
            | jq -r --arg c "$(lc "$class")" '[.[]|select((.class|ascii_downcase)==$c)][0].pid // empty')
    exe=""
    [ -n "$pid" ] && [ "$pid" -gt 0 ] 2>/dev/null && exe=$(basename "$(readlink -f "/proc/$pid/exe" 2>/dev/null)" 2>/dev/null)
    [ -z "$exe" ] && exe="$class"
    printf '%s|%s|%s\n' "$exe" "$class" "$(lc "$class")" >> "$PINS"
    eww close dock-menu dock-menu-backdrop 2>/dev/null
    ;;
  unpin)
    class="${2:-}"
    tmp=$(mktemp)
    grep -viE "^[^|]*\|${class}\|" "$PINS" > "$tmp" 2>/dev/null
    mv "$tmp" "$PINS"
    eww close dock-menu dock-menu-backdrop 2>/dev/null
    ;;
  move-up|move-down)
    # reorder the pinned app in dock-pins (swap with its neighbour). Order in
    # dock-pins == order in the dock. Menu stays open so you can move repeatedly.
    class="${2:-}"
    mapfile -t L < "$PINS" 2>/dev/null
    idx=-1
    for i in "${!L[@]}"; do case "${L[$i]}" in *"|${class}|"*) idx=$i; break;; esac; done
    if [ "$idx" -ge 0 ]; then
      if [ "$1" = move-up ]; then j=$((idx-1)); else j=$((idx+1)); fi
      if [ "$j" -ge 0 ] && [ "$j" -lt "${#L[@]}" ]; then
        t="${L[$idx]}"; L[$idx]="${L[$j]}"; L[$j]="$t"
        printf '%s\n' "${L[@]}" > "$PINS"
        eww update dock-apps="$("$0" apps)" 2>/dev/null   # reflect immediately
      fi
    fi
    ;;
  # --- INSTANT intent writes (safe inside eww handlers — no sleep to kill) ----
  show)      printf 'show' > "$HTOK" 2>/dev/null ;;       # trigger/dock hover → want shown
  keepalive) printf 'show' > "$HTOK" 2>/dev/null ;;       # pointer on dock/icon → cancel hide
  hide)      printf 'leave:%s' "$(date +%s%3N)" > "$HTOK" 2>/dev/null ;;  # pointer left at <ms>
  hide-now)  printf 'hidden' > "$HTOK" 2>/dev/null ;;     # close immediately

  # --- the auto-hide daemon: started once by launch.sh -----------------------
  # eww can't kill THIS (it's not an event handler), so the timing is safe here.
  #
  # The dock window is mapped on show and CLOSED on hide (so nothing is visible
  # when hidden — an always-mapped collapsed dock leaves a faint sliver). The
  # revealer animates because the window has no fixed width (the slide axis is
  # free to grow). All the open/sleep/close lives HERE in the daemon, never in an
  # eww handler, so eww can't kill it mid-animation.
  #
  # Reconciles to $HTOK:  show | leave:<epoch_ms> | hidden
  watch)
    # Start from a guaranteed-clean hidden state (closes any stale dock/catcher a
    # crash or restart left mapped — otherwise a collapsed dock shows a sliver).
    eww update dock-open=false 2>/dev/null
    eww close dock dock-catcher 2>/dev/null
    printf 'hidden' > "$HTOK" 2>/dev/null
    vis=0
    dlog "watch" "START (forced hidden)"
    while :; do
      st=$(cat "$HTOK" 2>/dev/null)
      case "$st" in
        show)
          if [ "$vis" = 0 ]; then
            dlog "watch" "SHOW open"
            eww update dock-open=false 2>/dev/null    # collapsed before map
            eww open dock-catcher 2>/dev/null          # catcher first (below)…
            eww open dock 2>/dev/null                  # …dock after → on top
            sleep 0.12                                 # let it realize collapsed
            [ "$(cat "$HTOK" 2>/dev/null)" = show ] || { eww close dock dock-catcher 2>/dev/null; continue; }
            eww update dock-open=true 2>/dev/null      # slide in (revealer)
            vis=1
          fi
          ;;
        leave:*)
          if [ "$vis" = 1 ]; then
            t=${st#leave:}; now=$(date +%s%3N)
            if [ "$((now - t))" -ge "$HIDE_MS" ]; then
              dlog "watch" "HIDE (leave aged $((now-t))ms)"
              eww update dock-open=false 2>/dev/null   # slide out
              sleep "$ANIM"
              if [ "$(cat "$HTOK" 2>/dev/null)" = show ]; then
                eww update dock-open=true 2>/dev/null  # came back mid-slide
              else
                eww close dock dock-catcher 2>/dev/null   # unmap both → no sliver
                vis=0
                printf 'hidden' > "$HTOK" 2>/dev/null
              fi
            fi
          fi
          ;;
        hidden)
          if [ "$vis" = 1 ]; then
            dlog "watch" "HIDE (hidden state)"
            eww update dock-open=false 2>/dev/null
            sleep "$ANIM"
            if [ "$(cat "$HTOK" 2>/dev/null)" = show ]; then eww update dock-open=true 2>/dev/null
            else eww close dock dock-catcher 2>/dev/null; vis=0; fi
          fi
          ;;
      esac
      sleep 0.05
    done
    ;;
esac
