#!/usr/bin/python3
"""moodtop-wm — turn ~/.config/mood/settings.json into the rest of the desktop's config.

Writes (atomically, only when changed):
  ~/.config/wayfire.ini            Wayfire reloads it live (effects, keys, input, title bars)
  ~/.config/gtk-3.0|gtk-4.0/settings.ini, gsettings (dark mode, accent, icons, cursor, font)
  ~/.config/kitty/moodtop-theme.conf  (+ SIGUSR1 to running kitty)
  ~/.config/mako/config            (+ makoctl reload)
  ~/.config/gtklock/*               lock screen (built by mood-lockscreen)
Run at session start (mood-session) and by the shell whenever settings change.
"""
import colorsys
import json
import os
import re
import subprocess
import sys
from pathlib import Path

sys.path.insert(0, "/usr/lib/mood/runtime")
import moodhost  # noqa: E402  (only uses load_settings; GTK import is cheap enough here)

HOME = Path.home()
CFG = HOME / ".config"
DARK = {"bg": "#0c0d14", "bg-2": "#11131d", "surface": "#171a27", "surface-2": "#1f2333", "surface-3": "#2a2f44", "text": "#eef0fa", "text-2": "#a7adc6"}
LIGHT = {"bg": "#f3f4fa", "bg-2": "#eaecf5", "surface": "#ffffff", "surface-2": "#f1f2f9", "surface-3": "#e3e6f2", "text": "#141726", "text-2": "#4b5170"}


def rgb(h):
    h = (h or "#8b5cf6").lstrip("#")
    if len(h) != 6:
        h = "8b5cf6"
    return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))


def hexa(h, a=1.0):
    r, g, b = rgb(h)
    return f"#{r:02X}{g:02X}{b:02X}{round(a * 255):02X}"


def mix(h1, h2, t):
    a, b = rgb(h1), rgb(h2)
    return "#" + "".join(f"{round(x + (y - x) * t):02x}" for x, y in zip(a, b))


def write(path, text):
    path = Path(path)
    try:
        if path.read_text() == text:
            return False
    except Exception:
        pass
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(text)
    tmp.replace(path)
    return True


def gnome_accent(h):
    """Nearest libadwaita accent-color name (GNOME 47+) for Flatpak GNOME apps."""
    r, g, b = (x / 255 for x in rgb(h))
    hh, ll, ss = colorsys.rgb_to_hls(r, g, b)
    if ss < 0.18:
        return "slate"
    deg = hh * 360
    for limit, name in ((15, "red"), (40, "orange"), (65, "yellow"), (160, "green"), (195, "teal"), (255, "blue"), (290, "purple"), (340, "pink"), (361, "red")):
        if deg < limit:
            return name
    return "blue"


