# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 Arne Svenson
#
# 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 __future__ import unicode_literals

import traceback
from threading import Thread
from Queue import Queue
import requests
import xbmc
import xbmcgui

from koditidal import HasListItem, AlbumItem, ArtistItem, MixItem, PlaylistItem, TrackItem, VideoItem, PromotionItem, CategoryItem, FolderItem, Quality
from koditidal import plugin, addon, log, _T, TidalSession, TidalUser, TidalFavorites, TidalConfig
from tidalapi import SubscriptionType
from metacache import MetaCache

ALL_SAERCH_FIELDS = ['ARTISTS','ALBUMS','PLAYLISTS','TRACKS','VIDEOS']


class ColoredListItem(HasListItem):

    _colored_labels = True if addon.getSetting('color_mode') == 'true' else False

    def setLabelFormat(self):
        HasListItem.setLabelFormat(self)
        if self._colored_labels:
            self.FOLDER_MASK = '[COLOR blue]{label}[/COLOR]'
            if self._favorites_in_labels:
                self.FAVORITE_MASK = '[COLOR yellow]{label}[/COLOR]'
            else:
                self.FAVORITE_MASK = '{label}'
            self.STREAM_LOCKED_MASK = '[COLOR maroon]{label} ({info})[/COLOR]'
            if self._user_playlists_in_labels:
                self.USER_PLAYLIST_MASK = '{label} [COLOR limegreen][{userpl}][/COLOR]'
            else:
                self.USER_PLAYLIST_MASK = '{label}'
            self.DEFAULT_PLAYLIST_MASK = '[COLOR limegreen]{label} ({mediatype})[/COLOR]'
            self.MASTER_AUDIO_MASK = '{label} [COLOR blue]MQA[/COLOR]'


class AlbumItem2(AlbumItem, ColoredListItem):

    _cached = False

    def __init__(self, item):
        self.__dict__.update(vars(item))
        self.artist = ArtistItem2(self.artist)
        self.artists = [ArtistItem2(artist) for artist in self.artists]
        self._ftArtists = [ArtistItem2(artist) for artist in self._ftArtists]

    @property
    def isMasterAlbum(self):
        return True if self.audioQuality == Quality.hi_res else False


class ArtistItem2(ArtistItem, ColoredListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))


class MixItem2(MixItem, ColoredListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))


class PlaylistItem2(PlaylistItem, ColoredListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))


class TrackItem2(TrackItem, ColoredListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))
        self.artist = ArtistItem2(self.artist)
        self.artists = [ArtistItem2(artist) for artist in self.artists]
        self._ftArtists = [ArtistItem2(artist) for artist in self._ftArtists]
        self.album = AlbumItem2(self.album)

    def getComment(self):
        txt = TrackItem.getComment(self)
        comments = ['track_id=%s' % self.id]
        if txt:
            comments.append(txt)
        userpl = self._userplaylists.keys()
        if len(userpl) > 0:
            comments.append('UserPlaylists: %s' % ', '.join([self._userplaylists.get(plid).get('title') for plid in userpl]))
        #if self.replayGain <> 0:
        #    comments.append("gain:%0.3f, peak:%0.3f" % (self.replayGain, self.peak))
        return ', '.join(comments)


class VideoItem2(VideoItem, ColoredListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))
        self.artist = ArtistItem2(self.artist)
        self.artists = [ArtistItem2(artist) for artist in self.artists]
        self._ftArtists = [ArtistItem2(artist) for artist in self._ftArtists]

    def getComment(self):
        txt = VideoItem.getComment(self)
        comments = ['video_id=%s' % self.id]
        if txt:
            comments.append(txt)
        userpl = self._userplaylists.keys()
        if len(userpl) > 0:
            comments.append('UserPlaylists: %s' % ', '.join([self._userplaylists.get(plid).get('title') for plid in userpl]))
        return ', '.join(comments)


class PromotionItem2(PromotionItem, ColoredListItem):

    def __init__(self, item):
        if item.type != 'EXTURL' and item.id.startswith('http:'):
            item.type = 'EXTURL' # Fix some defect TIDAL Promotions
        self.__dict__.update(vars(item))


class CategoryItem2(CategoryItem, ColoredListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))


class FolderItem2(FolderItem, ColoredListItem):

    def __init__(self, folder, url, thumb=None, fanart=None, isFolder=True, label=None):
        FolderItem.__init__(self, folder, url, thumb, fanart, isFolder, label)


