#!/usr/bin/env python3
"""Screen-absolute focus / window-move for the scrolling ("scrolling"/"columns")
layouts, so the direction keys stop *rotating* with the scroller's orientation.

Hyprland's scrolling layout resolves a direction that is *perpendicular* to the tape
onto "next in tape" (verified: with a vertical tape, Left/Right scroll DOWN; with a
horizontal tape, Up/Down jump to the next column). That makes h/j/k/l and the arrows
mean different things depending on `scrolling:direction`. This wrapper keeps every key
mapped to its literal on-screen direction:

  * Along the tape axis                 -> the layout's own move/focus (correct as-is).
  * Perpendicular with a real neighbour
    in the current column/strip          -> a genuine within-column step (kept).
  * Perpendicular with no neighbour      -> a no-op, instead of scrolling the tape.
    (Crossing to another monitor has its own explicit binds — Super+,/. and
    Super+Shift+,/. — so it isn't overloaded onto the arrows here.)

For any non-scrolling layout (dwindle/master/monocle) it is a straight pass-through, so
nothing else changes.

Usage:  hypr-nav focus  l|r|u|d
        hypr-nav move   l|r|u|d
"""

from __future__ import annotations

import json
import os
import subprocess
import sys

DIRS = {"l": ("x", -1, "left"), "r": ("x", 1, "right"),
        "u": ("y", -1, "up"),   "d": ("y", 1, "down")}
SCROLLING_LAYOUTS = {"scrolling", "columns"}
STATE = os.path.expanduser("~/.cache/astal-menu/layouts-state.json")


def hypr_json(*args):
    try:
        return json.loads(subprocess.check_output(["hyprctl", *args, "-j"]))
    except (subprocess.CalledProcessError, json.JSONDecodeError):
        return None


def dispatch(lua: str) -> None:
    # `hyprctl dispatch` evaluates its argument as Lua here (the hl.dsp.* API).
    subprocess.run(["hyprctl", "dispatch", f"hl.dispatch({lua})"],
                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def plain(kind: str, dir_word: str) -> None:
    if kind == "focus":
        dispatch(f'hl.dsp.focus({{ direction = "{dir_word}" }})')
    else:
        dispatch(f'hl.dsp.window.move({{ direction = "{dir_word}" }})')


def active_layout_name(wsid) -> str | None:
    try:
        with open(STATE) as f:
            name = json.load(f).get(str(wsid))
            if name:
                return name
    except (FileNotFoundError, json.JSONDecodeError, TypeError):
        pass
    opt = hypr_json("getoption", "general:layout")
    return opt.get("str") if opt else None


def tape_axis() -> str:
    opt = hypr_json("getoption", "scrolling:direction")
    d = (opt.get("str") if opt else "down") or "down"
    return "y" if d in ("down", "up") else "x"


def _idx(axis):
    return 0 if axis == "x" else 1


def _lo_hi(w, axis):
    i = _idx(axis)
    return w["at"][i], w["at"][i] + w["size"][i]


def _center(w, axis):
    i = _idx(axis)
    return w["at"][i] + w["size"][i] / 2


def _overlap(a, b):
    return a[0] < b[1] and b[0] < a[1]


def main() -> int:
    if len(sys.argv) != 3 or sys.argv[1] not in ("focus", "move") \
            or sys.argv[2] not in DIRS:
        print(__doc__)
        return 2
    kind, key = sys.argv[1], sys.argv[2]
    axis, sign, dir_word = DIRS[key]

    ws = hypr_json("activeworkspace") or {}
    if active_layout_name(ws.get("id")) not in SCROLLING_LAYOUTS:
        plain(kind, dir_word)          # dwindle/master/monocle: unchanged
        return 0

    primary = tape_axis()
    if axis == primary:
        plain(kind, dir_word)          # along the tape — already screen-correct
        return 0

    win = hypr_json("activewindow")
    if not win or not win.get("address"):
        plain(kind, dir_word)
        return 0

    # Perpendicular move: is there a real neighbour within this column/strip in the
    # requested screen direction? (Same column == overlapping along the tape's primary
    # axis; the requested `axis` is the secondary/stacking axis here.)
    wsid = win.get("workspace", {}).get("id")
    clients = [c for c in (hypr_json("clients") or [])
               if c.get("mapped", True) and c.get("workspace", {}).get("id") == wsid
               and c.get("address") != win["address"]]
    prange = _lo_hi(win, primary)
    my_c = _center(win, axis)
    neighbour = any(_overlap(prange, _lo_hi(c, primary))
                    and (_center(c, axis) - my_c) * sign > 1 for c in clients)

    if neighbour:
        plain(kind, dir_word)          # genuine within-column step
    # else: no neighbour that way — do nothing rather than let the layout scroll the
    # tape (that sideways/vertical "rotation" is exactly what we're suppressing).
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
