"""
Panorama panel: traditional desktop panel using Wayland layer-shell
protocol.
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 sys
import importlib
import time
import traceback
import faulthandler
import typing
import locale
from itertools import accumulate, chain
from pathlib import Path
import ruamel.yaml as yaml
from pywayland.client import Display, EventQueue
from pywayland.protocol.wayland.wl_output import WlOutputProxy
from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlOutput, WlShm
from pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1 import (
    ZwlrForeignToplevelManagerV1,
    ZwlrForeignToplevelHandleV1
)
from pywayland.protocol.wlr_screencopy_unstable_v1 import (
    ZwlrScreencopyManagerV1,
    ZwlrScreencopyFrameV1
)

faulthandler.enable()

os.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0"

from ctypes import CDLL
CDLL('libgtk4-layer-shell.so')

import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Gtk4LayerShell", "1.0")

from gi.repository import Gtk, GLib, Gtk4LayerShell, Gdk, Gio, GObject
from gi.events import GLibEventLoopPolicy

import ctypes
from cffi import FFI
ffi = FFI()
ffi.cdef("""
void * gdk_wayland_display_get_wl_display (void * display);
void * gdk_wayland_surface_get_wl_surface (void * surface);
void * gdk_wayland_monitor_get_wl_output (void * monitor);
""")
gtk = ffi.dlopen("libgtk-4.so.1")

sys.path.insert(0, str((Path(__file__).parent / "shared").resolve()))

locale.bindtextdomain("panorama-panel", "locale")
locale.textdomain("panorama-panel")
locale.setlocale(locale.LC_ALL)
_ = locale.gettext

import panorama_panel

import asyncio
asyncio.set_event_loop_policy(GLibEventLoopPolicy())

custom_css = """
.panel-flash {
    animation: flash 333ms ease-in-out 0s 2;
}