class TidalConfig2(TidalConfig):

    def __init__(self):
        TidalConfig.__init__(self)

    def load(self):
        TidalConfig.load(self)
        self.max_http_requests = int('0%s' % addon.getSetting('max_http_requests'))
        self.cache_albums = True if addon.getSetting('album_cache') == 'true' else False
        self.mqa_in_labels = True if addon.getSetting('mqa_in_labels') == 'true' and self.codec == 'MQA' else False


class TidalSession2(TidalSession):

    def __init__(self, config=TidalConfig2()):
        TidalSession.__init__(self, config=config)
        # Album Cache
        self.metaCache = MetaCache()
        self.albumJsonBuffer = {}
        self.abortAlbumThreads = True
        self.albumQueue = Queue()
        if addon.getSetting('mqa_cache_cleaned') <> 'true':
            self.metaCache.deleteOldMasterAlbums()
            addon.setSetting('mqa_cache_cleaned', 'true')
        self.progressWindow = None

    def init_user(self, user_id, subscription_type):
        return User2(self, user_id, subscription_type)

    def search(self, field, value, limit=50):
        search_field = field
        if isinstance(search_field, basestring) and search_field.upper() == 'ALL':
            search_field = ALL_SAERCH_FIELDS
        results = TidalSession.search(self, search_field, value, limit=limit)
        self.update_albums_in_items(results.tracks)
        return results

    def get_album(self, album_id, withCache=True):
        if withCache and self._config.cache_albums:
            # Try internal buffer first
            json_obj = self.albumJsonBuffer.get('%s' % album_id, None)
            if json_obj == None:
                # Now read from Cache Database
                json_obj = self.metaCache.getAlbumJson(album_id)
                if json_obj != None and 'id' in json_obj:
                    # Transfer into the local buffer
                    self.albumJsonBuffer['%s' % json_obj.get('id')] = json_obj
            if json_obj:
                return self._parse_album(json_obj)
        return TidalSession.get_album(self, album_id)

    def get_album_items(self, album_id, ret='playlistitems'):
        items = TidalSession.get_album_items(self, album_id, ret=ret)
        videos = [item for item in items if isinstance(item, VideoItem)]
        if len(videos) == 0 and self._config.cache_albums and not ret.startswith('track'):
            self.metaCache.delete('album_with_videos', album_id)
        return items

    def get_playlist_items(self, playlist, offset=0, limit=9999, ret='playlistitems'):
        items = TidalSession.get_playlist_items(self, playlist, offset=offset, limit=limit, ret=ret)
        self.update_albums_in_items(items, forceAll=True)
        return items

    def get_playlist_tracks(self, playlist_id, offset=0, limit=9999):
        items = TidalSession.get_playlist_tracks(self, playlist_id, offset=offset, limit=limit)
        self.update_albums_in_items(items, forceAll=True)
        return items

    def get_playlist_albums(self, playlist, offset=0, limit=9999):
        items = TidalSession.get_playlist_tracks(self, playlist, offset=offset, limit=limit)
        self.update_albums_in_items(items, forceAll=True)
        return self.get_item_albums(items)

    def get_category_content(self, group, path, content_type, offset=0, limit=999):
        items = TidalSession.get_category_content(self, group, path, content_type, offset=offset, limit=limit)
        if content_type.startswith('track'):
            self.update_albums_in_items(items)
        elif content_type.startswith('album'):
            self.save_album_cache()
        return items

    def _parse_one_item(self, json_obj, ret):
        if self._config.cache_albums and ret.startswith('album') and json_obj and 'id' in json_obj:
            # Update local Album Buffer
            self.albumJsonBuffer['%s' % json_obj.get('id')] = json_obj
        return TidalSession._parse_one_item(self, json_obj, ret=ret)

    def _parse_album(self, json_obj, artist=None):
        album = AlbumItem2(TidalSession._parse_album(self, json_obj, artist=artist))
        return album

    def _parse_artist(self, json_obj):
        artist = ArtistItem2(TidalSession._parse_artist(self, json_obj))
        return artist

    def _parse_mix(self, json_obj):
        mix = MixItem2(TidalSession._parse_mix(self, json_obj))
        return mix

    def _parse_playlist(self, json_obj):
        playlist = PlaylistItem2(TidalSession._parse_playlist(self, json_obj))
        return playlist

    def _parse_track(self, json_obj):
        track = TrackItem2(TidalSession._parse_track(self, json_obj))
        return track

    def _parse_video(self, json_obj):
        video = VideoItem2(TidalSession._parse_video(self, json_obj))
        return video

    def _parse_promotion(self, json_obj):
        promotion = PromotionItem2(TidalSession._parse_promotion(self, json_obj))
        return promotion

    def _parse_category(self, json_obj):
        return CategoryItem2(TidalSession._parse_category(self, json_obj))

    def get_media_url(self, track_id, quality=None, cut_id=None, fallback=True, album_id=None):
        soundQuality = quality if quality else self._config.quality
        media = self.get_track_url(track_id, quality=soundQuality, cut_id=cut_id)
        if not media:
            return None
        return media.url

    def add_list_items(self, items, content=None, end=True, withNextPage=False):
        TidalSession.add_list_items(self, items, content=content, end=end, withNextPage=withNextPage)
        if end:
            try:
                self.save_album_cache()
                kodiVersion = xbmc.getInfoLabel('System.BuildVersion').split()[0]
                kodiVersion = kodiVersion.split('.')[0]
                skinTheme = xbmc.getSkinDir().lower()
                if 'onfluence' in skinTheme:
                    if kodiVersion <= '16':
                        xbmc.executebuiltin('Container.SetViewMode(506)')
                    elif content == 'musicvideos':
                        xbmc.executebuiltin('Container.SetViewMode(511)')
                    elif content == 'artists':
                        xbmc.executebuiltin('Container.SetViewMode(512)')
                    else:
                        xbmc.executebuiltin('Container.SetViewMode(506)')
                elif 'estuary' in skinTheme:
                    xbmc.executebuiltin('Container.SetViewMode(55)')
            except:
                pass

    def add_directory_item(self, title, endpoint, thumb=None, fanart=None, end=False, isFolder=True, label=None):
        if callable(endpoint):
            endpoint = plugin.url_for(endpoint)
        item = FolderItem2(title, endpoint, thumb, fanart, isFolder, label)
        self.add_list_items([item], end=end)

    def get_album_json_thread(self):
        try:
            while not xbmc.Monitor().waitForAbort(timeout=0.01) and not self.abortAlbumThreads:
                try:
                    album_id = self.albumQueue.get_nowait()
                except:
                    break
                try:
                    self.get_album(album_id, withCache=False)
                except requests.HTTPError as e:
                    r = e.response
                    msg = _T(30505)
                    try:
                        msg = r.reason
                        msg = r.json().get('userMessage')
                    except:
                        pass
                    log('Error getting Album ID %s' % album_id, xbmc.LOGERROR)
                    if r.status_code == 429 and not self.abortAlbumThreads:
                        self.abortAlbumThreads = True
                        log('Too many requests. Aborting Workers ...', xbmc.LOGERROR)
                        self.albumQueue._init(9999)
                        xbmcgui.Dialog().notification(plugin.name, msg, xbmcgui.NOTIFICATION_ERROR)
        except Exception, e:
            traceback.print_exc()

    def update_albums_in_items(self, items, forceAll=False):
        if self._config.cache_albums:
            # Step 1: Read all available Albums from Cache
            self.albumQueue = Queue()
            missing_ids = []
            missing_items = []
            track_count = 0
            self.abortAlbumThreads = False
            for item in items:
                if isinstance(item, TrackItem):
                    track_count += 1
                    # Set Album quality to Track quality (for Album Playlists)
                    item.album.audioQuality = item.audioQuality
                    isAlbum = True
                    try:
                        # In Single Tracks the Album-ID is Track-ID - 1
                        if not forceAll and item.name == item.album.name and item.trackNumber == 1 and (int('%s' % item.id) - int('%s' % item.album.id)) == 1:
                            isAlbum = False
                    except:
                        pass
                    if item.available and not item.album.releaseDate and isAlbum:
                        #(item.title <> item.album.title or item.trackNumber > 1):
                        # Try to read Album from Cache
                        json_obj = self.albumJsonBuffer.get('%s' % item.album.id, None)
                        if json_obj == None:
                            json_obj = self.metaCache.getAlbumJson(item.album.id)
                        if json_obj != None:
                            item.album = self._parse_album(json_obj)
                        else:
                            missing_items.append(item)
                            if not item.album.id in missing_ids:
                                missing_ids.append(item.album.id)
                                self.albumQueue.put('%s' % item.album.id)
            # Step 2: Load JSon-Data from all missing Albums
            if len(missing_ids) <= 5 or self._config.max_http_requests <= 1:
                # Without threads
                self.get_album_json_thread()
            else:
                log('Starting Threads to load Albums')
                runningThreads = []
                while len(runningThreads) < self._config.max_http_requests:
                    try:
                        worker = Thread(target=self.get_album_json_thread)
                        worker.start()
                        runningThreads.append(worker)
                    except Exception, e:
                        log(str(e), xbmc.LOGERROR)
                log('Waiting until all Threads are terminated')
                for worker in runningThreads:
                    worker.join(20)
                    if worker.isAlive():
                        log('Worker %s is still running ...' % worker.ident, xbmc.LOGWARNING)
            # Step 3: Save JsonData into MetaCache
            if len(missing_items) > 0:
                numAlbums = self.save_album_cache()
                log('Cached %s from %s missing Albums for %s TrackItems' % (numAlbums, len(missing_items), track_count))
                # Step 4: Fill missing Albums into the TrackItems
                for item in missing_items:
                    json_obj = self.albumJsonBuffer.get('%s' % item.album.id, None)
                    if json_obj != None:
                        item.album = self._parse_album(json_obj)

    def save_album_cache(self):
        numAlbums = 0
        if self._config.cache_albums:
            album_ids = self.albumJsonBuffer.keys()
            for album_id in album_ids:
                if xbmc.Monitor().waitForAbort(timeout=0.01):
                    break
                json_obj = self.albumJsonBuffer.get(album_id, None)
                if json_obj != None and 'id' in json_obj and not json_obj.get('_cached', False):
                    numAlbums += 1
                    self.metaCache.insertAlbumJson(json_obj)
            if numAlbums > 0:
                log('Wrote %s from %s Albums into the MetaCache' % (numAlbums, len(album_ids)))
        return numAlbums

    def albums_with_videos(self):
        items = []
        if self._config.cache_albums:
            jsonList = self.metaCache.fetchAllData('album_with_videos')
            for json in jsonList:
                items.append(self._parse_one_item(json, ret='album'))
        return items

    def master_albums(self, offset=0, limit=999):
        items = self.get_category_content('master', 'recommended', 'albums', offset=offset, limit=limit)
        return items

    def master_playlists(self, offset=0, limit=999):
        items = self.get_category_content('master', 'recommended', 'playlists', offset=offset, limit=limit)
        return items

    def show_busydialog(self, headline='', textline=''):
        #self.progressWindow = xbmcgui.DialogProgress()
        #self.progressWindow.create(heading=_T(30267), line1=headline, line2=textline)
        # or:
        self.progressWindow = xbmcgui.DialogProgressBG()
        self.progressWindow.create(heading=headline, message=textline)
        self.progressWindow.update(percent=50)
        #if KODI_VERSION >= (18, 0):
        #    xbmc.executebuiltin('ActivateWindow(busydialognocancel)')
        #else:
        #    xbmc.executebuiltin('ActivateWindow(busydialog)')
    def hide_busydialog(self):
        try:
            if self.progressWindow:
                self.progressWindow.close()
            #if KODI_VERSION >= (18, 0):
            #    xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
            #else:
            #    xbmc.executebuiltin('Dialog.Close(busydialog)')
        except:
            pass
        self.progressWindow = None


class Favorites2(TidalFavorites):

    def __init__(self, session, user_id):
        TidalFavorites.__init__(self, session, user_id)

    def get(self, content_type, limit=9999):
        items = TidalFavorites.get(self, content_type, limit=limit)
        self._session.update_albums_in_items(items)
        return items


class User2(TidalUser):

    def __init__(self, session, user_id, subscription_type=SubscriptionType.hifi):
        TidalUser.__init__(self, session, user_id, subscription_type)
        self.favorites = Favorites2(session, user_id)

    def delete_cache(self):
        ok = TidalUser.delete_cache(self)
        try:
            if getattr(self._session, 'metaCache'):
                ok = self._session.metaCache.deleteDatabase()
        except:
            return False
        return ok

    def check_updated_playlist(self, playlist):
        old_cache_albums = self._session._config.cache_albums
        ok = False
        try:
            # Build Playlist Cache without Album Cache
            self._session._config.cache_albums = False
            ok = TidalUser.check_updated_playlist(self, playlist)
        except:
            pass
        self._session._config.cache_albums = old_cache_albums
        return ok