import math
import os
from typing import List

import gi
import json

gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gio, Gtk, GLib

from stillControl import GSetting
from stillControl.i18n import _
import stillControl.GSettingComboRow as GSettingComboRow


DTP_PANEL_SCHEMA = "org.gnome.shell.extensions.dash-to-panel"
DTP_PANEL_ELEMENTS_KEY = "panel-element-positions"
DTP_ELEMENT_DEFAULTS = [
    {"element": "showAppsButton", "visible": True, "position": "stackedTL"},
    {"element": "activitiesButton", "visible": False, "position": "stackedTL"},
    {"element": "leftBox", "visible": True, "position": "stackedTL"},
    {"element": "taskbar", "visible": True, "position": "stackedTL"},
    {"element": "centerBox", "visible": True, "position": "stackedBR"},
    {"element": "rightBox", "visible": True, "position": "stackedBR"},
    {"element": "dateMenu", "visible": True, "position": "stackedBR"},
    {"element": "systemMenu", "visible": True, "position": "stackedBR"},
    {"element": "desktopButton", "visible": True, "position": "stackedBR"},
]
DTP_ELEMENT_LABELS = {
    "showAppsButton": _("Show Applications Button"),
    "activitiesButton": _("Activities Button"),
    "leftBox": _("Left Box"),
    "taskbar": _("Taskbar"),
    "centerBox": _("Center Box"),
    "rightBox": _("Right Box"),
    "dateMenu": _("Date Menu"),
    "systemMenu": _("System Menu"),
    "desktopButton": _("Desktop Button"),
}
ARC_MENU_SCHEMA = "org.gnome.shell.extensions.arcmenu"
ARC_MENU_HOTKEY_KEY = "arcmenu-hotkey"
ARC_MENU_OVERLAY_KEY = "arcmenu-hotkey-overlay-key-enabled"
MUTTER_SCHEMA = "org.gnome.mutter"
MUTTER_OVERLAY_KEY = "overlay-key"
SUPER_KEYS = ("Super_L", "Super_R", "Super")

class DashToPanelMonitorComboRow(GSettingComboRow.GSettingComboRow):
    def __init__(self, gsetting: GSetting, values: List[str], display: List[str]):
        super().__init__(gsetting, values, display)

    def get_setting_value(self):
        settings = json.loads(self.gsetting.settings.get_string(self.gsetting.key))
        try:
            return list(settings.values())[0]
        except IndexError: 
            return ""

    def set_setting_value(self, value):
        settings = json.loads(self.gsetting.settings.get_string(self.gsetting.key))
        for monitor in settings:
            settings[monitor] = value
        self.gsetting.settings.set_string(self.gsetting.key, json.dumps(settings))

    def on_setting_changed(self, settings: Gio.Settings, key: str):
        if key == self.gsetting.key:
            current_value = self.get_setting_value()
            if current_value != self.values[self.get_selected()]:
                try:
                    index = self.values.index(current_value)
                except ValueError:
                    self.display_list_store.append(Gtk.StringObject.new(current_value))
                    self.values.append(current_value)
                    self.add_css_class("error")
                    index = len(self.values) - 1
                self.set_selected(index)

    def on_row_changed(self, row: Gtk.ComboBoxText, _selected):
        value_index = row.get_selected()
        if not value_index >= self.original_length:
            while len(self.values) > self.original_length:
                del self.values[-1]
                self.display_list_store.remove(self.display_list_store.get_n_items() - 1)
            self.remove_css_class("error")

        current_value = self.get_setting_value()
        if current_value != self.values[row.get_selected()]:
            self.set_setting_value(self.values[value_index])


def dtp_monitor_spin_setting_changed(settings, key, spin_row, gsetting):
    if key == gsetting.key:
        settings = json.loads(gsetting.settings.get_string(gsetting.key))

        try:
            new_value = list(settings.values())[0]

            if int(spin_row.get_value()) != int(new_value):
                spin_row.set_value(new_value)

        except IndexError: 
            new_value = 0

