# window_button_row.py
#
# Copyright 2025 Frederick Schenk (CodedOre)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, 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 License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from collections.abc import Callable
from gettext import gettext as _
from typing import Any, cast

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

from refine.logger import logger
from refine.typing import LogicalDirectionType
from refine.widgets.base import BaseInterface


class ButtonNotFoundError(AssertionError): ...


class WindowMenuButton(Gtk.Button):
    __gtype_name__ = "WindowMenuButton"

    in_reorderable_box = GObject.Property(type=bool, default=True)
    popover = GObject.Property(type=Gtk.Popover, default=None)

    def __init__(self, button_id: str, **kwargs: Any) -> None:
        super().__init__(
            tooltip_text=button_id.capitalize(),
            **kwargs,
        )
        self.button_id = button_id
        self.add_css_class(button_id)
        self.add_css_class("window-menu-button")

        self.set_child(
            Gtk.Image.new_from_icon_name(f"window-{self.button_id}-symbolic")
        )

        self.popover = Gtk.PopoverMenu()
        self.popover.set_parent(self)

        drag_controller = Gtk.DragSource()
        drag_controller.connect("prepare", self._drag_prepare)
        drag_controller.connect("drag-begin", self._drag_begin)
        drag_controller.connect("drag-cancel", self._drag_end)
        drag_controller.connect("drag-end", self._drag_end)
        self.add_controller(drag_controller)

        self.connect("clicked", self._clicked)

    def _clicked(self, *_args: Any) -> None:
        if self.in_reorderable_box:
            menu = self._reorder_menu()
        else:
            menu = self._hidden_item_menu()

        self.popover.set_menu_model(menu)
        self.popover.popup()

    def _translate_alignment(
        self, alignment: Gtk.DirectionType
    ) -> LogicalDirectionType:
        # fmt: off
        match (alignment, self.get_direction()):
            case (Gtk.DirectionType.LEFT, (Gtk.TextDirection.NONE | Gtk.TextDirection.LTR)):
                return LogicalDirectionType.START
            case (Gtk.DirectionType.LEFT, Gtk.TextDirection.RTL):
                return LogicalDirectionType.END
            case (Gtk.DirectionType.RIGHT, (Gtk.TextDirection.NONE | Gtk.TextDirection.LTR)):
                return LogicalDirectionType.END
            case (Gtk.DirectionType.RIGHT, Gtk.TextDirection.RTL):
                return LogicalDirectionType.START
            case _:
                raise AssertionError
        # fmt: on

    def _hidden_item_menu(self) -> Gio.Menu:
        menu = Gio.Menu()

        menu.append(
            _("Move to _Left Corner"),
            f'window_button_row.show_item(("{self.button_id}",{int(self._translate_alignment(Gtk.DirectionType.LEFT))}))',
        )
        menu.append(
            _("Move to _Right Corner"),
            f'window_button_row.show_item(("{self.button_id}",{int(self._translate_alignment(Gtk.DirectionType.RIGHT))}))',
        )

        return menu

    def _reorder_menu(self) -> Gio.Menu:
        menu = Gio.Menu()

        menu.append(
            _("Move _Left"),
            f'window_button_row.move_item(("{self.button_id}",{int(self._translate_alignment(Gtk.DirectionType.LEFT))}))',
        )
        menu.append(
            _("Move _Right"),
            f'window_button_row.move_item(("{self.button_id}",{int(self._translate_alignment(Gtk.DirectionType.RIGHT))}))',
        )
        menu.append(_("_Hide"), f"window_button_row.hide_item::{self.button_id}")

        return menu

    def get_sibling_button(self, alignment: LogicalDirectionType) -> Gtk.Widget | None:
        """
        Get the next or previous sibling `WindowMenuButton`.

        Depends on the `LogicalDirectionType` provided.
        """
        sibling = self

        while True:
            match alignment:
                case LogicalDirectionType.START:
                    sibling = sibling.get_prev_sibling()  # type: ignore [assignment]
                case LogicalDirectionType.END:
                    sibling = sibling.get_next_sibling()  # type: ignore [assignment]
                case _:
                    raise AssertionError
            if sibling is None:
                return None
            if type(sibling) is WindowMenuButton:
                return sibling

    def _drag_prepare(
        self, _ctrl: Gtk.DragSource, _x: float, _y: float
    ) -> Gdk.ContentProvider:
        return Gdk.ContentProvider.new_for_value(self)

    def _drag_begin(self, ctrl: Gtk.DragSource, _drag: Gdk.Drag) -> None:
        icon = Gtk.WidgetPaintable.new(self)
        ctrl.set_icon(icon, 0, 0)
        self.add_css_class("dimmed")

    def _drag_end(self, *_args: Any) -> None:
        self.remove_css_class("dimmed")


