#!/usr/bin/env python3
import sys
import os
import logging
import gi
import json

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GObject, GLib

# Add the lib directory to sys.path to find local modules
lib_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "lib")
sys.path.insert(0, lib_path)

# Import the original indicator class
from indicator_sound_switcher.indicator import SoundSwitcherIndicator, APP_ID, APP_NAME


# Mocking the Menu Item since we are not using a real Menu
class MockMenuItem:
    def __init__(self, widget):
        self.widget = widget
        self.handler_id = None

    def connect(self, signal, callback, data=None):
        if signal == "activate":
            # For a button, it is "clicked"
            if isinstance(self.widget, Gtk.Button):
                if data:
                    self.handler_id = self.widget.connect(
                        "clicked", lambda w: callback(w, data)
                    )
                else:
                    self.handler_id = self.widget.connect("clicked", callback)
            # For a RadioButton, it is "toggled" but we only care when it becomes active
            elif isinstance(self.widget, Gtk.RadioButton) or isinstance(
                self.widget, Gtk.CheckButton
            ):
                # Wrapper to match the signature expected by on_select_port
                def on_toggled(widget):
                    if widget.get_active():
                        if data:
                            callback(widget, data)
                        else:
                            callback(widget)

                self.handler_id = self.widget.connect("toggled", on_toggled)
            return self.handler_id

    def set_sensitive(self, sensitive):
        self.widget.set_sensitive(sensitive)

    def set_active(self, active):
        if isinstance(self.widget, Gtk.ToggleButton):
            self.widget.set_active(active)

    def get_active(self):
        if isinstance(self.widget, Gtk.ToggleButton):
            return self.widget.get_active()
        return False

    def handler_block(self, handler_id):
        return self.widget.handler_block(handler_id)


