#!/usr/bin/env python3
"""Tray icon showing JBL Quantum headset battery, coloured green (full) to red (low), grey if disconnected."""
import argparse
import fcntl
import os
import signal
import sys
import threading

import gi

gi.require_version("Gtk", "3.0")
gi.require_version("AyatanaAppIndicator3", "0.1")
from gi.repository import AyatanaAppIndicator3 as AppIndicator, GdkPixbuf, GLib, Gtk  # noqa: E402

sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
from icon import headset_svg  # noqa: E402
from jbl_battery import __version__, read_first  # noqa: E402

APP_ID = "io.github.runawaycoin.HeadsetBatteryIndicator"
ICON_DIR = os.path.join(GLib.get_user_runtime_dir(), APP_ID)
HOMEPAGE = "https://digial.co.uk/runawaycoin/headset-battery-indicator"


def icon_name(level, connected=True):
    """Write the SVG for this level (once) and return its name; unique names force the shell to refresh."""
    name = "headset-none" if level is None else f"headset-{level}"
    if level is not None and not connected:
        name += "-off"
    path = os.path.join(ICON_DIR, name + ".svg")
    if not os.path.exists(path):
        with open(path, "w") as f:
            f.write(headset_svg(level, connected))
    return name


class Indicator:
    def __init__(self, interval):
        self.interval = interval
        self.busy = False
        os.makedirs(ICON_DIR, exist_ok=True)

        self.ind = AppIndicator.Indicator.new(
            APP_ID, icon_name(None), AppIndicator.IndicatorCategory.HARDWARE)
        self.ind.set_icon_theme_path(ICON_DIR)
        self.ind.set_status(AppIndicator.IndicatorStatus.ACTIVE)
        self.ind.set_title("Headset battery")

        menu = Gtk.Menu()
        self.level_item = Gtk.MenuItem(label="Headset: checking…")
        self.level_item.set_sensitive(False)
        refresh = Gtk.MenuItem(label="Refresh now")
        refresh.connect("activate", lambda *_: self.refresh())
        about = Gtk.MenuItem(label="About")
        about.connect("activate", lambda *_: self.show_about())
        quit_ = Gtk.MenuItem(label="Quit")
        quit_.connect("activate", lambda *_: Gtk.main_quit())
        for item in (self.level_item, Gtk.SeparatorMenuItem(), refresh, about, quit_):
            menu.append(item)
        menu.show_all()
        self.ind.set_menu(menu)

        self.refresh()
        GLib.timeout_add_seconds(interval, self.refresh)

    def show_about(self):
        dialog = Gtk.AboutDialog(
            program_name="Headset Battery Indicator",
            version=__version__,
            comments="runawaycoin",
            website=HOMEPAGE,
            website_label=HOMEPAGE,
            logo_icon_name=APP_ID,
        )
        # Running from a source checkout the icon isn't in the theme; use the repo copy
        logo = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data", APP_ID + ".svg")
        if not Gtk.IconTheme.get_default().has_icon(APP_ID) and os.path.exists(logo):
            dialog.set_logo(GdkPixbuf.Pixbuf.new_from_file_at_size(logo, 96, 96))
        dialog.run()
        dialog.destroy()

    def refresh(self):
        # HID read can block for a few seconds; keep it off the GTK main loop
        if not self.busy:
            self.busy = True
            threading.Thread(target=self._read, daemon=True).start()
        return True

    def _read(self):
        try:
            result = read_first()
            text = None
        except PermissionError:
            result, text = None, "No permission – install udev rule"
        except OSError as e:
            result, text = None, f"Error: {e.strerror}"
        GLib.idle_add(self._update, result, text)

    def _update(self, result, text):
        self.busy = False
        level, connected = None, False
        if result is None:
            label = text or "Headset dongle not found"
        else:
            name, (level, connected) = result
            if connected and level is not None:
                label = f"{name}: {level}%"
            elif level is not None:
                label = f"{name}: not connected (last {level}%)"
            else:
                label = f"{name}: not connected"
        self.ind.set_icon_full(icon_name(level, connected), label)
        self.level_item.set_label(label)
        return False


def already_running():
    """Hold a lock for the process lifetime so autostart plus a manual launch gives one icon."""
    global _lock
    os.makedirs(ICON_DIR, exist_ok=True)
    _lock = open(os.path.join(ICON_DIR, "lock"), "w")
    try:
        fcntl.flock(_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except BlockingIOError:
        return True
    return False


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--interval", type=int, default=60, help="poll interval in seconds (default 60)")
    args = ap.parse_args()
    if already_running():
        print("headset-battery-indicator is already running.", file=sys.stderr)
        return
    Indicator(args.interval)
    signal.signal(signal.SIGINT, signal.SIG_DFL)  # let Ctrl+C kill the GTK loop
    Gtk.main()


if __name__ == "__main__":
    main()
