#!/usr/bin/env bash
# Mover ventanas y repartirlas por las pantallas. El motor de las acciones del
# grupo "ventanas": ellas solo llaman aqui con un verbo y un argumento.
#
# Por que un script del host y no un modulo dentro del flatpak: las acciones se
# ejecutan como comandos de shell, y toda la logica en Python obligaria a
# recompilar el flatpak por cada ajuste. Aqui se edita y ya esta, igual que los
# envoltorios de voz.
#
# Motor X11 (xdotool). En Wayland, mover ventanas AJENAS depende del compositor
# —cada uno tiene su via (swaymsg, hyprctl...) o directamente lo prohibe por su
# modelo de seguridad (GNOME/KDE), y los tiling ni tienen "mitad izquierda"—, asi
# que no hay una via universal. Aqui se detecta el entorno y se avisa claro; el
# resto de JARVIS funciona igual, solo estas ordenes de ventanas quedan fuera en
# Wayland. Soporte por compositor: pendiente (ver el roadmap del README).
#
#   ventanas.sh pantallas                    lista las pantallas
#   ventanas.sh listar                       lista las ventanas movibles
#   ventanas.sh mover <ventana> <pantalla>   la manda a esa pantalla
#   ventanas.sh zona  <ventana> <zona>       mitad izquierda, cuarto, centro...
#   ventanas.sh escritorio <ventana> <n>     la manda a ese escritorio virtual
#   ventanas.sh maximiza <ventana>           / restaura / minimiza
#   ventanas.sh alias <pantalla> <nombre>    le pone nombre a una salida
#
# <ventana> puede ser: parte del titulo o de la clase ("terminal", "firefox"),
# o "esta"/"activa" para la que tiene el foco.
# <pantalla> puede ser: un alias ("tele"), una salida ("HDMI-1"), un numero
# ("2") o una posicion ("derecha", "izquierda", "arriba", "abajo", "centro").
set -uo pipefail

RAIZ=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)
ALIAS_FICHERO=${JARVIS_ALIAS_PANTALLAS:-$HOME/.config/jarvis-pantallas.json}

hay_x11() { [ -n "${DISPLAY:-}" ] && command -v xdotool >/dev/null 2>&1; }
if ! hay_x11; then
    en_wayland() { [ "${XDG_SESSION_TYPE:-}" = "wayland" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; }
    # En Wayland con Sway o Hyprland, delega en el backend propio (actua sobre la
    # ventana activa). El verbo va en $1 y para las ordenes de ventana el destino
    # (izquierda/derecha/centro/maximiza...) suele venir en $3; se le pasa el que
    # tenga sentido.
    if en_wayland && (command -v swaymsg >/dev/null 2>&1 || command -v hyprctl >/dev/null 2>&1); then
        BACKEND="$(dirname "${BASH_SOURCE[0]:-$0}")/ventanas-wayland.sh"
        case "${1:-}" in
            zona)     exec bash "$BACKEND" "${3:-centro}" ;;   # zona <ventana> <izquierda|derecha|centro>
            maximiza|restaura|minimiza) exec bash "$BACKEND" "$1" ;;
            *) echo "en Wayland de momento van: maximiza, restaura, minimiza," \
                    "y mitades (izquierda/derecha/centro). El resto, en el roadmap."; exit 1 ;;
        esac
    fi
    if en_wayland; then
        echo "estas en Wayland (${XDG_CURRENT_DESKTOP:-tu compositor}) sin Sway ni" \
             "Hyprland: mover ventanas ajenas no tiene via universal ahi. El resto" \
             "de JARVIS funciona igual. Soporte por compositor: ver ROADMAP.md."
    elif [ -n "${DISPLAY:-}" ]; then
        echo "estas en X11 pero falta xdotool. Instalalo con tu gestor de paquetes."
    else
        echo "no hay sesion grafica (ni DISPLAY ni WAYLAND_DISPLAY)."
    fi
    exit 1
fi

# ---------------------------------------------------------------- pantallas