class SoundSwitcherWindow(SoundSwitcherIndicator):
    def __init__(self):
        # We act as a GObject, but we don't call SoundSwitcherIndicator.__init__
        # because it initializes AppIndicator which we don't want.
        # Instead, we manually initialize what we need from it.
        GObject.GObject.__init__(self)

        # -- Copied/Adapted from SoundSwitcherIndicator.__init__ --

        # Initialise PulseAudio object lists and references
        self.cards = {}
        self.sources = {}
        self.source_outputs = {}
        self.sinks = {}
        self.sink_inputs = {}
        # ... (PA callbacks initialized below) ...
        self.pa_context = None
        self.pa_context_connected = False
        self.pa_context_failed = False
        self.pa_connecting = False

        # Initialise menu items placeholders
        self.item_header_inputs = None
        self.item_separator_inputs = None
        self.item_header_outputs = None
        self.item_separator_outputs = None

        # Load configuration
        self.config_file_name = os.path.join(
            GLib.get_user_config_dir(), APP_ID + ".json"
        )
        self.config = self.config_load()
        self.config_devices = self.config["devices"]

        # Initialise the keyboard manager
        # Note: Keyboard shortcuts might not work as expected in a window without focus logic,
        # but we keep it to not break logic.
        from indicator_sound_switcher.config import KeyboardManager

        self.keyboard_manager = KeyboardManager(self.on_port_keyboard_shortcut)
        self.keyboard_manager.bind_keys(self.config)

        # -- Window Specific Setup --
        titulo = _(APP_NAME)
        self.window = Gtk.Window(title=titulo)
        self.window.set_icon_name("indicator-sound-switcher")
        self.window.set_border_width(10)
        self.window.set_default_size(450, -1)
        self.window.connect("destroy", Gtk.main_quit)

        # Main mock menu container
        self.main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        self.window.add(self.main_box)

        # We treat this box as "self.menu".
        # Since the original code calls self.menu.remove(), self.menu.insert(), etc.
        # We need to adapt those calls or override methods that use them.
        # It's easier to override the 'menu_*' methods.
        self.menu = self.main_box

        # Initialise the PulseAudio interface (same as original)
        self.pa_mainloop = None
        self.pa_mainloop_api = None

        # Setup PulseAudio callbacks (imports from .lib_pulseaudio are needed)
        from indicator_sound_switcher.lib_pulseaudio import (
            pa_card_info_cb_t,
            pa_context_notify_cb_t,
            pa_context_subscribe_cb_t,
            pa_context_success_cb_t,
            pa_server_info_cb_t,
            pa_sink_info_cb_t,
            pa_sink_input_info_cb_t,
            pa_source_info_cb_t,
            pa_source_output_info_cb_t,
        )

        self._pacb_card_info = pa_card_info_cb_t(self.pacb_card_info)
        self._pacb_context_notify = pa_context_notify_cb_t(self.pacb_context_notify)
        self._pacb_context_subscribe = pa_context_subscribe_cb_t(
            self.pacb_context_subscribe
        )
        self._pacb_context_success = pa_context_success_cb_t(self.pacb_context_success)
        self._pacb_server_info = pa_server_info_cb_t(self.pacb_server_info)
        self._pacb_sink_info = pa_sink_info_cb_t(self.pacb_sink_info)
        self._pacb_sink_input_info = pa_sink_input_info_cb_t(self.pacb_sink_input_info)
        self._pacb_source_info = pa_source_info_cb_t(self.pacb_source_info)
        self._pacb_source_output_info = pa_source_output_info_cb_t(
            self.pacb_source_output_info
        )

        # Connect to the daemon
        self.pulseaudio_connect()

        self.window.show_all()

    # -- Persistence Logic --
    PERSISTENCE_FILE = os.path.expanduser("~/.config/vx-indicator-sound-switcher.json")

    def save_persistence(self, card_name, port_name):
        """Save the current selection to a file."""
        try:
            data = {"card_name": card_name, "port_name": port_name}
            with open(self.PERSISTENCE_FILE, "w") as f:
                json.dump(data, f)
            logging.info("Saved persistence: %s", data)
        except Exception as e:
            logging.error("Failed to save persistence: %s", e)

    def on_select_port(self, widget, data):
        """Override to save selection."""
        # Call original handler
        super().on_select_port(widget, data)

        # Save selection
        # data is (card_index, port_name)
        card_index, port_name = data

        # We need the card name, not index, for meaningful persistence across reboots
        # because indexes can change.
        if card_index in self.cards:
            card = self.cards[card_index]
            self.save_persistence(card.name, port_name)
        elif card_index == -1:  # CARD_NONE_SINK
            # For virtual sinks, we might need a different strategy or just use 'Virtual'
            # But find_card_port_by_name expects card names.
            # Virtual devices in this app have virtual entries in self.sinks/sources
            # but activate_port handles them specially.
            # If it's a sink:
            if (
                port_name in self.sinks
            ):  # Actually port_name here is the index for virtual items in on_select_port call structure?
                # Wait, look at indicator.py:
                # port.menu_item.connect('activate', self.on_select_port, (CARD_NONE_SINK, index))
                # So data is (CARD_NONE_SINK, sink_index)
                idx = port_name  # it's the index
                if idx in self.sinks:
                    sink = self.sinks[idx]
                    # We can't easily persist virtual devices by name with current find logic easily??
                    # find_card_port_by_name iterates self.cards.
                    # But let's check find_card_port_by_name again.
                    pass
        # For now let's support physical cards which is the main use case

    # -- Persistence Logic -- ## FIN

    def run(self):
        # Schedule an immediate refresh to fix initial menu items
        GLib.idle_add(self.on_refresh)
        Gtk.main()

    # -- Overrides to handle UI construction in a Window/Box instead of a Menu --

    def menu_setup(self):
        """Initialise the window widgets."""
        # Remove all children
        for child in self.main_box.get_children():
            self.main_box.remove(child)

        # Make the input list section, if needed
        if bool(self.config["show_inputs", True]):
            self.item_header_inputs = self.menu_append_item(_("Inputs"), is_header=True)
            self.item_separator_inputs = self.menu_append_item(is_separator=True)
        else:
            self.item_header_inputs = None
            self.item_separator_inputs = None

        # Make the output list section, if needed
        if bool(self.config["show_outputs", True]):
            self.item_header_outputs = self.menu_append_item(
                _("Outputs"), is_header=True
            )
            self.item_separator_outputs = self.menu_append_item(is_separator=True)
        else:
            self.item_header_outputs = None
            self.item_separator_outputs = None

        # Add static items
        self.menu_append_item(is_separator=True)

        # Button box for actions
        button_box = Gtk.ButtonBox(orientation=Gtk.Orientation.HORIZONTAL)
        button_box.set_layout(Gtk.ButtonBoxStyle.EXPAND)

        btn_refresh = Gtk.Button(label=_("Refresh"))
        btn_refresh.connect("clicked", self.on_refresh)
        button_box.add(btn_refresh)

        btn_prefs = Gtk.Button(label=_("Preferences"))
        btn_prefs.connect("clicked", self.on_preferences)
        button_box.add(btn_prefs)

        # btn_about = Gtk.Button(label=_("About"))
        # btn_about.connect("clicked", self.on_about)
        # button_box.add(btn_about)

        btn_quit = Gtk.Button(label=_("Quit"))
        btn_quit.connect("clicked", self.on_quit)
        button_box.add(btn_quit)

        self.main_box.pack_start(button_box, False, False, 0)
        self.main_box.show_all()

    def menu_append_item(
        self,
        label: str = None,
        activate_signal: callable = None,
        is_header=False,
        is_separator=False,
    ):
        """Add a item to the box."""
        widget = None
        if is_separator:
            widget = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
        elif is_header:
            widget = Gtk.Label(label=label, xalign=0)
            attrs = Gtk.check_version(3, 12, 0)
            if attrs is None:  # None means version is OK
                widget.set_markup("<b>" + label + "</b>")
        else:
            # Just a generic label or button if we used this for regular items,
            # but regular items use menu_insert_ordered_item
            widget = Gtk.Label(label=label)

        if widget:
            self.main_box.pack_start(widget, False, False, 0)
            widget.show()
            return widget  # Return the widget itself as the "item"

    def menu_insert_ordered_item(self, after_item, before_item, label: str, show: bool):
        """Insert a radio button for the device."""

        # Determine the group. In the original code, `after_item` is the header.
        # All items in a section should share a group.
        # But here we need to find an existing RadioButton to group with.

        group = None

        # We need to find where to insert.
        # self.main_box.get_children() gives us the list.
        children = self.main_box.get_children()

        idx_from = 0 if after_item is None else children.index(after_item) + 1
        idx_to = (
            children.index(before_item) if before_item in children else len(children)
        )

        # Look for an existing RadioButton in the range to get the group
        for i in range(idx_from, idx_to):
            child = children[i]
            if isinstance(child, Gtk.RadioButton):
                group = child
                break

        new_item = Gtk.RadioButton.new_with_label_from_widget(group, label)
        new_item.set_alignment(0.0, 0.5)  # Align left

        # Find appropriate position
        insert_idx = idx_from
        while insert_idx < idx_to:
            child = children[insert_idx]
            if isinstance(child, Gtk.RadioButton):
                if label >= child.get_label():
                    insert_idx += 1
                else:
                    break
            else:
                # Should not really happen if structure is header -> [radios] -> separator
                # but if there are other things, skip them?
                # Actually, if we hit the separator (before_item), the loop condition (insert_idx < idx_to) handles it.
                insert_idx += 1

        self.main_box.pack_start(new_item, False, False, 0)
        self.main_box.reorder_child(new_item, insert_idx)

        if show:
            new_item.show()

        return MockMenuItem(new_item)

    # We need to override card_create_menu_items because it relies on `self.menu.remove(port.menu_item)`
    # The original accesses `port.menu_item` which we are wrapping in MockMenuItem.
    # The original usage: `self.menu.remove(port.menu_item)`
    # In our case `self.menu` is `self.main_box`.
    # `port.menu_item` is `MockMenuItem`. `MockMenuItem.widget` is the `Gtk.RadioButton`.
    # `Gtk.Box.remove` expects the widget.

    # Wait, the original code for `card_remove` calls:
    # `self.menu.remove(port.menu_item)`
    # We need to intercept this `remove` call if we want to support removal seamlessly,
    # OR we override `card_remove` etc.
    # Overriding the list cleanup methods seems safer but `menu` attribute is accessed directly in many places.

    # Hack: Monkey patch `self.main_box.remove` to handle MockMenuItem?
    # Or better define a `remove` method on our `SoundSwitcherWindow` that shadows `SoundSwitcherIndicator` usage?
    # No, `SoundSwitcherIndicator` uses `self.menu.remove(item)`. `self.menu` is `self.main_box`.
    # `self.main_box` is a Gtk.Box. It has a `remove` method.
    # But it expects a Widget, and we are storing MockMenuItem in port.menu_item.

    # We should override `menu_insert_ordered_item` to return the WIDGET,
    # but `MockMenuItem` is needed because `SoundSwitcherIndicator` calls `.connect` on the result.
    # And Gtk.MenuItem.connect is slightly different from Gtk.RadioButton (toggled vs activate).

    # Let's make MockMenuItem proxy everything to the widget, but when `remove` is called on the container,
    # it needs the widget.

    # Solution: We can wrap `self.menu` (the box) with a wrapper class that handles `remove` of MockMenuItem.

    class MenuWrapper:
        def __init__(self, box):
            self.box = box

        def remove(self, item):
            if isinstance(item, MockMenuItem):
                self.box.remove(item.widget)
            elif isinstance(item, Gtk.Widget):
                self.box.remove(item)
            else:
                pass  # item might be None or something else

        def append(self, widget):
            self.box.pack_start(widget, False, False, 0)

        def insert(self, widget, index):
            self.box.pack_start(widget, False, False, 0)
            self.box.reorder_child(widget, index)

        def get_children(self):
            return self.box.get_children()

        def pack_start(self, widget, expand, fill, padding):
            self.box.pack_start(widget, expand, fill, padding)

        def show_all(self):
            self.box.show_all()

        # Add getattr to proxy other calls to box
        def __getattr__(self, name):
            return getattr(self.box, name)

    # In __init__, we set self.menu = MenuWrapper(self.main_box)


