import os
import xml.etree.ElementTree as ET

import gi

gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
gi.require_version("GdkPixbuf", "2.0")
from gi.repository import Adw, Gdk, GdkPixbuf, Gio, GLib, Gtk, Pango
from stillControl import GSetting
from stillControl.i18n import _

ARCMENU_RESOURCE = (
    "/usr/share/gnome-shell/extensions/arcmenu@arcmenu.com/data/resources.gresource"
)
ARCMENU_ICON_PATH = "/org/gnome/shell/extensions/arcmenu/icons/scalable/emblems"
ARCMENU_SCHEMA_XML = (
    "/usr/share/glib-2.0/schemas/org.gnome.shell.extensions.arcmenu.gschema.xml"
)
_arcmenu_resource_registered = False
_css_provider_registered = False
_schema_defaults = None

# ArcMenu has renamed a few layout icons over time.  Keep the names used by
# older releases as fallbacks so the layout picker works with either resource
# bundle.
ARCMENU_ICON_ALIASES = {
    "menu-gnomemenu-symbolic": ("menu-gnome-menu-symbolic",),
    "menu-gnomeoverview-symbolic": ("menu-gnome-overview-symbolic",),
    "menu-eleven-symbolic": ("menu-11-symbolic",),
}


LAYOUT_SETTING_DEFAULT_KEYS = [
    "menu-height",
    "menu-width-adjustment",
    "default-menu-view",
    "all-apps-button-action",
    "arcmenu-layout-max-frequent-apps",
    "group-apps-alphabetically-list-layouts",
    "group-apps-alphabetically-grid-layouts",
    "show-category-sub-menus",
    "multi-lined-labels",
    "show-recently-installed-apps",
    "max-search-results",
    "search-provider-open-windows",
    "search-provider-recent-files",
    "show-search-result-details",
    "default-menu-view-redmond",
    "windows-show-pinned-apps",
    "windows-show-frequent-apps",
    "eleven-show-frequent-apps",
    "enable-unity-homescreen",
    "enable-clock-widget-unity",
    "enable-weather-widget-unity",
    "enable-horizontal-flip",
    "raven-position",
    "raven-search-display-style",
    "enable-clock-widget-raven",
    "enable-weather-widget-raven",
    "default-menu-view-az",
    "az-layout-max-frequent-apps",
    "az-layout-merge-panels",
    "runner-position",
    "runner-searchbar-location",
    "runner-search-display-style",
    "runner-menu-width",
    "runner-menu-height",
    "runner-menu-height-static",
    "runner-show-frequent-apps",
    "runner-show-settings-button",
]


MENU_LAYOUT_GROUPS = [
    (
        _("Traditional"),
        [
            ("ArcMenu", "Default", "menu-arcmenu-symbolic"),
            ("Brisk", "Brisk", "menu-brisk-symbolic"),
            ("Whisker", "Whisker", "menu-whisker-symbolic"),
            ("GNOME Menu", "GnomeMenu", "menu-gnomemenu-symbolic"),
            ("Mint", "Mint", "menu-mint-symbolic"),
            ("Budgie", "Budgie", "menu-budgie-symbolic"),
        ],
    ),
    (
        _("Modern"),
        [
            ("Unity", "Unity", "menu-unity-symbolic"),
            ("Plasma", "Plasma", "menu-plasma-symbolic"),
            ("Tognee", "Tognee", "menu-tognee-symbolic"),
            ("Insider", "Insider", "menu-insider-symbolic"),
            ("Redmond", "Redmond", "menu-redmond-symbolic"),
            ("Windows", "Windows", "menu-windows-symbolic"),
            ("11", "Eleven", "menu-eleven-symbolic"),
            ("a.z.", "AZ", "menu-az-symbolic"),
            ("Enterprise", "Enterprise", "menu-enterprise-symbolic"),
            ("Pop", "Pop", "menu-pop-symbolic"),
            ("Sleek", "Sleek", "menu-sleek-symbolic"),
            ("Zest", "Zest", "menu-zest-symbolic"),
        ],
    ),
    (
        _("Touch"),
        [
            ("Elementary", "Elementary", "menu-elementary-symbolic"),
            ("Chromebook", "Chromebook", "menu-chromebook-symbolic"),
        ],
    ),
    (
        _("Launcher"),
        [
            ("Runner", "Runner", "menu-runner-symbolic"),
            ("GNOME Overview", "GnomeOverview", "menu-gnomeoverview-symbolic"),
        ],
    ),
    (
        _("Alternative"),
        [
            ("Raven", "Raven", "menu-raven-symbolic"),
        ],
    ),
]

LAYOUTS_BY_VALUE = {
    value: (title, icon_name)
    for _group_title, layouts in MENU_LAYOUT_GROUPS
    for title, value, icon_name in layouts
}


