from random import randint
from typing import Any, cast, List, Union

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gio, GLib, GObject, Gtk, Pango

from sublime.cache_manager import CacheManager
from sublime.config import AppConfiguration
from sublime.server.api_objects import (
    AlbumID3,
    ArtistID3,
    ArtistInfo2,
    ArtistWithAlbumsID3,
    Child,
)
from sublime.ui import util
from sublime.ui.common import AlbumWithSongs, IconButton, SpinnerImage


class ArtistsPanel(Gtk.Paned):
    """Defines the arist panel."""

    __gsignals__ = {
        "song-clicked": (
            GObject.SignalFlags.RUN_FIRST,
            GObject.TYPE_NONE,
            (int, object, object),
        ),
        "refresh-window": (
            GObject.SignalFlags.RUN_FIRST,
            GObject.TYPE_NONE,
            (object, bool),
        ),
    }

    def __init__(self, *args, **kwargs):
        Gtk.Paned.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)

        self.artist_list = ArtistList()
        self.pack1(self.artist_list, False, False)

        self.artist_detail_panel = ArtistDetailPanel()
        self.artist_detail_panel.connect(
            "song-clicked", lambda _, *args: self.emit("song-clicked", *args),
        )
        self.pack2(self.artist_detail_panel, True, False)

    def update(self, app_config: AppConfiguration, force: bool = False):
        self.artist_list.update(app_config=app_config)
        self.artist_detail_panel.update(app_config=app_config)


class _ArtistModel(GObject.GObject):
    artist_id = GObject.Property(type=str)
    name = GObject.Property(type=str)
    album_count = GObject.Property(type=int)

    def __init__(self, artist_id: str, name: str, album_count: int):
        GObject.GObject.__init__(self)
        self.artist_id = artist_id
        self.name = name
        self.album_count = album_count


class ArtistList(Gtk.Box):
    def __init__(self):
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)

        list_actions = Gtk.ActionBar()

        refresh = IconButton("view-refresh-symbolic", "Refresh list of artists")
        refresh.connect("clicked", lambda *a: self.update(force=True))
        list_actions.pack_end(refresh)

        self.add(list_actions)

        self.loading_indicator = Gtk.ListBox()
        spinner_row = Gtk.ListBoxRow(activatable=False, selectable=False)
        spinner = Gtk.Spinner(name="artist-list-spinner", active=True)
        spinner_row.add(spinner)
        self.loading_indicator.add(spinner_row)
        self.pack_start(self.loading_indicator, False, False, 0)

        list_scroll_window = Gtk.ScrolledWindow(min_content_width=250)

        def create_artist_row(model: _ArtistModel) -> Gtk.ListBoxRow:
            label_text = [f"<b>{util.esc(model.name)}</b>"]

            album_count = model.album_count
            if album_count:
                label_text.append(
                    "{} {}".format(album_count, util.pluralize("album", album_count))
                )

            row = Gtk.ListBoxRow(
                action_name="app.go-to-artist",
                action_target=GLib.Variant("s", model.artist_id),
            )
            row.add(
                Gtk.Label(
                    label="\n".join(label_text),
                    use_markup=True,
                    margin=12,
                    halign=Gtk.Align.START,
                    ellipsize=Pango.EllipsizeMode.END,
                    max_width_chars=30,
                )
            )
            row.show_all()
            return row

        self.artists_store = Gio.ListStore()
        self.list = Gtk.ListBox(name="artist-list")
        self.list.bind_model(self.artists_store, create_artist_row)
        list_scroll_window.add(self.list)

        self.pack_start(list_scroll_window, True, True, 0)

    @util.async_callback(
        lambda *a, **k: CacheManager.get_artists(*a, **k),
        before_download=lambda self: self.loading_indicator.show_all(),
        on_failure=lambda self, e: self.loading_indicator.hide(),
    )
    def update(
        self, artists: List[ArtistID3], app_config: AppConfiguration, **kwargs,
    ):
        new_store = []
        selected_idx = None
        for i, artist in enumerate(artists):
            if app_config.state and app_config.state.selected_artist_id == artist.id:
                selected_idx = i

            new_store.append(
                _ArtistModel(artist.id, artist.name, artist.get("albumCount", ""),)
            )

        util.diff_model_store(self.artists_store, new_store)

        # Preserve selection
        if selected_idx is not None:
            row = self.list.get_row_at_index(selected_idx)
            self.list.select_row(row)

        self.loading_indicator.hide()


