# Released under the MIT License. See LICENSE for details.
#
"""Keyboard settings related UI functionality."""

from typing import TYPE_CHECKING, override

from bauiv1lib.popup import PopupMenuWindow
import bauiv1 as bui
from bauiv1 import _commonassets, classicassets

from bauiv1 import builtinassets
import bascenev1 as bs

if TYPE_CHECKING:
    from typing import Any

    from bauiv1lib.popup import PopupWindow


_kbstrs = classicassets.strings.settings.controllers.keyboard
_gpstrs = classicassets.strings.settings.controllers.gamepad


class ConfigKeyboardWindow(bui.MainWindow):
    """Window for configuring keyboards."""

    def __init__(
        self,
        c: bs.InputDevice,
        transition: str | None = 'in_right',
        origin_widget: bui.Widget | None = None,
    ):
        self._r = 'configKeyboardWindow'
        self._input = c
        self._name = self._input.name
        self._unique_id = self._input.unique_identifier
        dname_raw = self._name
        if self._unique_id != '#1':
            dname_raw += ' ' + self._unique_id.replace('#', 'P')
        self._displayname = bui.Lstr(translate=('inputDeviceNames', dname_raw))
        self._width = 700
        if self._unique_id != '#1':
            self._height = 480
        else:
            self._height = 375
        self._spacing = 40
        assert bui.app.classic is not None
        uiscale = bui.app.ui_v1.uiscale
        super().__init__(
            root_widget=bui.containerwidget(
                size=(self._width, self._height),
                scale=(
                    1.4
                    if uiscale is bui.UIScale.SMALL
                    else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0
                ),
                stack_offset=(0, 5) if uiscale is bui.UIScale.SMALL else (0, 0),
                transition=transition,
            ),
            transition=transition,
            origin_widget=origin_widget,
        )

        self._settings: dict[str, int] = {}
        self._get_config_mapping()

        self._rebuild_ui()

    @override
    def get_main_window_state(self) -> bui.MainWindowState:
        # Support recreating our window for back/refresh purposes.
        cls = type(self)

        # Pull things from self here; if we do it within the lambda
        # we'll keep self alive which is bad.
        inputdevice = self._input

        return bui.BasicMainWindowState(
            create_call=lambda transition, origin_widget: cls(
                transition=transition,
                origin_widget=origin_widget,
                c=inputdevice,
            )
        )

    @override
    def main_window_should_preserve_selection(self) -> bool:
        return False

    def _get_config_mapping(self, default: bool = False) -> None:
        for button in [
            'buttonJump',
            'buttonPunch',
            'buttonBomb',
            'buttonPickUp',
            'buttonStart',
            'buttonStart2',
            'buttonUp',
            'buttonDown',
            'buttonLeft',
            'buttonRight',
        ]:
            assert bui.app.classic is not None
            self._settings[button] = (
                bui.app.classic.get_input_device_mapped_value(
                    self._input, button, default
                )
            )

    def _rebuild_ui(self, is_reset: bool = False) -> None:
        assert bui.app.classic is not None

        for widget in self._root_widget.get_children():
            widget.delete()

        # b_off = 0 if self._unique_id != '#1' else 9
        cancel_button = bui.buttonwidget(
            parent=self._root_widget,
            autoselect=True,
            position=(38, self._height - 85),
            size=(170, 60),
            label=_commonassets.strings.actions.cancel,
            scale=0.9,
            on_activate_call=self.main_window_back,
        )
        save_button = bui.buttonwidget(
            parent=self._root_widget,
            autoselect=True,
            position=(self._width - 190, self._height - 85),
            size=(180, 60),
            label=_commonassets.strings.actions.save,
            scale=0.9,
            text_scale=0.9,
            on_activate_call=self._save,
        )
        bui.containerwidget(
            edit=self._root_widget,
            cancel_button=cancel_button,
            start_button=save_button,
        )

        v = self._height - 74.0
        bui.textwidget(
            parent=self._root_widget,
            position=(self._width * 0.5, v + 15),
            size=(0, 0),
            # Still legacy: the device display-name is an
            # inputDeviceNames translate (those port in the deferred
            # translate-category phase; see strings-asset-migration
            # D35), so the whole composition stays Lstr until then.
            text=bui.Lstr(
                resource='configKeyboardWindow.configuringText',
                subs=[('${DEVICE}', self._displayname)],
            ),
            color=bui.app.ui_v1.title_color,
            h_align='center',
            v_align='center',
            maxwidth=270,
            scale=0.83,
        )
        v -= 20

        if self._unique_id != '#1':
            v -= 20
            v -= self._spacing
            bui.textwidget(
                parent=self._root_widget,
                position=(0, v + 19),
                size=(self._width, 50),
                text=_kbstrs.keyboard2_note,
                scale=0.7,
                maxwidth=self._width * 0.75,
                max_height=110,
                color=bui.app.ui_v1.infotextcolor,
                h_align='center',
                v_align='top',
            )
            v -= 40
        v -= 10
        v -= self._spacing * 2.2
        v += 25
        v -= 42
        h_offs = 160
        dist = 70
        d_color = (0.4, 0.4, 0.8)
        self._capture_button(
            pos=(h_offs, v + 0.95 * dist),
            color=d_color,
            button='buttonUp',
            texture=classicassets.textures.up_button.get(),
            scale=1.0,
        )
        self._capture_button(
            pos=(h_offs - 1.2 * dist, v),
            color=d_color,
            button='buttonLeft',
            texture=classicassets.textures.left_button.get(),
            scale=1.0,
        )
        self._capture_button(
            pos=(h_offs + 1.2 * dist, v),
            color=d_color,
            button='buttonRight',
            texture=classicassets.textures.right_button.get(),
            scale=1.0,
        )
        self._capture_button(
            pos=(h_offs, v - 0.95 * dist),
            color=d_color,
            button='buttonDown',
            texture=classicassets.textures.down_button.get(),
            scale=1.0,
        )

        if self._unique_id == '#2':
            self._capture_button(
                pos=(self._width * 0.5, v + 0.1 * dist),
                color=(0.4, 0.4, 0.6),
                button='buttonStart',
                texture=builtinassets.textures.start_button.get(),
                scale=0.8,
            )

        h_offs = self._width - 160

        self._capture_button(
            pos=(h_offs, v + 0.95 * dist),
            color=(0.6, 0.4, 0.8),
            button='buttonPickUp',
            texture=classicassets.textures.button_pick_up.get(),
            scale=1.0,
        )
        self._capture_button(
            pos=(h_offs - 1.2 * dist, v),
            color=(0.7, 0.5, 0.1),
            button='buttonPunch',
            texture=classicassets.textures.button_punch.get(),
            scale=1.0,
        )
        self._capture_button(
            pos=(h_offs + 1.2 * dist, v),
            color=(0.5, 0.2, 0.1),
            button='buttonBomb',
            texture=classicassets.textures.button_bomb.get(),
            scale=1.0,
        )
        self._capture_button(
            pos=(h_offs, v - 0.95 * dist),
            color=(0.2, 0.5, 0.2),
            button='buttonJump',
            texture=classicassets.textures.button_jump.get(),
            scale=1.0,
        )

        self._more_button = bui.buttonwidget(
            parent=self._root_widget,
            autoselect=True,
            label='...',
            text_scale=0.9,
            color=(0.45, 0.4, 0.5),
            textcolor=(0.65, 0.6, 0.7),
            position=(self._width * 0.5 - 65, 30),
            size=(130, 40),
            on_activate_call=self._do_more,
        )

        if is_reset:
            bui.containerwidget(
                edit=self._root_widget,
                selected_child=self._more_button,
            )

    def _pretty_button_name(self, button_name: str) -> bui.LangStr:
        button_id = self._settings[button_name]
        if button_id == -1:
            return _gpstrs.unset
        return self._input.get_button_name(button_id)

    def _capture_button(
        self,
        pos: tuple[float, float],
        color: tuple[float, float, float],
        texture: bui.Texture,
        button: str,
        scale: float = 1.0,
    ) -> None:
        base_size = 79
        btn = bui.buttonwidget(
            parent=self._root_widget,
            autoselect=True,
            position=(
                pos[0] - base_size * 0.5 * scale,
                pos[1] - base_size * 0.5 * scale,
            ),
            size=(base_size * scale, base_size * scale),
            texture=texture,
            label='',
            color=color,
        )

        # Do this deferred so it shows up on top of other buttons. (ew.)
        def doit() -> None:
            if not self._root_widget:
                return
            uiscale = 0.66 * scale * 2.0
            maxwidth = 76.0 * scale
            txt = bui.textwidget(
                parent=self._root_widget,
                position=(pos[0] + 0.0 * scale, pos[1] - (57.0 - 18.0) * scale),
                color=(1, 1, 1, 0.3),
                size=(0, 0),
                h_align='center',
                v_align='top',
                scale=uiscale,
                maxwidth=maxwidth,
                text=self._pretty_button_name(button),
            )
            bui.buttonwidget(
                edit=btn,
                autoselect=True,
                on_activate_call=bui.CallStrict(
                    AwaitKeyboardInputWindow, button, txt, self._settings
                ),
            )

        bui.pushcall(doit)

    def _reset(self) -> None:
        from bauiv1lib.confirm import ConfirmWindow

        assert bui.app.classic is not None

        # efro note: I think it's ok to reset without a confirm here
        # because the user can see pretty clearly what changes and can
        # cancel out of the keyboard settings edit if they want.
        if bool(False):
            ConfirmWindow(
                # TODO: Implement a translation string for this!
                'Are you sure you want to reset your button mapping?',
                self._do_reset,
                width=480,
                height=95,
            )
        else:
            self._do_reset()

    def _do_reset(self) -> None:
        """Resets the input's mapping settings."""
        self._settings = {}
        self._get_config_mapping(default=True)
        self._rebuild_ui(is_reset=True)
        builtinassets.audio.gun_cocking.get().play()

    def _do_more(self) -> None:
        """Show a burger menu with extra settings."""
        # pylint: disable=cyclic-import
        choices: list[str] = [
            'reset',
        ]
        choices_display: list[bui.Lstr | bui.LangStr] = [
            _commonassets.strings.actions.reset,
        ]

        uiscale = bui.app.ui_v1.uiscale
        PopupMenuWindow(
            position=self._more_button.get_screen_space_center(),
            scale=(
                2.3
                if uiscale is bui.UIScale.SMALL
                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
            ),
            width=150,
            choices=choices,
            choices_display=choices_display,
            current_choice='reset',
            delegate=self,
        )

    def popup_menu_selected_choice(
        self, popup_window: PopupMenuWindow, choice: str
    ) -> None:
        """Called when a choice is selected in the popup."""
        del popup_window  # unused
        if choice == 'reset':
            self._reset()
        else:
            print(f'invalid choice: {choice}')

    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
        """Called when the popup is closing."""

    def _save(self) -> None:

        # no-op if we're not in control.
        if not self.main_window_has_control():
            return

        assert bui.app.classic is not None
        builtinassets.audio.gun_cocking.get().play()

        # There's a chance the device disappeared; handle that
        # gracefully.
        if not self._input:
            return

        dst = bui.app.classic.get_input_device_config(
            self._input, default=False
        )
        dst2: dict[str, Any] = dst[0][dst[1]]
        dst2.clear()

        # Store any values that aren't -1.
        for key, val in list(self._settings.items()):
            if val != -1:
                dst2[key] = val

        # Send this config to the master-server so we can generate more
        # defaults in the future.
        if bui.app.classic is not None:
            bui.app.classic.master_server_v1_post(
                'controllerConfig',
                {
                    'ua': bui.app.classic.legacy_user_agent_string,
                    'name': self._name,
                    'b': bui.app.env.engine_build_number,
                    'config': dst2,
                    'v': 2,
                },
            )
        bui.app.config.apply_and_commit()

        self.main_window_back()