def dtp_monitor_spin_row_changed(spin_row, _value, gsetting):
    spin_row.set_sensitive(False)
    # Makes the row not sensitive for 0.5 seconds to prevent spamming the setting and causing lag

    value = spin_row.get_value()

    settings = json.loads(gsetting.settings.get_string(gsetting.key))
    for monitor in settings:
        settings[monitor] = math.floor(value)
    gsetting.settings.set_string(gsetting.key, json.dumps(settings))

    # Makes the row sensitive again after 0.5 seconds
    GLib.timeout_add(500, lambda: spin_row.set_sensitive(True))


class DashToPanelElementsRow(Adw.ExpanderRow):
    def __init__(self):
        super().__init__(
            title=_("Panel Elements"),
            subtitle=_("Show, hide, and reorder built-in panel items."),
        )
        self.settings = Gio.Settings.new(DTP_PANEL_SCHEMA)
        self.element_rows = []
        self.elements = []
        self._updating = False

        self.settings.connect(f"changed::{DTP_PANEL_ELEMENTS_KEY}", self.on_setting_changed)
        self.refresh()

    def refresh(self):
        for row in self.element_rows:
            self.remove(row)
        self.element_rows.clear()

        self.elements = self.get_elements()
        visible_count = len([element for element in self.elements if element.get("visible", True)])
        self.set_subtitle(_("{visible} of {total} visible").format(
            visible=visible_count,
            total=len(self.elements),
        ))

        for index, element in enumerate(self.elements):
            row = DashToPanelElementItemRow(self, element, index)
            self.element_rows.append(row)
            self.add_row(row)

    def get_settings_dict(self):
        try:
            value = self.settings.get_string(DTP_PANEL_ELEMENTS_KEY)
            parsed = json.loads(value) if value else {}
            if isinstance(parsed, dict):
                return parsed
        except Exception:
            pass
        return {}

    def get_elements(self):
        settings_dict = self.get_settings_dict()
        template = None
        for monitor_elements in settings_dict.values():
            if isinstance(monitor_elements, list) and monitor_elements:
                template = monitor_elements
                break
        if template is None:
            template = DTP_ELEMENT_DEFAULTS

        elements = []
        seen = set()
        default_by_id = {
            element["element"]: element
            for element in DTP_ELEMENT_DEFAULTS
        }

        for item in template:
            if not isinstance(item, dict):
                continue
            element_id = item.get("element")
            if not element_id or element_id in seen:
                continue
            element = dict(default_by_id.get(element_id, {}))
            element.update(item)
            elements.append(element)
            seen.add(element_id)

        for default_element in DTP_ELEMENT_DEFAULTS:
            element_id = default_element["element"]
            if element_id not in seen:
                element = dict(default_element)
                element["visible"] = False
                elements.append(element)

        return elements

    def save_elements(self):
        settings_dict = self.get_settings_dict()
        monitors = list(settings_dict.keys()) or ["0"]
        serializable_elements = [
            {
                "element": element.get("element"),
                "visible": bool(element.get("visible", True)),
                "position": element.get("position", "stackedTL"),
            }
            for element in self.elements
            if element.get("element")
        ]

        self._updating = True
        for monitor in monitors:
            settings_dict[str(monitor)] = json.loads(json.dumps(serializable_elements))
        self.settings.set_string(DTP_PANEL_ELEMENTS_KEY, json.dumps(settings_dict))
        self._updating = False

    def set_element_visible(self, index, visible):
        self.elements[index]["visible"] = visible
        self.save_elements()
        self.refresh()

    def move_element(self, index, offset):
        new_index = index + offset
        if new_index < 0 or new_index >= len(self.elements):
            return
        self.elements[index], self.elements[new_index] = self.elements[new_index], self.elements[index]
        self.save_elements()
        self.refresh()

    def on_setting_changed(self, _settings, _key):
        if not self._updating:
            self.refresh()