class ArtistDetailPanel(Gtk.ScrolledWindow):
    """Defines the artists list."""

    __gsignals__ = {
        "song-clicked": (
            GObject.SignalFlags.RUN_FIRST,
            GObject.TYPE_NONE,
            (int, object, object),
        ),
    }

    update_order_token = 0

    def __init__(self, *args, **kwargs):
        super().__init__(*args, name="artist-detail-panel", **kwargs)
        self.albums: Union[List[AlbumID3], List[Child]] = []
        self.artist_id = None

        artist_info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # Artist info panel
        self.big_info_panel = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)

        self.artist_artwork = SpinnerImage(
            loading=False,
            image_name="artist-album-artwork",
            spinner_name="artist-artwork-spinner",
            image_size=300,
        )
        self.big_info_panel.pack_start(self.artist_artwork, False, False, 0)

        # Action buttons, name, comment, number of songs, etc.
        artist_details_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # Action buttons (note we are packing end here, so we have to put them
        # in right-to-left).
        self.artist_action_buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)

        view_refresh_button = IconButton("view-refresh-symbolic", "Refresh artist info")
        view_refresh_button.connect("clicked", self.on_view_refresh_click)
        self.artist_action_buttons.pack_end(view_refresh_button, False, False, 5)

        download_all_btn = IconButton(
            "folder-download-symbolic", "Download all songs by this artist"
        )
        download_all_btn.connect("clicked", self.on_download_all_click)
        self.artist_action_buttons.pack_end(download_all_btn, False, False, 5)

        artist_details_box.pack_start(self.artist_action_buttons, False, False, 5)

        artist_details_box.pack_start(Gtk.Box(), True, False, 0)

        self.artist_indicator = self.make_label(name="artist-indicator")
        artist_details_box.add(self.artist_indicator)

        self.artist_name = self.make_label(name="artist-name")
        artist_details_box.add(self.artist_name)

        self.artist_bio = self.make_label(
            name="artist-bio", justify=Gtk.Justification.LEFT
        )
        self.artist_bio.set_line_wrap(True)
        artist_details_box.add(self.artist_bio)

        self.similar_artists_scrolledwindow = Gtk.ScrolledWindow()
        similar_artists_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)

        self.similar_artists_label = self.make_label(name="similar-artists")
        similar_artists_box.add(self.similar_artists_label)

        self.similar_artists_button_box = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL
        )
        similar_artists_box.add(self.similar_artists_button_box)
        self.similar_artists_scrolledwindow.add(similar_artists_box)

        artist_details_box.add(self.similar_artists_scrolledwindow)

        self.artist_stats = self.make_label(name="artist-stats")
        artist_details_box.add(self.artist_stats)

        self.play_shuffle_buttons = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            name="playlist-play-shuffle-buttons",
        )

        play_button = IconButton(
            "media-playback-start-symbolic", label="Play All", relief=True,
        )
        play_button.connect("clicked", self.on_play_all_clicked)
        self.play_shuffle_buttons.pack_start(play_button, False, False, 0)

        shuffle_button = IconButton(
            "media-playlist-shuffle-symbolic", label="Shuffle All", relief=True,
        )
        shuffle_button.connect("clicked", self.on_shuffle_all_button)
        self.play_shuffle_buttons.pack_start(shuffle_button, False, False, 5)
        artist_details_box.add(self.play_shuffle_buttons)

        self.big_info_panel.pack_start(artist_details_box, True, True, 10)

        artist_info_box.pack_start(self.big_info_panel, False, True, 0)

        self.albums_list = AlbumsListWithSongs()
        self.albums_list.connect(
            "song-clicked", lambda _, *args: self.emit("song-clicked", *args),
        )
        artist_info_box.pack_start(self.albums_list, True, True, 0)

        self.add(artist_info_box)

    def update(self, app_config: AppConfiguration):
        self.artist_id = app_config.state.selected_artist_id
        if app_config.state.selected_artist_id is None:
            self.artist_action_buttons.hide()
            self.artist_indicator.set_text("")
            self.artist_name.set_markup("")
            self.artist_stats.set_markup("")

            self.artist_bio.set_markup("")
            self.similar_artists_scrolledwindow.hide()
            self.play_shuffle_buttons.hide()

            self.artist_artwork.set_from_file(None)

            self.albums = cast(List[Child], [])
            self.albums_list.update(None)
        else:
            self.update_order_token += 1
            self.artist_action_buttons.show()
            self.update_artist_view(
                app_config.state.selected_artist_id,
                app_config=app_config,
                order_token=self.update_order_token,
            )

    @util.async_callback(
        lambda *a, **k: CacheManager.get_artist(*a, **k),
        before_download=lambda self: self.set_all_loading(True),
        on_failure=lambda self, e: self.set_all_loading(False),
    )
    def update_artist_view(
        self,
        artist: ArtistWithAlbumsID3,
        app_config: AppConfiguration,
        force: bool = False,
        order_token: int = None,
    ):
        if order_token != self.update_order_token:
            return

        self.artist_indicator.set_text("ARTIST")
        self.artist_name.set_markup(util.esc(f"<b>{artist.name}</b>"))
        self.artist_stats.set_markup(self.format_stats(artist))

        self.update_artist_info(
            artist.id, force=force, order_token=order_token,
        )
        self.update_artist_artwork(
            artist, force=force, order_token=order_token,
        )

        self.albums = artist.get("album", artist.get("child", []))
        self.albums_list.update(artist)

    @util.async_callback(lambda *a, **k: CacheManager.get_artist_info(*a, **k),)
    def update_artist_info(
        self,
        artist_info: ArtistInfo2,
        app_config: AppConfiguration,
        force: bool = False,
        order_token: int = None,
    ):
        if order_token != self.update_order_token:
            return

        self.artist_bio.set_markup(util.esc("".join(artist_info.biography)))
        self.play_shuffle_buttons.show_all()

        if len(artist_info.similarArtist or []) > 0:
            self.similar_artists_label.set_markup("<b>Similar Artists:</b> ")
            for c in self.similar_artists_button_box.get_children():
                self.similar_artists_button_box.remove(c)

            for artist in artist_info.similarArtist[:5]:
                self.similar_artists_button_box.add(
                    Gtk.LinkButton(
                        label=artist.name,
                        name="similar-artist-button",
                        action_name="app.go-to-artist",
                        action_target=GLib.Variant("s", artist.id),
                    )
                )
            self.similar_artists_scrolledwindow.show_all()
        else:
            self.similar_artists_scrolledwindow.hide()

    @util.async_callback(
        lambda *a, **k: CacheManager.get_artist_artwork(*a, **k),
        before_download=lambda self: self.artist_artwork.set_loading(True),
        on_failure=lambda self, e: self.artist_artwork.set_loading(False),
    )
    def update_artist_artwork(
        self,
        cover_art_filename: str,
        app_config: AppConfiguration,
        force: bool = False,
        order_token: int = None,
    ):
        if order_token != self.update_order_token:
            return

        self.artist_artwork.set_from_file(cover_art_filename)
        self.artist_artwork.set_loading(False)

    # Event Handlers
    # =========================================================================
    def on_view_refresh_click(self, *args):
        self.update_artist_view(
            self.artist_id, force=True, order_token=self.update_order_token,
        )

    def on_download_all_click(self, btn: Any):
        CacheManager.batch_download_songs(
            self.get_artist_song_ids(),
            before_download=lambda: self.update_artist_view(
                self.artist_id, order_token=self.update_order_token,
            ),
            on_song_download_complete=lambda i: self.update_artist_view(
                self.artist_id, order_token=self.update_order_token,
            ),
        )

    def on_play_all_clicked(self, btn: Any):
        songs = self.get_artist_song_ids()
        self.emit(
            "song-clicked", 0, songs, {"force_shuffle_state": False},
        )

    def on_shuffle_all_button(self, btn: Any):
        songs = self.get_artist_song_ids()
        self.emit(
            "song-clicked",
            randint(0, len(songs) - 1),
            songs,
            {"force_shuffle_state": True},
        )

    # Helper Methods
    # =========================================================================
    def set_all_loading(self, loading_state: bool):
        if loading_state:
            self.albums_list.spinner.start()
            self.albums_list.spinner.show()
            self.artist_artwork.set_loading(True)
        else:
            self.albums_list.spinner.hide()
            self.artist_artwork.set_loading(False)

    def make_label(self, text: str = None, name: str = None, **params,) -> Gtk.Label:
        return Gtk.Label(
            label=text, name=name, halign=Gtk.Align.START, xalign=0, **params,
        )

    def format_stats(self, artist: ArtistWithAlbumsID3) -> str:
        album_count = artist.get("albumCount", 0)
        song_count = sum(a.songCount for a in artist.album)
        duration = sum(a.duration for a in artist.album)
        return util.dot_join(
            "{} {}".format(album_count, util.pluralize("album", album_count)),
            "{} {}".format(song_count, util.pluralize("song", song_count)),
            util.format_sequence_duration(duration),
        )

    def get_artist_song_ids(self) -> List[int]:
        songs = []
        for album in CacheManager.get_artist(self.artist_id).result().album:
            album_songs = CacheManager.get_album(album.id).result()
            for song in album_songs.get("song", []):
                songs.append(song.id)

        return songs