@keyframes flash {
    0% {
        background-color: initial;
    }
    50% {
        background-color: #ffff0080;
    }
    0% {
        background-color: initial;
    }
}
"""

css_provider = Gtk.CssProvider()
css_provider.load_from_data(custom_css)
Gtk.StyleContext.add_provider_for_display(
    Gdk.Display.get_default(),
    css_provider,
    100
)


@Gtk.Template(filename="panel-configurator.ui")
class PanelConfigurator(Gtk.Frame):
    __gtype_name__ = "PanelConfigurator"

    panel_size_adjustment: Gtk.Adjustment = Gtk.Template.Child()
    monitor_number_adjustment: Gtk.Adjustment = Gtk.Template.Child()
    top_position_radio: Gtk.CheckButton = Gtk.Template.Child()
    bottom_position_radio: Gtk.CheckButton = Gtk.Template.Child()
    left_position_radio: Gtk.CheckButton = Gtk.Template.Child()
    right_position_radio: Gtk.CheckButton = Gtk.Template.Child()
    autohide_switch: Gtk.Switch = Gtk.Template.Child()

    def __init__(self, panel: Panel, **kwargs):
        super().__init__(**kwargs)
        self.panel = panel
        self.panel_size_adjustment.set_value(panel.size)

        match self.panel.position:
            case Gtk.PositionType.TOP:
                self.top_position_radio.set_active(True)
            case Gtk.PositionType.BOTTOM:
                self.bottom_position_radio.set_active(True)
            case Gtk.PositionType.LEFT:
                self.left_position_radio.set_active(True)
            case Gtk.PositionType.RIGHT:
                self.right_position_radio.set_active(True)

        self.top_position_radio.panel_position_target = Gtk.PositionType.TOP
        self.bottom_position_radio.panel_position_target = Gtk.PositionType.BOTTOM
        self.left_position_radio.panel_position_target = Gtk.PositionType.LEFT
        self.right_position_radio.panel_position_target = Gtk.PositionType.RIGHT
        self.autohide_switch.set_active(self.panel.autohide)

    @Gtk.Template.Callback()
    def update_panel_size(self, adjustment: Gtk.Adjustment):
        if not self.get_root():
            return
        self.panel.set_size(int(adjustment.get_value()))

        self.panel.get_application().save_config()

    @Gtk.Template.Callback()
    def toggle_autohide(self, switch: Gtk.Switch, value: bool):
        if not self.get_root():
            return
        self.panel.set_autohide(value, self.panel.hide_time)

        self.panel.get_application().save_config()

    @Gtk.Template.Callback()
    def move_panel(self, button: Gtk.CheckButton):
        if not self.get_root():
            return
        if not button.get_active():
            return

        self.panel.set_position(button.panel_position_target)
        self.update_panel_size(self.panel_size_adjustment)

        # Make the applets aware of the changed orientation
        for area in (self.panel.left_area, self.panel.centre_area, self.panel.right_area):
            applet = area.get_first_child()
            while applet:
                applet.set_orientation(self.panel.get_orientation())
                applet.set_panel_position(self.panel.position)
                applet.queue_resize()
                applet = applet.get_next_sibling()

        self.panel.get_application().save_config()

    @Gtk.Template.Callback()
    def move_to_monitor(self, adjustment: Gtk.Adjustment):
        if not self.get_root():
            return
        app: PanoramaPanel = self.get_root().get_application()
        monitor = app.monitors[int(self.monitor_number_adjustment.get_value())]
        self.panel.unmap()
        self.panel.monitor_index = int(self.monitor_number_adjustment.get_value())
        Gtk4LayerShell.set_monitor(self.panel, monitor)
        self.panel.show()

        # Make the applets aware of the changed monitor
        for area in (self.panel.left_area, self.panel.centre_area, self.panel.right_area):
            applet = area.get_first_child()
            while applet:
                applet.output_changed()
                applet = applet.get_next_sibling()

        self.panel.get_application().save_config()


PANEL_POSITIONS_HUMAN = {
    Gtk.PositionType.TOP: "top",
    Gtk.PositionType.BOTTOM: "bottom",
    Gtk.PositionType.LEFT: "left",
    Gtk.PositionType.RIGHT: "right",
}


@Gtk.Template(filename="panel-manager.ui")
class PanelManager(Gtk.Window):
    __gtype_name__ = "PanelManager"

    panel_editing_switch: Gtk.Switch = Gtk.Template.Child()
    panel_stack: Gtk.Stack = Gtk.Template.Child()
    current_panel: typing.Optional[Panel] = None

    def __init__(self, application: Gtk.Application, **kwargs):
        super().__init__(application=application, **kwargs)

        self.connect("close-request", lambda *args: self.destroy())

        action_group = Gio.SimpleActionGroup()

        self.next_panel_action = Gio.SimpleAction(name="next-panel")
        action_group.add_action(self.next_panel_action)
        self.next_panel_action.connect("activate", lambda *args: self.panel_stack.set_visible_child(self.panel_stack.get_visible_child().get_next_sibling()))

        self.previous_panel_action = Gio.SimpleAction(name="previous-panel")
        action_group.add_action(self.previous_panel_action)
        self.previous_panel_action.connect("activate", lambda *args: self.panel_stack.set_visible_child(self.panel_stack.get_visible_child().get_prev_sibling()))

        self.insert_action_group("win", action_group)
        if isinstance(self.get_application(), PanoramaPanel):
            self.panel_editing_switch.set_active(application.edit_mode)
        self.panel_editing_switch.connect("state-set", self.set_edit_mode)

        self.connect("close-request", lambda *args: self.unflash_old_panel())
        self.connect("close-request", lambda *args: self.destroy())

        if isinstance(self.get_application(), PanoramaPanel):
            app: PanoramaPanel = self.get_application()
            for panel in app.panels:
                configurator = PanelConfigurator(panel)
                self.panel_stack.add_child(configurator)
                configurator.monitor_number_adjustment.set_upper(len(app.monitors))

        self.panel_stack.set_visible_child(self.panel_stack.get_first_child())
        self.panel_stack.connect("notify::visible-child", self.set_visible_panel)
        self.panel_stack.notify("visible-child")

    def unflash_old_panel(self):
        if self.current_panel:
            for area in (self.current_panel.left_area, self.current_panel.centre_area, self.current_panel.right_area):
                area.unflash()

    def set_visible_panel(self, stack: Gtk.Stack, pspec: GObject.ParamSpec):
        self.unflash_old_panel()

        panel: Panel = stack.get_visible_child().panel

        self.current_panel = panel
        self.next_panel_action.set_enabled(stack.get_visible_child().get_next_sibling() is not None)
        self.previous_panel_action.set_enabled(stack.get_visible_child().get_prev_sibling() is not None)

        # Start an animation to show the user what panel is being edited
        for area in (panel.left_area, panel.centre_area, panel.right_area):
            area.flash()

    def set_edit_mode(self, switch, value):
        if isinstance(self.get_application(), PanoramaPanel):
            self.get_application().set_edit_mode(value)

    @Gtk.Template.Callback()
    def save_settings(self, *args):
        if isinstance(self.get_application(), PanoramaPanel):
            self.get_application().save_config()


def get_applet_directories():
    data_home = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share"))
    data_dirs = [Path(d) for d in os.getenv("XDG_DATA_DIRS", "/usr/local/share:/usr/share").split(":")]

    all_paths = [data_home / "panorama-panel" / "applets"] + [d / "panorama-panel" / "applets" for d in data_dirs]
    return [d for d in all_paths if d.is_dir()]


def get_config_file():
    config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))

    return config_home / "panorama-panel" / "config.yaml"


class AppletArea(Gtk.Box):
    def __init__(self, orientation=Gtk.Orientation.HORIZONTAL):
        super().__init__()

        self.drop_target = Gtk.DropTarget.new(GObject.TYPE_UINT64, Gdk.DragAction.MOVE)
        self.drop_target.set_gtypes([GObject.TYPE_UINT64])
        self.drop_target.connect("drop", self.drop_applet)

    def drop_applet(self, drop_target: Gtk.DropTarget, value: int, x: float, y: float):
        applet = self.get_root().get_application().drags[value]
        old_area: AppletArea = applet.get_parent()
        old_area.remove(applet)
        # Find the position where to insert the applet
        child = self.get_first_child()
        while child:
            allocation = child.get_allocation()
            child_x, child_y = self.translate_coordinates(self, 0, 0)
            if self.get_orientation() == Gtk.Orientation.HORIZONTAL:
                midpoint = child_x + allocation.width / 2
                if x < midpoint:
                    applet.insert_before(self, child)
                    break
            elif self.get_orientation() == Gtk.Orientation.VERTICAL:
                midpoint = child_y + allocation.height / 2
                if y < midpoint:
                    applet.insert_before(self, child)
                    break
            child = child.get_next_sibling()
        else:
            self.append(applet)
        applet.show()

        self.get_root().get_application().drags.pop(value)
        self.get_root().get_application().save_config()

        return True

    def set_edit_mode(self, value):
        panel: Panel = self.get_root()
        child = self.get_first_child()
        while child is not None:
            if value:
                child.make_draggable()
            else:
                child.restore_drag()
            child.set_opacity(0.75 if value else 1)
            child = child.get_next_sibling()

        if value:
            self.add_controller(self.drop_target)
            if panel.get_orientation() == Gtk.Orientation.HORIZONTAL:
                self.set_size_request(48, 0)
            elif panel.get_orientation() == Gtk.Orientation.VERTICAL:
                self.set_size_request(0, 48)
        else:
            self.remove_controller(self.drop_target)
            self.set_size_request(0, 0)

    def flash(self):
        self.add_css_class("panel-flash")

    def unflash(self):
        self.remove_css_class("panel-flash")


POSITION_TO_LAYER_SHELL_EDGE = {
    Gtk.PositionType.TOP: Gtk4LayerShell.Edge.TOP,
    Gtk.PositionType.BOTTOM: Gtk4LayerShell.Edge.BOTTOM,
    Gtk.PositionType.LEFT: Gtk4LayerShell.Edge.LEFT,
    Gtk.PositionType.RIGHT: Gtk4LayerShell.Edge.RIGHT,
}


class Panel(Gtk.Window):
    def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor, position: Gtk.PositionType = Gtk.PositionType.TOP, size: int = 40, autohide: bool = False, hide_time: int = 0, can_capture_keyboard: bool = False):
        super().__init__(application=application)
        self.drop_motion_controller = None
        self.motion_controller = None
        self.set_decorated(False)
        self.position = None
        self.autohide = None
        self.monitor_name = monitor.get_connector()
        self.hide_time = None
        self.monitor_index = 0
        self.can_capture_keyboard = can_capture_keyboard
        self.open_popovers: set[int] = set()

        self.add_css_class("panorama-panel")

        Gtk4LayerShell.init_for_window(self)
        Gtk4LayerShell.set_namespace(self, "com.roundabout_host.panorama.panel")
        Gtk4LayerShell.set_monitor(self, monitor)

        Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.TOP)
        if can_capture_keyboard:
            Gtk4LayerShell.set_keyboard_mode(self, Gtk4LayerShell.KeyboardMode.ON_DEMAND)
        else:
            Gtk4LayerShell.set_keyboard_mode(self, Gtk4LayerShell.KeyboardMode.NONE)

        box = Gtk.CenterBox()

        self.left_area = AppletArea(orientation=box.get_orientation())
        self.centre_area = AppletArea(orientation=box.get_orientation())
        self.right_area = AppletArea(orientation=box.get_orientation())

        box.set_start_widget(self.left_area)
        box.set_center_widget(self.centre_area)
        box.set_end_widget(self.right_area)

        self.set_child(box)
        self.set_position(position)
        self.set_size(size)
        self.set_autohide(autohide, hide_time)

        # Add a context menu
        menu = Gio.Menu()

        menu.append(_("Open _manager"), "panel.manager")

        self.context_menu = Gtk.PopoverMenu.new_from_model(menu)
        self.context_menu.set_has_arrow(False)
        self.context_menu.set_parent(self)
        self.context_menu.set_halign(Gtk.Align.START)
        self.context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
        panorama_panel.track_popover(self.context_menu)

        right_click_controller = Gtk.GestureClick()
        right_click_controller.set_button(3)
        right_click_controller.connect("pressed", self.show_context_menu)

        self.add_controller(right_click_controller)

        action_group = Gio.SimpleActionGroup()
        manager_action = Gio.SimpleAction.new("manager", None)
        manager_action.connect("activate", self.show_manager)
        action_group.add_action(manager_action)
        self.insert_action_group("panel", action_group)

    def reset_margins(self):
        for edge in POSITION_TO_LAYER_SHELL_EDGE.values():
            Gtk4LayerShell.set_margin(self, edge, 0)

    def set_edit_mode(self, value):
        for area in (self.left_area, self.centre_area, self.right_area):
            area.set_edit_mode(value)

    def show_context_menu(self, gesture, n_presses, x, y):
        rect = Gdk.Rectangle()
        rect.x = int(x)
        rect.y = int(y)
        rect.width = 1
        rect.height = 1

        self.context_menu.set_pointing_to(rect)
        self.context_menu.popup()

    def slide_in(self):
        if not self.autohide:
            Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 0)
            return False

        if Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) >= 0:
            return False

        Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) + 1)
        return True

    def slide_out(self):
        if not self.autohide:
            Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 0)
            return False

        if Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) <= 1 - self.size:
            return False

        Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) - 1)
        return True

    def show_manager(self, _0=None, _1=None):
        if self.get_application():
            if not self.get_application().manager_window:
                self.get_application().manager_window = PanelManager(self.get_application())
                self.get_application().manager_window.connect("close-request", self.get_application().reset_manager_window)
            self.get_application().manager_window.present()

    def get_orientation(self):
        box = self.get_first_child()
        return box.get_orientation()

    def set_size(self, value: int):
        self.size = int(value)
        if self.get_orientation() == Gtk.Orientation.HORIZONTAL:
            self.set_size_request(800, self.size)
            self.set_default_size(800, self.size)
        else:
            self.set_size_request(self.size, 600)
            self.set_default_size(self.size, 600)

    def set_position(self, position: Gtk.PositionType):
        self.position = position
        self.reset_margins()
        match self.position:
            case Gtk.PositionType.TOP:
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, False)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
            case Gtk.PositionType.BOTTOM:
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, False)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
            case Gtk.PositionType.LEFT:
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, False)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
            case Gtk.PositionType.RIGHT:
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, False)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
        box = self.get_first_child()
        match self.position:
            case Gtk.PositionType.TOP | Gtk.PositionType.BOTTOM:
                box.set_orientation(Gtk.Orientation.HORIZONTAL)
                for area in (self.left_area, self.centre_area, self.right_area):
                    area.set_orientation(Gtk.Orientation.HORIZONTAL)
            case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT:
                box.set_orientation(Gtk.Orientation.VERTICAL)
                for area in (self.left_area, self.centre_area, self.right_area):
                    area.set_orientation(Gtk.Orientation.VERTICAL)

        if self.autohide:
            if not self.open_popovers:
                GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out)

    def set_autohide(self, autohide: bool, hide_time: int):
        self.autohide = autohide
        self.hide_time = hide_time
        if not self.autohide:
            self.reset_margins()
            Gtk4LayerShell.auto_exclusive_zone_enable(self)
            if self.motion_controller is not None:
                self.remove_controller(self.motion_controller)
            if self.drop_motion_controller is not None:
                self.remove_controller(self.drop_motion_controller)
            self.motion_controller = None
            self.drop_motion_controller = None
            self.reset_margins()
        else:
            Gtk4LayerShell.set_exclusive_zone(self, 0)
            # Only leave 1px of the window as a "mouse sensor"
            Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 1 - self.size)
            self.motion_controller = Gtk.EventControllerMotion()
            self.add_controller(self.motion_controller)
            self.drop_motion_controller = Gtk.DropControllerMotion()
            self.add_controller(self.drop_motion_controller)

            self.motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in))
            self.drop_motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in))
            self.motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out))
            self.drop_motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out))

    def wl_output_enter(self, output, name):
        for area in (self.left_area, self.centre_area, self.right_area):
            applet = area.get_first_child()
            while applet:
                applet.wl_output_enter(output, name)
                applet = applet.get_next_sibling()

    def foreign_toplevel_new(self, manager, toplevel):
        for area in (self.left_area, self.centre_area, self.right_area):
            applet = area.get_first_child()
            while applet:
                applet.foreign_toplevel_new(manager, toplevel)
                applet = applet.get_next_sibling()

    def foreign_toplevel_output_enter(self, toplevel, output):
        for area in (self.left_area, self.centre_area, self.right_area):
            applet = area.get_first_child()
            while applet:
                applet.foreign_toplevel_output_enter(toplevel, output)
                applet = applet.get_next_sibling()

    def foreign_toplevel_output_leave(self, toplevel, output):
        for area in (self.left_area, self.centre_area, self.right_area):
            applet = area.get_first_child()
            while applet:
                applet.foreign_toplevel_output_leave(toplevel, output)
                applet = applet.get_next_sibling()

    def foreign_toplevel_app_id(self, toplevel, app_id):
        for area in (self.left_area, self.centre_area, self.right_area):
            applet = area.get_first_child()
            while applet:
                applet.foreign_toplevel_app_id(toplevel, app_id)
                applet = applet.get_next_sibling()

    def foreign_toplevel_title(self, toplevel, title):
        for area in (self.left_area, self.centre_area, self.right_area):
            applet = area.get_first_child()
            while applet:
                applet.foreign_toplevel_title(toplevel, title)
                applet = applet.get_next_sibling()

    def foreign_toplevel_state(self, toplevel, state):
        for area in (self.left_area, self.centre_area, self.right_area):
            applet = area.get_first_child()
            while applet:
                applet.foreign_toplevel_state(toplevel, state)
                applet = applet.get_next_sibling()

    def foreign_toplevel_closed(self, toplevel):
        for area in (self.left_area, self.centre_area, self.right_area):
            applet = area.get_first_child()
            while applet:
                applet.foreign_toplevel_closed(toplevel)
                applet = applet.get_next_sibling()

    def foreign_toplevel_refresh(self):
        for area in (self.left_area, self.centre_area, self.right_area):
            applet = area.get_first_child()
            while applet:
                applet.foreign_toplevel_refresh()
                applet = applet.get_next_sibling()


def get_all_subclasses(klass: type) -> list[type]:
    subclasses = []
    for subclass in klass.__subclasses__():
        subclasses.append(subclass)
        subclasses += get_all_subclasses(subclass)

    return subclasses

def load_packages_from_dir(dir_path: Path):
    loaded_modules = []

    for path in dir_path.iterdir():
        if path.name.startswith("_"):
            continue

        if path.is_dir() and (path / "__init__.py").exists():
            try:
                module_name = path.name
                spec = importlib.util.spec_from_file_location(module_name, path / "__init__.py")
                module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(module)
                loaded_modules.append(module)
            except Exception as e:
                print(f"Failed to load applet module {path.name}: {e}", file=sys.stderr)
        else:
            continue

    return loaded_modules


PANEL_POSITIONS = {
    "top": Gtk.PositionType.TOP,
    "bottom": Gtk.PositionType.BOTTOM,
    "left": Gtk.PositionType.LEFT,
    "right": Gtk.PositionType.RIGHT,
}


PANEL_POSITIONS_REVERSE = {
    Gtk.PositionType.TOP: "top",
    Gtk.PositionType.BOTTOM: "bottom",
    Gtk.PositionType.LEFT: "left",
    Gtk.PositionType.RIGHT: "right",
}


class PanoramaPanel(Gtk.Application):
    def __init__(self):
        super().__init__(
                application_id="com.roundabout_host.roundabout.PanoramaPanel",
                flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE
        )
        self.display = Gdk.Display.get_default()
        self.monitors = self.display.get_monitors()
        self.applets_by_name: dict[str, panorama_panel.Applet] = {}
        self.panels: list[Panel] = []
        self.wl_globals = []
        self.wl_display = None
        self.wl_seat = None
        self.wl_shm = None
        self.foreign_toplevel_manager = None
        self.screencopy_manager = None
        self.manager_window = None
        self.edit_mode = False
        self.output_proxies = []
        self.drags = {}
        self.wl_output_ids = set()
        self.started = False

        self.add_main_option(
            "trigger",
            ord("t"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING,
            _("Trigger an action which can be processed by the applets"),
            _("NAME")
        )

    def destroy_foreign_toplevel_manager(self):
        self.foreign_toplevel_manager.destroy()
        self.wl_globals.remove(self.foreign_toplevel_manager)
        self.foreign_toplevel_manager = None

    def on_foreign_toplevel_manager_finish(self, manager: ZwlrForeignToplevelManagerV1):
        if self.foreign_toplevel_manager:
            self.destroy_foreign_toplevel_manager()

    def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1,
                         handle: ZwlrForeignToplevelHandleV1):
        for panel in self.panels:
            panel.foreign_toplevel_new(manager, handle)
        handle.dispatcher["title"] = self.on_title_changed
        handle.dispatcher["app_id"] = self.on_app_id_changed
        handle.dispatcher["output_enter"] = self.on_output_enter
        handle.dispatcher["output_leave"] = self.on_output_leave
        handle.dispatcher["state"] = self.on_state_changed
        handle.dispatcher["closed"] = self.on_closed

    def on_output_enter(self, handle, output):
        for panel in self.panels:
            panel.foreign_toplevel_output_enter(handle, output)

    def on_output_leave(self, handle, output):
        for panel in self.panels:
            panel.foreign_toplevel_output_leave(handle, output)

    def on_title_changed(self, handle, title):
        for panel in self.panels:
            panel.foreign_toplevel_title(handle, title)

    def on_app_id_changed(self, handle, app_id):
        for panel in self.panels:
            panel.foreign_toplevel_app_id(handle, app_id)

    def on_state_changed(self, handle, state):
        for panel in self.panels:
            panel.foreign_toplevel_state(handle, state)

    def on_closed(self, handle):
        for panel in self.panels:
            panel.foreign_toplevel_closed(handle)

    def foreign_toplevel_refresh(self):
        for panel in self.panels:
            panel.foreign_toplevel_refresh()

    def receive_output_name(self, output: WlOutput, name: str):
        output.name = name

    def idle_proxy_cover(self, output_name):
        for monitor in self.display.get_monitors():
            wl_output = gtk.gdk_wayland_monitor_get_wl_output(ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(monitor.__gpointer__, None)))
            found = False
            for proxy in self.output_proxies:
                if proxy._ptr == wl_output:
                    found = True
            if wl_output and not found:
                print("Create proxy")
                output_proxy = WlOutputProxy(wl_output, self.wl_display)
                output_proxy.interface.registry[output_proxy._ptr] = output_proxy
                self.output_proxies.append(output_proxy)

    def on_global(self, registry, name, interface, version):
        if interface == "zwlr_foreign_toplevel_manager_v1":
            self.foreign_toplevel_manager_id = name
            self.foreign_toplevel_version = version
        elif interface == "wl_output":
            output = registry.bind(name, WlOutput, version)
            output.id = name
            self.wl_globals.append(output)
            print(f"global {name} {interface} {version}")
            output.global_name = name
            output.name = None
            output.dispatcher["name"] = self.receive_output_name
            while not output.name or not self.foreign_toplevel_version:
                self.wl_display.dispatch(block=True)
            print(output.name)
            self.wl_output_ids.add(output.global_name)
            if not output.name.startswith("live-preview"):
                self.generate_panels(output.name)
            for panel in self.panels:
                panel.wl_output_enter(output, output.name)
            if not output.name.startswith("live-preview"):
                if self.foreign_toplevel_manager:
                    self.foreign_toplevel_manager.stop()
                    self.wl_display.roundtrip()
                    if self.foreign_toplevel_manager:
                        self.destroy_foreign_toplevel_manager()
                    self.foreign_toplevel_refresh()
                self.foreign_toplevel_manager = self.registry.bind(self.foreign_toplevel_manager_id, ZwlrForeignToplevelManagerV1, self.foreign_toplevel_version)
                self.foreign_toplevel_manager.id = self.foreign_toplevel_manager_id
                self.wl_globals.append(self.foreign_toplevel_manager)
                self.foreign_toplevel_manager.dispatcher["toplevel"] = self.on_new_toplevel
                self.foreign_toplevel_manager.dispatcher["finished"] = self.on_foreign_toplevel_manager_finish
                self.wl_display.roundtrip()
            print(f"Monitor {output.name} ({interface}) connected", file=sys.stderr)
        elif interface == "wl_seat":
            self.wl_seat = registry.bind(name, WlSeat, version)
            self.wl_seat.id = name
            self.wl_globals.append(self.wl_seat)
        elif interface == "wl_shm":
            self.wl_shm = registry.bind(name, WlShm, version)
            self.wl_shm.id = name
            self.wl_globals.append(self.wl_shm)
        elif interface == "zwlr_screencopy_manager_v1":
            self.screencopy_manager = registry.bind(name, ZwlrScreencopyManagerV1, version)
            self.screencopy_manager.id = name
            self.wl_globals.append(self.screencopy_manager)

    def on_global_remove(self, registry, name):
        if name in self.wl_output_ids:
            print(f"Monitor {name} disconnected", file=sys.stderr)
            self.wl_output_ids.discard(name)
        for g in self.wl_globals:
            if g.id == name:
                g.destroy()
                self.wl_globals.remove(g)
                break

    def generate_panels(self, output_name):
        print("Generating panels...", file=sys.stderr)
        self.exit_all_applets()

        for i, monitor in enumerate(self.monitors):
            geometry = monitor.get_geometry()
            print(f"Monitor {i}: {geometry.width}x{geometry.height} at {geometry.x},{geometry.y}")

        for panel in self.panels:
            if panel.monitor_name == output_name:
                panel.destroy()
        with open(get_config_file(), "r") as config_file:
            yaml_loader = yaml.YAML(typ="rt")
            yaml_file = yaml_loader.load(config_file)
            for panel_data in yaml_file["panels"]:
                if panel_data["monitor"] != output_name:
                    continue
                position = PANEL_POSITIONS[panel_data["position"]]
                my_monitor = None
                for monitor in self.display.get_monitors():
                    if monitor.get_connector() == panel_data["monitor"]:
                        my_monitor = monitor
                        break
                if not my_monitor:
                    continue
                GLib.idle_add(self.idle_proxy_cover, my_monitor.get_connector())
                size = panel_data["size"]
                autohide = panel_data["autohide"]
                hide_time = panel_data["hide_time"]
                can_capture_keyboard = panel_data["can_capture_keyboard"]

                panel = Panel(self, my_monitor, position, size, autohide, hide_time, can_capture_keyboard)
                self.panels.append(panel)

                print(f"{size}px panel on {position}, autohide is {autohide} ({hide_time}ms)")

                for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
                    applet_list = panel_data["applets"].get(area_name)
                    if applet_list is None:
                        continue

                    for applet in applet_list:
                        item = list(applet.items())[0]
                        AppletClass = self.applets_by_name[item[0]]
                        options = item[1]
                        applet_widget = AppletClass(orientation=panel.get_orientation(), config=options)
                        applet_widget.connect("config-changed", self.save_config)
                        applet_widget.set_panel_position(panel.position)

                        area.append(applet_widget)

                panel.present()
                panel.realize()
                panel.monitor_name = my_monitor.get_connector()

    def do_startup(self):
        Gtk.Application.do_startup(self)

        ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
        ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (ctypes.py_object,)

        all_applets = list(
            chain.from_iterable(load_packages_from_dir(d) for d in get_applet_directories()))
        print("Applets:")
        subclasses = get_all_subclasses(panorama_panel.Applet)
        for subclass in subclasses:
            if subclass.__name__ in self.applets_by_name:
                print(f"Name conflict for applet {subclass.__name__}. Only one will be loaded.",
                      file=sys.stderr)
            self.applets_by_name[subclass.__name__] = subclass

        self.wl_display = Display()
        wl_display_ptr = gtk.gdk_wayland_display_get_wl_display(
                ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(self.display.__gpointer__, None)))
        self.wl_display._ptr = wl_display_ptr

        self.foreign_toplevel_manager_id = None
        self.foreign_toplevel_version = None

        self.idle_proxy_cover(None)
        self.registry = self.wl_display.get_registry()
        self.registry.dispatcher["global"] = self.on_global
        self.registry.dispatcher["global_remove"] = self.on_global_remove
        self.wl_display.roundtrip()
        if self.foreign_toplevel_version:
            print("Foreign Toplevel Interface: Found")
        else:
            print("Foreign Toplevel Interface: Not Found")

        self.hold()

    def do_activate(self):
        Gio.Application.do_activate(self)

    def save_config(self, *args):
        print("Saving configuration file")
        with open(get_config_file(), "w") as config_file:
            yaml_writer = yaml.YAML(typ="rt")
            data = {"panels": []}
            for panel in self.panels:
                panel_data = {
                    "position": PANEL_POSITIONS_REVERSE[panel.position],
                    "monitor": panel.monitor_index,
                    "size": panel.size,
                    "autohide": panel.autohide,
                    "hide_time": panel.hide_time,
                    "can_capture_keyboard": panel.can_capture_keyboard,
                    "applets": {}
                }

                for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
                    panel_data["applets"][area_name] = []
                    applet = area.get_first_child()
                    while applet is not None:
                        panel_data["applets"][area_name].append({
                            applet.__class__.__name__: applet.get_config(),
                        })

                        applet = applet.get_next_sibling()

                data["panels"].append(panel_data)

            yaml_writer.dump(data, config_file)

    def exit_all_applets(self):
        for panel in self.panels:
            for area in (panel.left_area, panel.centre_area, panel.right_area):
                applet = area.get_first_child()
                while applet is not None:
                    applet.shutdown(self)

                    applet = applet.get_next_sibling()

    def do_shutdown(self):
        print("Shutting down")
        self.exit_all_applets()
        Gtk.Application.do_shutdown(self)

    def do_command_line(self, command_line: Gio.ApplicationCommandLine):
        options = command_line.get_options_dict()
        args = command_line.get_arguments()[1:]

        trigger_variant = options.lookup_value("trigger", GLib.VariantType("s"))
        if trigger_variant:
            action_name = trigger_variant.get_string()
            print(app.list_actions())
            self.activate_action(action_name, None)
            print(f"Triggered {action_name}")

        return 0

    def set_edit_mode(self, value):
        self.edit_mode = value
        for panel in self.panels:
            panel.set_edit_mode(value)

    def reset_manager_window(self, *args):
        self.manager_window = None


if __name__ == "__main__":
    app = PanoramaPanel()
    app.run(sys.argv)
