"""
Nesting file menu applet 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/>.
"""
import hashlib
import os
import locale
from pathlib import Path
import panorama_panel

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

from gi.repository import Gtk, GLib, Gio, Gdk, GObject, GioUnix

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


@Gtk.Template(filename=str(module_directory / "panorama-panel-file-listing.ui"))
class FileListingOptions(Gtk.Window):
    __gtype_name__ = "FileListingOptions"
    icon_size_adjustment: Gtk.Adjustment = Gtk.Template.Child()
    icon_name_entry: Gtk.Entry = Gtk.Template.Child()
    label_entry: Gtk.Label = Gtk.Template.Child()
    hidden_button: Gtk.CheckButton = Gtk.Template.Child()
    root_button: Gtk.Button = Gtk.Template.Child()

    @GObject.Signal(arg_types=(str,))
    def new_path(self, path):
        self.root_button.set_label(path)

    def open_done(self, browser, result, *args):
        self.emit("new_path", browser.select_folder_finish(result).get_path())

    def show_browser(self, button):
        browser = Gtk.FileDialog()

        browser.select_folder(self, None, self.open_done)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.root_button.connect("clicked", self.show_browser)

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


class FileMenuAttributeIter(Gio.MenuAttributeIter, GObject.Object):
    def __init__(self, attributes: dict, **kwargs):
        GObject.Object.__init__(self, **kwargs)
        self._attributes: dict = attributes
        self._values = list(attributes.items())
        self._i = 0

    def do_get_next(self):
        if self._i >= len(self._values):
            return False, None, None

        return True, *self._values[self._i]


class FileMenuLinkIter(Gio.MenuLinkIter, GObject.Object):
    def __init__(self, attributes: dict, **kwargs):
        GObject.Object.__init__(self, **kwargs)
        self._attributes: dict = attributes
        self._values = list(attributes.items())
        self._i = 0

    def do_get_next(self):
        if self._i >= len(self._values):
            return False, None, None

        return True, *self._values[self._i]


class FileMenu(Gio.MenuModel, GObject.Object):
    def __init__(self, root: Path, storage: dict[str, GObject.Object], new_action_callback, prune_action_callback, show_hidden=False, **kwargs):
        GObject.Object.__init__(self, **kwargs)
        self.root: Path = root
        self.links: dict[str, Gio.MenuModel] = {}
        self.files: list[Path] = []
        self.storage = storage
        self.callback = new_action_callback
        self.callback_prune = prune_action_callback
        self.show_hidden = show_hidden
        self.enabled = False

    def do_is_mutable(self):
        return True

    def do_get_n_items(self):
        if not self.enabled:
            return 0
        if self.show_hidden:
            self.files = sorted(
                    self.root.iterdir(),
                    key=lambda p: GLib.utf8_collate_key_for_filename(p.name, len(p.name))
            )
        else:
            self.files = sorted(
                    (p for p in self.root.iterdir() if not p.name.startswith(".")),
                    key=lambda p: GLib.utf8_collate_key_for_filename(p.name, len(p.name))
            )
        return len(self.files)

    def do_get_item_attribute_value(self, item_index, attribute, expected_type = None):
        file = self.files[item_index]
        match attribute:
            case Gio.MENU_ATTRIBUTE_ACTION:
                return GLib.Variant("s", "applet.open")
            case Gio.MENU_ATTRIBUTE_TARGET:
                return GLib.Variant("s", str(file.resolve()))
            case Gio.MENU_ATTRIBUTE_LABEL:
                return GLib.Variant("s", file.name.replace("_", "＿"))
            case "submenu-action":
                return GLib.Variant("s", f"applet.submenu-status-{hashlib.md5(str(file).encode()).hexdigest()}")
            case _:
                return None

    def make_submenu(self, name):
        action_id = hashlib.md5(str(self.root / name).encode())
        if name not in self.links:
            menu = FileMenu(self.root / name, self.storage, self.callback, self.callback_prune, show_hidden=self.show_hidden)
            container = Gio.Menu()
            options_section = Gio.Menu()
            open_directory = Gio.MenuItem()
            open_directory.set_label(_("Open directory"))
            open_directory.set_action_and_target_value("applet.open", GLib.Variant("s", str((self.root / name).resolve())))
            options_section.append_item(open_directory)
            container.append_section(None, options_section)
            container.append_section(None, menu)
            container.file_list = menu
            self.links[name] = container
            self.storage[action_id.hexdigest()] = menu
            self.callback(action_id)

    def do_get_item_link(self, item_index, link):
        file = self.files[item_index]
        if file.is_dir() and link == Gio.MENU_LINK_SUBMENU:
            self.make_submenu(file.name)
            return self.links[file.name]
        return None

    def do_iterate_item_attributes(self, item_index):
        file = self.files[item_index]
        return FileMenuAttributeIter(
                {
                    Gio.MENU_ATTRIBUTE_ACTION: GLib.Variant("s", "applet.open"),
                    Gio.MENU_ATTRIBUTE_TARGET: GLib.Variant("s", str(file.resolve())),
                    Gio.MENU_ATTRIBUTE_LABEL: GLib.Variant("s", file.name.replace("_", "＿")),
                    "submenu-action": GLib.Variant("s", f"applet.submenu-status-{hashlib.md5(str(file).encode()).hexdigest()}"),
                }
        )

    def do_iterate_item_links(self, item_index):
        file = self.files[item_index]
        if file.is_dir():
            self.make_submenu(file.name)
            return FileMenuLinkIter({Gio.MENU_LINK_SUBMENU: self.links[file.name]})
        return FileMenuLinkIter({})

    def do_get_item_attributes(self, item_index):
        file = self.files[item_index]
        return {
                Gio.MENU_ATTRIBUTE_ACTION: GLib.Variant("s", "applet.open"),
                Gio.MENU_ATTRIBUTE_TARGET: GLib.Variant("s", str(file.resolve())),
                Gio.MENU_ATTRIBUTE_LABEL: GLib.Variant("s", file.name.replace("_", "＿")),
                "submenu-action": GLib.Variant("s", f"applet.submenu-status-{hashlib.md5(str(file).encode()).hexdigest()}"),
        }

    def do_get_item_links(self, item_index):
        file = self.files[item_index]
        if file.is_dir():
            self.make_submenu(file.name)
            return {Gio.MENU_LINK_SUBMENU: self.links[file.name]}
        return {}

    def generate(self):
        self.enabled = True
        self.items_changed(0, 0, self.get_n_items())

    def prune(self):
        self.enabled = False
        for file in self.files:
            if file.is_dir():
                action_id = hashlib.md5(str(self.root / file.name).encode())
                self.links[file.name].file_list.prune()
                del self.storage[action_id.hexdigest()]
                self.callback_prune(action_id)
        self.items_changed(0, len(self.files), 0)
        self.files.clear()
        self.links.clear()


