# Released under the MIT License. See LICENSE for details.
#
"""Popup window/menu related functionality."""

import weakref
from typing import TYPE_CHECKING, override

import bauiv1 as bui
from bauiv1 import builtinassets

if TYPE_CHECKING:
    from typing import Any, Sequence, Callable, Literal


class PopupWindow:
    """A transient window that pops up from some position."""

    def __init__(
        self,
        position: tuple[float, float],
        size: tuple[float, float],
        scale: float = 1.0,
        *,
        offset: tuple[float, float] = (0, 0),
        bg_color: tuple[float, float, float] = (0.35, 0.55, 0.15),
        focus_position: tuple[float, float] = (0, 0),
        focus_size: tuple[float, float] | None = None,
        toolbar_visibility: Literal[
            'inherit',
            'menu_minimal_no_back',
            'menu_store_no_back',
        ] = 'menu_minimal_no_back',
        edge_buffer_scale: float = 1.0,
        darken_behind: bool = True,
    ):
        # pylint: disable=too-many-locals

        # Unique-per-instance prefix for widget ids -- the popup
        # analogue of MainWindow.main_window_id_prefix, and the same
        # `_idprefix` several popups already roll by hand (which is why
        # we leave theirs alone if they set it before calling us).
        #
        # Popups are the surface automation has the most trouble
        # reaching: they are modal, they appear unbidden, and several
        # can be onscreen at once, so labels alone are often ambiguous.
        # Subclasses should id at least their close/action buttons as
        # f'{self._idprefix}|<thing>'.
        if not hasattr(self, '_idprefix'):
            self._idprefix = bui.app.ui_v1.new_id_prefix(
                type(self).__name__.lower()
            )

        if focus_size is None:
            focus_size = size

        # In vr mode we can't have windows going outside the screen.
        if bui.app.env.vr:
            focus_size = size
            focus_position = (0, 0)

        width = focus_size[0]
        height = focus_size[1]

        # Ok, we've been given a desired width, height, and scale;
        # we now need to ensure that we're all onscreen by scaling down if
        # need be and clamping it to the UI bounds.
        bounds = bui.uibounds()
        edge_buffer = 15 * edge_buffer_scale
        bounds_width = bounds[1] - bounds[0] - edge_buffer * 2
        bounds_height = bounds[3] - bounds[2] - edge_buffer * 2

        fin_width = width * scale
        fin_height = height * scale
        if fin_width > bounds_width:
            scale /= fin_width / bounds_width
            fin_width = width * scale
            fin_height = height * scale
        if fin_height > bounds_height:
            scale /= fin_height / bounds_height
            fin_width = width * scale
            fin_height = height * scale

        x_min = bounds[0] + edge_buffer + fin_width * 0.5
        y_min = bounds[2] + edge_buffer + fin_height * 0.5
        x_max = bounds[1] - edge_buffer - fin_width * 0.5
        y_max = bounds[3] - edge_buffer - fin_height * 0.5

        x_fin = min(max(x_min, position[0] + offset[0]), x_max)
        y_fin = min(max(y_min, position[1] + offset[1]), y_max)

        # ok, we've calced a valid x/y position and a scale based on or
        # focus area. ..now calc the difference between the center of our
        # focus area and the center of our window to come up with the
        # offset we'll need to plug in to the window
        x_offs = (
            (focus_position[0] + focus_size[0] * 0.5) - (size[0] * 0.5)
        ) * scale
        y_offs = (
            (focus_position[1] + focus_size[1] * 0.5) - (size[1] * 0.5)
        ) * scale

        # NOTE: We do NOT need to suppress main-window-recreates here
        # (like regular windows do) since we are always in the overlay
        # stack and thus aren't affected by main-window recreation.

        self.root_widget = bui.containerwidget(
            transition='in_scale',
            scale=scale,
            toolbar_visibility=toolbar_visibility,
            size=size,
            parent=bui.get_special_widget('overlay_stack'),
            stack_offset=(x_fin - x_offs, y_fin - y_offs),
            scale_origin_stack_offset=(position[0], position[1]),
            on_outside_click_call=self.on_popup_cancel,
            claim_outside_clicks=True,
            color=bg_color,
            on_cancel_call=self.on_popup_cancel,
            darken_behind=darken_behind,
        )
        # Complain if we outlive our root widget.
        bui.app.ui_v1.add_ui_cleanup_check(self, self.root_widget)

    def on_popup_cancel(self) -> None:
        """Called when the popup is canceled.

        Cancels can occur due to clicking outside the window,
        hitting escape, etc.
        """


