"""
Panorama background: desktop wallpaper 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/>.
"""

import os
import faulthandler
import pathlib

import ruamel.yaml as yaml
from pywayland.client import Display, EventQueue
from pywayland.protocol.wayland import WlOutput
from pathlib import Path

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

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


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

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


FITTING_MODES = {
    "cover": Gtk.ContentFit.COVER,
    "contain": Gtk.ContentFit.CONTAIN,
    "fill": Gtk.ContentFit.FILL,
    "scale_down": Gtk.ContentFit.SCALE_DOWN
}


class PanoramaBG(Gtk.Application):
    def __init__(self):
        super().__init__(
                application_id="com.roundabout_host.roundabout.PanoramaBG"
        )
        self.wl_output_ids: dict[int, WlOutput] = {}
        self.wl_display = None
        self.display = None

    def make_backgrounds(self):
        for window in self.get_windows():
            window.destroy()

        with open(get_config_file(), "r") as config_file:
            yaml_loader = yaml.YAML(typ="rt")
            yaml_file = yaml_loader.load(config_file)

            monitors = self.display.get_monitors()

            for monitor in monitors:
                if monitor.get_connector() in yaml_file["monitors"]:
                    all_sources = []
                    for source in yaml_file["monitors"][monitor.get_connector()]["sources"]:
                        path = pathlib.Path(source)
                        if path.is_dir():
                            all_sources.extend(f for f in path.iterdir() if f.is_file())
                        else:
                            all_sources.append(path)

                    background_window = BackgroundWindow(self, monitor)
                    background_window.all_sources = all_sources
                    background_window.time_per_image = yaml_file["monitors"][monitor.get_connector()]["time_per_image"]
                    background_window.change_image()
                    background_window.picture.set_content_fit(FITTING_MODES[yaml_file["monitors"][monitor.get_connector()]["fitting_mode"]])
                    background_window.present()

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

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

        self.display = Gdk.Display.get_default()

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

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

    def on_global(self, registry, name, interface, version):
        if interface == "wl_output":
            output = registry.bind(name, WlOutput, version)
            output.id = name
            output.name = None
            output.dispatcher["name"] = self.receive_output_name
            while not output.name:
                self.wl_display.dispatch(block=True)
            if not output.name.startswith("live-preview"):
                self.make_backgrounds()
                self.wl_output_ids[name] = output

    def on_global_remove(self, registry, name):
        if name in self.wl_output_ids:
            self.wl_output_ids[name].destroy()
            del self.wl_output_ids[name]


class BackgroundWindow(Gtk.Window):
    def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor):
        super().__init__(application=application)
        self.set_decorated(False)

        self.all_sources = []
        self.time_per_image = 0

        Gtk4LayerShell.init_for_window(self)
        Gtk4LayerShell.set_namespace(self, "com.roundabout_host.panorama.background")
        Gtk4LayerShell.set_monitor(self, monitor)
        Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.BACKGROUND)

        Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
        Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
        Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
        Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)

        self.picture = Gtk.Picture()
        self.set_child(self.picture)

    def change_image(self):
        now = GLib.DateTime.new_now_local()
        self.picture.set_filename(str(self.all_sources[now.to_unix() // self.time_per_image % len(self.all_sources)]))
        time_until_next = self.time_per_image - now.to_unix() % self.time_per_image
        GLib.timeout_add_seconds(time_until_next, self.change_image)


if __name__ == "__main__":
    app = PanoramaBG()
    app.run()