def touch_state():
    try:
        return json.loads((Path(os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")) / "mood-touch.json").read_text())
    except (OSError, ValueError):
        return {}


def wayfire_ini(s):
    w, ws, inp, tp, kb, cur = s["windows"], s["workspaces"], s.get("touchpad", {}), s.get("touchpad", {}), s.get("keyboard", {}), s.get("cursor", {})
    light = s.get("theme") == "light"
    pal = {**(LIGHT if light else DARK), **(s.get("palette") or {})}
    acc, acc2 = s.get("accent", "#8b5cf6"), s.get("accent2") or s.get("accent", "#8b5cf6")
    ts = touch_state()
    tablet = bool(ts.get("tablet"))
    gestures = (s.get("touch") or {}).get("gestures", True) is not False
    anim = w.get("animation", "zoom")
    anim = anim if anim in ("zoom", "fade", "fire", "none") else "zoom"
    plugins = ["alpha", "animate", "autostart", "command", "decoration", "expo", "fast-switcher", "foreign-toplevel", "grid",
               "gtk-shell", "idle", "ipc", "ipc-rules", "move", "oswitch", "place", "resize", "scale", "session-lock",
               "shortcuts-inhibit", "switcher", "vswipe", "vswitch", "window-rules", "wm-actions", "xdg-activation", "zoom", "extra-gestures"]
    if w.get("blur", True):
        plugins.append("blur")
    if w.get("wobbly", True) and s.get("motion") != "reduced":
        plugins.append("wobbly")
    if w.get("cube", True):
        plugins.append("cube")
    if s.get("motion") == "reduced":
        anim = "none"
    blur_px = int(s.get("blur", 22))
    font = s.get("font", "Inter")
    font = "Inter" if font in ("Comic Neue",) else font
    buttons = "close maximize minimize" if w.get("buttons") == "left" else "minimize maximize close"
    title_active = mix(pal["surface"], acc, 0.28 if not light else 0.16)
    speed = 0 if s.get("motion") == "reduced" else 1
    ms = lambda v: max(1, int(v * speed)) if speed else 1  # noqa: E731
    cols, rows = max(1, min(3, int(ws.get("cols", 2)))), max(1, min(3, int(ws.get("rows", 2))))
    # layer surfaces report their namespace as app_id: the orbs layer and the island are mostly transparent
    # full-size surfaces, so blurring them would smear a box over the windows underneath
    blur_rule = ('(type is "toplevel" | type is "panel" | type is "overlay" | type is "unmanaged") & !(app_id is "mood-orbs" | app_id is "mood-island")'
                 if w.get("blur", True) else 'type is "none"')
    L = []
    add = L.append
    add("# Generated by moodtop-wm from ~/.config/mood/settings.json — edit with Theme Studio.")
    add("# Put your own overrides in ~/.config/wayfire-user.ini (appended verbatim).\n")
    add("[core]")
    add("plugins = " + " ".join(plugins))
    add(f"vwidth = {cols}\nvheight = {rows}")
    add("preferred_decoration_mode = server\nxwayland = true")
    add("close_top_view = <super> KEY_Q | <alt> KEY_F4")
    add(f"background_color = {hexa(pal['bg'])}\n")
    add("[decoration]")
    add(f"active_color = {hexa(title_active, 0.97)}\ninactive_color = {hexa(pal['bg-2'], 0.93)}")
    add(f"title_height = {max(int(w.get('titlebar', 30)), 44) if tablet else int(w.get('titlebar', 30))}\nborder_size = {int(w.get('border', 2))}")
    add(f"button_order = {buttons}\nfont = {font}")
    add('ignore_views = app_id is "os.mood.shell"\n')
    add("[animate]")
    add(f"open_animation = {anim}\nclose_animation = {anim}")
    add(f"duration = {ms(320)}\nzoom_duration = {ms(300)}\nfade_duration = {ms(260)}\nfire_duration = {ms(650)}")
    add(f"startup_duration = {ms(700)}\nfire_particles = 1600\nfire_particle_size = 14\nrandom_fire_color = false\nfire_color = {hexa(acc)}\n")
    add("[blur]")
    add(f"blur_by_default = {blur_rule}")
    add(f"method = kawase\nkawase_offset = {max(1.0, blur_px / 8):.1f}\nkawase_iterations = {2 if blur_px < 18 else 3}\nkawase_degrade = 1\nsaturation = 1.25\n")
    add("[wobbly]\nfriction = 3.0\nspring_k = 8.0\ngrid_resolution = 6\n")
    add("[cube]")
    add("activate = <ctrl> <super> BTN_LEFT\nrotate_left = <ctrl> <alt> KEY_LEFT\nrotate_right = <ctrl> <alt> KEY_RIGHT")
    add(f"background_mode = simple\nbackground = {hexa(mix(pal['bg'], acc, 0.18))}\ndeform = 1\nlight = true\nzoom = 0.1\n")
    add("[expo]\ntoggle = <super> KEY_TAB | hotspot top-right 4x4 400" + (" | pinch in 4" if gestures else ""))
    add(f"background = {hexa(pal['bg'])}\nduration = {ms(300)}\noffset = 12\ninactive_brightness = 0.65\n")
    add("[scale]\ntoggle = <super> KEY_W | hotspot top-left 4x4 400" + (" | swipe up 3" if gestures else "") + "\ntoggle_all = <super> <shift> KEY_W")
    add(f"duration = {ms(300)}\nspacing = 40\nallow_zoom = true\ninclude_minimized = true\nmiddle_click_close = true\ntitle_overlay = all")
    add(f"bg_color = {hexa(acc, 0.85)}\ntext_color = #FFFFFFFF\ninactive_alpha = 0.75\n")
    add("[vswitch]")
    sw = (lambda d: f" | swipe {d} 3") if gestures else (lambda d: "")
    add(f"binding_left = <super> <ctrl> KEY_LEFT{sw('right')}\nbinding_right = <super> <ctrl> KEY_RIGHT{sw('left')}\nbinding_up = <super> <ctrl> KEY_UP\nbinding_down = <super> <ctrl> KEY_DOWN")
    add("with_win_left = <super> <ctrl> <shift> KEY_LEFT\nwith_win_right = <super> <ctrl> <shift> KEY_RIGHT\nwith_win_up = <super> <ctrl> <shift> KEY_UP\nwith_win_down = <super> <ctrl> <shift> KEY_DOWN")
    add(f"duration = {ms(380)}\ngap = 24\nbackground = {hexa(pal['bg'])}\nwraparound = true\n")
    add("[vswipe]\nenable_horizontal = true\nenable_vertical = true\nfingers = 4\n")
    add("[grid]")
    add("slot_l = <super> KEY_LEFT\nslot_r = <super> KEY_RIGHT\nslot_c = <super> KEY_UP\nrestore = <super> KEY_DOWN")
    add("slot_tl = <super> KEY_KP7\nslot_t = <super> KEY_KP8\nslot_tr = <super> KEY_KP9\nslot_bl = <super> KEY_KP1\nslot_b = <super> KEY_KP2\nslot_br = <super> KEY_KP3")
    add(f"type = {'wobbly' if 'wobbly' in plugins else 'crossfade'}\nduration = {ms(300)}\n")
    add("[move]\nactivate = <super> BTN_LEFT")
    add(f"enable_snap = {str(bool(w.get('snap', True))).lower()}\nsnap_threshold = 10\nquarter_snap_threshold = 50\njoin_views = false")
    add(f"preview_base_color = {hexa(acc, 0.25)}\npreview_base_border = {hexa(acc, 0.8)}\npreview_border_width = 3\n")
    add("[resize]\nactivate = <super> BTN_RIGHT\n")
    add("[switcher]\nnext_view = <alt> KEY_TAB\nprev_view = <alt> <shift> KEY_TAB")
    add(f"speed = {ms(420)}\nview_thumbnail_scale = 1.0\nview_thumbnail_rotation = 30\n")
    add("[fast-switcher]\nactivate = <super> KEY_GRAVE\nactivate_backward = <super> <shift> KEY_GRAVE\n")
    add("[place]\nmode = center\n")
    add("[wm-actions]\ntoggle_fullscreen = <super> KEY_F\ntoggle_always_on_top = <super> <shift> KEY_T\nminimize = <super> KEY_H\ntoggle_sticky = <super> <shift> KEY_P\n")
    add("[alpha]\nmodifier = <super> <alt>\nmin_value = 0.3\n")
    add("[zoom]\nmodifier = <super> <shift>\nspeed = 0.01\nsmoothing_duration = 300\n")
    add("[idle]\ntoggle = none\ndpms_timeout = 900\ndisable_on_fullscreen = true")
    add(f"screensaver_timeout = {600 if 'cube' in plugins else -1}\ncube_rotate_speed = 1.0\ncube_zoom_speed = 1000\ncube_max_zoom = 1.5\n")
    add("[input]")
    lay, var = kb.get("layout", "gb") or "gb", kb.get("variant", "") or ""
    add(f"xkb_layout = {lay}\nxkb_variant = {var}\nxkb_options = grp:alt_shift_toggle")
    add(f"kb_repeat_rate = {int(kb.get('repeatRate', 40))}\nkb_repeat_delay = {int(kb.get('repeatDelay', 300))}")
    add(f"tap_to_click = {str(bool(tp.get('tapToClick', True))).lower()}\nnatural_scroll = {str(bool(tp.get('naturalScroll', True))).lower()}")
    add(f"touchpad_cursor_speed = {max(-1.0, min(1.0, float(tp.get('speed', 0)))):.2f}\ndisable_touchpad_while_typing = true\nclick_method = clickfinger\nscroll_method = two-finger")
    add(f"cursor_theme = {cur.get('theme', 'Bibata-Modern-Classic')}\ncursor_size = {int(cur.get('size', 24))}\n")
    add("[extra-gestures]\nmove_fingers = 3\nmove_delay = 600\nclose_fingers = 5\n")
    # pin touchscreens + pens to the built-in panel (otherwise they span every monitor once one is plugged in)
    out = ts.get("output")
    for dev in (ts.get("touchDevices") or []) + (ts.get("penDevices") or []):
        if out and dev and not any(c in dev for c in "[]\n"):
            add(f"[input-device:{dev}]\noutput = {out}\n")
    add("[autostart]\nautostart_wf_shell = false")
    add("0_env = dbus-update-activation-environment --systemd WAYLAND_DISPLAY WAYFIRE_SOCKET XDG_CURRENT_DESKTOP XDG_SESSION_TYPE XCURSOR_THEME XCURSOR_SIZE DISPLAY")
    add("1_shell = /usr/lib/mood/shell/mood-shell.py")
    add("2_notify = mako")
    add("3_usb = udiskie --automount --notify --no-tray")
    add("4_clip = wl-paste --watch cliphist store")
    lock = s.get("lock") or {}
    idle = max(0, int(lock.get("idleMinutes", 20) or 0)) * 60
    add("5_idle = swayidle -w" + (f" timeout {idle} 'mood-lock'" if idle else "") + (" before-sleep 'mood-lock'" if lock.get("onSleep", True) else ""))
    add("6_display = /usr/lib/mood/runtime/mood-display-restore")
    add("7_first = /usr/lib/mood/runtime/mood-firstrun")
    add("7_touch = /usr/lib/mood/runtime/mood-touch")
    add("8_backlight = mood-backlight floor")
    add("8_portal = sh -c 'sleep 2; systemctl --user restart xdg-desktop-portal xdg-desktop-portal-gtk xdg-desktop-portal-wlr'\n")
    add("[command]")
    binds = [
        ("launcher", "<super>", "mood-shell --toggle launcher"), ("launcher2", "<super> KEY_SPACE", "mood-shell --toggle launcher"),
        ("control", "<super> KEY_A", "mood-shell --toggle control"), ("power", "<ctrl> <alt> KEY_DELETE", "mood-shell --toggle power"),
        ("clip", "<super> KEY_V", "mood-shell --toggle clipboard"), ("cal", "<super> KEY_N", "mood-shell --toggle calendar"),
        ("term", "<super> KEY_T", "mood-launch terminal"), ("term2", "<ctrl> <alt> KEY_T", "mood-launch terminal"),
        ("files", "<super> KEY_E", "mood-launch files"), ("browser", "<super> KEY_B", "mood-launch browser"),
        ("settings", "<super> KEY_I", "mood-launch settings"), ("studio", "<super> <shift> KEY_I", "mood-launch studio"),
        ("lock", "<super> KEY_L", "mood-lock"), ("shot", "KEY_SYSRQ", "mood-screenshot full"),
        ("shotarea", "<super> <shift> KEY_S", "mood-screenshot area"), ("sysmon", "<ctrl> <shift> KEY_ESC", "mood-launch sysmon"), ("forcestop", "<ctrl> <alt> KEY_ESC", "mood-shell --forcestop"),
        ("osk", "<super> <ctrl> KEY_O", "/usr/lib/mood/runtime/mood-touch --osk toggle"),
        ("mute", "KEY_MUTE", "mood-osd mute"), ("micmute", "KEY_MICMUTE", "mood-osd micmute"),
        ("play", "KEY_PLAYPAUSE", "playerctl play-pause"), ("next", "KEY_NEXTSONG", "playerctl next"), ("prev", "KEY_PREVIOUSSONG", "playerctl previous"),
    ]
    if (s.get("power") or {}).get("button") == "menu":
        binds.append(("powerkey", "KEY_POWER", "mood-shell --toggle power"))
    if gestures:
        binds += [("edge_top", "edge-swipe down 1", "mood-shell --open control"), ("edge_bottom", "edge-swipe up 1", "mood-shell --open launcher"),
                  ("pinch_close", "pinch in 3", "mood-shell --hide")]
    for name, key, cmd in binds:
        add(f"binding_{name} = {key}\ncommand_{name} = {cmd}")
    for name, key, cmd in [("volup", "KEY_VOLUMEUP", "mood-osd volume up"), ("voldown", "KEY_VOLUMEDOWN", "mood-osd volume down"),
                           ("brup", "KEY_BRIGHTNESSUP", "mood-osd brightness up"), ("brdown", "KEY_BRIGHTNESSDOWN", "mood-osd brightness down")]:
        add(f"repeatable_binding_{name} = {key}\ncommand_{name} = {cmd}")
    add("")
    add("[window-rules]")
    add('mood_float = on created if app_id contains "pavucontrol" | app_id contains "blueman" then maximize false')
    add("")
    # wf-config treats an unescaped "#" as the start of a comment, so colours must be written "\#RRGGBBAA"
    L = [re.sub(r"= #([0-9A-Fa-f]{8})\b", r"= \\#\1", line) for line in L]
    user = CFG / "wayfire-user.ini"
    if user.exists():
        add(user.read_text())
    return "\n".join(L) + "\n"


def gtk(s):
    light = s.get("theme") == "light"
    cur = s.get("cursor", {})
    icons = s.get("icons", "Papirus-Dark")
    if icons in ("Papirus", "Papirus-Dark", "Papirus-Light"):
        icons = "Papirus-Light" if light and icons == "Papirus-Dark" else icons
    font = s.get("font", "Inter")
    ini = (f"[Settings]\ngtk-theme-name={'Adwaita' if light else 'Adwaita-dark'}\ngtk-application-prefer-dark-theme={'false' if light else 'true'}\n"
           f"gtk-icon-theme-name={icons}\ngtk-cursor-theme-name={cur.get('theme', 'Bibata-Modern-Classic')}\ngtk-cursor-theme-size={int(cur.get('size', 24))}\n"
           f"gtk-font-name={font} 10.5\ngtk-decoration-layout={'close,maximize,minimize:' if s.get('windows', {}).get('buttons') == 'left' else ':minimize,maximize,close'}\n"
           "gtk-enable-animations=true\n")
    write(CFG / "gtk-3.0/settings.ini", ini)
    write(CFG / "gtk-4.0/settings.ini", ini.replace(f"gtk-theme-name={'Adwaita' if light else 'Adwaita-dark'}\n", ""))
    gs = [("org.gnome.desktop.interface", "color-scheme", "default" if light else "prefer-dark"),
          ("org.gnome.desktop.interface", "gtk-theme", "Adwaita" if light else "Adwaita-dark"),
          ("org.gnome.desktop.interface", "icon-theme", icons),
          ("org.gnome.desktop.interface", "cursor-theme", cur.get("theme", "Bibata-Modern-Classic")),
          ("org.gnome.desktop.interface", "cursor-size", str(int(cur.get("size", 24)))),
          ("org.gnome.desktop.interface", "font-name", f"{font} 10.5"),
          ("org.gnome.desktop.interface", "accent-color", gnome_accent(s.get("accent")))]
    for schema, key, val in gs:
        try:
            subprocess.run(["gsettings", "set", schema, key, val], capture_output=True, timeout=5)
        except (OSError, subprocess.SubprocessError):
            pass


def kitty(s):
    light = s.get("theme") == "light"
    pal = {**(LIGHT if light else DARK), **(s.get("palette") or {})}
    acc = s.get("accent", "#8b5cf6")
    glass = s.get("windows", {}).get("glassApps") or True
    theme = (f"# Generated by moodtop-wm\nbackground {pal['bg']}\nforeground {pal['text']}\ncursor {acc}\nselection_background {acc}\n"
             f"selection_foreground {pal['bg']}\nurl_color {s.get('accent2') or acc}\nactive_tab_background {acc}\nactive_tab_foreground #ffffff\n"
             f"inactive_tab_background {pal['surface-2']}\ninactive_tab_foreground {pal['text-2']}\nbackground_opacity {0.86 if glass else 1.0}\n"
             f"window_padding_width 10\nfont_family {'JetBrains Mono' if s.get('font') not in ('Fira Code', 'Cascadia Code') else s.get('font')}\n")
    if write(CFG / "kitty/moodtop-theme.conf", theme):
        subprocess.run(["pkill", "-USR1", "-u", str(os.getuid()), "-x", "kitty"], capture_output=True)


def mako(s):
    light = s.get("theme") == "light"
    pal = {**(LIGHT if light else DARK), **(s.get("palette") or {})}
    acc = s.get("accent", "#8b5cf6")
    conf = (f"# Generated by moodtop-wm\nfont={s.get('font', 'Inter')} 10.5\nbackground-color={hexa(pal['surface'], 0.94)}\ntext-color={pal['text']}\n"
            f"border-color={hexa(acc, 0.85)}\nborder-size=2\nborder-radius={int(s.get('radius', 16))}\nprogress-color=over {hexa(acc, 0.5)}\n"
            "width=380\nheight=160\nmargin=58,14,14,14\npadding=14,16\nicons=1\nmax-icon-size=40\ndefault-timeout=6000\nlayer=overlay\nanchor=top-right\n"
            "\n[urgency=high]\nborder-color=#fb7185\ndefault-timeout=0\n\n[mode=do-not-disturb]\ninvisible=1\n")
    if write(CFG / "mako/config", conf):
        subprocess.run(["makoctl", "reload"], capture_output=True, timeout=5)


def gtklock(s):
    """Pre-build the lock screen so the first lock is instant (mood-lockscreen rebuilds it on every lock anyway)."""
    subprocess.Popen(["/usr/lib/mood/runtime/mood-lockscreen", "--build"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)


def main():
    s = moodhost.load_settings()
    write(CFG / "wayfire.ini", wayfire_ini(s))
    for fn in (gtk, kitty, mako, gtklock):
        try:
            fn(s)
        except Exception as e:  # never block the session on cosmetic config
            print(f"moodtop-wm: {fn.__name__}: {e}", file=sys.stderr)


if __name__ == "__main__":
    main()