class PopupMenuWindow(PopupWindow):
    """A menu built using popup-window functionality."""

    def __init__(
        self,
        position: tuple[float, float],
        choices: Sequence[str],
        current_choice: str,
        *,
        delegate: Any = None,
        width: float = 230.0,
        maxwidth: float | None = None,
        scale: float = 1.0,
        choices_disabled: Sequence[str] | None = None,
        choices_display: Sequence[bui.Lstr | bui.LangStr] | None = None,
    ):
        # FIXME: Clean up a bit.
        # pylint: disable=too-many-branches
        if choices_disabled is None:
            choices_disabled = []
        if choices_display is None:
            choices_display = []

        # FIXME: For the moment we base our width on these strings so we
        #  need to flatten them.
        choices_display_fin: list[str] = []
        for choice_display in choices_display:
            choices_display_fin.append(choice_display.evaluate())

        if maxwidth is None:
            maxwidth = width * 1.5

        self._transitioning_out = False
        self._choices = list(choices)
        self._choices_display = list(choices_display_fin)
        self._current_choice = current_choice
        self._choices_disabled = list(choices_disabled)
        self._done_building = False
        if not choices:
            raise TypeError('Must pass at least one choice')
        self._width = width
        self._scale = scale
        if len(choices) > 8:
            self._height = 280
            self._use_scroll = True
        else:
            self._height = 20 + len(choices) * 33
            self._use_scroll = False
        self._delegate = None  # Don't want this stuff called just yet.

        # Extend width to fit our longest string (or our max-width).
        for index, choice in enumerate(choices):
            if len(choices_display_fin) == len(choices):
                choice_display_name = choices_display_fin[index]
            else:
                choice_display_name = choice
            if self._use_scroll:
                self._width = max(
                    self._width,
                    min(
                        maxwidth,
                        bui.get_string_width(
                            choice_display_name, suppress_warning=True
                        ),
                    )
                    + 75,
                )
            else:
                self._width = max(
                    self._width,
                    min(
                        maxwidth,
                        bui.get_string_width(
                            choice_display_name, suppress_warning=True
                        ),
                    )
                    + 60,
                )

        # Init parent class - this will rescale and reposition things as
        # needed and create our root widget.
        super().__init__(
            position,
            size=(self._width, self._height),
            scale=self._scale,
            darken_behind=False,  # Looks too intense for a menu.
        )

        if self._use_scroll:
            self._scrollwidget = bui.scrollwidget(
                parent=self.root_widget,
                position=(20, 20),
                highlight=False,
                color=(0.35, 0.55, 0.15),
                size=(self._width - 40, self._height - 40),
                border_opacity=0.5,
            )
            self._columnwidget = bui.columnwidget(
                parent=self._scrollwidget, border=2, margin=0
            )
        else:
            self._offset_widget = bui.containerwidget(
                parent=self.root_widget,
                position=(12, 12),
                size=(self._width - 40, self._height),
                background=False,
            )
            self._columnwidget = bui.columnwidget(
                parent=self._offset_widget, border=2, margin=0
            )
        for index, choice in enumerate(choices):
            if len(choices_display_fin) == len(choices):
                choice_display_name = choices_display_fin[index]
            else:
                choice_display_name = choice
            inactive = choice in self._choices_disabled
            wdg = bui.textwidget(
                parent=self._columnwidget,
                size=(self._width - 40, 28),
                on_select_call=bui.CallStrict(self._select, index),
                click_activate=True,
                color=(
                    (0.5, 0.5, 0.5, 0.5)
                    if inactive
                    else (
                        (0.5, 1, 0.5, 1)
                        if choice == self._current_choice
                        else (0.8, 0.8, 0.8, 1.0)
                    )
                ),
                padding=0,
                maxwidth=maxwidth,
                text=choice_display_name,
                on_activate_call=self._activate,
                v_align='center',
                selectable=(not inactive),
                glow_type='uniform',
            )
            bui.widget(edit=wdg, allow_preserve_selection=False)

            if choice == self._current_choice:
                bui.containerwidget(
                    edit=self._columnwidget,
                    selected_child=wdg,
                    visible_child=wdg,
                )

        # ok from now on our delegate can be called
        self._delegate = weakref.ref(delegate)
        self._done_building = True

    def _select(self, index: int) -> None:
        if self._done_building:
            self._current_choice = self._choices[index]

    def _activate(self) -> None:
        builtinassets.audio.swish.get().play()
        bui.apptimer(0.05, self._transition_out)
        delegate = self._getdelegate()
        if delegate is not None:
            # Call this in a timer so it doesn't interfere with us killing
            # our widgets and whatnot.
            call = bui.CallStrict(
                delegate.popup_menu_selected_choice, self, self._current_choice
            )
            bui.apptimer(0, call)

    def _getdelegate(self) -> Any:
        return None if self._delegate is None else self._delegate()

    def _transition_out(self) -> None:
        if not self.root_widget:
            return
        if not self._transitioning_out:
            self._transitioning_out = True
            delegate = self._getdelegate()
            if delegate is not None:
                delegate.popup_menu_closing(self)
            bui.containerwidget(edit=self.root_widget, transition='out_scale')

    @override
    def on_popup_cancel(self) -> None:
        if not self._transitioning_out:
            builtinassets.audio.swish.get().play()
        self._transition_out()


