from datetime import timedelta
from functools import partial
from random import randint
from typing import cast, List, Sequence

import bleach

from gi.repository import Gio, GLib, GObject, Gtk, Pango, Handy

from ..adapters import (
    AdapterManager,
    api_objects as API,
    CacheMissError,
    SongCacheStatus,
)
from ..config import AppConfiguration
from ..ui import util
from ..ui.common import AlbumWithSongs, IconButton, IconToggleButton, LoadError, SpinnerImage, Sizer
from .actions import run_action


class ArtistsPanel(Handy.Leaflet):
    """Defines the arist panel."""

    def __init__(self, *args, **kwargs):
        super().__init__(transition_type=Handy.LeafletTransitionType.SLIDE, can_swipe_forward=False, interpolate_size=False)

        list_sizer = Sizer(natural_width=400)
        self.artist_list = ArtistList()
        list_sizer.add(self.artist_list)
        self.add(list_sizer)

        details_sizer = Sizer(hexpand=True, natural_width=800)
        self.artist_detail_panel = ArtistDetailPanel()
        details_sizer.add(self.artist_detail_panel)
        self.add(details_sizer)

        def artist_clicked(_):
            if self.get_folded():
                self.set_visible_child(details_sizer)
        self.artist_list.connect("artist-clicked", artist_clicked)

        def back_clicked(_):
            self.set_visible_child(list_sizer)
        self.artist_detail_panel.connect("back-clicked", back_clicked)

        def folded_changed(*_):
            if not self.get_folded():
                self.set_visible_child(list_sizer)

            self.artist_detail_panel.show_mobile = self.get_folded()
        self.connect("notify::folded", folded_changed)

    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: API.Artist):
        GObject.GObject.__init__(self)
        self.artist_id = artist.id
        self.name = artist.name
        self.album_count = artist.album_count or 0