# Una linea por pantalla: salida<TAB>x<TAB>y<TAB>ancho<TAB>alto<TAB>numero
#
# Se usa `xrandr --listmonitors`, que da la geometria ya resuelta (con el
# escalado aplicado); `--query` daria los modos, que no es lo que hace falta.
# El numero es el orden de izquierda a derecha, no el que diga xrandr: es lo
# que espera cualquiera que diga "la pantalla dos".
pantallas_crudas() {
    xrandr --listmonitors 2>/dev/null | tail -n +2 | while read -r _ _ geo salida; do
        # geo viene como 1920/344x1080/194+0+0
        local an al x y
        an=${geo%%/*}; geo=${geo#*x}
        al=${geo%%/*}; geo=${geo#*+}
        x=${geo%%+*}; y=${geo##*+}
        printf '%s\t%s\t%s\t%s\t%s\n' "$salida" "$x" "$y" "$an" "$al"
    done | sort -t$'\t' -k2 -n | awk -F'\t' '{printf "%s\t%s\t%s\t%s\t%s\t%d\n", $1,$2,$3,$4,$5,NR}'
}

alias_de() {
    [ -f "$ALIAS_FICHERO" ] || { echo ""; return; }
    python3 -c "
import json, sys
try:
    print(json.load(open('$ALIAS_FICHERO')).get(sys.argv[1], ''))
except Exception:
    print('')
" "$1" 2>/dev/null
}

# izquierda / derecha / arriba / abajo / centro, deducido de donde cae el
# centro de esa pantalla respecto al centro de todas.
posicion_de() {
    local x=$1 an=$2 y=$3 al=$4
    local n; n=$(pantallas_crudas | wc -l)
    [ "$n" -le 1 ] && { echo "unica"; return; }
    local minx maxx miny maxy
    minx=$(pantallas_crudas | awk -F'\t' 'NR==1||$2<m{m=$2}END{print m+0}')
    maxx=$(pantallas_crudas | awk -F'\t' '{v=$2+$4; if(v>m)m=v}END{print m+0}')
    miny=$(pantallas_crudas | awk -F'\t' 'NR==1||$3<m{m=$3}END{print m+0}')
    maxy=$(pantallas_crudas | awk -F'\t' '{v=$3+$5; if(v>m)m=v}END{print m+0}')
    local cx=$(( x + an/2 )) cy=$(( y + al/2 ))
    local mx=$(( (minx+maxx)/2 )) my=$(( (miny+maxy)/2 ))
    local anchura=$(( maxx-minx )) altura=$(( maxy-miny ))
    # el reparto manda por el eje donde hay mas separacion
    if [ $(( anchura - altura )) -gt 0 ]; then
        [ "$cx" -lt $(( mx - anchura/8 )) ] && { echo izquierda; return; }
        [ "$cx" -gt $(( mx + anchura/8 )) ] && { echo derecha; return; }
    else
        [ "$cy" -lt $(( my - altura/8 )) ] && { echo arriba; return; }
        [ "$cy" -gt $(( my + altura/8 )) ] && { echo abajo; return; }
    fi
    echo centro
}

listar_pantallas() {
    local salida x y an al num pos ali
    while IFS=$'\t' read -r salida x y an al num; do
        pos=$(posicion_de "$x" "$an" "$y" "$al")
        ali=$(alias_de "$salida")
        printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
            "$num" "$salida" "${ali:--}" "$pos" "$x" "$y" "$an" "$al"
    done < <(pantallas_crudas)
}

# Resuelve lo que se dice a una pantalla. Orden a proposito: primero lo que el
# usuario ha nombrado el mismo, que es lo mas especifico, y la posicion la
# ultima porque es la mas facil de confundir.
resuelve_pantalla() {
    local q; q=$(echo "$1" | tr 'A-ZÁÉÍÓÚ' 'a-záéíóú')
    local num salida ali pos x y an al
    # 1. alias
    while IFS=$'\t' read -r num salida ali pos x y an al; do
        [ "$ali" != "-" ] && [ "$(echo "$ali" | tr 'A-Z' 'a-z')" = "$q" ] && {
            printf '%s\t%s\t%s\t%s\t%s\n' "$salida" "$x" "$y" "$an" "$al"; return 0; }
    done < <(listar_pantallas)
    # 2. salida exacta o su prefijo (hdmi, dp)
    while IFS=$'\t' read -r num salida ali pos x y an al; do
        local s; s=$(echo "$salida" | tr 'A-Z' 'a-z')
        [ "$s" = "$q" ] || [ "${s%%-*}" = "$q" ] && {
            printf '%s\t%s\t%s\t%s\t%s\n' "$salida" "$x" "$y" "$an" "$al"; return 0; }
    done < <(listar_pantallas)
    # 3. numero, en cifra o en letra
    local n=""
    case "$q" in
        1|una|uno|primera) n=1;; 2|dos|segunda) n=2;; 3|tres|tercera) n=3;; 4|cuatro|cuarta) n=4;;
    esac
    if [ -n "$n" ]; then
        while IFS=$'\t' read -r num salida ali pos x y an al; do
            [ "$num" = "$n" ] && { printf '%s\t%s\t%s\t%s\t%s\n' "$salida" "$x" "$y" "$an" "$al"; return 0; }
        done < <(listar_pantallas)
    fi
    # 4. posicion
    while IFS=$'\t' read -r num salida ali pos x y an al; do
        [ "$pos" = "$q" ] && { printf '%s\t%s\t%s\t%s\t%s\n' "$salida" "$x" "$y" "$an" "$al"; return 0; }
    done < <(listar_pantallas)
    return 1
}

# ----------------------------------------------------------------- ventanas

# El area util de la pantalla que contiene un punto: hay que descontar barras
# y paneles, o las ventanas acaban debajo de la barra de GNOME.
area_util() {
    local v; v=$(xprop -root _NET_WORKAREA 2>/dev/null | sed 's/.*= //' | tr -d ' ')
    if [ -n "$v" ]; then
        echo "$(echo "$v" | cut -d, -f1) $(echo "$v" | cut -d, -f2) $(echo "$v" | cut -d, -f3) $(echo "$v" | cut -d, -f4)"
    else
        set -- $(xdotool getdisplaygeometry); echo "0 0 $1 $2"
    fi
}

# Se descartan las que no son ventanas de verdad: el guardian de mutter, el
# propio shell y las de 1x1 que usan algunos programas como buzon.
listar_ventanas() {
    local w n cls x y an al
    for w in $(xdotool search --onlyvisible --name "." 2>/dev/null); do
        n=$(xdotool getwindowname "$w" 2>/dev/null) || continue
        [ -z "$n" ] && continue
        case "$n" in "mutter guard window"|"gnome-shell") continue;; esac
        eval "$(xdotool getwindowgeometry --shell "$w" 2>/dev/null)" || continue
        [ "${WIDTH:-0}" -lt 50 ] && continue
        cls=$(xprop -id "$w" WM_CLASS 2>/dev/null | sed 's/.*, *//' | tr -d '"')
        printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$w" "$n" "${cls:--}" "$X" "$Y" "$WIDTH" "$HEIGHT"
    done
}

# Devuelve el id, o varios si hay empate: quien llama decide si preguntar.
resuelve_ventana() {
    local q; q=$(echo "$1" | tr 'A-ZÁÉÍÓÚ' 'a-záéíóú')
    case "$q" in
        esta|activa|"esta ventana"|"la activa"|"ventana activa")
            xdotool getactivewindow 2>/dev/null; return;;
    esac
    listar_ventanas | awk -F'\t' -v q="$q" '
        BEGIN{IGNORECASE=1}
        { t=tolower($2); c=tolower($3);
          if (index(t,q) || index(c,q)) print $1"\t"$2 }'
}