class PopupMenu:
    """A complete popup-menu control.

    This creates a button and wrangles its pop-up menu.
    """

    def __init__(
        self,
        parent: bui.Widget,
        position: tuple[float, float],
        choices: Sequence[str],
        *,
        button_id: str | None = None,
        current_choice: str | None = None,
        on_value_change_call: Callable[[str], Any] | None = None,
        opening_call: Callable[[], Any] | None = None,
        closing_call: Callable[[], Any] | None = None,
        width: float = 230.0,
        maxwidth: float | None = None,
        scale: float | None = None,
        choices_disabled: Sequence[str] | None = None,
        choices_display: Sequence[bui.Lstr | bui.LangStr] | None = None,
        button_size: tuple[float, float] = (160.0, 50.0),
        autoselect: bool = True,
    ):
        if choices_disabled is None:
            choices_disabled = []
        if choices_display is None:
            choices_display = []
        assert bui.app.classic is not None
        uiscale = bui.app.ui_v1.uiscale
        if scale is None:
            scale = (
                2.3
                if uiscale is bui.UIScale.SMALL
                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
            )
        if current_choice not in choices:
            current_choice = None
        self._choices = list(choices)
        if not choices:
            raise TypeError('no choices given')
        self._choices_display = list(choices_display)
        self._choices_disabled = list(choices_disabled)
        self._width = width
        self._maxwidth = maxwidth
        self._scale = scale
        self._current_choice = (
            current_choice if current_choice is not None else self._choices[0]
        )
        self._position = position
        self._parent = parent
        if not choices:
            raise TypeError('Must pass at least one choice')
        self._parent = parent
        self._button_size = button_size

        self._button = bui.buttonwidget(
            parent=self._parent,
            id=button_id,
            position=(self._position[0], self._position[1]),
            autoselect=autoselect,
            size=self._button_size,
            scale=1.0,
            label='',
            on_activate_call=lambda: bui.apptimer(0, self._make_popup),
        )
        self._on_value_change_call = None  # Don't wanna call for initial set.
        self._opening_call = opening_call
        self._autoselect = autoselect
        self._closing_call = closing_call
        self.set_choice(self._current_choice)
        self._on_value_change_call = on_value_change_call
        self._window_widget: bui.Widget | None = None

        # Complain if we outlive our button.
        bui.app.ui_v1.add_ui_cleanup_check(self, self._button)

    def _make_popup(self) -> None:
        if not self._button:
            return
        if self._opening_call:
            self._opening_call()
        self._window_widget = PopupMenuWindow(
            position=self._button.get_screen_space_center(),
            delegate=self,
            width=self._width,
            maxwidth=self._maxwidth,
            scale=self._scale,
            choices=self._choices,
            current_choice=self._current_choice,
            choices_disabled=self._choices_disabled,
            choices_display=self._choices_display,
        ).root_widget

    def get_button(self) -> bui.Widget:
        """Return the menu's button widget."""
        return self._button

    def get_window_widget(self) -> bui.Widget | None:
        """Return the menu's window widget (or None if nonexistent)."""
        return self._window_widget

    def popup_menu_selected_choice(
        self, popup_window: PopupWindow, choice: str
    ) -> None:
        """Called when a choice is selected."""
        del popup_window  # Unused here.
        self.set_choice(choice)
        if self._on_value_change_call:
            self._on_value_change_call(choice)

    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
        """Called when the menu is closing."""
        del popup_window  # Unused here.
        if self._button:
            bui.containerwidget(edit=self._parent, selected_child=self._button)
        self._window_widget = None
        if self._closing_call:
            self._closing_call()

    def set_choice(self, choice: str) -> None:
        """Set the selected choice."""
        self._current_choice = choice
        displayname: str | bui.Lstr | bui.LangStr
        if len(self._choices_display) == len(self._choices):
            displayname = self._choices_display[self._choices.index(choice)]
        else:
            displayname = choice
        if self._button:
            bui.buttonwidget(edit=self._button, label=displayname)