class FileListingApplet(panorama_panel.Applet):
    name = _("File listing")
    description = _("View a nesting menu of your files")
    icon = Gio.ThemedIcon.new("system-file-manager")

    def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None, **kwargs):
        super().__init__(orientation=orientation, config=config, **kwargs)
        locale.bindtextdomain("panorama-panel-file-listing", module_directory / "locale")
        _ = lambda x: locale.dgettext("panorama-panel-file-listing", x)
        if config is None:
            config = {}
        self.options_window = None
        self.action_group = Gio.SimpleActionGroup()
        self.directory = Path(config.get("directory", "~")).expanduser().resolve()
        self.button = Gtk.MenuButton()
        self.button.set_has_frame(False)
        self.append(self.button)

        self.inner_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        self.button.set_child(self.inner_box)
        self.icon_name = config.get("icon_name")
        self.show_hidden = config.get("show_hidden", False)
        self.icon_size = config.get("icon_size", 24)
        self.label = config.get("label")
        self.icon = None
        if config.get("icon_name"):
            self.icon = Gtk.Image(icon_name=config["icon_name"], pixel_size=config.get("icon_size", 24))
            self.inner_box.append(self.icon)
        if config.get("label"):
            self.inner_box.append(Gtk.Label.new(config["label"]))

        self.button.set_always_show_arrow(True)
        self.popover = Gtk.PopoverMenu(flags=Gtk.PopoverMenuFlags.NESTED, has_arrow=False, halign=Gtk.Align.START)
        self.actions = {}
        self.container_menu = Gio.Menu()
        self.options_section = Gio.Menu()
        open_directory = Gio.MenuItem()
        open_directory.set_label(_("Open directory"))
        open_directory.set_action_and_target_value("applet.open", GLib.Variant("s", str(self.directory)))
        self.options_section.append_item(open_directory)
        self.container_menu.append_section(None, self.options_section)

        self.menu = FileMenu(self.directory, self.actions, self.new_action, self.delete_action, show_hidden=self.show_hidden)
        self.container_menu.append_section(None, self.menu)
        self.container_menu.file_list = self.menu

        self.popover.set_menu_model(self.container_menu)
        self.button.set_popover(self.popover)
        panorama_panel.track_popover(self.popover)

        self.open_action = Gio.SimpleAction(name="open", parameter_type=GLib.VariantType("s"))
        self.open_action.connect("activate", self.open_file)

        self.submenu_action = Gio.SimpleAction(name="submenu-status", state=GLib.Variant("b", False))
        self.submenu_action.connect("notify", self.submenu_changed)

        self.action_group.add_action(self.open_action)
        self.action_group.add_action(self.submenu_action)
        self.insert_action_group("applet", self.action_group)

        self.popover.connect("show", lambda *args: self.menu.generate())
        self.popover.connect("hide", lambda *args: self.menu.prune())

        self.context_menu = self.make_context_menu()
        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)

        options_action = Gio.SimpleAction.new("options", None)
        options_action.connect("activate", self.show_options)
        self.action_group.add_action(options_action)

    def new_action(self, action_id):
        action = Gio.SimpleAction(name=f"submenu-status-{action_id.hexdigest()}", state=GLib.Variant("b", False))
        action.submenu = self.actions[action_id.hexdigest()]
        action.connect("notify", self.submenu_changed)
        self.action_group.add_action(action)

    def delete_action(self, action_id):
        self.action_group.remove(f"submenu-status-{action_id.hexdigest()}")

    def open_file(self, action, data: GLib.Variant):
        uri = GLib.filename_to_uri(data.get_string())
        try:
            GioUnix.DesktopAppInfo.new_from_filename(data.get_string()).launch()
        except TypeError:
            Gio.AppInfo.launch_default_for_uri(uri)

    def submenu_changed(self, action, parameter):
        if action.get_state():
            action.submenu.generate()
        else:
            action.submenu.prune()

    def make_context_menu(self):
        menu = Gio.Menu()
        menu.append(_("File menu _options"), "applet.options")
        context_menu = Gtk.PopoverMenu(menu_model=menu, has_arrow=False, halign=Gtk.Align.START, flags=Gtk.PopoverMenuFlags.NESTED)
        context_menu.set_parent(self)
        return context_menu

    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 update_icon_size(self, adjustment, *args):
        self.icon_size = int(adjustment.get_value())
        if self.icon is not None:
            self.icon.set_pixel_size(self.icon_size)
        self.emit("config-changed")

    def show_options(self, _0=None, _1=None):
        if self.options_window is None:
            self.options_window = FileListingOptions()
            # Setting the icon size
            self.options_window.icon_size_adjustment.set_value(self.icon_size)
            self.options_window.icon_size_adjustment.connect("value-changed", self.update_icon_size)

            def reset_window(*args):
                self.options_window = None

            self.options_window.connect("close-request", reset_window)
            # Setting the path
            self.options_window.connect("new_path", self.set_path)
            self.options_window.root_button.set_label(str(self.directory))
            # Setting whether to show hidden files
            self.options_window.hidden_button.set_active(self.show_hidden)
            self.options_window.hidden_button.connect("toggled", self.set_show_hidden)
            # Setting the icon name and label
            self.options_window.icon_name_entry.connect("changed", self.update_button_content)
            self.options_window.label_entry.connect("changed", self.update_button_content)
        self.options_window.present()

    def set_path(self, options_window, path):
        self.menu.root = self.directory = Path(path)
        self.emit("config-changed")

    def update_button_content(self, *args):
        if self.options_window:
            self.icon = None
            icon_name = self.options_window.icon_name_entry.get_text()
            label = self.options_window.label_entry.get_text()
            while self.inner_box.get_first_child():
                self.inner_box.remove(self.inner_box.get_first_child())
            if icon_name:
                self.icon = Gtk.Image(icon_name=icon_name, pixel_size=self.icon_size)
                self.inner_box.append(self.icon)
            if label:
                self.inner_box.append(Gtk.Label.new(label))

    def set_show_hidden(self, checkbutton):
        self.menu.show_hidden = self.show_hidden = checkbutton.get_active()
        self.emit("config-changed")

    def get_config(self):
        return {
            "directory": str(self.directory),
            "icon_name": self.icon_name,
            "icon_size": self.icon_size,
            "label": self.label,
            "show_hidden": self.show_hidden,
        }

    def set_panel_position(self, position):
        self.button.set_direction(panorama_panel.POSITION_TO_ARROW[panorama_panel.OPPOSITE_POSITION[position]])