geo_de() { eval "$(xdotool getwindowgeometry --shell "$1" 2>/dev/null)"; echo "$X $Y $WIDTH $HEIGHT"; }

# Maximizar NO se hace con `xdotool windowstate --add MAXIMIZED`: comprobado,
# en mutter no hace nada. Lo que funciona es activar la ventana y mandar el
# atajo del escritorio. Y una ventana maximizada IGNORA windowmove, asi que
# hay que desmaximizar antes de mover.
desmaximiza() { xdotool windowactivate "$1" 2>/dev/null; sleep 0.2; xdotool key super+Down 2>/dev/null; sleep 0.4; }
maximiza()    { xdotool windowactivate "$1" 2>/dev/null; sleep 0.2; xdotool key super+Up   2>/dev/null; sleep 0.4; }

coloca() {  # coloca <id> <x> <y> <ancho> <alto>
    desmaximiza "$1"
    xdotool windowsize "$1" "$4" "$5" 2>/dev/null
    xdotool windowmove "$1" "$2" "$3" 2>/dev/null
    sleep 0.3
}

# --------------------------------------------------------------------- api

# Imprime el id, o una frase que se pueda decir en voz alta. Nada de
# marcadores tipo NINGUNA/VARIAS: lo que salga de aqui puede acabar en el
# altavoz tal cual, asi que tiene que ser castellano.
una_ventana() {
    local r; r=$(resuelve_ventana "$1")
    local n; n=$(echo "$r" | grep -c . )
    if [ "$n" -eq 0 ]; then
        echo "No encuentro ninguna ventana que sea $1, señor."; return 1
    fi
    if [ "$n" -gt 1 ]; then
        # Ambiguo: se enumeran y decide la persona. Elegir una al azar es lo
        # peor que puede hacer algo que mueve ventanas ajenas.
        printf 'Hay %s que encajan, señor: %s. Digame cual.\n' "$n" \
            "$(echo "$r" | cut -f2 | head -4 | paste -sd'|' - | sed 's/|/; /g')"
        return 2
    fi
    echo "$r" | cut -f1
}