class WindowButtonBin(Adw.Bin):
    __gtype_name__ = "WindowButtonBin"

    button_layout = GObject.Property(type=str, default="")
    available_buttons = GObject.Property(type=GObject.Strv)

    alignment = GObject.Property(type=Gtk.Align, default=Gtk.Align.START)
    reorderable = GObject.Property(type=bool, default=True)

    next_box = GObject.Property(type=Gtk.Widget, default=None)
    prev_box = GObject.Property(type=Gtk.Widget, default=None)

    _unknown_buttons: list[str] = []

    @GObject.Signal
    def changed(self) -> None:
        pass

    def __init__(self, **kwargs: Any) -> None:
        self.set_css_name("windowbuttonbox")
        super().__init__(**kwargs)
        self.connect("notify::button-layout", self._button_layout_set_callback)

        self.button_box = Gtk.Box(css_name="windowcontrols", homogeneous=True)
        self.bind_property("alignment", self.button_box, "halign")
        self.set_child(self.button_box)

        self._drop_place = Gtk.Image.new_from_icon_name("list-drag-handle-symbolic")
        self._drop_place.add_css_class("dimmed")
        self.button_box.append(self._drop_place)

        drop_controller = Gtk.DropTarget.new(
            type=WindowMenuButton, actions=Gdk.DragAction.COPY
        )
        drop_controller.connect("drop", self._on_drop)
        drop_controller.connect("motion", self._on_drag_motion)
        drop_controller.connect("leave", self._check_remaining_handles)
        self.add_controller(drop_controller)

    def _for_each_button(self, func: Callable[[WindowMenuButton], None]) -> None:
        next_child = self.button_box.get_first_child()

        while next_child is not None:
            child = next_child
            next_child = next_child.get_next_sibling()
            if type(child) is WindowMenuButton:
                func(child)

    def _button_layout_set_callback(self, *_args: Any) -> None:
        self._for_each_button(lambda button: self.button_box.remove(button))
        self._unknown_buttons = []

        if len(self.button_layout) != 0:
            buttons = self.button_layout.split(",")
            for button_id in buttons:
                if button_id in self.available_buttons:
                    button = WindowMenuButton(button_id)
                    button.in_reorderable_box = self.reorderable
                    self.button_box.append(button)
                else:
                    self._unknown_buttons.append(button_id)

        self._check_remaining_handles()

    def update_layout(self, *_args: Any) -> None:
        layout = []
        self._for_each_button(lambda button: layout.append(button.button_id))

        if self.alignment is Gtk.Align.END:
            layout = self._unknown_buttons + layout
        else:
            layout = layout + self._unknown_buttons

        self.button_layout = ",".join(layout)

    def _check_remaining_handles(self, *_args: Any) -> None:
        button_count = 0
        child = self.button_box.get_first_child()

        while child is not None:
            if type(child) is WindowMenuButton:
                button_count += 1
            child = child.get_next_sibling()
        self._drop_place.set_visible(button_count == 0)

    def _on_drag_motion(
        self, _ctrl: Gtk.DropTarget, x: float, y: float
    ) -> Gdk.DragAction:
        if self.reorderable:
            self._drop_place.set_visible(True)
            child = self._get_place_sibling(x, y)
            self.button_box.reorder_child_after(self._drop_place, child)
        return Gdk.DragAction.COPY

    def _on_drop(
        self, _ctrl: Gtk.DropTarget, button: WindowMenuButton, x: float, y: float
    ) -> None:
        button.unparent()
        button.in_reorderable_box = self.reorderable

        if self.reorderable:
            sibling = self._get_place_sibling(x, y)
            self.button_box.insert_child_after(button, sibling)
        else:
            self.button_box.append(button)

        self.emit("changed")

    def _get_place_sibling(self, x: float, y: float) -> Gtk.Widget | None:
        child = self.pick(x, y, Gtk.PickFlags.DEFAULT)
        child = self._get_box_child(child)  # type: ignore [arg-type]

        if child is None:
            if self.alignment is Gtk.Align.END:
                return None
            return self.button_box.get_last_child()

        child_width = child.get_width()

        if (x % child_width) > child_width / 2:
            return child
        return child.get_prev_sibling()

    def _get_box_child(self, child: Gtk.Widget) -> Gtk.Widget | None:
        if child is self:
            return None

        while True:
            parent = child.get_parent()
            if parent is None:
                return None
            if parent is self.button_box:
                return child
            child = parent

    def pick_button(self, button_id: str) -> Gtk.Widget | None:
        """Get button widget with the given button ID."""
        child = self.button_box.get_first_child()

        while child is not None:
            child = child.get_next_sibling()
            if type(child) is WindowMenuButton and child.button_id == button_id:
                return child

        return None

    def add_button(self, button: WindowMenuButton) -> None:
        """Append or prepend a button."""
        button.unparent()

        if self.alignment is Gtk.Align.END:
            self.button_box.prepend(button)
        else:
            self.button_box.append(button)

        self.emit("changed")

    def reorder_items(self, widget: Gtk.Widget, sibling: Gtk.Widget) -> None:
        """Reorder buttons and emit the `::changed` signal."""
        self.button_box.reorder_child_after(widget, sibling)
        self.emit("changed")


