#!/usr/bin/env python3
"""GTK4 GUI for laptop RGB control."""

from __future__ import annotations

import os
import sys

sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))

import gi

gi.require_version("Gtk", "4.0")
gi.require_version("Gdk", "4.0")
from gi.repository import Gdk, GLib, Gtk  # noqa: E402

import rgb_core as rc  # noqa: E402

APP_ID = "com.laserlloyd.rgbcontrol"

PRESETS = [
    ("Red", "#ff0000"),
    ("Orange", "#ff7a00"),
    ("Yellow", "#ffd400"),
    ("Green", "#00ff44"),
    ("Cyan", "#00e1ff"),
    ("Blue", "#1e5bff"),
    ("Purple", "#a000ff"),
    ("Pink", "#ff30c0"),
    ("White", "#ffffff"),
]


def _rgba(hex_: str) -> Gdk.RGBA:
    r, g, b = rc.parse_hex(hex_)
    rgba = Gdk.RGBA()
    rgba.red = r / 255.0
    rgba.green = g / 255.0
    rgba.blue = b / 255.0
    rgba.alpha = 1.0
    return rgba


def _rgba_to_ints(rgba: Gdk.RGBA) -> tuple[int, int, int]:
    return (
        int(round(rgba.red * 255)),
        int(round(rgba.green * 255)),
        int(round(rgba.blue * 255)),
    )


