#!/usr/bin/env python3
"""
D-Bus app indicator/system tray for the Panorama panel.
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public Licence as published by
the Free Software Foundation, either version 3 of the Licence, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public Licence for more details.

You should have received a copy of the GNU General Public Licence
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

from __future__ import annotations
import os
import traceback
import uuid
import locale
from pathlib import Path

import gi
from ctypes import CDLL
CDLL("libdbusmenu-glib.so")
gi.require_version("Dbusmenu", "0.4")

import panorama_panel
from gi.repository import Gtk, GLib, Gio

from . import dbusmenu

module_directory = Path(__file__).resolve().parent
locale.bindtextdomain("panorama-indicator-applet", module_directory / "locale")
_ = lambda x: locale.dgettext("panorama-indicator-applet", x)


class StatusNotifierService:
    """
    <node>
      <interface name="org.kde.StatusNotifierHost">
      </interface>
    </node>
    """

    def __init__(self, applet: IndicatorApplet):
        self.applet = applet
        self.xml = """
        <node>
          <interface name="org.kde.StatusNotifierHost">
          </interface>
        </node>
        """
        self.node_info = Gio.DBusNodeInfo.new_for_xml(self.xml)
        self.iface_info = self.node_info.interfaces[0]

    def export(self, connection: Gio.DBusConnection):
        connection.register_object(
            "/StatusNotifierHost",
            self.iface_info,
            self.handle_method_call,
            None,
            None,
        )

    def handle_method_call(self, conn, sender, obj_path, iface_name, method, params, invocation):
        # It has no methods
        invocation.return_value(None)


class StatusIcon(Gtk.MenuButton):
    def __init__(self, applet: IndicatorApplet, service: str):
        super().__init__(has_frame=False)
        self.applet = applet
        self.service = service
        self.icon = Gtk.Image(icon_name="image-missing", pixel_size=self.applet.icon_size)
        self.set_child(self.icon)
        self.popover_menu = Gtk.PopoverMenu(flags=Gtk.PopoverMenuFlags.NESTED, has_arrow=False, halign=Gtk.Align.START)
        self.set_popover(self.popover_menu)

        connection, _0, path = self.service.rpartition(":")
        self.menu = None

        try:
            self.proxy = Gio.DBusProxy.new_sync(
                self.applet.connection,
                Gio.DBusProxyFlags.NONE,
                None,
                connection,
                path,
                "org.kde.StatusNotifierItem",
                None
            )
            self.applet.item_id += 1
            icon_name = self.proxy.get_cached_property("IconName")
            if icon_name:
                icon_name = icon_name.unpack()
                self.icon.set_from_icon_name(icon_name)
            menu = self.proxy.get_cached_property("Menu")
            if menu:
                self.menu = dbusmenu.CanonicalMenuModel()
                self.menu.connect("menu_changed", lambda x: panorama_panel.add_icons_to_menu(self.popover_menu, Gtk.IconSize.NORMAL))
                self.menu.connect_dbus(connection, menu.get_string(), f"ac{self.applet.item_id}")
                self.insert_action_group(f"ac{self.applet.item_id}", self.menu.actions)
                self.popover_menu.set_menu_model(self.menu.menu)

            # TODO: NewTitle etc.
            self.applet.connection.signal_subscribe(
                connection,
                "org.kde.StatusNotifierItem",
                "NewIcon",
                path,
                None,
                Gio.DBusSignalFlags.NONE,
                self.on_icon_change,
            )
        except Exception as e:
            print(f"Could not get properties from {service}: {e}")
            traceback.print_exc()

    def on_icon_change(self, *args):
        icon_name = self.proxy.call_sync(
                "org.freedesktop.DBus.Properties.Get",
                GLib.Variant("(ss)", ("org.kde.StatusNotifierItem", "IconName")),
                Gio.DBusCallFlags.NONE,
                -1,
                None
        )
        if icon_name:
            self.icon.set_from_icon_name(icon_name.unpack()[0])


class IndicatorApplet(panorama_panel.Applet):
    name = _("Indicators")
    description = _("Show application status icons")

    def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
        super().__init__(orientation=orientation, config=config or {})
        self.items = {}
        self.item_id = 0

        if config is None:
            config = {}

        self.icon_size = config.get("icon_size", 24)

        self.connection = Gio.bus_get_sync(Gio.BusType.SESSION)

        # Make the host name unique
        self.bus_name = f"org.kde.StatusNotifierHost-{os.getpid()}-pan-{uuid.uuid4()}"

        self.service = StatusNotifierService(self)
        self.service.export(self.connection)
        Gio.bus_own_name(
            Gio.BusType.SESSION,
            self.bus_name,
            Gio.BusNameOwnerFlags.NONE,
            None,
            None,
            None,
        )

        try:
            self.watcher = Gio.DBusProxy.new_sync(
                self.connection,
                Gio.DBusProxyFlags.NONE,
                None,
                "org.kde.StatusNotifierWatcher",
                "/StatusNotifierWatcher",
                "org.kde.StatusNotifierWatcher",
                None,
            )
        except Exception as e:
            print("Could not connect to StatusNotifierWatcher:", e)
            self.watcher = None

        if self.watcher:
            try:
                self.watcher.call_sync(
                    "RegisterStatusNotifierHost",
                    GLib.Variant("(s)", (self.bus_name,)),
                    Gio.DBusCallFlags.NONE,
                    -1,
                    None,
                )
            except Exception as e:
                print("Failed to register host:", e)

            # Watch for new items
            self.connection.signal_subscribe(
                None,
                "org.kde.StatusNotifierWatcher",
                "StatusNotifierItemRegistered",
                "/StatusNotifierWatcher",
                None,
                Gio.DBusSignalFlags.NONE,
                self.on_item_registered_signal,
            )
            # Watch for vanishing items
            self.connection.signal_subscribe(
                None,
                "org.kde.StatusNotifierWatcher",
                "StatusNotifierItemUnregistered",
                "/StatusNotifierWatcher",
                None,
                Gio.DBusSignalFlags.NONE,
                self.on_item_unregistered_signal,
            )
            # TODO: watch for things such as icon changes.

    def on_item_registered_signal(self, conn, sender_name, obj_path, iface_name, signal_name, params):
        (service,) = params.unpack()
        self.on_item_registered(service)

    def on_item_unregistered_signal(self, conn, sender_name, obj_path, iface_name, signal_name, params):
        (service,) = params.unpack()
        self.on_item_unregistered(service)

    def on_item_registered(self, service):
        icon = StatusIcon(self, service)
        self.append(icon)
        self.items[service] = icon

    def on_item_unregistered(self, service):
        if service in self.items:
            self.remove(self.items[service])
            del self.items[service]

    def get_config(self):
        return {
            "icon_size": self.icon_size,
        }
