# Copyright (c) 2014-2017 Cedric Bellegarde <cedric.bellegarde@adishatz.org>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from gi.repository import GLib, Soup, GObject

import json
from base64 import b64encode
from time import time, sleep

from lollypop.sqlcursor import SqlCursor
from lollypop.tagreader import TagReader
from lollypop.logger import Logger
from lollypop.objects import Album
from lollypop.helper_task import TaskHelper
from lollypop.define import SPOTIFY_CLIENT_ID, SPOTIFY_SECRET, App, Type


class SpotifyHelper(GObject.Object):
    """
        Helper for Spotify
    """
    __CHARTS = "https://spotifycharts.com/regional/%s/weekly/latest/download"
    __gsignals__ = {
        "new-album": (GObject.SignalFlags.RUN_FIRST, None,
                      (GObject.TYPE_PYOBJECT, str)),
        "new-artist": (GObject.SignalFlags.RUN_FIRST, None, (str, str)),
        "search-finished": (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    def __init__(self):
        """
            Init object
        """
        GObject.Object.__init__(self)
        self.__token_expires = 0
        self.__token = None
        self.__loading_token = False

    def get_token(self):
        """
            Get a new auth token
        """
        try:
            token_uri = "https://accounts.spotify.com/api/token"
            credentials = "%s:%s" % (SPOTIFY_CLIENT_ID, SPOTIFY_SECRET)
            encoded = b64encode(credentials.encode("utf-8"))
            credentials = encoded.decode("utf-8")
            session = Soup.Session.new()
            data = {"grant_type": "client_credentials"}
            msg = Soup.form_request_new_from_hash("POST", token_uri, data)
            msg.request_headers.append("Authorization",
                                       "Basic %s" % credentials)
            status = session.send_message(msg)
            if status == 200:
                body = msg.get_property("response-body")
                data = body.flatten().get_data()
                decode = json.loads(data.decode("utf-8"))
                self.__token_expires = int(time()) + int(decode["expires_in"])
                self.__token = decode["access_token"]
        except Exception as e:
            Logger.error("SpotifyHelper::get_token(): %s", e)

    def wait_for_token(self):
        """
            True if should wait for token
            @return bool
        """
        def on_token(token):
            self.__loading_token = False
        # Remove 60 seconds to be sure
        wait = int(time()) + 60 > self.__token_expires or\
            self.__token is None
        if wait and not self.__loading_token:
            self.__loading_token = True
            App().task_helper.run(self.get_token, callback=(on_token,))
        return wait

    def get_artist_id(self, artist_name, callback):
        """
            Get artist id
            @param artist_name as str
            @param callback as function
        """
        if self.wait_for_token():
            GLib.timeout_add(
                500, self.get_artist_id, artist_name, callback)
            return
        try:
            def on_content(uri, status, data):
                found = False
                if status:
                    decode = json.loads(data.decode("utf-8"))
                    for item in decode["artists"]["items"]:
                        found = True
                        artist_id = item["uri"].split(":")[-1]
                        callback(artist_id)
                        return
                if not found:
                    callback(None)
            artist_name = GLib.uri_escape_string(
                artist_name, None, True).replace(" ", "+")
            token = "Bearer %s" % self.__token
            helper = TaskHelper()
            helper.add_header("Authorization", token)
            uri = "https://api.spotify.com/v1/search?q=%s&type=artist" %\
                artist_name
            helper.load_uri_content(uri, None, on_content)
        except Exception as e:
            Logger.error("SpotifyHelper::get_artist_id(): %s", e)
            callback(None)

    def get_similar_artists(self, artist_id, cancellable):
        """
           Get similar artists
           @param artist_id as int
           @return artists as [str]
        """
        artists = []
        try:
            while self.wait_for_token():
                if cancellable.is_cancelled():
                    raise Exception("cancelled")
                sleep(1)
            token = "Bearer %s" % self.__token
            helper = TaskHelper()
            helper.add_header("Authorization", token)
            uri = "https://api.spotify.com/v1/artists/%s/related-artists" %\
                artist_id
            (status, data) = helper.load_uri_content_sync(uri, cancellable)
            if status:
                decode = json.loads(data.decode("utf-8"))
                for item in decode["artists"]:
                    artists.append(item["name"])
        except Exception as e:
            Logger.error("SpotifyHelper::get_similar_artists(): %s", e)
        return artists

    def search_similar_artists(self, artist_id, cancellable):
        """
            Search similar artists
            @param artist_name as str
            @param cancellable as Gio.Cancellable
            @param callback as function
        """
        try:
            while self.wait_for_token():
                if cancellable.is_cancelled():
                    raise Exception("cancelled")
                sleep(1)
            found = False
            token = "Bearer %s" % self.__token
            helper = TaskHelper()
            helper.add_header("Authorization", token)
            uri = "https://api.spotify.com/v1/artists/%s/related-artists" %\
                artist_id
            (status, data) = helper.load_uri_content_sync(uri, cancellable)
            if status:
                decode = json.loads(data.decode("utf-8"))
                for item in decode["artists"]:
                    if cancellable.is_cancelled():
                        raise Exception("cancelled")
                    found = True
                    artist_name = item["name"]
                    cover_uri = item["images"][1]["url"]
                    GLib.idle_add(self.emit, "new-artist",
                                  artist_name, cover_uri)
        except Exception as e:
            Logger.error("SpotifyHelper::search_similar_artists(): %s", e)
        if not found:
            GLib.idle_add(self.emit, "new-artist", None, None)

    def search(self, search, cancellable):
        """
            Get albums related to search
            We need a thread because we are going to populate DB
            @param search as str
            @param cancellable as Gio.Cancellable
        """
        try:
            while self.wait_for_token():
                if cancellable.is_cancelled():
                    raise Exception("cancelled")
                sleep(1)
            token = "Bearer %s" % self.__token
            helper = TaskHelper()
            helper.add_header("Authorization", token)
            uri = "https://api.spotify.com/v1/search?"
            uri += "q=%s&type=album,track" % search
            (status, data) = helper.load_uri_content_sync(uri, cancellable)
            if status:
                decode = json.loads(data.decode("utf-8"))
                album_ids = []
                self.__create_albums_from_album_payload(
                                                 decode["albums"]["items"],
                                                 album_ids,
                                                 cancellable)
                self.__create_albums_from_tracks_payload(
                                                 decode["tracks"]["items"],
                                                 album_ids,
                                                 cancellable)
        except Exception as e:
            Logger.warning("SpotifyHelper::search(): %s", e)
            # Do not emit search-finished on cancel
            if str(e) == "cancelled":
                return
        GLib.idle_add(self.emit, "search-finished")

    def charts(self, cancellable, language="global"):
        """
            Get albums related to search
            We need a thread because we are going to populate DB
            @param cancellable as Gio.Cancellable
        """
        from csv import reader
        try:
            while self.wait_for_token():
                if cancellable.is_cancelled():
                    raise Exception("cancelled")
                sleep(1)
            token = "Bearer %s" % self.__token
            helper = TaskHelper()
            helper.add_header("Authorization", token)
            uri = self.__CHARTS % language
            spotify_ids = []
            (status, data) = helper.load_uri_content_sync(uri, cancellable)
            if status:
                decode = data.decode("utf-8")
                for line in decode.split("\n"):
                    try:
                        for row in reader([line]):
                            if not row:
                                continue
                            url = row[4]
                            if url == "URL":
                                continue
                            spotify_id = url.split("/")[-1]
                            if spotify_id:
                                spotify_ids.append(spotify_id)
                    except Exception as e:
                        Logger.warning("SpotifyHelper::charts(): %s", e)
            album_ids = []
            for spotify_id in spotify_ids:
                if cancellable.is_cancelled():
                    raise Exception("cancelled")
                payload = self.__get_track_payload(helper,
                                                   spotify_id,
                                                   cancellable)
                self.__create_albums_from_tracks_payload(
                                                 [payload],
                                                 album_ids,
                                                 cancellable)
        except Exception as e:
            Logger.warning("SpotifyHelper::charts(): %s", e)
            # Do not emit search-finished on cancel
            if str(e) == "cancelled":
                return
        GLib.idle_add(self.emit, "search-finished")

#######################
# PRIVATE             #
#######################
    def __get_track_payload(self, helper, spotify_id, cancellable):
        """
            Get track payload
            @param helper as TaskHelper
            @param spotify_id as str
            @param cancellable as Gio.Cancellable
            @return {}
        """
        try:
            uri = "https://api.spotify.com/v1/tracks/%s" % spotify_id
            (status, data) = helper.load_uri_content_sync(uri, cancellable)
            if status:
                return json.loads(data.decode("utf-8"))
        except Exception as e:
            Logger.error("SpotifyHelper::__get_track_payload(): %s", e)
        return None

    def __create_album(self, album_id, cover_uri, cancellable):
        """
            Create album and download cover
            @param cancellable as Gio.Cancellable
        """
        if not cancellable.is_cancelled():
            GLib.idle_add(self.emit, "new-album", Album(album_id), cover_uri)

    def __create_albums_from_tracks_payload(self, payload, album_ids,
                                            cancellable):
        """
            Get albums from a track payload
            @param payload as {}
            @param album_ids as [int]
            @param cancellable as Gio.Cancellable
        """
        new_album_ids = {}
        # Populate tracks
        for item in payload:
            if cancellable.is_cancelled():
                raise Exception("cancelled")
            if App().db.exists_in_db(item["album"]["name"],
                                     [artist["name"]
                                     for artist in item["artists"]],
                                     item["name"]):
                continue
            (album_id,
             track_id,
             cover_uri) = self.__save_track(item)
            if album_id not in new_album_ids.keys():
                new_album_ids[album_id] = cover_uri
        for album_id in new_album_ids.keys():
            if album_id not in album_ids:
                album_ids.append(album_id)
                self.__create_album(album_id,
                                    new_album_ids[album_id],
                                    cancellable)

    def __create_albums_from_album_payload(self, payload, album_ids,
                                           cancellable):
        """
            Get albums from an album payload
            @param payload as {}
            @param album_ids as [int]
            @param cancellable as Gio.Cancellable
        """
        # Populate tracks
        for album_item in payload:
            if cancellable.is_cancelled():
                return
            if App().db.exists_in_db(album_item["name"],
                                     [artist["name"]
                                     for artist in album_item["artists"]],
                                     None):
                continue
            uri = "https://api.spotify.com/v1/albums/%s" % album_item["id"]
            token = "Bearer %s" % self.__token
            helper = TaskHelper()
            helper.add_header("Authorization", token)
            (status, data) = helper.load_uri_content_sync(uri, cancellable)
            if status:
                decode = json.loads(data.decode("utf-8"))
                track_payload = decode["tracks"]["items"]
                for item in track_payload:
                    item["album"] = album_item
                self.__create_albums_from_tracks_payload(track_payload,
                                                         album_ids,
                                                         cancellable)

    def __save_track(self, payload):
        """
            Save track to DB as non persistent
            @param payload as {}
            @return track_id
        """
        t = TagReader()
        title = payload["name"]
        _artists = []
        for artist in payload["artists"]:
            _artists.append(artist["name"])
        _album_artists = []
        for artist in payload["album"]["artists"]:
            _album_artists.append(artist["name"])
        # Translate to tag value
        artists = ";".join(_artists)
        album_artists = ";".join(_album_artists)
        album_name = payload["album"]["name"]
        discnumber = int(payload["disc_number"])
        discname = None
        tracknumber = int(payload["track_number"])
        try:
            release_date = "%sT00:00:00" % payload["album"]["release_date"]
            dt = GLib.DateTime.new_from_iso8601(release_date,
                                                GLib.TimeZone.new_local())
            timestamp = dt.to_unix()
            year = dt.get_year()
        except Exception as e:
            Logger.warning("SpotifyHelper::__save_track(): %s", e)
            timestamp = None
            year = None
        duration = payload["duration_ms"] // 1000
        mb_album_id = mb_track_id = None
        a_sortnames = aa_sortnames = ""
        cover_uri = payload["album"]["images"][1]["url"]
        uri = "web://%s" % payload["id"]
        Logger.debug("SpotifyHelper::__save_track(): Add artists %s" % artists)
        artist_ids = t.add_artists(artists, a_sortnames)

        Logger.debug("SpotifyHelper::__save_track(): "
                     "Add album artists %s" % album_artists)
        album_artist_ids = t.add_artists(album_artists, aa_sortnames)
        # User does not want compilations
        if not App().settings.get_value("show-compilations") and\
                not album_artist_ids:
            album_artist_ids = artist_ids

        missing_artist_ids = list(set(album_artist_ids) - set(artist_ids))
        # https://github.com/gnumdk/lollypop/issues/507#issuecomment-200526942
        # Special case for broken tags
        # Can't do more because don't want to break split album behaviour
        if len(missing_artist_ids) == len(album_artist_ids):
            artist_ids += missing_artist_ids

        Logger.debug("SpotifyHelper::__save_track(): Add album: "
                     "%s, %s" % (album_name, album_artist_ids))
        (added, album_id) = t.add_album(album_name, mb_album_id,
                                        album_artist_ids,
                                        "", False, 0, 0, 0)

        genre_ids = [Type.WEB]

        # Add track to db
        Logger.debug("SpotifyHelper::__save_track(): Add track")
        track_id = App().tracks.get_id_by(title, album_id)
        # Track already in DB
        if track_id is not None:
            return (album_id, track_id, cover_uri)
        track_id = App().tracks.add(title, uri, duration,
                                    tracknumber, discnumber, discname,
                                    album_id, year, timestamp, 0,
                                    0, False, 0,
                                    0, mb_track_id, 0)
        Logger.debug("SpotifyHelper::__save_track(): Update track")
        App().scanner.update_track(track_id, artist_ids, genre_ids)
        Logger.debug("SpotifyHelper::__save_track(): Update album")
        SqlCursor.commit(App().db)
        App().scanner.update_album(album_id, album_artist_ids,
                                   genre_ids, year, timestamp)
        SqlCursor.commit(App().db)
        return (album_id, track_id, cover_uri)