class ArtistList(Gtk.Box):
    __gsignals__ = {
        "artist-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()),
    }

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

        list_actions = Gtk.ActionBar()

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

        self.add(list_actions)

        self.error_container = Gtk.Box()
        self.add(self.error_container)

        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>{model.name}</b>"]

            if album_count := model.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=bleach.clean("\n".join(label_text)),
                    use_markup=True,
                    margin=12,
                    halign=Gtk.Align.START,
                    ellipsize=Pango.EllipsizeMode.END,
                )
            )
            row.show_all()
            return row

        self.artists_store = Gio.ListStore()
        self.list = Gtk.ListBox(name="artist-list", selection_mode=Gtk.SelectionMode.BROWSE)
        self.list.bind_model(self.artists_store, create_artist_row)
        self.list.connect("row-selected", lambda *_: self.emit("artist-clicked"))
        list_scroll_window.add(self.list)

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

    _app_config = None

    @util.async_callback(
        AdapterManager.get_artists,
        before_download=lambda self: self.loading_indicator.show_all(),
        on_failure=lambda self, e: self.loading_indicator.hide(),
    )
    def update(
        self,
        artists: Sequence[API.Artist],
        app_config: AppConfiguration = None,
        is_partial: bool = False,
        **kwargs,
    ):
        if app_config:
            self._app_config = app_config
            self.refresh_button.set_sensitive(not app_config.offline_mode)

        for c in self.error_container.get_children():
            self.error_container.remove(c)
        if is_partial:
            load_error = LoadError(
                "Artist list",
                "load artists",
                has_data=len(artists) > 0,
                offline_mode=(
                    self._app_config.offline_mode if self._app_config else False
                ),
            )
            self.error_container.pack_start(load_error, True, True, 0)
            self.error_container.show_all()
        else:
            self.error_container.hide()

        new_store = []
        selected_idx = None
        for i, artist in enumerate(artists):
            if (
                self._app_config
                and self._app_config.state
                and self._app_config.state.selected_artist_id == artist.id
            ):
                selected_idx = i
            new_store.append(_ArtistModel(artist))

        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.Box):
    """Defines the artists list."""

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

    show_mobile = GObject.Property(type=bool, default=False)

    update_order_token = 0

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            name="artist-detail-panel",
            orientation=Gtk.Orientation.VERTICAL,
            **kwargs,
        )

        self.connect("notify::show-mobile", self.on_show_mobile_changed)

        self.albums: Sequence[API.Album] = []
        self.artist_id = None

        action_bar = Gtk.ActionBar()

        back_button_revealer = Gtk.Revealer(transition_type=Gtk.RevealerTransitionType.CROSSFADE)
        self.bind_property("show-mobile", back_button_revealer, "reveal-child", GObject.BindingFlags.SYNC_CREATE)

        back_button = IconButton("go-previous-symbolic")
        back_button.connect("clicked", lambda *_: self.emit("back-clicked"))
        back_button_revealer.add(back_button)

        action_bar.pack_start(back_button_revealer)

        self.refresh_button = IconButton("view-refresh-symbolic", "Refresh artist info")
        self.refresh_button.connect("clicked", self.on_view_refresh_click)
        action_bar.pack_end(self.refresh_button)

        self.download_all_button = IconButton(
            "folder-download-symbolic", "Download all songs by this artist"
        )
        self.download_all_button.connect("clicked", self.on_download_all_click)
        action_bar.pack_end(self.download_all_button)

        self.shuffle_button = IconButton("media-playlist-shuffle-symbolic")
        self.shuffle_button.connect("clicked", self.on_shuffle_all_button)
        action_bar.pack_end(self.shuffle_button)

        self.play_button = IconButton("media-playback-start-symbolic")
        self.play_button.connect("clicked", self.on_play_all_clicked)
        action_bar.pack_end(self.play_button)

        self.pack_start(action_bar, False, False, 0)
        self.pack_start(Gtk.Separator(), False, False, 0)

        self.scrolled_window = Gtk.ScrolledWindow(hscrollbar_policy=Gtk.PolicyType.NEVER)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # Artist info panel
        info_panel = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL, name="artist-info-panel"
        )

        self.artist_artwork = SpinnerImage(
            loading=False,
            image_size=200,
            valign=Gtk.Align.START,
        )
        info_panel.pack_start(self.artist_artwork, False, False, 10)

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

        self.artist_name = self.make_label(
            name="artist-name", ellipsize=Pango.EllipsizeMode.END
        )
        details_box.add(self.artist_name)

        self.artist_bio_revealer = Gtk.Revealer(transition_type=Gtk.RevealerTransitionType.SLIDE_DOWN, reveal_child=True)
        self.artist_bio = self.make_label(
            name="artist-bio", justify=Gtk.Justification.LEFT
        )
        self.artist_bio.set_line_wrap(True)
        self.artist_bio_revealer.add(self.artist_bio)
        details_box.add(self.artist_bio_revealer)

        details_bottom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)

        artist_stats_squeezer = Handy.Squeezer(homogeneous=False)

        self.artist_stats_long = self.make_label(name="artist-stats")
        artist_stats_squeezer.add(self.artist_stats_long)

        self.artist_stats_short = self.make_label(name="artist-stats")
        artist_stats_squeezer.add(self.artist_stats_short)

        details_bottom_box.pack_start(artist_stats_squeezer, False, False, 0)

        self.expand_button_revealer = Gtk.Revealer(transition_type=Gtk.RevealerTransitionType.CROSSFADE, margin_left=10)
        self.expand_button = IconToggleButton(
            "pan-down-symbolic", "Expand"
        )
        self.expand_button.bind_property("active", self.artist_bio_revealer, "reveal-child")
        self.expand_button.connect("clicked", self.on_expand_button_clicked)
        self.expand_button_revealer.add(self.expand_button)
        details_bottom_box.pack_end(self.expand_button_revealer, False, False, 0)

        details_box.pack_end(details_bottom_box, False, False, 0)

        info_panel.pack_start(details_box, True, True, 0)

        box.pack_start(info_panel, False, False, 0)

        self.error_container = Gtk.Box()
        # self.add(self.error_container)

        self.albums_list = AlbumsListWithSongs()
        box.pack_start(self.albums_list, True, True, 0)

        self.scrolled_window.add(box)

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

    def on_show_mobile_changed(self, *_):
        self.expand_button.set_active(not self.show_mobile)
        self.artist_bio_revealer.set_reveal_child(not self.show_mobile)
        self.expand_button_revealer.set_reveal_child(self.show_mobile)
        self.artist_artwork.set_image_size( 120 if self.show_mobile else 200)

    def on_expand_button_clicked(self, *_):
        up_down = "up" if self.expand_button.get_active() else "down"
        self.expand_button.set_icon(f"pan-{up_down}-symbolic")
        self.expand_button.set_tooltip_text(
            "Collapse" if self.expand_button.get_active() else "Expand"
        )

    def update(self, app_config: AppConfiguration):
        self.artist_id = app_config.state.selected_artist_id
        self.offline_mode = app_config.offline_mode
        if app_config.state.selected_artist_id is None:
            self.shuffle_button.set_sensitive(False)
            self.play_button.set_sensitive(False)
        else:
            self.update_order_token += 1
            self.update_artist_view(
                app_config.state.selected_artist_id,
                app_config=app_config,
                order_token=self.update_order_token,
            )
            self.refresh_button.set_sensitive(not self.offline_mode)
            self.download_all_button.set_sensitive(not self.offline_mode)

    @util.async_callback(
        AdapterManager.get_artist,
        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: API.Artist,
        app_config: AppConfiguration,
        force: bool = False,
        order_token: int = None,
        is_partial: bool = False,
    ):
        if order_token != self.update_order_token:
            return

        # Scroll to top
        self.scrolled_window.get_vadjustment().set_value(0)

        self.artist_name.set_markup(bleach.clean(f"<b>{artist.name}</b>"))
        self.artist_name.set_tooltip_text(artist.name)

        self.artist_stats_long.set_markup(self.format_stats(artist, short=False))
        self.artist_stats_short.set_markup(self.format_stats(artist, short=True))

        biography = ""
        if artist.biography:
            biography += bleach.clean(artist.biography)

        if artist.similar_artists:
            biography += "\n\n<b>Similar Artists:</b> "

            # TODO: Make links work
            biography += ", ".join(f"<a href=\"{a.id}\">{bleach.clean(a.name)}</a>" for a in artist.similar_artists[:6])

        self.artist_bio.set_markup(biography)
        self.expand_button.set_sensitive(bool(biography))

        self.update_artist_artwork(
            artist.artist_image_url,
            force=force,
            order_token=order_token,
        )

        for c in self.error_container.get_children():
            self.error_container.remove(c)
        if is_partial:
            has_data = len(artist.albums or []) > 0
            load_error = LoadError(
                "Artist data",
                "load artist details",
                has_data=has_data,
                offline_mode=self.offline_mode,
            )
            self.error_container.pack_start(load_error, True, True, 0)
            self.error_container.show_all()
        else:
            self.error_container.hide()

        self.albums = artist.albums or []

        # (Dis|En)able the "Play All" and "Shuffle All" buttons. If in offline mode, it
        # depends on whether or not there are any cached songs.
        if self.offline_mode:
            has_cached_song = False
            playable_statuses = (
                SongCacheStatus.CACHED,
                SongCacheStatus.PERMANENTLY_CACHED,
            )

            for album in self.albums:
                if album.id:
                    try:
                        songs = AdapterManager.get_album(album.id).result().songs or []
                    except CacheMissError as e:
                        if e.partial_data:
                            songs = cast(API.Album, e.partial_data).songs or []
                        else:
                            songs = []
                    statuses = AdapterManager.get_cached_statuses([s.id for s in songs])
                    if any(s in playable_statuses for s in statuses):
                        has_cached_song = True
                        break

            self.play_button.set_sensitive(has_cached_song)
            self.shuffle_button.set_sensitive(has_cached_song)
        else:
            self.play_button.set_sensitive(not self.offline_mode)
            self.shuffle_button.set_sensitive(not self.offline_mode)

        self.albums_list.update(artist, app_config, force=force)

    @util.async_callback(
        partial(AdapterManager.get_cover_art_uri, scheme="file"),
        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,
        is_partial: bool = False,
    ):
        if order_token != self.update_order_token:
            return
        self.artist_artwork.set_from_file(cover_art_filename)
        self.artist_artwork.set_loading(False)

        # if self.artist_details_expanded:
        #     self.artist_artwork.set_image_size(300)
        # else:
        #     self.artist_artwork.set_image_size(70)

    # 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, _):
        AdapterManager.batch_download_songs(
            self.get_artist_song_ids(),
            before_download=lambda _: GLib.idle_add(
                lambda: self.update_artist_view(
                    self.artist_id,
                    order_token=self.update_order_token,
                )
            ),
            on_song_download_complete=lambda _: GLib.idle_add(
                lambda: self.update_artist_view(
                    self.artist_id,
                    order_token=self.update_order_token,
                )
            ),
        )

    def on_play_all_clicked(self, _):
        songs = self.get_artist_song_ids()
        run_action(self, 'app.play-song', 0, songs, {"force_shuffle_state": False})

    def on_shuffle_all_button(self, _):
        songs = self.get_artist_song_ids()
        song_idx = randint(0, len(songs) - 1)
        run_action(self, 'app.play-song', song_idx, 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: API.Artist, short=False) -> str:
        album_count = artist.album_count or len(artist.albums or [])
        song_count, duration = 0, timedelta(0)
        for album in artist.albums or []:
            song_count += album.song_count or 0
            duration += album.duration or timedelta(0)

        return util.dot_join(
            "{} {}".format(album_count, util.pluralize("album", album_count)),
            "{} {}".format(song_count, util.pluralize("song", song_count)),
            util.format_song_duration(duration)
                if short else util.format_sequence_duration(duration),
        )

    def get_artist_song_ids(self) -> List[str]:
        try:
            artist = AdapterManager.get_artist(self.artist_id).result()
        except CacheMissError as c:
            artist = cast(API.Artist, c.partial_data)

        if not artist:
            return []

        songs = []
        for album in artist.albums or []:
            assert album.id
            try:
                album_with_songs = AdapterManager.get_album(album.id).result()
            except CacheMissError as c:
                album_with_songs = cast(API.Album, c.partial_data)
            if not album_with_songs:
                continue
            for song in album_with_songs.songs or []:
                songs.append(song.id)

        return songs


class AlbumsListWithSongs(Gtk.Overlay):
    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: API.Artist, app_config: AppConfiguration, force: bool = False
    ):
        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 = sorted(
            artist.albums or [], key=lambda a: (a.year or float("inf"), a.name)
        )

        if self.albums == new_albums:
            # Just go through all of the colidren and update them.
            for c, album in zip(self.box.get_children(), self.albums):
                c.update(album, app_config=app_config, force=force)

            self.spinner.hide()
            return

        self.albums = new_albums

        remove_all()

        for album in self.albums:
            album_with_songs = AlbumWithSongs(show_artist_name=False)
            album_with_songs.update(album, app_config, force=force)
            # album_with_songs.connect("song-selected", self.on_song_selected)
            album_with_songs.show_all()
            self.box.add(album_with_songs)

        # Update everything (no force to ensure that if we are online, then everything
        # is clickable)
        # for c in self.box.get_children():
        #     c.update(app_config=app_config)

        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()