def register_arcmenu_resources():
    global _arcmenu_resource_registered, _css_provider_registered

    if not _arcmenu_resource_registered and os.path.exists(ARCMENU_RESOURCE):
        Gio.Resource._register(Gio.resource_load(ARCMENU_RESOURCE))
        _arcmenu_resource_registered = True

    display = Gdk.Display.get_default()
    if display is not None:
        icon_theme = Gtk.IconTheme.get_for_display(display)
        icon_theme.add_resource_path(ARCMENU_ICON_PATH)

        if not _css_provider_registered:
            css_provider = Gtk.CssProvider()
            css_provider.load_from_data(
                b"""
                .arc-menu-layout-tile image,
                .arc-menu-layout-tile label {
                    color: @window_fg_color;
                }

                .arc-menu-layout-tile-selected image,
                .arc-menu-layout-tile-selected label {
                    color: @accent_color;
                }
                """
            )
            Gtk.StyleContext.add_provider_for_display(
                display,
                css_provider,
                Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
            )
            _css_provider_registered = True


def load_schema_defaults():
    global _schema_defaults

    if _schema_defaults is not None:
        return _schema_defaults

    _schema_defaults = {}
    if not os.path.exists(ARCMENU_SCHEMA_XML):
        return _schema_defaults

    root = ET.parse(ARCMENU_SCHEMA_XML).getroot()
    for schema in root.findall("schema"):
        if schema.get("id") != "org.gnome.shell.extensions.arcmenu":
            continue

        for key in schema.findall("key"):
            default = key.find("default")
            if default is not None and default.text:
                _schema_defaults[key.get("name")] = default.text.strip()
        break

    return _schema_defaults


def apply_upstream_layout_defaults(settings):
    defaults = load_schema_defaults()
    schema = settings.props.settings_schema
    for key in LAYOUT_SETTING_DEFAULT_KEYS:
        if key not in defaults or not schema.has_key(key):
            continue

        value_type = settings.get_value(key).get_type()
        default_value = GLib.Variant.parse(value_type, defaults[key], None, None)
        settings.set_value(key, default_value)


def get_recolored_layout_icon(icon_name, max_width, max_height):
    register_arcmenu_resources()

    icon_bytes = None
    for candidate in (icon_name, *ARCMENU_ICON_ALIASES.get(icon_name, ())):
        icon_path = f"{ARCMENU_ICON_PATH}/{candidate}.svg"
        try:
            icon_bytes = Gio.resources_lookup_data(
                icon_path, Gio.ResourceLookupFlags.NONE
            ).get_data()
            break
        except GLib.Error:
            continue

    # A missing icon should not prevent the entire layout dialog from opening.
    if icon_bytes is None:
        return None

    style_manager = Adw.StyleManager.get_default()
    foreground = "#ffffff" if style_manager.get_dark() else "#000000"
    svg_text = icon_bytes.decode("utf-8")
    svg_text = svg_text.replace("#000000", foreground)
    svg_text = svg_text.replace("#000", foreground)

    loader = GdkPixbuf.PixbufLoader.new_with_type("svg")
    loader.write(svg_text.encode("utf-8"))
    loader.close()
    pixbuf = loader.get_pixbuf()
    if pixbuf is None:
        return None

    width = pixbuf.get_width()
    height = pixbuf.get_height()
    if width <= 0 or height <= 0:
        return pixbuf

    scale = min(max_width / width, max_height / height)
    scaled_width = max(1, int(width * scale))
    scaled_height = max(1, int(height * scale))
    return pixbuf.scale_simple(
        scaled_width,
        scaled_height,
        GdkPixbuf.InterpType.BILINEAR,
    )


class ArcMenuLayoutRow(Adw.ActionRow):
    def __init__(self, gsetting: GSetting):
        self.gsetting = gsetting
        register_arcmenu_resources()

        super().__init__(title=self.gsetting.title, subtitle=self.gsetting.subtitle)
        self.set_activatable(True)

        self.layout_button = Gtk.Button(valign=Gtk.Align.CENTER)
        self.layout_button.connect("clicked", self.open_layout_dialog)
        self.add_suffix(self.layout_button)
        self.set_activatable_widget(self.layout_button)

        self.update_button_label()
        self.gsetting.settings.connect("changed", self.on_setting_changed)

    def get_current_value(self):
        return self.gsetting.settings.get_string(self.gsetting.key)

    def get_current_title(self):
        value = self.get_current_value()
        return _(LAYOUTS_BY_VALUE.get(value, (value, None))[0])

    def update_button_label(self):
        self.layout_button.set_label(self.get_current_title())

    def on_setting_changed(self, _settings, key):
        if key == self.gsetting.key:
            self.update_button_label()

    def open_layout_dialog(self, _button):
        dialog = ArcMenuLayoutDialog(self.gsetting)
        dialog.present(self.get_root())