class AwaitKeyboardInputWindow(bui.Window):
    """Window for capturing a keypress."""

    def __init__(self, button: str, ui: bui.Widget, settings: dict):
        self._capture_button = button
        self._capture_key_ui = ui
        self._settings = settings

        width = 400
        height = 150
        assert bui.app.classic is not None
        uiscale = bui.app.ui_v1.uiscale
        super().__init__(
            root_widget=bui.containerwidget(
                size=(width, height),
                transition='in_right',
                scale=(
                    2.0
                    if uiscale is bui.UIScale.SMALL
                    else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
                ),
            )
        )
        bui.textwidget(
            parent=self._root_widget,
            position=(0, height - 60),
            size=(width, 25),
            text=_kbstrs.press_any_key,
            h_align='center',
            v_align='top',
        )

        self._counter = 5
        self._count_down_text = bui.textwidget(
            parent=self._root_widget,
            h_align='center',
            position=(0, height - 110),
            size=(width, 25),
            color=(1, 1, 1, 0.3),
            text=str(self._counter),
        )
        self._decrement_timer: bui.AppTimer | None = bui.AppTimer(
            1.0, self._decrement, repeat=True
        )
        bs.capture_keyboard_input(bui.WeakCallPartial(self._button_callback))

    def __del__(self) -> None:
        bs.release_keyboard_input()

    def _die(self) -> None:
        # This strong-refs us; killing it allows us to die now.
        self._decrement_timer = None
        if self._root_widget:
            bui.containerwidget(edit=self._root_widget, transition='out_left')

    def _button_callback(self, event: dict[str, Any]) -> None:
        self._settings[self._capture_button] = event['button']
        if event['type'] == 'BUTTONDOWN':
            bname = event['input_device'].get_button_name(event['button'])
            bui.textwidget(edit=self._capture_key_ui, text=bname)
            builtinassets.audio.gun_cocking.get().play()
            self._die()

    def _decrement(self) -> None:
        self._counter -= 1
        if self._counter >= 1:
            bui.textwidget(edit=self._count_down_text, text=str(self._counter))
        else:
            self._die()