class AlbumsListWithSongs(Gtk.Overlay):
    __gsignals__ = {
        "song-clicked": (
            GObject.SignalFlags.RUN_FIRST,
            GObject.TYPE_NONE,
            (int, object, object),
        ),
    }

    def __init__(self):
        Gtk.Overlay.__init__(self)
        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.add(self.box)

        self.spinner = Gtk.Spinner(
            name="albumslist-with-songs-spinner",
            active=False,
            halign=Gtk.Align.CENTER,
            valign=Gtk.Align.CENTER,
        )
        self.add_overlay(self.spinner)

        self.albums = []

    def update(self, artist: ArtistWithAlbumsID3):
        def remove_all():
            for c in self.box.get_children():
                self.box.remove(c)

        if artist is None:
            remove_all()
            self.spinner.hide()
            return

        new_albums = artist.get("album", artist.get("child", []))

        if self.albums == new_albums:
            # No need to do anything.
            self.spinner.hide()
            return

        self.albums = new_albums

        remove_all()

        for album in self.albums:
            album_with_songs = AlbumWithSongs(album, show_artist_name=False)
            album_with_songs.connect(
                "song-clicked", lambda _, *args: self.emit("song-clicked", *args),
            )
            album_with_songs.connect("song-selected", self.on_song_selected)
            album_with_songs.show_all()
            self.box.add(album_with_songs)

        self.spinner.stop()
        self.spinner.hide()

    def on_song_selected(self, album_component: AlbumWithSongs):
        for child in self.box.get_children():
            if album_component != child:
                child.deselect_all()