class ArcMenuLayoutDialog(Adw.Dialog):
    def __init__(self, gsetting: GSetting):
        self.gsetting = gsetting
        self.layout_tiles = {}
        register_arcmenu_resources()

        super().__init__(title=_("Menu Layout"))
        self.set_content_width(780)
        self.set_content_height(660)

        toolbar_view = Adw.ToolbarView()
        header = Adw.HeaderBar()
        toolbar_view.add_top_bar(header)

        scrolled = Gtk.ScrolledWindow(vexpand=True)
        clamp = Adw.Clamp(margin_top=18, margin_bottom=18)
        clamp.set_maximum_size(640)
        content_box = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL,
            spacing=18,
            margin_start=18,
            margin_end=18,
        )
        clamp.set_child(content_box)
        scrolled.set_child(clamp)
        toolbar_view.set_content(scrolled)
        self.set_child(toolbar_view)

        for group_title, layouts in MENU_LAYOUT_GROUPS:
            group = Adw.PreferencesGroup(title=group_title)
            flow_box = Gtk.FlowBox(
                column_spacing=8,
                row_spacing=8,
                homogeneous=True,
                min_children_per_line=3,
                max_children_per_line=3,
                selection_mode=Gtk.SelectionMode.NONE,
                valign=Gtk.Align.START,
            )
            group.add(flow_box)
            content_box.append(group)

            for title, value, icon_name in layouts:
                tile = ArcMenuLayoutTile(
                    title, value, icon_name, self.on_tile_activated
                )
                self.layout_tiles[value] = tile
                flow_box.append(tile)

        self.update_selection()
        self.gsetting.settings.connect("changed", self.on_setting_changed)
        Adw.StyleManager.get_default().connect(
            "notify::dark", self.on_style_dark_changed
        )

    def on_tile_activated(self, tile):
        if self.gsetting.settings.get_string(self.gsetting.key) != tile.value:
            apply_upstream_layout_defaults(self.gsetting.settings)
            self.gsetting.settings.set_string(self.gsetting.key, tile.value)
        self.close()

    def on_setting_changed(self, _settings, key):
        if key == self.gsetting.key:
            self.update_selection()

    def update_selection(self):
        current_value = self.gsetting.settings.get_string(self.gsetting.key)
        for value, tile in self.layout_tiles.items():
            tile.set_selected(value == current_value)

    def on_style_dark_changed(self, _style_manager, _dark):
        for tile in self.layout_tiles.values():
            tile.refresh_icon()


class ArcMenuLayoutTile(Gtk.FlowBoxChild):
    def __init__(self, title, value, icon_name, activate_callback):
        self.title = title
        self.value = value
        self.icon_name = icon_name
        self.activate_callback = activate_callback

        super().__init__()
        self.add_css_class("card")
        self.add_css_class("activatable")
        self.add_css_class("arc-menu-layout-tile")
        self.set_focusable(True)
        self.set_size_request(164, 154)

        box = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL,
            spacing=8,
            margin_top=12,
            margin_bottom=12,
            margin_start=8,
            margin_end=8,
        )
        self.set_child(box)

        self.image = Gtk.Image()
        self.image.set_halign(Gtk.Align.CENTER)
        self.image.set_valign(Gtk.Align.CENTER)
        self.image.set_size_request(112, 82)
        box.append(self.image)
        self.refresh_icon()

        label_box = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            spacing=4,
            halign=Gtk.Align.CENTER,
        )
        box.append(label_box)

        self.check = Gtk.Image(icon_name="object-select-symbolic", pixel_size=16)
        self.check.add_css_class("accent")
        label_box.append(self.check)

        self.label = Gtk.Label(label=_(title))
        self.label.add_css_class("caption")
        self.label.set_ellipsize(Pango.EllipsizeMode.END)
        self.label.set_max_width_chars(16)
        label_box.append(self.label)

        click = Gtk.GestureClick()
        click.connect("released", self.on_released)
        self.add_controller(click)

        key = Gtk.EventControllerKey()
        key.connect("key-released", self.on_key_released)
        self.add_controller(key)

    def on_released(self, _gesture, _n_press, _x, _y):
        self.activate_callback(self)

    def on_key_released(self, _controller, keyval, _keycode, _state):
        if keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter, Gdk.KEY_space):
            self.activate_callback(self)
            return GLib.SOURCE_STOP
        return GLib.SOURCE_CONTINUE

    def refresh_icon(self):
        pixbuf = get_recolored_layout_icon(self.icon_name, 112, 82)
        if pixbuf is None:
            self.image.set_from_icon_name("application-x-executable-symbolic")
            self.image.set_pixel_size(64)
        else:
            self.image.set_from_pixbuf(pixbuf)

    def set_selected(self, selected):
        self.check.set_visible(selected)
        if selected:
            self.add_css_class("arc-menu-layout-tile-selected")
            self.image.add_css_class("accent")
            self.label.add_css_class("accent")
        else:
            self.remove_css_class("arc-menu-layout-tile-selected")
            self.image.remove_css_class("accent")
            self.label.remove_css_class("accent")