class DashToPanelElementItemRow(Adw.ActionRow):
    def __init__(self, parent_row, element, index):
        self.parent_row = parent_row
        self.element = element
        self.index = index
        element_id = element.get("element", "")
        label = DTP_ELEMENT_LABELS.get(element_id, element_id)

        super().__init__(title=label)

        self.visible_button = Gtk.ToggleButton(
            active=bool(element.get("visible", True)),
            valign=Gtk.Align.CENTER,
            tooltip_text=_("Show or hide"),
        )
        self.visible_button.add_css_class("flat")
        self.visible_button.add_css_class("circular")
        self.visible_image = Gtk.Image()
        self.visible_button.set_child(self.visible_image)
        self.update_visible_icon()
        self.visible_button.connect("toggled", self.on_visible_toggled)

        self.up_button = self.create_icon_button("go-up-symbolic", _("Move up"))
        self.up_button.set_sensitive(index > 0)
        self.up_button.connect("clicked", lambda _button: self.parent_row.move_element(self.index, -1))

        self.down_button = self.create_icon_button("go-down-symbolic", _("Move down"))
        self.down_button.set_sensitive(index < len(self.parent_row.elements) - 1)
        self.down_button.connect("clicked", lambda _button: self.parent_row.move_element(self.index, 1))

        self.add_suffix(self.visible_button)
        self.add_suffix(self.up_button)
        self.add_suffix(self.down_button)

    def create_icon_button(self, icon_name, tooltip):
        button = Gtk.Button.new_from_icon_name(icon_name)
        button.set_valign(Gtk.Align.CENTER)
        button.set_tooltip_text(tooltip)
        button.add_css_class("flat")
        button.add_css_class("circular")
        return button

    def update_visible_icon(self):
        icon_name = (
            "eye-open-negative-filled-symbolic"
            if self.visible_button.get_active()
            else "eye-not-looking-symbolic"
        )
        self.visible_image.set_from_icon_name(icon_name)

    def on_visible_toggled(self, button):
        self.update_visible_icon()
        self.parent_row.set_element_visible(self.index, button.get_active())


class ArcMenuSuperKeyRow(Adw.ComboRow):
    ACTIVITIES = "activities"
    ARC_MENU = "arc-menu"

    def __init__(self):
        super().__init__(
            title=_("Super Key Opens"),
            subtitle=_("Choose whether pressing Super opens Activities or Arc Menu."),
        )
        self.arc_settings = Gio.Settings.new(ARC_MENU_SCHEMA)
        self.mutter_settings = Gio.Settings.new(MUTTER_SCHEMA)
        self.values = [self.ACTIVITIES, self.ARC_MENU]
        self._updating = False

        self.display_list_store = Gio.ListStore()
        self.display_list_store.append(Gtk.StringObject.new(_("Activities")))
        self.display_list_store.append(Gtk.StringObject.new(_("Arc Menu")))
        self.set_model(self.display_list_store)

        self.arc_settings.connect(f"changed::{ARC_MENU_HOTKEY_KEY}", self.on_setting_changed)
        self.arc_settings.connect(f"changed::{ARC_MENU_OVERLAY_KEY}", self.on_setting_changed)
        self.refresh()
        self.connect("notify::selected", self.on_row_changed)

    def get_current_value(self):
        hotkeys = self.arc_settings.get_strv(ARC_MENU_HOTKEY_KEY)
        super_key_enabled = any(hotkey in SUPER_KEYS for hotkey in hotkeys)
        overlay_enabled = self.arc_settings.get_boolean(ARC_MENU_OVERLAY_KEY)
        if super_key_enabled and overlay_enabled:
            return self.ARC_MENU
        return self.ACTIVITIES

    def refresh(self):
        current_value = self.get_current_value()
        selected = self.values.index(current_value)
        if self.get_selected() != selected:
            self._updating = True
            self.set_selected(selected)
            self._updating = False

    def on_setting_changed(self, *_args):
        if self._updating:
            return
        self.refresh()

    def on_row_changed(self, row, _selected):
        if self._updating:
            return

        value = self.values[row.get_selected()]
        hotkeys = [
            hotkey for hotkey in self.arc_settings.get_strv(ARC_MENU_HOTKEY_KEY)
            if hotkey not in SUPER_KEYS
        ]

        self._updating = True
        try:
            if value == self.ARC_MENU:
                hotkeys.insert(0, "Super_L")
                self.arc_settings.set_strv(ARC_MENU_HOTKEY_KEY, hotkeys)
                self.arc_settings.set_boolean(ARC_MENU_OVERLAY_KEY, True)
            else:
                self.arc_settings.set_strv(ARC_MENU_HOTKEY_KEY, hotkeys)
                self.arc_settings.set_boolean(ARC_MENU_OVERLAY_KEY, False)
                self.mutter_settings.set_string(MUTTER_OVERLAY_KEY, "Super_L")
        finally:
            self._updating = False
            self.refresh()
