# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for editing a game config."""

import copy
import random
import logging
from typing import TYPE_CHECKING, cast, override

import bascenev1 as bs
import bauiv1 as bui
from bauiv1 import _commonassets, classicassets

if TYPE_CHECKING:
    from typing import Any, Callable


class PlaylistEditGameWindow(bui.MainWindow):
    """Window for editing a game config."""

    def __init__(
        self,
        gametype: type[bs.GameActivity],
        sessiontype: type[bs.Session],
        config: dict[str, Any] | None,
        completion_call: Callable[[dict[str, Any] | None, bui.MainWindow], Any],
        default_selection: str | None = None,
        transition: str | None = 'in_right',
        origin_widget: bui.Widget | None = None,
        edit_info: dict[str, Any] | None = None,
    ):
        # pylint: disable=too-many-statements
        # pylint: disable=too-many-branches
        # pylint: disable=too-many-positional-arguments
        # pylint: disable=too-many-locals
        from bascenev1 import (
            get_filtered_map_name,
            get_map_class,
            get_map_display_string,
        )

        assert bui.app.classic is not None
        store = bui.app.classic.store

        self._gametype = gametype
        self._sessiontype = sessiontype

        # If we're within an editing session we get passed edit_info
        # (returning from map selection window, etc).
        if edit_info is not None:
            self._edit_info = edit_info

        # ..otherwise determine whether we're adding or editing a game based
        # on whether an existing config was passed to us.
        else:
            if config is None:
                self._edit_info = {'editType': 'add'}
            else:
                self._edit_info = {'editType': 'edit'}

        self._r = 'gameSettingsWindow'

        valid_maps = gametype.get_supported_maps(sessiontype)
        if not valid_maps:
            bui.screenmessage(classicassets.strings.playlist.no_valid_maps)
            raise RuntimeError('No valid maps found.')

        self._config = config
        self._settings_defs = gametype.get_available_settings(sessiontype)
        self._completion_call = completion_call

        # To start with, pick a random map out of the ones we own.
        unowned_maps = store.get_unowned_maps()
        valid_maps_owned = [m for m in valid_maps if m not in unowned_maps]
        if valid_maps_owned:
            self._map = valid_maps[random.randrange(len(valid_maps_owned))]

        # Hmmm.. we own none of these maps.. just pick a random un-owned one
        # I guess.. should this ever happen?
        else:
            self._map = valid_maps[random.randrange(len(valid_maps))]

        is_add = self._edit_info['editType'] == 'add'

        # If there's a valid map name in the existing config, use that.
        try:
            if (
                config is not None
                and 'settings' in config
                and 'map' in config['settings']
            ):
                filtered_map_name = get_filtered_map_name(
                    config['settings']['map']
                )
                if filtered_map_name in valid_maps:
                    self._map = filtered_map_name
        except Exception:
            logging.exception('Error getting map for editor.')

        if config is not None and 'settings' in config:
            self._settings = config['settings']
        else:
            self._settings = {}

        self._choice_selections: dict[str, int] = {}

        uiscale = bui.app.ui_v1.uiscale
        width = 820 if uiscale is bui.UIScale.SMALL else 620
        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
        height = (
            400
            if uiscale is bui.UIScale.SMALL
            else 460 if uiscale is bui.UIScale.MEDIUM else 550
        )
        spacing = 52
        y_extra = 15
        y_extra2 = 21
        yoffs = -30 if uiscale is bui.UIScale.SMALL else 0

        map_tex = get_map_class(self._map).get_preview_texture()
        if map_tex is None:
            raise RuntimeError(f'No map preview tex found for {self._map}.')

        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
        super().__init__(
            root_widget=bui.containerwidget(
                size=(width, height + top_extra),
                scale=(
                    2.3
                    if uiscale is bui.UIScale.SMALL
                    else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0
                ),
                toolbar_visibility=(
                    'menu_minimal_no_back'
                    if uiscale is bui.UIScale.SMALL
                    else 'menu_full'
                ),
            ),
            transition=transition,
            origin_widget=origin_widget,
        )

        btn = bui.buttonwidget(
            parent=self._root_widget,
            position=(45 + x_inset, height - 82 + y_extra2 + yoffs),
            size=(60, 48) if is_add else (180, 65),
            label=(
                bui.charstr(bui.SpecialChar.BACK)
                if is_add
                else _commonassets.strings.actions.cancel
            ),
            button_type='backSmall' if is_add else None,
            autoselect=True,
            scale=1.0 if is_add else 0.75,
            text_scale=1.3,
            on_activate_call=bui.CallStrict(self._cancel),
        )
        bui.containerwidget(edit=self._root_widget, cancel_button=btn)

        add_button = bui.buttonwidget(
            parent=self._root_widget,
            position=(width - (193 + x_inset), height - 82 + y_extra2 + yoffs),
            size=(200, 65),
            scale=0.75,
            text_scale=1.3,
            label=(
                classicassets.strings.playlist.add_game_title
                if is_add
                else _commonassets.strings.actions.apply
            ),
        )

        pbtn = bui.get_special_widget('squad_button')
        bui.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn)

        bui.textwidget(
            parent=self._root_widget,
            position=(-8, height - 70 + y_extra2 + yoffs),
            size=(width, 25),
            text=gametype.get_display_string(langstr=True),
            color=bui.app.ui_v1.title_color,
            maxwidth=235,
            scale=1.1,
            h_align='center',
            v_align='center',
        )

        map_height = 100

        scroll_height = map_height + 10  # map select and margin

        # Calc our total height we'll need
        scroll_height += spacing * len(self._settings_defs)

        scroll_width = width - (86 + 2 * x_inset)
        self._scrollwidget = bui.scrollwidget(
            parent=self._root_widget,
            position=(
                44 + x_inset,
                (80 if uiscale is bui.UIScale.SMALL else 35) + y_extra + yoffs,
            ),
            size=(
                scroll_width,
                height - (166 if uiscale is bui.UIScale.SMALL else 116),
            ),
            highlight=False,
            claims_left_right=True,
            selection_loops_to_parent=True,
            border_opacity=0.4,
        )
        self._subcontainer = bui.containerwidget(
            parent=self._scrollwidget,
            size=(scroll_width, scroll_height),
            background=False,
            claims_left_right=True,
            selection_loops_to_parent=True,
        )

        v = scroll_height - 5
        h = -40

        # Keep track of all the selectable widgets we make so we can wire
        # them up conveniently.
        widget_column: list[list[bui.Widget]] = []

        # Map select button.
        bui.textwidget(
            parent=self._subcontainer,
            position=(h + 49, v - 63),
            size=(100, 30),
            maxwidth=110,
            text=classicassets.strings.ui.map,
            h_align='left',
            color=(0.8, 0.8, 0.8, 1.0),
            v_align='center',
        )

        mesh_trans = classicassets.meshes.level_select_button_transparent.get()
        bui.imagewidget(
            parent=self._subcontainer,
            size=(256 * 0.7, 125 * 0.7),
            position=(h + 261 - 128 + 128.0 * 0.56, v - 90),
            texture=map_tex,
            mesh_opaque=classicassets.meshes.level_select_button_opaque.get(),
            mesh_transparent=mesh_trans,
            mask_texture=classicassets.textures.map_preview_mask.get(),
        )
        map_button = btn = bui.buttonwidget(
            parent=self._subcontainer,
            size=(140, 60),
            position=(h + 448, v - 72),
            on_activate_call=bui.CallStrict(self._select_map),
            scale=0.7,
            label=_commonassets.strings.actions.select_ellipsis,
        )
        widget_column.append([btn])

        bui.textwidget(
            parent=self._subcontainer,
            position=(h + 363 - 123, v - 114),
            size=(100, 30),
            flatness=1.0,
            shadow=1.0,
            scale=0.55,
            maxwidth=256 * 0.7 * 0.8,
            text=get_map_display_string(self._map, langstr=True),
            h_align='center',
            color=(0.6, 1.0, 0.6, 1.0),
            v_align='center',
        )
        v -= map_height

        for setting in self._settings_defs:
            value = setting.default
            value_type = type(value)

            # Now, if there's an existing value for it in the config,
            # override with that.
            try:
                if (
                    config is not None
                    and 'settings' in config
                    and setting.name in config['settings']
                ):
                    value = value_type(config['settings'][setting.name])
            except Exception:
                logging.exception('Error getting game setting.')

            # Shove the starting value in there to start.
            self._settings[setting.name] = value

            name_translated = self._get_localized_setting_name(setting.name)

            mw1 = 280
            mw2 = 70

            # Handle types with choices specially:
            if isinstance(setting, bs.ChoiceSetting):
                for choice in setting.choices:
                    if len(choice) != 2:
                        raise ValueError(
                            "Expected 2-member tuples for 'choices'; got: "
                            + repr(choice)
                        )
                    if not isinstance(choice[0], str):
                        raise TypeError(
                            'First value for choice tuple must be a str; got: '
                            + repr(choice)
                        )
                    if not isinstance(choice[1], value_type):
                        raise TypeError(
                            'Choice type does not match default value; choice:'
                            + repr(choice)
                            + '; setting:'
                            + repr(setting)
                        )
                if value_type not in (int, float):
                    raise TypeError(
                        'Choice type setting must have int or float default; '
                        'got: ' + repr(setting)
                    )

                # Start at the choice corresponding to the default if possible.
                self._choice_selections[setting.name] = 0
                for index, choice in enumerate(setting.choices):
                    if choice[1] == value:
                        self._choice_selections[setting.name] = index
                        break

                v -= spacing
                bui.textwidget(
                    parent=self._subcontainer,
                    position=(h + 50, v),
                    size=(100, 30),
                    maxwidth=mw1,
                    text=name_translated,
                    h_align='left',
                    color=(0.8, 0.8, 0.8, 1.0),
                    v_align='center',
                )
                txt = bui.textwidget(
                    parent=self._subcontainer,
                    position=(h + 509 - 95, v),
                    size=(0, 28),
                    text=self._get_localized_setting_name(
                        setting.choices[self._choice_selections[setting.name]][
                            0
                        ]
                    ),
                    editable=False,
                    color=(0.6, 1.0, 0.6, 1.0),
                    maxwidth=mw2,
                    h_align='right',
                    v_align='center',
                    padding=2,
                )
                btn1 = bui.buttonwidget(
                    parent=self._subcontainer,
                    position=(h + 509 - 50 - 1, v),
                    size=(28, 28),
                    label='<',
                    autoselect=True,
                    on_activate_call=bui.CallStrict(
                        self._choice_inc, setting.name, txt, setting, -1
                    ),
                    repeat=True,
                )
                btn2 = bui.buttonwidget(
                    parent=self._subcontainer,
                    position=(h + 509 + 5, v),
                    size=(28, 28),
                    label='>',
                    autoselect=True,
                    on_activate_call=bui.CallStrict(
                        self._choice_inc, setting.name, txt, setting, 1
                    ),
                    repeat=True,
                )
                widget_column.append([btn1, btn2])

            elif isinstance(setting, (bs.IntSetting, bs.FloatSetting)):
                v -= spacing
                min_value = setting.min_value
                max_value = setting.max_value
                increment = setting.increment
                bui.textwidget(
                    parent=self._subcontainer,
                    position=(h + 50, v),
                    size=(100, 30),
                    text=name_translated,
                    h_align='left',
                    color=(0.8, 0.8, 0.8, 1.0),
                    v_align='center',
                    maxwidth=mw1,
                )
                txt = bui.textwidget(
                    parent=self._subcontainer,
                    position=(h + 509 - 95, v),
                    size=(0, 28),
                    text=str(value),
                    editable=False,
                    color=(0.6, 1.0, 0.6, 1.0),
                    maxwidth=mw2,
                    h_align='right',
                    v_align='center',
                    padding=2,
                )
                btn1 = bui.buttonwidget(
                    parent=self._subcontainer,
                    position=(h + 509 - 50 - 1, v),
                    size=(28, 28),
                    label='-',
                    autoselect=True,
                    on_activate_call=bui.CallStrict(
                        self._inc,
                        txt,
                        min_value,
                        max_value,
                        -increment,
                        value_type,
                        setting.name,
                    ),
                    repeat=True,
                )
                btn2 = bui.buttonwidget(
                    parent=self._subcontainer,
                    position=(h + 509 + 5, v),
                    size=(28, 28),
                    label='+',
                    autoselect=True,
                    on_activate_call=bui.CallStrict(
                        self._inc,
                        txt,
                        min_value,
                        max_value,
                        increment,
                        value_type,
                        setting.name,
                    ),
                    repeat=True,
                )
                widget_column.append([btn1, btn2])

            elif value_type == bool:
                v -= spacing
                bui.textwidget(
                    parent=self._subcontainer,
                    position=(h + 50, v),
                    size=(100, 30),
                    text=name_translated,
                    h_align='left',
                    color=(0.8, 0.8, 0.8, 1.0),
                    v_align='center',
                    maxwidth=mw1,
                )
                txt = bui.textwidget(
                    parent=self._subcontainer,
                    position=(h + 509 - 95, v),
                    size=(0, 28),
                    text=(
                        _commonassets.strings.values.on
                        if value
                        else _commonassets.strings.values.off
                    ),
                    editable=False,
                    color=(0.6, 1.0, 0.6, 1.0),
                    maxwidth=mw2,
                    h_align='right',
                    v_align='center',
                    padding=2,
                )
                cbw = bui.checkboxwidget(
                    parent=self._subcontainer,
                    text='',
                    position=(h + 505 - 50 - 5, v - 2),
                    size=(200, 30),
                    autoselect=True,
                    textcolor=(0.8, 0.8, 0.8),
                    value=value,
                    on_value_change_call=bui.CallPartial(
                        self._check_value_change, setting.name, txt
                    ),
                )
                widget_column.append([cbw])

            else:
                raise TypeError(f'Invalid value type: {value_type}.')

        # Ok now wire up the column.
        try:
            prev_widgets: list[bui.Widget] | None = None
            for cwdg in widget_column:
                if prev_widgets is not None:
                    # Wire our rightmost to their rightmost.
                    bui.widget(edit=prev_widgets[-1], down_widget=cwdg[-1])
                    bui.widget(edit=cwdg[-1], up_widget=prev_widgets[-1])

                    # Wire our leftmost to their leftmost.
                    bui.widget(edit=prev_widgets[0], down_widget=cwdg[0])
                    bui.widget(edit=cwdg[0], up_widget=prev_widgets[0])
                prev_widgets = cwdg
        except Exception:
            logging.exception(
                'Error wiring up game-settings-select widget column.'
            )

        bui.buttonwidget(
            edit=add_button, on_activate_call=bui.CallStrict(self._add)
        )
        bui.containerwidget(
            edit=self._root_widget,
            selected_child=add_button,
            start_button=add_button,
        )

        if default_selection == 'map':
            bui.containerwidget(
                edit=self._root_widget, selected_child=self._scrollwidget
            )
            bui.containerwidget(
                edit=self._subcontainer, selected_child=map_button
            )

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

        # Pull things out of self here so we don't refer to self in the
        # lambda below which would keep us alive.
        gametype = self._gametype
        sessiontype = self._sessiontype
        config = self._config
        completion_call = self._completion_call

        return bui.BasicMainWindowState(
            create_call=lambda transition, origin_widget: cls(
                transition=transition,
                origin_widget=origin_widget,
                gametype=gametype,
                sessiontype=sessiontype,
                config=config,
                completion_call=completion_call,
            )
        )

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

    def _get_localized_setting_name(self, name: str) -> bui.LangStr:
        """Authored display name for a game setting or preset value.

        Explicit table rather than a built lookup, per the standing
        convention: a removed accessor must be a type error, not a
        silent miss. Anything absent -- a mod's setting, or a value we
        never authored -- shows its own name untranslated, which is the
        honest result for text we have no translation for.
        """
        return _setting_name_table().get(name, bui.LangStr.from_text(name))

    def _select_map(self) -> None:
        # pylint: disable=cyclic-import
        from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow

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

        config = self._config = self._getconfig()

        # Replace ourself with the map-select UI.
        self.main_window_replace(
            lambda: PlaylistMapSelectWindow(
                self._gametype,
                self._sessiontype,
                config,
                self._edit_info,
                self._completion_call,
            )
        )

    def _choice_inc(
        self,
        setting_name: str,
        widget: bui.Widget,
        setting: bs.ChoiceSetting,
        increment: int,
    ) -> None:
        choices = setting.choices
        if increment > 0:
            self._choice_selections[setting_name] = min(
                len(choices) - 1, self._choice_selections[setting_name] + 1
            )
        else:
            self._choice_selections[setting_name] = max(
                0, self._choice_selections[setting_name] - 1
            )
        bui.textwidget(
            edit=widget,
            text=self._get_localized_setting_name(
                choices[self._choice_selections[setting_name]][0]
            ),
        )
        self._settings[setting_name] = choices[
            self._choice_selections[setting_name]
        ][1]

    def _cancel(self) -> None:
        self._completion_call(None, self)

    def _check_value_change(
        self, setting_name: str, widget: bui.Widget, value: int
    ) -> None:
        bui.textwidget(
            edit=widget,
            text=(
                _commonassets.strings.values.on
                if value
                else _commonassets.strings.values.off
            ),
        )
        self._settings[setting_name] = value

    def _getconfig(self) -> dict[str, Any]:
        settings = copy.deepcopy(self._settings)
        settings['map'] = self._map
        return {'settings': settings}

    def _add(self) -> None:
        self._completion_call(self._getconfig(), self)

    def _inc(
        self,
        ctrl: bui.Widget,
        min_val: int | float,
        max_val: int | float,
        increment: int | float,
        setting_type: type,
        setting_name: str,
    ) -> None:
        # pylint: disable=too-many-positional-arguments
        if setting_type == float:
            val = float(cast(str, bui.textwidget(query=ctrl)))
        else:
            val = int(cast(str, bui.textwidget(query=ctrl)))
        val += increment
        val = max(min_val, min(val, max_val))
        if setting_type == float:
            bui.textwidget(edit=ctrl, text=str(round(val, 2)))
        elif setting_type == int:
            bui.textwidget(edit=ctrl, text=str(int(val)))
        else:
            raise TypeError('invalid vartype: ' + str(setting_type))
        self._settings[setting_name] = val


