# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for viewing/creating/editing playlists."""
from __future__ import annotations
import copy
import time
from typing import TYPE_CHECKING, override
import bauiv1 as bui
if TYPE_CHECKING:
    from typing import Any, Callable
    import bascenev1 as bs
REQUIRE_PRO = False
[docs]
class PlaylistCustomizeBrowserWindow(bui.MainWindow):
    """Window for viewing a playlist."""
    def __init__(
        self,
        sessiontype: type[bs.Session],
        transition: str | None = 'in_right',
        origin_widget: bui.Widget | None = None,
        select_playlist: str | None = None,
    ):
        # pylint: disable=too-many-locals
        # pylint: disable=too-many-statements
        # pylint: disable=cyclic-import
        from bauiv1lib import playlist
        self._sessiontype = sessiontype
        self._pvars = playlist.PlaylistTypeVars(sessiontype)
        self._max_playlists = 30
        self._r = 'gameListWindow'
        assert bui.app.classic is not None
        uiscale = bui.app.ui_v1.uiscale
        self._width = 1200.0 if uiscale is bui.UIScale.SMALL else 650.0
        self._height = (
            800.0
            if uiscale is bui.UIScale.SMALL
            else 420.0 if uiscale is bui.UIScale.MEDIUM else 500.0
        )
        # 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.8
            if uiscale is bui.UIScale.SMALL
            else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
        )
        # Calc screen size in our local container space and clamp to a
        # bit smaller than our container size.
        target_width = min(self._width - 70, screensize[0] / scale)
        target_height = min(self._height - 40, 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.
        yoffs = (
            0.5 * self._height
            + 0.5 * target_height
            + (30.0 if uiscale is bui.UIScale.SMALL else 50)
        )
        self._button_width = 90
        self._x_inset = 10
        self._scroll_width = (
            target_width - self._button_width - 2.0 * self._x_inset
        )
        self._scroll_height = target_height - 75
        self._scroll_bottom = yoffs - 98 - self._scroll_height
        self._button_height = self._scroll_height / 6.0
        super().__init__(
            root_widget=bui.containerwidget(
                size=(self._width, self._height),
                scale=scale,
                toolbar_visibility=(
                    'menu_minimal'
                    if uiscale is bui.UIScale.SMALL
                    else 'menu_full'
                ),
            ),
            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,
        )
        self._back_button: bui.Widget | None
        if uiscale is bui.UIScale.SMALL:
            self._back_button = None
            bui.containerwidget(
                edit=self._root_widget, on_cancel_call=self.main_window_back
            )
        else:
            self._back_button = bui.buttonwidget(
                parent=self._root_widget,
                id=f'{self.main_window_id_prefix}|back',
                position=(43, yoffs - 87),
                size=(60, 60),
                scale=0.77,
                autoselect=True,
                text_scale=1.3,
                label=bui.charstr(bui.SpecialChar.BACK),
                button_type='backSmall',
            )
        bui.textwidget(
            parent=self._root_widget,
            position=(0, yoffs - (77 if uiscale is bui.UIScale.SMALL else 77)),
            size=(self._width, 25),
            text=bui.Lstr(
                resource=f'{self._r}.titleText',
                subs=[('${TYPE}', self._pvars.window_title_name)],
            ),
            color=bui.app.ui_v1.heading_color,
            maxwidth=290,
            h_align='center',
            v_align='center',
        )
        h = self._width * 0.5 - (self._scroll_width + self._button_width) * 0.5
        b_color = (0.6, 0.53, 0.63)
        b_textcolor = (0.75, 0.7, 0.8)
        self._lock_images: list[bui.Widget] = []
        xmargin = 0.06
        ymargin = 0.05
        def _make_button(
            i: int, button_id: str, label: bui.Lstr, call: Callable[[], None]
        ) -> bui.Widget:
            v = self._scroll_bottom + self._button_height * i
            return bui.buttonwidget(
                parent=self._root_widget,
                id=button_id,
                position=(
                    h + xmargin * self._button_width,
                    v + ymargin * self._button_height,
                ),
                size=(
                    self._button_width * (1.0 - 2.0 * xmargin),
                    self._button_height * (1.0 - 2.0 * ymargin),
                ),
                on_activate_call=call,
                color=b_color,
                autoselect=True,
                button_type='square',
                textcolor=b_textcolor,
                text_scale=0.7,
                label=label,
            )
        new_button = _make_button(
            5,
            f'{self.main_window_id_prefix}|new',
            bui.Lstr(
                resource='newText', fallback_resource=f'{self._r}.newText'
            ),
            self._new_playlist,
        )
        self._edit_button = _make_button(
            4,
            f'{self.main_window_id_prefix}|edit',
            bui.Lstr(
                resource='editText',
                fallback_resource=f'{self._r}.editText',
            ),
            self._edit_playlist,
        )
        duplicate_button = _make_button(
            3,
            f'{self.main_window_id_prefix}|duplicate',
            bui.Lstr(
                resource='duplicateText',
                fallback_resource=f'{self._r}.duplicateText',
            ),
            self._duplicate_playlist,
        )
        delete_button = _make_button(
            2,
            f'{self.main_window_id_prefix}|delete',
            bui.Lstr(
                resource='deleteText', fallback_resource=f'{self._r}.deleteText'
            ),
            self._delete_playlist,
        )
        self._import_button = _make_button(
            1,
            f'{self.main_window_id_prefix}|import',
            bui.Lstr(resource='importText'),
            self._import_playlist,
        )
        share_button = _make_button(
            0,
            f'{self.main_window_id_prefix}|share',
            bui.Lstr(resource='shareText'),
            self._share_playlist,
        )
        scrollwidget = bui.scrollwidget(
            parent=self._root_widget,
            size=(self._scroll_width, self._scroll_height),
            position=(
                self._width * 0.5
                - (self._scroll_width + self._button_width) * 0.5
                + self._button_width,
                self._scroll_bottom,
            ),
            highlight=False,
            border_opacity=0.4,
        )
        if self._back_button is not None:
            bui.widget(edit=self._back_button, right_widget=scrollwidget)
        self._columnwidget = bui.columnwidget(
            parent=scrollwidget, border=2, margin=0
        )
        h = 145
        self._do_randomize_val = bui.app.config.get(
            self._pvars.config_name + ' Playlist Randomize', 0
        )
        h += 210
        for btn in [
            new_button,
            delete_button,
            self._edit_button,
            duplicate_button,
            self._import_button,
            share_button,
        ]:
            bui.widget(edit=btn, right_widget=scrollwidget)
        bui.widget(
            edit=scrollwidget,
            left_widget=new_button,
            right_widget=bui.get_special_widget('squad_button'),
        )
        # Make sure config exists.
        self._config_name_full = f'{self._pvars.config_name} Playlists'
        if self._config_name_full not in bui.app.config:
            bui.app.config[self._config_name_full] = {}
        self._selected_playlist_name: str | None = None
        self._selected_playlist_index: int | None = None
        self._playlist_widgets: list[bui.Widget] = []
        self._refresh(select_playlist=select_playlist)
        if self._back_button is not None:
            bui.buttonwidget(
                edit=self._back_button, on_activate_call=self.main_window_back
            )
            bui.containerwidget(
                edit=self._root_widget, cancel_button=self._back_button
            )
        bui.containerwidget(edit=self._root_widget, selected_child=scrollwidget)
        # Keep our lock images up to date/etc.
        self._update_timer = bui.AppTimer(
            1.0, bui.WeakCallStrict(self._update), repeat=True
        )
        self._update()
[docs]
    @override
    def get_main_window_state(self) -> bui.MainWindowState:
        # Support recreating our window for back/refresh purposes.
        cls = type(self)
        # Avoid dereferencing self within the lambda or we'll keep
        # ourself alive indefinitely.
        stype = self._sessiontype
        return bui.BasicMainWindowState(
            create_call=lambda transition, origin_widget: cls(
                transition=transition,
                origin_widget=origin_widget,
                sessiontype=stype,
            )
        ) 
[docs]
    @override
    def main_window_should_preserve_selection(self) -> bool:
        return True 
[docs]
    @override
    def on_main_window_close(self) -> None:
        if self._selected_playlist_name is not None:
            cfg = bui.app.config
            cfg[f'{self._pvars.config_name} Playlist Selection'] = (
                self._selected_playlist_name
            )
            cfg.commit() 
    def _update(self) -> None:
        assert bui.app.classic is not None
        have = bui.app.classic.accounts.have_pro_options()
        for lock in self._lock_images:
            bui.imagewidget(
                edit=lock, opacity=0.0 if (have or not REQUIRE_PRO) else 1.0
            )
    def _select(self, name: str, index: int) -> None:
        self._selected_playlist_name = name
        self._selected_playlist_index = index
    def _refresh(self, select_playlist: str | None = None) -> None:
        from efro.util import asserttype
        old_selection = self._selected_playlist_name
        # If there was no prev selection, look in prefs.
        if old_selection is None:
            old_selection = bui.app.config.get(
                self._pvars.config_name + ' Playlist Selection'
            )
        old_selection_index = self._selected_playlist_index
        # Delete old.
        while self._playlist_widgets:
            self._playlist_widgets.pop().delete()
        items = list(bui.app.config[self._config_name_full].items())
        # Make sure everything is unicode now.
        items = [
            (i[0].decode(), i[1]) if not isinstance(i[0], str) else i
            for i in items
        ]
        items.sort(key=lambda x: asserttype(x[0], str).lower())
        items = [['__default__', None]] + items  # Default is always first.
        index = 0
        for pname, _ in items:
            assert pname is not None
            txtw = bui.textwidget(
                parent=self._columnwidget,
                size=(self._width - 40, 30),
                maxwidth=440,
                text=self._get_playlist_display_name(pname),
                h_align='left',
                v_align='center',
                color=(
                    (0.6, 0.6, 0.7, 1.0)
                    if pname == '__default__'
                    else (0.85, 0.85, 0.85, 1)
                ),
                always_highlight=True,
                on_select_call=bui.CallStrict(self._select, pname, index),
                on_activate_call=bui.CallStrict(self._edit_button.activate),
                selectable=True,
            )
            # We don't give these widgets ids because we handle
            # re-selecting them ourself, but we need to suppress the
            # warning this usually causes.
            bui.widget(
                edit=txtw,
                show_buffer_top=50,
                show_buffer_bottom=50,
                allow_preserve_selection=False,
            )
            # Hitting up from top widget should jump to 'back'.
            if index == 0:
                bui.widget(
                    edit=txtw,
                    up_widget=(
                        self._back_button
                        if self._back_button is not None
                        else bui.get_special_widget('back_button')
                    ),
                )
            self._playlist_widgets.append(txtw)
            # Select this one if the user requested it.
            if select_playlist is not None:
                if pname == select_playlist:
                    bui.columnwidget(
                        edit=self._columnwidget,
                        selected_child=txtw,
                        visible_child=txtw,
                    )
            else:
                # Select this one if it was previously selected. Go by
                # index if there's one.
                if old_selection_index is not None:
                    if index == old_selection_index:
                        bui.columnwidget(
                            edit=self._columnwidget,
                            selected_child=txtw,
                            visible_child=txtw,
                        )
                else:  # Otherwise look by name.
                    if pname == old_selection:
                        bui.columnwidget(
                            edit=self._columnwidget,
                            selected_child=txtw,
                            visible_child=txtw,
                        )
            index += 1
    def _save_playlist_selection(self) -> None:
        # Store the selected playlist in prefs. This serves dual
        # purposes of letting us re-select it next time if we want and
        # also lets us pass it to the game (since we reset the whole
        # python environment that's not actually easy).
        cfg = bui.app.config
        cfg[self._pvars.config_name + ' Playlist Selection'] = (
            self._selected_playlist_name
        )
        cfg[self._pvars.config_name + ' Playlist Randomize'] = (
            self._do_randomize_val
        )
        cfg.commit()
    def _new_playlist(self) -> None:
        # pylint: disable=cyclic-import
        from bauiv1lib.playlist.editcontroller import PlaylistEditController
        from bauiv1lib.purchase import PurchaseWindow
        # No-op if we're not in control.
        if not self.main_window_has_control():
            return
        assert bui.app.classic is not None
        if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options():
            PurchaseWindow(items=['pro'])
            return
        # Clamp at our max playlist number.
        if len(bui.app.config[self._config_name_full]) > self._max_playlists:
            bui.screenmessage(
                bui.Lstr(
                    translate=(
                        'serverResponses',
                        'Max number of playlists reached.',
                    )
                ),
                color=(1, 0, 0),
            )
            bui.getsound('error').play()
            return
        # In case they cancel so we can return to this state.
        self._save_playlist_selection()
        # Kick off the edit UI.
        PlaylistEditController(sessiontype=self._sessiontype, from_window=self)
    def _edit_playlist(self) -> None:
        # pylint: disable=cyclic-import
        from bauiv1lib.playlist.editcontroller import PlaylistEditController
        from bauiv1lib.purchase import PurchaseWindow
        assert bui.app.classic is not None
        if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options():
            PurchaseWindow(items=['pro'])
            return
        if self._selected_playlist_name is None:
            return
        if self._selected_playlist_name == '__default__':
            bui.getsound('error').play()
            bui.screenmessage(
                bui.Lstr(resource=f'{self._r}.cantEditDefaultText')
            )
            return
        self._save_playlist_selection()
        PlaylistEditController(
            existing_playlist_name=self._selected_playlist_name,
            sessiontype=self._sessiontype,
            from_window=self,
        )
    def _do_delete_playlist(self) -> None:
        plus = bui.app.plus
        assert plus is not None
        plus.add_v1_account_transaction(
            {
                'type': 'REMOVE_PLAYLIST',
                'playlistType': self._pvars.config_name,
                'playlistName': self._selected_playlist_name,
            }
        )
        plus.run_v1_account_transactions()
        bui.getsound('shieldDown').play()
        # (we don't use len()-1 here because the default list adds one)
        assert self._selected_playlist_index is not None
        self._selected_playlist_index = min(
            self._selected_playlist_index,
            len(bui.app.config[self._pvars.config_name + ' Playlists']),
        )
        self._refresh()
    def _import_playlist(self) -> None:
        # pylint: disable=cyclic-import
        from bauiv1lib.playlist import share
        plus = bui.app.plus
        assert plus is not None
        # Gotta be signed in for this to work.
        if plus.get_v1_account_state() != 'signed_in':
            bui.screenmessage(
                bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
            )
            bui.getsound('error').play()
            return
        share.SharePlaylistImportWindow(
            origin_widget=self._import_button,
            on_success_callback=bui.WeakCallStrict(
                self._on_playlist_import_success
            ),
        )
    def _on_playlist_import_success(self) -> None:
        self._refresh()
    def _on_share_playlist_response(self, name: str, response: Any) -> None:
        # pylint: disable=cyclic-import
        from bauiv1lib.playlist import share
        if response is None:
            bui.screenmessage(
                bui.Lstr(resource='internal.unavailableNoConnectionText'),
                color=(1, 0, 0),
            )
            bui.getsound('error').play()
            return
        share.SharePlaylistResultsWindow(name, response)
    def _share_playlist(self) -> None:
        # pylint: disable=cyclic-import
        from bauiv1lib.purchase import PurchaseWindow
        plus = bui.app.plus
        assert plus is not None
        assert bui.app.classic is not None
        if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options():
            PurchaseWindow(items=['pro'])
            return
        # Gotta be signed in for this to work.
        if plus.get_v1_account_state() != 'signed_in':
            bui.screenmessage(
                bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
            )
            bui.getsound('error').play()
            return
        if self._selected_playlist_name == '__default__':
            bui.getsound('error').play()
            bui.screenmessage(
                bui.Lstr(resource=f'{self._r}.cantShareDefaultText'),
                color=(1, 0, 0),
            )
            return
        if self._selected_playlist_name is None:
            return
        plus.add_v1_account_transaction(
            {
                'type': 'SHARE_PLAYLIST',
                'expire_time': time.time() + 5,
                'playlistType': self._pvars.config_name,
                'playlistName': self._selected_playlist_name,
            },
            callback=bui.WeakCallPartial(
                self._on_share_playlist_response, self._selected_playlist_name
            ),
        )
        plus.run_v1_account_transactions()
        bui.screenmessage(bui.Lstr(resource='sharingText'))
    def _delete_playlist(self) -> None:
        # pylint: disable=cyclic-import
        from bauiv1lib.purchase import PurchaseWindow
        from bauiv1lib.confirm import ConfirmWindow
        assert bui.app.classic is not None
        if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options():
            PurchaseWindow(items=['pro'])
            return
        if self._selected_playlist_name is None:
            return
        if self._selected_playlist_name == '__default__':
            bui.getsound('error').play()
            bui.screenmessage(
                bui.Lstr(resource=f'{self._r}.cantDeleteDefaultText')
            )
        else:
            ConfirmWindow(
                bui.Lstr(
                    resource=f'{self._r}.deleteConfirmText',
                    subs=[('${LIST}', self._selected_playlist_name)],
                ),
                self._do_delete_playlist,
                width=450,
                height=150,
            )
    def _get_playlist_display_name(self, playlist: str | bui.Lstr) -> bui.Lstr:
        if playlist == '__default__':
            return self._pvars.default_list_name
        return (
            playlist
            if isinstance(playlist, bui.Lstr)
            else bui.Lstr(value=playlist)
        )
    def _duplicate_playlist(self) -> None:
        # pylint: disable=too-many-branches
        # pylint: disable=cyclic-import
        from bauiv1lib.purchase import PurchaseWindow
        plus = bui.app.plus
        assert plus is not None
        assert bui.app.classic is not None
        if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options():
            PurchaseWindow(items=['pro'])
            return
        if self._selected_playlist_name is None:
            return
        plst: list[dict[str, Any]] | None
        if self._selected_playlist_name == '__default__':
            plst = self._pvars.get_default_list_call()
        else:
            plst = bui.app.config[self._config_name_full].get(
                self._selected_playlist_name
            )
            if plst is None:
                bui.getsound('error').play()
                return
        # Clamp at our max playlist number.
        if len(bui.app.config[self._config_name_full]) > self._max_playlists:
            bui.screenmessage(
                bui.Lstr(
                    translate=(
                        'serverResponses',
                        'Max number of playlists reached.',
                    )
                ),
                color=(1, 0, 0),
            )
            bui.getsound('error').play()
            return
        copy_text = bui.Lstr(resource='copyOfText').evaluate()
        # Get just 'Copy' or whatnot.
        copy_word = copy_text.replace('${NAME}', '').strip()
        # Find a valid dup name that doesn't exist.
        test_index = 1
        base_name = self._get_playlist_display_name(
            self._selected_playlist_name
        ).evaluate()
        # If it looks like a copy, strip digits and spaces off the end.
        if copy_word in base_name:
            while base_name[-1].isdigit() or base_name[-1] == ' ':
                base_name = base_name[:-1]
        while True:
            if copy_word in base_name:
                test_name = base_name
            else:
                test_name = copy_text.replace('${NAME}', base_name)
            if test_index > 1:
                test_name += ' ' + str(test_index)
            if test_name not in bui.app.config[self._config_name_full]:
                break
            test_index += 1
        plus.add_v1_account_transaction(
            {
                'type': 'ADD_PLAYLIST',
                'playlistType': self._pvars.config_name,
                'playlistName': test_name,
                'playlist': copy.deepcopy(plst),
            }
        )
        plus.run_v1_account_transactions()
        bui.getsound('gunCocking').play()
        self._refresh(select_playlist=test_name) 
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
  Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum