# Released under the MIT License. See LICENSE for details.
#
"""Provides UI functionality for watching replays."""

import os
import logging
from enum import Enum
from typing import TYPE_CHECKING, cast, override

import bascenev1 as bs
import bauiv1 as bui
from bauiv1 import _commonassets, _classicassets
from bauiv1 import _builtinassets

from bauiv1lib.utils import get_screen_margins

if TYPE_CHECKING:
    from typing import Any

# Module-level alias to keep long accessor chains under the line limit.
_ws = _classicassets.strings.watch


class WatchWindow(bui.MainWindow):
    """Window for watching replays."""

    class TabID(Enum):
        """Our available tab types."""

        MY_REPLAYS = 'my_replays'
        TEST_TAB = 'test_tab'

    def __init__(
        self,
        transition: str | None = 'in_right',
        origin_widget: bui.Widget | None = None,
    ):
        from bauiv1lib.tabs import TabRow

        bui.set_analytics_screen('Watch Window')
        self._tab_data: dict[str, Any] = {}
        self._my_replays_scroll_width: float | None = None
        self._my_replays_watch_replay_button: bui.Widget | None = None
        self._scrollwidget: bui.Widget | None = None
        self._columnwidget: bui.Widget | None = None
        self._my_replay_selected: str | None = None
        self._replay_play_in_flight = False
        self._my_replays_rename_window: bui.Widget | None = None
        self._my_replay_rename_text: bui.Widget | None = None
        self._r = 'watchWindow'
        uiscale = bui.app.ui_v1.uiscale
        self._width = 1440 if uiscale is bui.UIScale.SMALL else 1040
        self._height = (
            900
            if uiscale is bui.UIScale.SMALL
            else 670 if uiscale is bui.UIScale.MEDIUM else 800
        )
        self._current_tab: WatchWindow.TabID | None = None

        # Do some fancy math to fill all available screen area up to the
        # size of our backing container. This lets us fit to the exact
        # screen shape at small ui scale.
        screensize = bui.get_virtual_screen_size()
        scale = (
            1.5
            if uiscale is bui.UIScale.SMALL
            else 0.85 if uiscale is bui.UIScale.MEDIUM else 0.65
        )
        # Calc screen size in our local container space and clamp to a
        # bit smaller than our container size.
        target_width = min(self._width - 120, screensize[0] / scale)
        target_height = min(self._height - 120, screensize[1] / scale)

        # To get top/left coords, go to the center of our window and
        # offset by half the width/height of our target area.
        self.yoffs = 0.5 * self._height + 0.5 * target_height + 30.0

        self._scroll_width = target_width
        self._scroll_height = target_height - 55
        self._scroll_y = self.yoffs - 85 - self._scroll_height

        # In small ui (where we cover the screen), extend our backing
        # imagery out to cover any margins between the virtual rect and
        # the visible screen edges (cutout insets and whatnot). Content
        # positioning is unaffected; only the imagery reaches further.
        margin_left, margin_right, margin_bottom, _margin_top = (
            get_screen_margins(scale)
            if uiscale is bui.UIScale.SMALL
            else (0.0, 0.0, 0.0, 0.0)
        )

        super().__init__(
            root_widget=bui.containerwidget(
                size=(self._width, self._height),
                toolbar_visibility=(
                    'menu_minimal'
                    if uiscale is bui.UIScale.SMALL
                    else 'menu_full'
                ),
                scale=scale,
            ),
            transition=transition,
            origin_widget=origin_widget,
            # We're affected by screen size only at small ui-scale.
            refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
        )

        if uiscale is bui.UIScale.SMALL:
            bui.containerwidget(
                edit=self._root_widget, on_cancel_call=self.main_window_back
            )
            self._back_button = None
        else:
            self._back_button = btn = bui.buttonwidget(
                parent=self._root_widget,
                id=f'{self.main_window_id_prefix}|back',
                autoselect=True,
                position=(70, self.yoffs - 50),
                size=(60, 60),
                scale=1.1,
                label=bui.charstr(bui.SpecialChar.BACK),
                button_type='backSmall',
                on_activate_call=self.main_window_back,
            )
            bui.containerwidget(edit=self._root_widget, cancel_button=btn)

        bui.textwidget(
            parent=self._root_widget,
            position=(
                (
                    self._width * 0.5
                    + (
                        (self._scroll_width * -0.5 + 93)
                        if uiscale is bui.UIScale.SMALL
                        else 0
                    )
                ),
                self.yoffs - (63 if uiscale is bui.UIScale.SMALL else 10),
            ),
            size=(0, 0),
            color=bui.app.ui_v1.title_color,
            scale=1.3 if uiscale is bui.UIScale.SMALL else 1.5,
            h_align='left' if uiscale is bui.UIScale.SMALL else 'center',
            v_align='center',
            text=_ws.title,
            maxwidth=200,
        )

        tabdefs = [
            (
                self.TabID.MY_REPLAYS,
                _ws.my_replays,
            ),
        ]

        tab_bar_width = 200.0 * len(tabdefs)
        tab_bar_inset = (self._scroll_width - tab_bar_width) * 0.5

        self._tab_row = TabRow(
            self._root_widget,
            tabdefs,
            pos=(
                self._width * 0.5 - self._scroll_width * 0.5 + tab_bar_inset,
                self._scroll_y + self._scroll_height - 4.0,
            ),
            size=(self._scroll_width - 2.0 * tab_bar_inset, 50),
            on_select_call=self._set_tab,
            idprefix=self.main_window_id_prefix,
        )

        first_tab = self._tab_row.tabs[tabdefs[0][0]]
        last_tab = self._tab_row.tabs[tabdefs[-1][0]]
        bui.widget(
            edit=last_tab.button,
            right_widget=bui.get_special_widget('squad_button'),
        )
        if uiscale is bui.UIScale.SMALL:
            bbtn = bui.get_special_widget('back_button')
            bui.widget(edit=first_tab.button, up_widget=bbtn, left_widget=bbtn)

        # Not actually using a scroll widget anymore; just an image.
        # It extends left/right/bottom across any screen margins; the
        # top edge stays put since the tab row hangs off it.
        bui.imagewidget(
            parent=self._root_widget,
            size=(
                self._scroll_width + margin_left + margin_right,
                self._scroll_height + margin_bottom,
            ),
            position=(
                self._width * 0.5 - self._scroll_width * 0.5 - margin_left,
                self._scroll_y - margin_bottom,
            ),
            texture=_builtinassets.textures.scroll_widget.get(),
            mesh_transparent=_builtinassets.meshes.soft_edge_outside.get(),
            opacity=0.4,
        )
        self._tab_container: bui.Widget | None = None

        try:
            current_tab = self.TabID(bui.app.config.get('Watch Tab'))
        except ValueError:
            current_tab = self.TabID.MY_REPLAYS
        self._set_tab(current_tab)

    @override
    def get_main_window_state(self) -> bui.MainWindowState:
        # Support recreating our window for back/refresh purposes.
        cls = type(self)
        return bui.BasicMainWindowState(
            create_call=lambda transition, origin_widget: cls(
                transition=transition, origin_widget=origin_widget
            )
        )

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

    def _set_tab(self, tab_id: TabID) -> None:

        if self._current_tab == tab_id:
            return
        self._current_tab = tab_id

        # Preserve our current tab between runs.
        cfg = bui.app.config
        cfg['Watch Tab'] = tab_id.value
        cfg.commit()

        # Update tab colors based on which is selected.
        self._tab_row.update_appearance(tab_id)

        if self._tab_container:
            self._tab_container.delete()
        scroll_left = (self._width - self._scroll_width) * 0.5
        scroll_bottom = self._scroll_y

        # A place where tabs can store data to get cleared when
        # switching to a different tab
        self._tab_data = {}

        assert bui.app.classic is not None
        uiscale = bui.app.ui_v1.uiscale
        if tab_id is self.TabID.MY_REPLAYS:
            c_width = self._scroll_width
            c_height = self._scroll_height - 20
            sub_scroll_height = c_height - 63
            self._my_replays_scroll_width = sub_scroll_width = (
                680 if uiscale is bui.UIScale.SMALL else 640
            )

            self._tab_container = cnt = bui.containerwidget(
                parent=self._root_widget,
                position=(
                    scroll_left,
                    scroll_bottom + (self._scroll_height - c_height) * 0.5,
                ),
                size=(c_width, c_height),
                background=False,
                selection_loops_to_parent=True,
            )

            v = c_height - 30
            bui.textwidget(
                parent=cnt,
                position=(c_width * 0.5, v + 6.0),
                color=(0.6, 1.0, 0.6),
                scale=0.7,
                size=(0, 0),
                maxwidth=c_width * 0.9,
                h_align='center',
                v_align='center',
                text=_ws.rename_warning(replay=_ws.replay_name_default),
            )

            b_width = 140 if uiscale is bui.UIScale.SMALL else 178
            b_height = (
                110
                if uiscale is bui.UIScale.SMALL
                else 142 if uiscale is bui.UIScale.MEDIUM else 180
            )
            b_space_extra = (
                0
                if uiscale is bui.UIScale.SMALL
                else -2 if uiscale is bui.UIScale.MEDIUM else -5
            )

            b_color = (0.6, 0.53, 0.63)
            b_textcolor = (0.75, 0.7, 0.8)
            btnv = (
                c_height
                - (
                    40
                    if uiscale is bui.UIScale.SMALL
                    else 40 if uiscale is bui.UIScale.MEDIUM else 40
                )
                - b_height
            )
            # Roughly center buttons and scroll-widget in the middle.
            xextra = (
                self._scroll_width - (sub_scroll_width + b_width)
            ) * 0.5 - 50.0
            btnh = (40 if uiscale is bui.UIScale.SMALL else 40) + xextra
            smlh = (190 if uiscale is bui.UIScale.SMALL else 225) + xextra
            tscl = 1.0 if uiscale is bui.UIScale.SMALL else 1.2
            self._my_replays_watch_replay_button = btn1 = bui.buttonwidget(
                parent=cnt,
                id=f'{self.main_window_id_prefix}|watch',
                size=(b_width, b_height),
                position=(btnh, btnv),
                button_type='square',
                color=b_color,
                textcolor=b_textcolor,
                on_activate_call=self._on_my_replay_play_press,
                text_scale=tscl,
                label=_ws.watch_replay_button,
                autoselect=True,
            )
            bui.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button)
            assert bui.app.classic is not None
            if uiscale is bui.UIScale.SMALL:
                bui.widget(
                    edit=btn1,
                    left_widget=bui.get_special_widget('back_button'),
                )
            btnv -= b_height + b_space_extra
            bui.buttonwidget(
                parent=cnt,
                id=f'{self.main_window_id_prefix}|rename',
                size=(b_width, b_height),
                position=(btnh, btnv),
                button_type='square',
                color=b_color,
                textcolor=b_textcolor,
                on_activate_call=self._on_my_replay_rename_press,
                text_scale=tscl,
                label=_ws.rename_replay_button,
                autoselect=True,
            )
            btnv -= b_height + b_space_extra
            bui.buttonwidget(
                parent=cnt,
                id=f'{self.main_window_id_prefix}|delete',
                size=(b_width, b_height),
                position=(btnh, btnv),
                button_type='square',
                color=b_color,
                textcolor=b_textcolor,
                on_activate_call=self._on_my_replay_delete_press,
                text_scale=tscl,
                label=_ws.delete_replay_button,
                autoselect=True,
            )

            v -= sub_scroll_height + 23
            self._scrollwidget = scrlw = bui.scrollwidget(
                parent=cnt,
                position=(smlh, v),
                size=(sub_scroll_width, sub_scroll_height),
            )
            bui.containerwidget(edit=cnt, selected_child=scrlw)
            self._columnwidget = bui.columnwidget(
                parent=scrlw,
                id=f'{self.main_window_id_prefix}|column',
                left_border=10,
                border=2,
                margin=0,
            )

            bui.widget(
                edit=scrlw,
                autoselect=True,
                left_widget=btn1,
                up_widget=self._tab_row.tabs[tab_id].button,
            )
            bui.widget(
                edit=self._tab_row.tabs[tab_id].button, down_widget=scrlw
            )

            self._my_replay_selected = None
            self._refresh_my_replays()

    def _no_replay_selected_error(self) -> None:
        bui.screenmessage(
            _ws.no_replay_selected,
            color=(1, 0, 0),
        )
        _builtinassets.audio.error.get().play()

    def _on_my_replay_play_press(self) -> None:
        if self._my_replay_selected is None:
            self._no_replay_selected_error()
            return
        bui.increment_analytics_count('Replay watch')

        # Starting a replay disconnects any connected clients (we don't
        # broadcast replays -- they never prepped the replay's content);
        # confirm first if we have any. Roster client_id -1 is us.
        num_clients = sum(
            1 for c in bs.get_game_roster() if c['client_id'] != -1
        )
        if num_clients > 0:
            from bauiv1lib import confirm

            confirm.ConfirmWindow(
                _classicassets.strings.gather.disconnect_clients(
                    count=num_clients
                ),
                self._start_replay_playback,
            )
        else:
            self._start_replay_playback()

    def _start_replay_playback(self) -> None:
        # A double press (or a second confirm) would otherwise spawn two
        # concurrent launch tasks: two launch_replay calls and a second
        # transition-out on our root widget (seen in the field as
        # 'ContainerWidget was set to transition out twice' reports).
        if self._replay_play_in_flight:
            return
        self._replay_play_in_flight = True
        bui.app.create_async_task(self._prepare_and_play_replay())

    async def _prepare_and_play_replay(self) -> None:
        assert self._my_replay_selected is not None
        path = f'{bui.get_replays_dir()}/{self._my_replay_selected}'

        # Resolve the replay's required content FIRST, while this window
        # is still up. That way a cancel/failure just closes the
        # progress dialog and leaves the user here, rather than
        # stranding them on a torn-down UI (mirrors how connect_to_party
        # preps with the gather UI still present).
        if not await bs.prepare_replay(path):
            # Prep failed/cancelled; we're staying here, so allow
            # another play attempt.
            self._replay_play_in_flight = False
            return

        # Content is ready; now do the fade-out -> launch -> fade-in and
        # tear our window down. Save our place in the UI so we return
        # here when the replay ends.
        if bui.app.classic is not None:
            bui.app.classic.save_ui_state()

        def do_it() -> None:
            try:
                # Reset to normal speed.
                bs.set_replay_speed_exponent(0)
                bui.fade_screen(True)
                # Content already resolved above, so launch directly.
                bs.launch_replay(path)
            except Exception:
                logging.exception('Error running replay session.')

                # Drop back into a fresh main menu session
                # in case we half-launched or something.
                from bascenev1lib import mainmenu

                bs.new_host_session(mainmenu.MainMenuSession)

        bui.fade_screen(False, endcall=bui.CallStrict(bui.pushcall, do_it))
        # The user may have navigated away during the async prep above,
        # in which case our window is already gone or on its way out;
        # transitioning it out again logs a warning.
        if self._root_widget and not self._root_widget.transitioning_out:
            bui.containerwidget(edit=self._root_widget, transition='out_left')

    def _on_my_replay_rename_press(self) -> None:
        if self._my_replay_selected is None:
            self._no_replay_selected_error()
            return
        c_width = 600
        c_height = 250
        assert bui.app.classic is not None
        uiscale = bui.app.ui_v1.uiscale
        self._my_replays_rename_window = cnt = bui.containerwidget(
            scale=(
                1.8
                if uiscale is bui.UIScale.SMALL
                else 1.55 if uiscale is bui.UIScale.MEDIUM else 1.0
            ),
            size=(c_width, c_height),
            transition='in_scale',
            parent=bui.get_special_widget('overlay_stack'),
            darken_behind=True,
        )
        dname = self._get_replay_display_name(self._my_replay_selected)
        bui.textwidget(
            parent=cnt,
            size=(0, 0),
            h_align='center',
            v_align='center',
            text=_ws.rename_replay(replay=dname),
            maxwidth=c_width * 0.8,
            position=(c_width * 0.5, c_height - 60),
        )
        self._my_replay_rename_text = txt = bui.textwidget(
            parent=cnt,
            id=f'{self.main_window_id_prefix}|replayrenametext',
            size=(c_width * 0.8, 40),
            h_align='left',
            v_align='center',
            text=dname,
            editable=True,
            description=_ws.replay_name,
            position=(c_width * 0.1, c_height - 140),
            autoselect=True,
            maxwidth=c_width * 0.7,
            max_chars=200,
        )
        cbtn = bui.buttonwidget(
            parent=cnt,
            id=f'{self.main_window_id_prefix}|replayrenamecancel',
            label=_commonassets.strings.actions.cancel,
            on_activate_call=bui.CallStrict(
                lambda c: bui.containerwidget(edit=c, transition='out_scale'),
                cnt,
            ),
            size=(180, 60),
            position=(30, 30),
            autoselect=True,
        )
        okb = bui.buttonwidget(
            parent=cnt,
            id=f'{self.main_window_id_prefix}|replayrename',
            label=_commonassets.strings.actions.rename,
            size=(180, 60),
            position=(c_width - 230, 30),
            on_activate_call=bui.CallStrict(
                self._rename_my_replay, self._my_replay_selected
            ),
            autoselect=True,
        )
        bui.widget(edit=cbtn, right_widget=okb)
        bui.widget(edit=okb, left_widget=cbtn)
        bui.textwidget(edit=txt, on_return_press_call=okb.activate)
        bui.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb)

    def _rename_my_replay(self, replay: str) -> None:
        new_name = None
        try:
            if not self._my_replay_rename_text:
                return
            new_name_raw = cast(
                str, bui.textwidget(query=self._my_replay_rename_text)
            )
            new_name = new_name_raw + '.brp'

            # Ignore attempts to change it to what it already is
            # (or what it looks like to the user).
            if (
                replay != new_name
                and self._get_replay_display_name(replay) != new_name_raw
            ):
                old_name_full = (bui.get_replays_dir() + '/' + replay).encode(
                    'utf-8'
                )
                new_name_full = (bui.get_replays_dir() + '/' + new_name).encode(
                    'utf-8'
                )
                # False alarm; bui.textwidget can return non-None val.
                # pylint: disable=unsupported-membership-test
                if os.path.exists(new_name_full):
                    _builtinassets.audio.error.get().play()
                    bui.screenmessage(
                        (
                            _classicassets.strings.watch
                        ).replay_rename_error_already_exists,
                        color=(1, 0, 0),
                    )
                elif any(char in new_name_raw for char in ['/', '\\', ':']):
                    _builtinassets.audio.error.get().play()
                    bui.screenmessage(
                        _ws.replay_rename_error_invalid,
                        color=(1, 0, 0),
                    )
                else:
                    bui.increment_analytics_count('Replay rename')
                    os.rename(old_name_full, new_name_full)
                    self._refresh_my_replays()
                    _builtinassets.audio.gun_cocking.get().play()
        except Exception:
            logging.exception(
                "Error renaming replay '%s' to '%s'.", replay, new_name
            )
            _builtinassets.audio.error.get().play()
            bui.screenmessage(
                _ws.replay_rename_error,
                color=(1, 0, 0),
            )

        bui.containerwidget(
            edit=self._my_replays_rename_window, transition='out_scale'
        )

    def _on_my_replay_delete_press(self) -> None:
        from bauiv1lib import confirm

        if self._my_replay_selected is None:
            self._no_replay_selected_error()
            return
        confirm.ConfirmWindow(
            _ws.delete_confirm(
                replay=self._get_replay_display_name(self._my_replay_selected)
            ),
            bui.CallStrict(self._delete_replay, self._my_replay_selected),
            width=450,
            height=150,
        )

    def _get_replay_display_name(self, replay: str) -> str:
        if replay.endswith('.brp'):
            replay = replay[:-4]
        if replay == '__lastReplay':
            return _ws.replay_name_default.evaluate()
        return replay

    def _delete_replay(self, replay: str) -> None:
        try:
            bui.increment_analytics_count('Replay delete')
            os.remove((bui.get_replays_dir() + '/' + replay).encode('utf-8'))
            self._refresh_my_replays()
            _classicassets.audio.shield_down.get().play()
            if replay == self._my_replay_selected:
                self._my_replay_selected = None
        except Exception:
            logging.exception("Error deleting replay '%s'.", replay)
            _builtinassets.audio.error.get().play()
            bui.screenmessage(
                _ws.replay_delete_error,
                color=(1, 0, 0),
            )

    def _on_my_replay_select(self, replay: str) -> None:
        self._my_replay_selected = replay

    def _refresh_my_replays(self) -> None:
        assert self._columnwidget is not None
        for child in self._columnwidget.get_children():
            child.delete()
        t_scale = 1.6
        try:
            names = os.listdir(bui.get_replays_dir())

            # Ignore random other files in there.
            names = [n for n in names if n.endswith('.brp')]
            names.sort(key=lambda x: x.lower())
        except Exception:
            logging.exception('Error listing replays dir.')
            names = []

        assert self._my_replays_scroll_width is not None
        assert self._my_replays_watch_replay_button is not None
        for i, name in enumerate(names):
            txt = bui.textwidget(
                parent=self._columnwidget,
                id=f'{self.main_window_id_prefix}|replay{i}',
                size=(self._my_replays_scroll_width / t_scale, 30),
                selectable=True,
                color=(
                    (1.0, 1, 0.4) if name == '__lastReplay.brp' else (1, 1, 1)
                ),
                always_highlight=True,
                on_select_call=bui.CallStrict(self._on_my_replay_select, name),
                on_activate_call=self._my_replays_watch_replay_button.activate,
                text=self._get_replay_display_name(name),
                h_align='left',
                v_align='center',
                corner_scale=t_scale,
                maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93,
            )
            if i == 0:
                bui.widget(
                    edit=txt,
                    up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button,
                )
                self._my_replay_selected = name