@Gtk.Template.from_resource("/page/tesk/Refine/widgets/window-buttons-row.ui")
class WindowButtonsRow(Adw.PreferencesRow, BaseInterface):
    __gtype_name__ = "WindowButtonsRow"

    start_box = Gtk.Template.Child()
    end_box = Gtk.Template.Child()
    hidden_box = Gtk.Template.Child()
    reset_button = Gtk.Template.Child()

    settings = cast("Gio.Settings", GObject.Property(type=Gio.Settings))
    schema_id = cast("str", GObject.Property(type=str))
    schema = cast("Gio.SettingsSchema", GObject.Property(type=Gio.SettingsSchema))
    key = cast("str", GObject.Property(type=str))
    is_valid_setting = cast("bool", GObject.Property(type=bool, default=False))

    is_default = GObject.Property(type=bool, default=True)
    button_layout = GObject.Property(type=str, default="")
    title = GObject.Property(type=str, default=None)
    subtitle = GObject.Property(type=str, default=None)
    available_buttons = GObject.Property(type=GObject.Strv)

    def _move_item(self, _widget: Any, _action_name: str, variant: Any) -> None:
        (button_id, alignment) = variant.unpack()
        alignment = LogicalDirectionType(alignment)
        box = None
        button = None

        for searched_box in [self.start_box, self.end_box]:
            button = searched_box.pick_button(button_id)
            if button is not None:
                box = searched_box
                break

        if not box or not button:
            raise IndexError

        button_sibling = button.get_sibling_button(alignment)

        match alignment:
            case LogicalDirectionType.START:
                if button_sibling is not None:
                    box.reorder_items(button_sibling, button)
                if button_sibling is None and box.prev_box is not None:
                    box.prev_box.add_button(button)
            case LogicalDirectionType.END:
                if button_sibling is not None:
                    box.reorder_items(button, button_sibling)
                if button_sibling is None and box.next_box is not None:
                    box.next_box.add_button(button)
            case _:
                logger.error("Unsupported alignment used in move_item")

    def _show_item(self, _widget: Any, _action_name: str, variant: Any) -> None:
        (button_id, alignment) = variant.unpack()
        alignment = LogicalDirectionType(alignment)
        button = self.hidden_box.pick_button(button_id)

        if not button:
            raise ButtonNotFoundError(button_id)

        match alignment:
            case LogicalDirectionType.START:
                self.start_box.add_button(button)
            case LogicalDirectionType.END:
                self.end_box.add_button(button)
            case _:
                logger.error("Unsupported alignment used in show_item")

    def _hide_item(self, _widget: Any, _action_name: str, variant: Any) -> None:
        button_id = variant.unpack()
        button = self.start_box.pick_button(button_id)

        if button is None:
            button = self.end_box.pick_button(button_id)

            if button is None:
                raise ButtonNotFoundError(button_id)

        self.hidden_box.add_button(button)

    def _layout_changed(self, *_args: Any) -> None:
        self.start_box.update_layout()
        self.end_box.update_layout()

        layout = f"{self.start_box.button_layout}:{self.end_box.button_layout}"
        logger.debug(f"Set key “{self.key}” to “{layout}”")

        self.settings.set_value(self.key, GLib.Variant.new_string(layout))

    def _button_layout_set_callback(self, *_args: Any) -> None:
        layout = self.button_layout.split(":")
        self.start_box.button_layout = layout[0]
        self.end_box.button_layout = layout[1]

        hidden_buttons = filter(
            lambda handle: handle not in self.button_layout, self.available_buttons
        )
        self.hidden_box.button_layout = ",".join(hidden_buttons)

        default_value = self.settings.get_default_value(self.key)
        self.is_default = default_value == self.settings.get_value(self.key)

        self.reset_button.set_visible(not self.is_default)

    def _reset_button_clicked(self, *_args: Any) -> None:
        logger.debug("Reset button pressed")
        self.settings.reset(self.key)

    def do_setup(self) -> None:
        self.install_action("window_button_row.show_item", "(si)", self._show_item)
        self.install_action("window_button_row.move_item", "(si)", self._move_item)
        self.install_action("window_button_row.hide_item", "s", self._hide_item)

        self.start_box.connect("changed", self._layout_changed)
        self.end_box.connect("changed", self._layout_changed)
        self.hidden_box.connect("changed", self._layout_changed)

        self.connect("notify::button-layout", self._button_layout_set_callback)
        self.settings.bind(
            self.key, self, "button-layout", Gio.SettingsBindFlags.DEFAULT
        )
        self.reset_button.connect("clicked", self._reset_button_clicked)