class RGBWindow(Gtk.ApplicationWindow):
    def __init__(self, app: Gtk.Application):
        super().__init__(application=app, title="RGB Control")
        self.set_default_size(480, 520)

        self.zones = rc.discover_zones()
        self.zone_ui: dict[str, dict] = {}
        self._status_timeout: int | None = None
        self._brightness_timeout: dict[str, int] = {}
        # Guards the color-mirror loop: when we copy a color into the other
        # zones' buttons for display, their notify::rgba must not re-apply to
        # hardware (the originating change already wrote every in-scope zone).
        self._suppress_mirror = False

        root = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL,
            spacing=14,
            margin_top=16, margin_bottom=16, margin_start=16, margin_end=16,
        )
        self.set_child(root)

        if not self.zones:
            root.append(Gtk.Label(label="No RGB zones detected on this system."))
            return

        header = Gtk.Label(xalign=0.0)
        backends = ", ".join(sorted({z.backend for z in self.zones}))
        header.set_markup(
            "<b>RGB Control</b>\n"
            f"<small>{len(self.zones)} zone(s) detected via {backends}</small>"
        )
        root.append(header)

        # Sync toggle
        sync_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        sync_label = Gtk.Label(label="Apply changes to all zones", xalign=0.0, hexpand=True)
        self.sync_switch = Gtk.Switch(active=True, valign=Gtk.Align.CENTER)
        sync_row.append(sync_label)
        sync_row.append(self.sync_switch)
        root.append(sync_row)

        # Preset row (applies to selected scope)
        preset_label = Gtk.Label(label="Presets", xalign=0.0)
        preset_label.add_css_class("heading")
        root.append(preset_label)
        preset_flow = Gtk.FlowBox(
            selection_mode=Gtk.SelectionMode.NONE,
            max_children_per_line=9,
            homogeneous=True,
            column_spacing=6,
            row_spacing=6,
        )
        for name, hex_ in PRESETS:
            btn = Gtk.Button()
            btn.set_tooltip_text(f"{name} ({hex_})")
            swatch = Gtk.Box()
            swatch.set_size_request(36, 28)
            css = Gtk.CssProvider()
            css.load_from_data(
                f"box {{ background-color: {hex_}; border-radius: 6px; border: 1px solid rgba(0,0,0,0.2); }}".encode()
            )
            swatch.get_style_context().add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
            btn.set_child(swatch)
            btn.connect("clicked", self._on_preset, hex_)
            preset_flow.append(btn)
        root.append(preset_flow)

        root.append(Gtk.Separator())

        # Per-zone controls
        scroll = Gtk.ScrolledWindow(hexpand=True, vexpand=True)
        zone_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        scroll.set_child(zone_box)
        root.append(scroll)

        for z in self.zones:
            zone_box.append(self._build_zone_frame(z))

        # Footer actions
        footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        off_btn = Gtk.Button(label="Lights off")
        off_btn.add_css_class("destructive-action")
        off_btn.connect("clicked", lambda *_: self._all_brightness(0))
        on_btn = Gtk.Button(label="Lights on (max)")
        on_btn.connect("clicked", self._all_max)
        footer.append(off_btn)
        footer.append(on_btn)
        root.append(footer)

        self.status = Gtk.Label(xalign=0.0)
        self.status.add_css_class("dim-label")
        root.append(self.status)

    def _build_zone_frame(self, z: rc.Zone) -> Gtk.Widget:
        frame = Gtk.Frame(label=z.name)
        box = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL,
            spacing=8,
            margin_top=10, margin_bottom=10, margin_start=10, margin_end=10,
        )
        frame.set_child(box)

        # Color row
        color_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        color_row.append(Gtk.Label(label="Color", xalign=0.0, width_chars=11))
        color_btn = Gtk.ColorDialogButton(dialog=Gtk.ColorDialog(with_alpha=False))
        try:
            r, g, b = rc.read_color(z)
            color_btn.set_rgba(_rgba(f"#{r:02x}{g:02x}{b:02x}"))
        except Exception:
            pass
        color_btn.connect("notify::rgba", self._on_color_changed, z.id)
        color_row.append(color_btn)
        box.append(color_row)

        # Brightness row
        b_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        b_row.append(Gtk.Label(label="Brightness", xalign=0.0, width_chars=11))
        adj = Gtk.Adjustment(
            lower=0, upper=z.max_brightness, step_increment=1, page_increment=1,
            value=rc.read_brightness(z),
        )
        scale = Gtk.Scale(adjustment=adj, draw_value=True, digits=0, hexpand=True)
        scale.set_value_pos(Gtk.PositionType.RIGHT)
        for i in range(z.max_brightness + 1):
            scale.add_mark(i, Gtk.PositionType.BOTTOM, None)
        scale.connect("value-changed", self._on_brightness_changed, z.id)
        b_row.append(scale)
        box.append(b_row)

        self.zone_ui[z.id] = {"color_btn": color_btn, "scale": scale}
        return frame

    # --- Event handlers ---

    def _on_preset(self, _btn: Gtk.Button, hex_: str) -> None:
        r, g, b = rc.parse_hex(hex_)
        self._apply_color_scope(None, r, g, b)
        # Update any color buttons in scope so UI reflects change
        rgba = _rgba(hex_)
        scope = self._scope_ids(None)
        for zid in scope:
            self.zone_ui[zid]["color_btn"].set_rgba(rgba)

    def _on_color_changed(self, btn: Gtk.ColorDialogButton, _pspec, origin_zid: str) -> None:
        if self._suppress_mirror:
            return
        r, g, b = _rgba_to_ints(btn.get_rgba())
        self._apply_color_scope(origin_zid, r, g, b)
        if self.sync_switch.get_active():
            # Mirror the color into the other buttons for display only — the
            # apply above already wrote every in-scope zone to hardware.
            self._mirror_color(origin_zid, btn.get_rgba())

    def _mirror_color(self, origin: str, rgba: Gdk.RGBA) -> None:
        self._suppress_mirror = True
        try:
            for zid, ui in self.zone_ui.items():
                if zid == origin:
                    continue
                btn = ui["color_btn"]
                cur = btn.get_rgba()
                if (cur.red, cur.green, cur.blue) == (rgba.red, rgba.green, rgba.blue):
                    continue
                btn.set_rgba(rgba)
        finally:
            self._suppress_mirror = False

    def _on_brightness_changed(self, scale: Gtk.Scale, origin_zid: str) -> None:
        level = int(scale.get_value())
        # Debounce so drags don't hammer sysfs
        prev = self._brightness_timeout.get(origin_zid)
        if prev is not None:
            GLib.source_remove(prev)

        def apply() -> bool:
            self._brightness_timeout.pop(origin_zid, None)
            self._apply_brightness_scope(origin_zid, level)
            if self.sync_switch.get_active():
                for zid, ui in self.zone_ui.items():
                    if zid == origin_zid:
                        continue
                    s = ui["scale"]
                    z = self._zone(zid)
                    target = min(level, z.max_brightness)
                    if int(s.get_value()) != target:
                        s.set_value(target)
            return False

        self._brightness_timeout[origin_zid] = GLib.timeout_add(60, apply)

    def _all_brightness(self, level: int) -> None:
        try:
            for z in self.zones:
                rc.set_brightness(z, min(level, z.max_brightness))
                self.zone_ui[z.id]["scale"].set_value(min(level, z.max_brightness))
            self._set_status(f"all zones → brightness {level}")
        except rc.RGBError as e:
            self._set_status(str(e), error=True)

    def _all_max(self, _btn: Gtk.Button) -> None:
        self._all_brightness(max(z.max_brightness for z in self.zones))

    # --- Scope helpers ---

    def _scope_ids(self, origin: str | None) -> list[str]:
        if self.sync_switch.get_active() or origin is None:
            return [z.id for z in self.zones]
        return [origin]

    def _zone(self, zid: str) -> rc.Zone:
        return next(z for z in self.zones if z.id == zid)

    def _apply_color_scope(self, origin: str | None, r: int, g: int, b: int) -> None:
        try:
            targets = self._scope_ids(origin)
            for zid in targets:
                rc.set_color(self._zone(zid), r, g, b)
            self._set_status(f"#{r:02x}{g:02x}{b:02x} → {len(targets)} zone(s)")
        except (rc.RGBError, ValueError) as e:
            self._set_status(str(e), error=True)

    def _apply_brightness_scope(self, origin: str | None, level: int) -> None:
        try:
            targets = self._scope_ids(origin)
            for zid in targets:
                z = self._zone(zid)
                rc.set_brightness(z, min(level, z.max_brightness))
            self._set_status(f"brightness {level} → {len(targets)} zone(s)")
        except (rc.RGBError, ValueError) as e:
            self._set_status(str(e), error=True)

    def _set_status(self, msg: str, error: bool = False) -> None:
        self.status.set_label(msg)
        if error:
            self.status.remove_css_class("dim-label")
            self.status.add_css_class("error")
        else:
            self.status.remove_css_class("error")
            self.status.add_css_class("dim-label")
        if self._status_timeout is not None:
            GLib.source_remove(self._status_timeout)

        def clear() -> bool:
            self.status.set_label("")
            self._status_timeout = None
            return False

        self._status_timeout = GLib.timeout_add_seconds(4, clear)


class RGBApp(Gtk.Application):
    def __init__(self):
        super().__init__(application_id=APP_ID)

    def do_activate(self):
        win = RGBWindow(self)
        win.present()


def main() -> None:
    app = RGBApp()
    sys.exit(app.run(sys.argv))


if __name__ == "__main__":
    main()