if __name__ == "__main__":
    # Initialize translation (copied from __init__.py) and setup locale
    import gettext
    import locale

    # Set the default locale from the environment variables
    try:
        locale.setlocale(locale.LC_ALL, "")
    except locale.Error:
        logging.warning("Unable to set locale")

    # Explicitly tell gettext where to look for translations
    # The standard path is usually /usr/share/locale
    localedir = "/usr/share/locale"
    _ = gettext.gettext
    gettext.bindtextdomain("vx-indicator-sound-switcher", localedir)
    gettext.textdomain("vx-indicator-sound-switcher")
    # gettext.bindtextdomain(APP_ID, localedir)
    # gettext.textdomain(APP_ID)
    # gettext.install(APP_ID, localedir)

    # Set up logging
    logging.basicConfig(level=logging.WARNING, format="%(levelname)-8s %(message)s")

    # Check for lock? Maybe skip for window version or use a different lock.
    # Skipping lock to allow multiple windows if desired, or just to avoid conflict with the indicator.

    win = SoundSwitcherWindow()
    # Replace self.menu with the wrapper properly
    win.menu = SoundSwitcherWindow.MenuWrapper(win.main_box)

    # We also need to fix `menu_insert_ordered_item` in `SoundSwitcherWindow`
    # because I defined it there but `SoundSwitcherIndicator` calls `self.menu_insert_ordered_item`.
    # Wait, `SoundSwitcherIndicator` allows overriding.

    win.run()
