#!/usr/bin/python3
"""mood-lockscreen — build the Moodtop lock screen for the current theme, then lock.

Every lock regenerates ~/.config/gtklock/{config.ini,style.css,layout.ui} from settings.json and
a blurred backdrop in $XDG_RUNTIME_DIR (the wallpaper image, or a snapshot of the desktop for
live wallpapers), so the lock screen always matches the theme. gtklock does the actual locking
(ext-session-lock: if it ever crashes the session stays locked). If the themed lock can't start,
a plain gtklock is used so the screen still locks.
"""
import base64
import html
import os
import pwd
import subprocess
import sys
import time
from pathlib import Path

sys.path.insert(0, "/usr/lib/mood/runtime")

HOME = Path.home()
CFG = HOME / ".config/gtklock"
RUN = Path(os.environ.get("XDG_RUNTIME_DIR") or f"/run/user/{os.getuid()}")
BG = RUN / "mood-lock-bg.png"
AVATAR = RUN / "mood-lock-avatar.png"
MODDIR = Path("/usr/lib/x86_64-linux-gnu/gtklock")


def load_settings():
    try:
        import moodhost
        return moodhost.load_settings()
    except Exception:
        return {}


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 rgba(h, a):
    r, g, b = rgb(h)
    return f"rgba({r},{g},{b},{a})"


def screen_size():
    try:
        import json
        outs = json.loads(subprocess.run(["wlr-randr", "--json"], capture_output=True, text=True, timeout=3).stdout or "[]")
        for o in outs:
            if o.get("enabled"):
                m = next((m for m in o.get("modes", []) if m.get("current")), None)
                if m:
                    sc = o.get("scale") or 1
                    return o["name"], int(m["width"] / sc), int(m["height"] / sc)
    except Exception:
        pass
    return None, 1920, 1080