def _setting_name_table() -> dict[str, bui.LangStr]:
    """Game-setting name/value -> its authored display string.

    Keys are the legacy ``settingNames`` translate keys, which are also
    the English text. Several entries are *values* rather than setting
    names (durations, Short/Normal/Long), so they must each read
    correctly standing alone.
    """
    s = classicassets.strings.game_settings
    return {
        '1 Second': s.one_second,
        '2 Seconds': s.two_seconds,
        '4 Seconds': s.four_seconds,
        '8 Seconds': s.eight_seconds,
        '1 Minute': s.one_minute,
        '2 Minutes': s.two_minutes,
        '5 Minutes': s.five_minutes,
        '10 Minutes': s.ten_minutes,
        '20 Minutes': s.twenty_minutes,
        'Shorter': s.shorter,
        'Short': s.short,
        'Normal': s.normal,
        'Long': s.long,
        'Longer': s.longer,
        'None': s.none,
        'No Mines': s.no_mines,
        'Epic Mode': s.epic_mode,
        'Pro Mode': s.pro_mode,
        'Solo Mode': s.solo_mode,
        'Enable Triple Bombs': s.enable_triple_bombs,
        'Enable Impact Bombs': s.enable_impact_bombs,
        'Allow Negative Scores': s.allow_negative_scores,
        'Balance Total Lives': s.balance_total_lives,
        'Entire Team Must Finish': s.entire_team_must_finish,
        'Chosen One Gets Gloves': s.chosen_one_gets_gloves,
        'Chosen One Gets Shield': s.chosen_one_gets_shield,
        'Chosen One Time': s.chosen_one_time,
        'Bomb Spawning': s.bomb_spawning,
        'Mine Spawning': s.mine_spawning,
        'Flag Idle Return Time': s.flag_idle_return_time,
        'Flag Touch Return Time': s.flag_touch_return_time,
        'Hold Time': s.hold_time,
        'Kills to Win Per Player': s.kills_to_win_per_player,
        'Laps': s.laps,
        'Lives Per Player': s.lives_per_player,
        'Respawn Times': s.respawn_times,
        'Score to Win': s.score_to_win,
        'Target Count': s.target_count,
        'Time Limit': s.time_limit,
    }