case "${1:-}" in
  pantallas)
    listar_pantallas | while IFS=$'\t' read -r num salida ali pos x y an al; do
        printf 'pantalla %s: %s%s, %s, %sx%s\n' "$num" "$salida" \
            "$([ "$ali" != "-" ] && echo " ($ali)")" "$pos" "$an" "$al"
    done
    ;;

  listar)
    listar_ventanas | while IFS=$'\t' read -r w n cls x y an al; do
        printf '%s  (%s)  %sx%s en %s,%s\n' "$n" "$cls" "$an" "$al" "$x" "$y"
    done
    ;;

  alias)
    salida=$(resuelve_pantalla "${2:-}" | cut -f1)
    [ -n "$salida" ] || { echo "No se cual es esa pantalla, señor"; exit 1; }
    python3 - "$ALIAS_FICHERO" "$salida" "${3:-}" <<'FIN'
import json, os, sys
fichero, salida, nombre = sys.argv[1], sys.argv[2], sys.argv[3]
os.makedirs(os.path.dirname(fichero), exist_ok=True)
try:
    d = json.load(open(fichero))
except Exception:
    d = {}
d[salida] = nombre
json.dump(d, open(fichero, "w"), ensure_ascii=False, indent=2)
print(f"{salida} se llama ahora {nombre}")
FIN
    ;;

  mover)
    w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }
    destino=$(resuelve_pantalla "${3:-}") || { echo "No se cual es esa pantalla, señor"; exit 1; }
    IFS=$'\t' read -r salida px py pan pal <<< "$destino"
    read -r ax ay aan aal <<< "$(area_util)"
    # se centra en la pantalla destino, a tres cuartos de su tamaño
    an=$(( pan * 3 / 4 )); al=$(( pal * 3 / 4 ))
    x=$(( px + (pan - an) / 2 )); y=$(( py + (pal - al) / 2 ))
    [ "$y" -lt "$ay" ] && y=$ay
    coloca "$w" "$x" "$y" "$an" "$al"
    # "eDP-1" en voz alta se lee letra por letra: se dice el mote si lo tiene,
    # y si no el numero, que es lo que entiende cualquiera.
    comodo=$(listar_pantallas | awk -F'\t' -v s="$salida" '$2==s {print ($3!="-") ? $3 : "numero " $1}')
    echo "Movida a la pantalla ${comodo:-$salida}"
    ;;

  zona)
    w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }
    read -r ax ay aan aal <<< "$(area_util)"
    m2=$(( aan / 2 )); h2=$(( aal / 2 ))
    case "$(echo "${3:-}" | tr 'A-ZÁÉÍÓÚ' 'a-záéíóú')" in
      izquierda|"mitad izquierda") coloca "$w" "$ax" "$ay" "$m2" "$aal"; echo "Ya la tiene a la izquierda";;
      derecha|"mitad derecha")     coloca "$w" $((ax+m2)) "$ay" "$m2" "$aal"; echo "Ya la tiene a la derecha";;
      arriba)                      coloca "$w" "$ax" "$ay" "$aan" "$h2"; echo "Ya la tiene arriba";;
      abajo)                       coloca "$w" "$ax" $((ay+h2)) "$aan" "$h2"; echo "Ya la tiene abajo";;
      "arriba izquierda")          coloca "$w" "$ax" "$ay" "$m2" "$h2"; echo "Colocada arriba a la izquierda";;
      "arriba derecha")            coloca "$w" $((ax+m2)) "$ay" "$m2" "$h2"; echo "Colocada arriba a la derecha";;
      "abajo izquierda")           coloca "$w" "$ax" $((ay+h2)) "$m2" "$h2"; echo "Colocada abajo a la izquierda";;
      "abajo derecha")             coloca "$w" $((ax+m2)) $((ay+h2)) "$m2" "$h2"; echo "Colocada abajo a la derecha";;
      centro|centrada)             coloca "$w" $((ax+aan/4)) $((ay+aal/4)) $((aan/2)) $((aal/2)); echo "Ya la tiene centrada";;
      *) echo "No conozco esa zona, señor"; exit 1;;
    esac
    ;;

  maximiza)  w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }; maximiza "$w"; echo "Maximizada";;
  restaura)  w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }; desmaximiza "$w"; echo "Restaurada a su tamaño";;
  minimiza)  w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }
             xdotool windowminimize "$w" 2>/dev/null; echo "Minimizada";;

  escritorio)
    w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }
    n=${3:-1}
    case "$n" in una|uno|primero) n=1;; dos|segundo) n=2;; tres|tercero) n=3;; cuatro) n=4;; esac
    total=$(xdotool get_num_desktops 2>/dev/null || echo 1)
    [ "$n" -ge 1 ] 2>/dev/null && [ "$n" -le "$total" ] || { echo "no hay escritorio $n, solo $total"; exit 1; }
    xdotool set_desktop_for_window "$w" $(( n - 1 )) 2>/dev/null
    echo "Movida al escritorio $n"
    ;;

  *) sed -n '2,26p' "$0"; exit 1;;
esac
