from __future__ import annotations

import os
import sys
import importlib
import typing
from itertools import accumulate, chain
from pathlib import Path
import ruamel.yaml as yaml

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

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

import panorama_panel


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,
    Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)


@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()

    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

    @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()))

    @Gtk.Template.Callback()
    def move_panel(self, button: Gtk.CheckButton):
        if not self.get_root():
            return
        if not button.get_active():
            return
        print("Moving panel")
        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()

    @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()
        Gtk4LayerShell.set_monitor(self.panel, monitor)
        self.panel.show()


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):
            print("Saving settings as user requested")
            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):
        print(f"Dropping applet: {value}")
        applet: panorama_panel.Applet = self.get_root().get_application().drags.pop(value)
        print(f"Type: {applet.__class__.__name__}")
        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()
        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")


class Panel(Gtk.Window):
    def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor, position: Gtk.PositionType = Gtk.PositionType.TOP, size: int = 40, monitor_index: int = 0):
        super().__init__(application=application)
        self.set_decorated(False)
        self.position = None
        self.monitor_index = monitor_index

        Gtk4LayerShell.init_for_window(self)
        Gtk4LayerShell.set_monitor(self, monitor)

        Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.TOP)

        Gtk4LayerShell.auto_exclusive_zone_enable(self)

        box = Gtk.CenterBox()

        self.set_child(box)
        self.set_position(position)
        self.set_size(size)

        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)

        # 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)

        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 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 show_manager(self, _0=None, _1=None):
        print("Showing manager")
        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
        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)
            case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT:
                box.set_orientation(Gtk.Orientation.VERTICAL)

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():
            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)
        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.panorama.panel")
        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.manager_window = None
        self.edit_mode = False
        self.drags = {}

    def do_startup(self):
        Gtk.Application.do_startup(self)
        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}")

        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

        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"]:
                position = PANEL_POSITIONS[panel_data["position"]]
                monitor_index = panel_data["monitor"]
                if monitor_index >= len(self.monitors):
                    continue
                monitor = self.monitors[monitor_index]
                size = panel_data["size"]

                panel = Panel(self, monitor, position, size, monitor_index)
                self.panels.append(panel)
                panel.show()

                print(f"{size}px panel on {position} edge of monitor {monitor_index}")

                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.set_panel_position(panel.position)

                        area.append(applet_widget)

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

    def save_config(self):
        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,
                    "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 do_shutdown(self):
        print("Shutting down")
        Gtk.Application.do_shutdown(self)
        self.save_config()

    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)