# ------------------------------------------------------------------ backdrop
def blur_to(pb, w, h, out):
    """Cover-crop to the screen, then a downscale/upscale pyramid: cheap, smooth, gaussian-looking blur."""
    from gi.repository import GdkPixbuf
    bw, bh = pb.get_width(), pb.get_height()
    s = max(w / bw, h / bh)
    cw, ch = min(bw, round(w / s)), min(bh, round(h / s))
    pb = pb.new_subpixbuf((bw - cw) // 2, (bh - ch) // 2, cw, ch)
    sizes = []
    cw, ch = w, h
    while cw > 48:
        cw, ch = max(1, cw // 2), max(1, ch // 2)
        sizes.append((cw, ch))
    cur = pb.scale_simple(*sizes[0], GdkPixbuf.InterpType.HYPER)
    for sz in sizes[1:]:
        cur = cur.scale_simple(*sz, GdkPixbuf.InterpType.BILINEAR)
    for sz in reversed(sizes[:-1]):
        cur = cur.scale_simple(*sz, GdkPixbuf.InterpType.BILINEAR)
    cur.scale_simple(w, h, GdkPixbuf.InterpType.BILINEAR).savev(str(out), "png", ["compression"], ["1"])


def gradient_to(s, w, h, out):
    import cairo
    acc, acc2 = s.get("accent", "#8b5cf6"), s.get("accent2") or s.get("accent", "#8b5cf6")
    surf = cairo.ImageSurface(cairo.FORMAT_RGB24, w, h)
    cr = cairo.Context(surf)
    cr.set_source_rgb(0.03, 0.03, 0.06)
    cr.paint()
    for (x, y, r, col, a) in ((0.22, 0.2, 0.75, acc, 0.9), (0.85, 0.9, 0.8, acc2, 0.85), (0.7, 0.15, 0.45, acc2, 0.35)):
        g = cairo.RadialGradient(w * x, h * y, 0, w * x, h * y, max(w, h) * r)
        cr_rgb = [c / 255 for c in rgb(col)]
        g.add_color_stop_rgba(0, *cr_rgb, a)
        g.add_color_stop_rgba(1, *cr_rgb, 0)
        cr.set_source(g)
        cr.paint()
    surf.write_to_png(str(out))


def make_backdrop(s):
    import gi
    gi.require_version("GdkPixbuf", "2.0")
    from gi.repository import GdkPixbuf
    out, w, h = screen_size()
    wp = s.get("wallpaper") or {}
    mode = (s.get("lock") or {}).get("background", "auto")
    tmp = BG.with_suffix(".tmp.png")
    try:
        if mode in ("auto", "wallpaper") and wp.get("type") == "image" and wp.get("value"):
            v = wp["value"]
            if v.startswith("data:"):
                loader = GdkPixbuf.PixbufLoader()
                loader.write(base64.b64decode(v.split(",", 1)[1]))
                loader.close()
                pb = loader.get_pixbuf()
            else:
                pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(v, 1280, 1280, True)
            blur_to(pb, w, h, tmp)
        elif mode in ("auto", "desktop"):
            shot = RUN / "mood-lock-shot.png"
            cmd = ["grim", "-s", "0.25"] + (["-o", out] if out else []) + [str(shot)]
            subprocess.run(cmd, check=True, capture_output=True, timeout=4)
            blur_to(GdkPixbuf.Pixbuf.new_from_file(str(shot)), w, h, tmp)
            shot.unlink(missing_ok=True)
        else:
            gradient_to(s, w, h, tmp)
    except Exception as e:
        print(f"mood-lockscreen: backdrop: {e}", file=sys.stderr)
        gradient_to(s, w, h, tmp)
    os.chmod(tmp, 0o600)
    tmp.replace(BG)


# ------------------------------------------------------------------ avatar
def full_name():
    pw = pwd.getpwuid(os.getuid())
    return (pw.pw_gecos.split(",")[0].strip() or pw.pw_name.capitalize())


def make_avatar(s, name):
    import cairo
    face = next((p for p in (HOME / ".face", HOME / ".face.icon") if p.is_file()), None)
    size = 92
    surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size)
    cr = cairo.Context(surf)
    cr.arc(size / 2, size / 2, size / 2 - 3, 0, 6.2832)
    cr.clip()
    drawn = False
    if face:
        try:
            import gi
            gi.require_version("Gdk", "3.0")
            from gi.repository import Gdk, GdkPixbuf
            pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(str(face), size, size, False)
            Gdk.cairo_set_source_pixbuf(cr, pb, 0, 0)
            cr.paint()
            drawn = True
        except Exception:
            pass
    if not drawn:
        a, b = rgb(s.get("accent", "#8b5cf6")), rgb(s.get("accent2") or s.get("accent", "#8b5cf6"))
        g = cairo.LinearGradient(0, 0, size, size)
        g.add_color_stop_rgb(0, *[c / 255 for c in a])
        g.add_color_stop_rgb(1, *[c / 255 for c in b])
        cr.set_source(g)
        cr.paint()
        cr.select_font_face(s.get("font") or "Inter", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
        cr.set_font_size(40)
        letter = (name or "S")[0].upper()
        ext = cr.text_extents(letter)
        cr.move_to(size / 2 - ext.x_bearing - ext.width / 2, size / 2 - ext.y_bearing - ext.height / 2)
        cr.set_source_rgb(1, 1, 1)
        cr.show_text(letter)
    cr.reset_clip()
    cr.new_path()
    cr.arc(size / 2, size / 2, size / 2 - 1.5, 0, 6.2832)
    cr.set_source_rgba(1, 1, 1, 0.55)
    cr.set_line_width(3)
    cr.stroke()
    surf.write_to_png(str(AVATAR))
    os.chmod(AVATAR, 0o600)


# ------------------------------------------------------------------ gtklock files
LAYOUT = """<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <!-- window-box is a scroller on purpose: the lock surface must never ask for more than the
       screen (ext-session-lock kills a client whose surface outgrows the output). -->
  <object class="GtkScrolledWindow" id="window-box">
    <property name="name">window-box</property>
    <property name="hexpand">1</property>
    <property name="vexpand">1</property>
    <property name="hscrollbar-policy">never</property>
    <property name="vscrollbar-policy">external</property>
    <property name="min-content-height">0</property>
    <property name="min-content-width">0</property>
    <child>
      <object class="GtkViewport">
        <property name="shadow-type">none</property>
        <child>
  <object class="GtkBox" id="content-box">
    <property name="name">content-box</property>
    <property name="orientation">vertical</property>
    <property name="hexpand">1</property>
    <property name="vexpand">1</property>
    <child>
      <object class="GtkBox" id="info-box">
        <property name="name">info-box</property>
        <property name="halign">center</property>
        <property name="valign">start</property>
        <property name="orientation">vertical</property>
        <property name="margin-top">52</property>
        <child>
          <object class="GtkBox" id="lock-chip">
            <property name="name">lock-chip</property>
            <property name="halign">center</property>
            <property name="spacing">7</property>
            <child>
              <object class="GtkImage">
                <property name="icon-name">changes-prevent-symbolic</property>
                <property name="pixel-size">14</property>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="lock-text">
                <property name="name">lock-text</property>
                <property name="label">Locked</property>
              </object>
            </child>
          </object>
        </child>
        <child>
          <object class="GtkBox" id="time-box">
            <property name="name">time-box</property>
            <property name="halign">center</property>
            <property name="orientation">vertical</property>
            <child>
              <object class="GtkLabel" id="clock-label">
                <property name="name">clock-label</property>
                <property name="halign">center</property>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="date-label">
                <property name="name">date-label</property>
                <property name="halign">center</property>
              </object>
            </child>
          </object>
        </child>
      </object>
    </child>
    <child>
      <object class="GtkBox">
        <property name="vexpand">1</property>
      </object>
    </child>
    <child>
      <object class="GtkRevealer" id="body-revealer">
        <property name="transition-type">crossfade</property>
        <property name="transition-duration">260</property>
        <property name="reveal-child">0</property>
        <property name="halign">center</property>
        <property name="margin-bottom">100</property>
        <child>
          <object class="GtkBox" id="body-card">
            <property name="name">body-card</property>
            <property name="orientation">vertical</property>
            <property name="spacing">10</property>
            <child>
              <object class="GtkImage" id="avatar">
                <property name="name">avatar</property>
                <property name="file">@AVATAR@</property>
                <property name="halign">center</property>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="user-name">
                <property name="name">user-name</property>
                <property name="label">@NAME@</property>
                <property name="halign">center</property>
              </object>
            </child>
            <child>
              <object class="GtkGrid" id="body-grid">
                <property name="name">body-grid</property>
                <property name="row-spacing">10</property>
                <property name="column-spacing">10</property>
                <property name="halign">center</property>
                <child>
                  <object class="GtkLabel" id="input-label">
                    <property name="label"></property>
                    <property name="no-show-all">1</property>
                    <property name="visible">0</property>
                  </object>
                  <packing>
                    <property name="left-attach">2</property>
                    <property name="top-attach">0</property>
                  </packing>
                </child>
                <child>
                  <object class="GtkEntry" id="input-field">
                    <property name="name">input-field</property>
                    <property name="width-request">320</property>
                    <property name="visibility">0</property>
                    <property name="caps-lock-warning">0</property>
                    <property name="input-purpose">password</property>
                    <property name="placeholder-text">Password</property>
                    <property name="secondary-icon-name">view-reveal-symbolic</property>
                    <signal name="icon-release" handler="window_pw_toggle_vis"/>
                    <signal name="activate" handler="window_pw_check"/>
                  </object>
                  <packing>
                    <property name="left-attach">0</property>
                    <property name="top-attach">0</property>
                  </packing>
                </child>
                <child>
                  <object class="GtkButton" id="unlock-button">
                    <property name="name">unlock-button</property>
                    <property name="tooltip-text">Unlock</property>
                    <property name="image">unlock-icon</property>
                    <property name="always-show-image">1</property>
                    <signal name="clicked" handler="window_pw_check"/>
                  </object>
                  <packing>
                    <property name="left-attach">1</property>
                    <property name="top-attach">0</property>
                  </packing>
                </child>
                <child>
                  <object class="GtkRevealer" id="message-revealer">
                    <property name="transition-type">none</property>
                    <property name="no-show-all">1</property>
                    <child>
                      <object class="GtkScrolledWindow" id="message-scrolled-window">
                        <property name="hscrollbar-policy">never</property>
                        <property name="max-content-height">160</property>
                        <property name="propagate-natural-height">1</property>
                        <child>
                          <object class="GtkViewport">
                            <child>
                              <object class="GtkBox" id="message-box">
                                <property name="orientation">vertical</property>
                                <property name="homogeneous">1</property>
                              </object>
                            </child>
                          </object>
                        </child>
                      </object>
                    </child>
                  </object>
                  <packing>
                    <property name="left-attach">0</property>
                    <property name="top-attach">1</property>
                    <property name="width">2</property>
                  </packing>
                </child>
                <child>
                  <object class="GtkBox">
                    <property name="halign">center</property>
                    <property name="spacing">10</property>
                    <child>
                      <object class="GtkLabel" id="warning-label">
                        <property name="name">warning-label</property>
                      </object>
                    </child>
                    <child>
                      <object class="GtkLabel" id="error-label">
                        <property name="name">error-label</property>
                      </object>
                    </child>
                  </object>
                  <packing>
                    <property name="left-attach">0</property>
                    <property name="top-attach">2</property>
                    <property name="width">2</property>
                  </packing>
                </child>
              </object>
            </child>
          </object>
        </child>
      </object>
    </child>
  </object>
        </child>
      </object>
    </child>
  </object>
  <object class="GtkImage" id="unlock-icon">
    <property name="icon-name">go-next-symbolic</property>
    <property name="pixel-size">20</property>
  </object>
</interface>
"""


def css(s):
    acc, acc2 = s.get("accent", "#8b5cf6"), s.get("accent2") or s.get("accent", "#8b5cf6")
    font = (s.get("font") or "Inter").replace('"', "")
    bgurl = "file://" + str(BG)
    return f"""/* Generated by mood-lockscreen on every lock — edit the theme in Theme Studio instead. */
* {{ font-family: "{font}", "Inter", sans-serif; }}
window {{ background: #07070d url("{bgurl}") center / cover no-repeat; }}
#window-box, #window-box viewport, #content-box {{ background: transparent; border: none; }}
#window-box {{ background-image: linear-gradient(to bottom, rgba(5,6,12,0.30), rgba(5,6,12,0.06) 38%, rgba(5,6,12,0.10) 60%, rgba(5,6,12,0.62)); }}
label {{ color: #f4f5fb; }}
#lock-chip {{ padding: 6px 14px; border-radius: 999px; background: rgba(255,255,255,0.10); border: 1px solid rgba(255,255,255,0.14); margin-bottom: 10px; color: rgba(255,255,255,0.85); }}
#lock-chip image {{ color: rgba(255,255,255,0.85); }}
#lock-text {{ font-size: 13px; font-weight: 700; letter-spacing: 1px; color: rgba(255,255,255,0.85); }}
#clock-label {{ font-size: 124px; font-weight: 300; letter-spacing: -3px; color: #ffffff; text-shadow: 0 6px 40px rgba(0,0,0,0.35); margin-bottom: -6px; }}
#date-label {{ font-size: 23px; font-weight: 600; color: rgba(255,255,255,0.90); text-shadow: 0 2px 16px rgba(0,0,0,0.40); }}
#body-card {{ padding: 20px 28px 16px 28px; border-radius: 30px; background-color: rgba(14,15,24,0.46); border: 1px solid rgba(255,255,255,0.13); }}
#user-name {{ font-size: 21px; font-weight: 800; color: #ffffff; margin-bottom: 4px; }}
#input-field {{ min-height: 48px; padding: 0 20px; border-radius: 999px; font-size: 16px; color: #ffffff; caret-color: {acc};
  background-color: rgba(255,255,255,0.12); background-image: none; border: 1px solid rgba(255,255,255,0.22); box-shadow: none; outline: none; }}
#input-field:focus {{ border-color: {acc}; box-shadow: 0 0 0 4px {rgba(acc, 0.32)}; background-color: rgba(255,255,255,0.16); }}
#input-field image {{ color: rgba(255,255,255,0.7); }}
#input-field selection {{ background-color: {acc}; }}
#unlock-button {{ min-width: 50px; min-height: 50px; padding: 0; border-radius: 999px; border: none; color: #ffffff; background-color: {acc};
  background-image: linear-gradient(135deg, {acc}, {acc2}); box-shadow: 0 10px 30px {rgba(acc, 0.45)}; }}
#unlock-button:hover {{ box-shadow: 0 12px 36px {rgba(acc, 0.65)}; }}
#unlock-button:disabled {{ opacity: 0.55; }}
#unlock-button image {{ color: #ffffff; }}
#error-label {{ color: #fecdd3; font-weight: 700; }}
#warning-label {{ color: #fde68a; font-weight: 700; }}
#message-box label {{ color: rgba(255,255,255,0.8); }}
#face-label {{ color: rgba(255,255,255,0.78); font-size: 13px; font-weight: 600; }}
#powerbar-box {{ margin-bottom: 26px; }}
#powerbar-box button {{ min-height: 42px; padding: 0 18px; margin: 0 6px; border-radius: 999px; color: #ffffff; border: 1px solid rgba(255,255,255,0.16);
  background-color: rgba(14,15,24,0.46); background-image: none; box-shadow: none; }}
#powerbar-box button:hover {{ background-color: rgba(255,255,255,0.20); }}
#powerbar-box button label, #powerbar-box button image {{ color: #ffffff; font-weight: 700; }}
#powerbar-box button image {{ margin-right: 7px; }}
#poweroff-button:hover {{ background-color: rgba(239,68,68,0.55); border-color: rgba(239,68,68,0.8); }}
#playerctl-box {{ margin: 28px; padding: 12px 16px 12px 12px; border-radius: 22px; background-color: rgba(14,15,24,0.46); border: 1px solid rgba(255,255,255,0.13); }}
#playerctl-box button {{ min-width: 36px; min-height: 36px; padding: 0; border-radius: 999px; border: none; background-image: none; background-color: rgba(255,255,255,0.12); color: #fff; box-shadow: none; }}
#play-pause-button {{ background-image: linear-gradient(135deg, {acc}, {acc2}); }}
#title-label {{ font-weight: 800; font-size: 14px; }}
#artist-label, #album-label {{ color: rgba(255,255,255,0.72); font-size: 12px; }}
"""


def face_state():
    """(use face module, auto-scan) — enrolled, enabled for the lock screen and a camera is plugged in."""
    try:
        import glob
        import moodface
        c = moodface.config()
        on = bool(c.get("enabled") and c.get("lock") and moodface.enrolment(pwd.getpwuid(os.getuid()).pw_name) and glob.glob("/dev/video*"))
        return on, on and bool(c.get("lockAuto", True))
    except Exception:
        return False, False


def config(s, modules, face_auto=False):
    lock = s.get("lock") or {}
    h24 = s.get("clock24", True) is not False
    lines = ["[main]", f"style={CFG / 'style.css'}", f"layout={CFG / 'layout.ui'}",
             "time-format=" + ("%H:%M" if h24 else "%-I:%M"), "date-format=%A %-d %B",
             "start-hidden=true", "idle-hide=true", f"idle-timeout={int(lock.get('idleHide', 15))}"]
    if modules:
        lines.append("modules=" + ";".join(modules))
    lines += ["", "[playerctl]", "position=top-left", "art-size=56", "show-hidden=true",
              "", "[powerbar]", "show-labels=true", "suspend-command=systemctl suspend",
              "reboot-command=systemctl reboot", "poweroff-command=systemctl poweroff",
              "", "[face]", f"auto-scan={'true' if face_auto else 'false'}", "grace=3", ""]
    return "\n".join(lines)


def write(path, text):
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(text)
    tmp.replace(path)


def build(backdrop=True):
    s = load_settings()
    name = full_name()
    t = time.monotonic()
    if backdrop or not BG.exists():
        make_backdrop(s)
    make_avatar(s, name)
    face, face_auto = face_state()
    mods = [str(MODDIR / f"{m}-module.so") for m in ("playerctl", "powerbar", "face")
            if (MODDIR / f"{m}-module.so").is_file() and ((s.get("lock") or {}).get("showMedia", True) or m != "playerctl") and (face or m != "face")]
    write(CFG / "layout.ui", LAYOUT.replace("@AVATAR@", html.escape(str(AVATAR))).replace("@NAME@", html.escape(name)))
    write(CFG / "style.css", css(s))
    write(CFG / "config.ini", config(s, mods, face_auto))
    print(f"mood-lockscreen: built in {time.monotonic() - t:.2f}s", file=sys.stderr)


def running():
    return subprocess.run(["pgrep", "-u", str(os.getuid()), "-x", "gtklock"], capture_output=True).returncode == 0


def main():
    if "--build" in sys.argv:
        build(backdrop=False)
        return 0
    if running():
        return 0
    try:
        build()
        if subprocess.run(["gtklock", "-d", "-c", str(CFG / "config.ini")]).returncode == 0:
            return 0
    except Exception as e:
        print(f"mood-lockscreen: {e}", file=sys.stderr)
    time.sleep(0.5)
    if running():  # themed lock came up, just slower than gtklock's 1 s handshake
        return 0
    return subprocess.run(["gtklock", "-d", "-c", "/dev/null"]).returncode


if __name__ == "__main__":
    sys.exit(main())
