# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.
#
# Author: Alessandro Decina <alessandro@fluendo.com>

import dbus
import dbus.service
import dbus.glib
import gobject

from twisted.internet import defer, task

from elisa.core import common
from elisa.core.media_uri import MediaUri
from elisa.core.log import getFailureMessage
from elisa.core.components.service_provider import ServiceProvider
from elisa.core.utils import caching

try:
    from elisa.plugins.pigment.message import PigmentFrontendLoadedMessage
except ImportError:
    PigmentFrontendLoadedMessage = None

from elisa.plugins.poblesec.music_library import album_cover_retriever

from elisa.plugins.database.models import Artist, MusicAlbum, MusicTrack, \
        PhotoAlbum, Image, File, PlayableModel

from storm.expr import Desc, Asc

import os


def result_set_order_offset_and_limit(result_set, field, offset, limit):
    result_set.order_by(field).config(offset=offset, limit=limit)
    return result_set


def result_set_first(result_set):
    return result_set.first()


def result_set_one(result_set):
    return result_set.one()


def result_set_any(result_set):
    return result_set.any()


def result_set_all(result_set):
    return result_set.all()


def dbus_errback(failure, errback):
    errback(failure.value)


def build_last_track_played_tuple(music_track):
    def get_artist(album):
        def build_tuple(artist):
            return (artist.name, album.name,
                    music_track.title, music_track.file_path)

        dfr = music_track.artists.find()
        dfr.addCallback(result_set_any)
        dfr.addCallback(build_tuple)

        return dfr

    if music_track is None:
        raise MusicTrackNotFoundError()

    dfr = music_track.album
    dfr.addCallback(get_artist)

    return dfr


def build_last_picture_played_tuple(picture):
    def build_tuple(album):
        return album.name, picture.file_path

    if picture is None:
        raise PictureNotFoundError()

    dfr = picture.album
    dfr.addCallback(build_tuple)

    return dfr


class DatabaseError(Exception):
    pass


class ArtistNotFoundError(DatabaseError):
    pass


class MusicAlbumNotFoundError(DatabaseError):
    pass


class MusicTrackNotFoundError(DatabaseError):
    pass


class PhotoAlbumNotFoundError(DatabaseError):
    pass


class PictureNotFoundError(DatabaseError):
    pass


class AlbumArtworkNotAvailable(Exception):
    pass


class MusicImpl(object):
    def _build_artists_albums_list(self, albums):
        def find_album_tracks_iterator(albums, artists_albums_set):
            for album in albums:
                dfr = album.tracks.find(MusicTrack.file_path == File.path,
                                        File.hidden == False)
                dfr.addCallback(result_set_all)
                dfr.addCallback(self._find_tracks_artists,
                        album, artists_albums_set)
                yield dfr

        def find_album_tracks_iterator_cb(iterator, artists_albums_set):
            return list(artists_albums_set)

        artists_albums_set = set()
        dfr = task.coiterate(find_album_tracks_iterator(albums,
                artists_albums_set))
        dfr.addCallback(find_album_tracks_iterator_cb, artists_albums_set)
        return dfr

    def _find_tracks_artists(self, tracks, album, artists_albums_set):
        def fill_artists_albums_set(artists, album, artists_albums_set):
            for artist in artists:
                artists_albums_set.add((artist.name, album.name))

        def find_tracks_artists_iterator(tracks, album, artists_albums_set):
            for track in tracks:
                dfr = track.artists.find()
                dfr.addCallback(result_set_all)
                dfr.addCallback(fill_artists_albums_set, album, artists_albums_set)

                yield dfr

        def find_tracks_artists_iterator_cb(iterator, artists_albums_set):
            return list(artists_albums_set)

        dfr = task.coiterate(find_tracks_artists_iterator(tracks, album,
                artists_albums_set))
        dfr.addCallback(find_tracks_artists_iterator_cb, artists_albums_set)

        return dfr

    def get_albums(self, offset, count):
        if offset < 0 or count <= 0:
            return defer.fail(IndexError())

        dfr = common.application.store.find(MusicAlbum)
        dfr.addCallback(result_set_order_offset_and_limit, MusicAlbum.name, offset, count)
        dfr.addCallback(result_set_all)
        dfr.addCallback(self._build_artists_albums_list)

        return dfr

    def get_album_artwork(self, artist_name, album_name):
        def get_album(music_track):
            if music_track is None:
                raise MusicAlbumNotFoundError()

            return music_track.album

        def get_uri(album):
            return album.cover_uri

        def get_album_cover_uri(album, artist):
            if album.cover_uri:
                return album.cover_uri

            # album_cover_retriever.get_cover_uri() returns an Album [sic!]
            dfr = album_cover_retriever.get_cover_uri(album, artist.name)
            dfr.addCallback(get_uri)

            return dfr

        def find_cover_art(artist):
            if artist is None:
                raise ArtistNotFoundError()

            dfr = artist.tracks.find(MusicTrack.album_name == album_name)
            dfr.addCallback(result_set_any)
            dfr.addCallback(get_album)
            dfr.addCallback(get_album_cover_uri, artist)
            dfr.addCallback(self._get_cover_thumbnail)

            return dfr

        dfr = common.application.store.find(Artist, Artist.name == artist_name)
        dfr.addCallback(result_set_one)
        dfr.addCallback(find_cover_art)
    
        return dfr

    def get_album_tracks(self, artist_name, album_name):
        def build_track_list(tracks):
            if not tracks:
                raise MusicAlbumNotFoundError()

            return [(track.title, track.file_path) for track in tracks]

        def find_tracks(artist):
            if artist is None:
                raise ArtistNotFoundError()

            dfr = artist.tracks.find(MusicTrack.album_name == album_name,
                                     MusicTrack.file_path == File.path,
                                     File.hidden == False)
            dfr.addCallback(result_set_all)
            dfr.addCallback(build_track_list)

            return dfr

        dfr = common.application.store.find(Artist, Artist.name == artist_name)
        dfr.addCallback(result_set_one)
        dfr.addCallback(find_tracks)

        return dfr

    def get_last_played_track(self):
        dfr = common.application.store.find(MusicTrack,
                MusicTrack.file_path == File.path,
                File.last_played != 0)
        dfr.addCallback(result_set_order_offset_and_limit,
                Desc(File.last_played), offset=None, limit=None)
        dfr.addCallback(result_set_first)
        dfr.addCallback(build_last_track_played_tuple)

        return dfr

    def _get_cover_thumbnail(self, thumbnail_uri):
        if thumbnail_uri is None:
            # The cover art has never been requested from the UI for this album
            # FIXME: request it (from Amazon)
            raise AlbumArtworkNotAvailable()

        uri = MediaUri(thumbnail_uri)
        thumbnail_file = caching.get_cached_image_path(uri)
        if os.path.exists(thumbnail_file):
            return defer.succeed(thumbnail_file)

        return self._download_cover_thumbnail(uri, thumbnail_file)

    def _download_cover_thumbnail(self, uri, filename):
        data_model, dfr = common.application.resource_manager.get(uri)
        dfr.addCallback(lambda model: \
                        caching.cache_to_file(model.data, filename))
        return dfr


class Music(dbus.service.Object):
    interface = 'com.fluendo.Elisa.Plugins.Database.Music'
    implFactory = MusicImpl

    def __init__(self, *args, **kw):
        dbus.service.Object.__init__(self, *args, **kw)
        self.impl = self.implFactory()

    @dbus.service.method(dbus_interface=interface,
            in_signature='ii', out_signature='aas',
            async_callbacks=('callback', 'errback')) 
    def get_albums(self, offset, count, callback, errback):
        dfr = self.impl.get_albums(offset, count)
        dfr.addCallback(callback)
        dfr.addErrback(dbus_errback, errback)

    @dbus.service.method(dbus_interface=interface,
            in_signature='ss', out_signature='s',
            async_callbacks=('callback', 'errback')) 
    def get_album_artwork(self, artist_name, album_name, callback, errback):
        dfr = self.impl.get_album_artwork(artist_name, album_name)
        dfr.addCallback(callback)
        dfr.addErrback(dbus_errback, errback)
    
    @dbus.service.method(dbus_interface=interface,
            in_signature='ss', out_signature='aas',
            async_callbacks=('callback', 'errback')) 
    def get_album_tracks(self, artist_name, album_name, callback, errback):
        dfr = self.impl.get_album_tracks(artist_name, album_name)
        dfr.addCallback(callback)
        dfr.addErrback(dbus_errback, errback)

    @dbus.service.method(dbus_interface=interface,
            in_signature='', out_signature='as',
            async_callbacks=('callback', 'errback')) 
    def get_last_played_track(self, callback, errback):
        dfr = self.impl.get_last_played_track()
        dfr.addCallback(callback)
        dfr.addErrback(dbus_errback, errback)

    @dbus.service.signal(dbus_interface=interface, signature='ssss')
    def last_played_track(self, artist_name, album_name,
            track_title, track_path):
        # don't do anything, just so that the signal is emitted
        pass


class PhotoImpl(object):
    def get_albums(self, offset, count):
        if offset < 0 or count <= 0:
            return defer.fail(IndexError())
        
        def build_album_list(albums):
            return [album.name for album in albums]

        dfr = common.application.store.find(PhotoAlbum)
        dfr.addCallback(result_set_order_offset_and_limit, PhotoAlbum.name, offset, count)
        dfr.addCallback(result_set_all)
        dfr.addCallback(build_album_list)

        return dfr

    def get_album_pictures(self, album_name):
        def build_pictures_list(pictures):
            return [picture.file_path for picture in pictures]

        def get_album_pictures(album):
            if album is None:
                raise PhotoAlbumNotFoundError()

            dfr = album.photos.find(Image.file_path == File.path,
                                    File.hidden == False)
            dfr.addCallback(result_set_all)
            dfr.addCallback(build_pictures_list)

            return dfr

        dfr = common.application.store.find(PhotoAlbum,
                PhotoAlbum.name == album_name)
        dfr.addCallback(result_set_one)
        dfr.addCallback(get_album_pictures)

        return dfr
    
    def get_last_displayed_picture(self):
        dfr = common.application.store.find(Image,
                Image.file_path == File.path,
                File.hidden == False,
                File.last_played != 0)
        dfr.addCallback(result_set_order_offset_and_limit,
                Desc(File.last_played), offset=None, limit=None)
        dfr.addCallback(result_set_first)
        dfr.addCallback(build_last_picture_played_tuple)

        return dfr


class Photo(dbus.service.Object):
    interface = 'com.fluendo.Elisa.Plugins.Database.Photo'
    implFactory = PhotoImpl

    def __init__(self, *args, **kw):
        dbus.service.Object.__init__(self, *args, **kw)
        self.impl = self.implFactory()

    @dbus.service.method(dbus_interface=interface,
            in_signature='ii', out_signature='as',
            async_callbacks=('callback', 'errback')) 
    def get_albums(self, offset, count, callback, errback):
        dfr = self.impl.get_albums(offset, count)
        dfr.addCallback(callback)
        dfr.addErrback(dbus_errback, errback)

    @dbus.service.method(dbus_interface=interface,
            in_signature='s', out_signature='as',
            async_callbacks=('callback', 'errback')) 
    def get_album_pictures(self, album_name, callback, errback):
        dfr = self.impl.get_album_pictures(album_name)
        dfr.addCallback(callback)
        dfr.addErrback(dbus_errback, errback)
    
    @dbus.service.method(dbus_interface=interface,
            in_signature='', out_signature='as',
            async_callbacks=('callback', 'errback')) 
    def get_last_displayed_picture(self, callback, errback):
        dfr = self.impl.get_last_displayed_picture()
        dfr.addCallback(callback)
        dfr.addErrback(dbus_errback, errback)

    @dbus.service.signal(dbus_interface=interface, signature='ss')
    def last_displayed_picture(self, photo_album_name, picture_path):
        # don't do anything, just so that the signal is emitted
        pass


class DatabaseDBusServiceProvider(ServiceProvider):
    def start(self): 
        self._last_played_track_path = None
        self._last_displayed_picture_path = None

        self._start_dbus()
        self._register_player_status_changes()

        return defer.succeed(self)

    def stop(self):
        self._stop_dbus()
        return defer.succeed(self)

    def _start_dbus(self):
        bus = dbus.SessionBus()
        
        # get known names
        self.bus_name = \
                dbus.service.BusName('com.fluendo.Elisa', bus)
    
        # create the objects we export
        self.photo = Photo(bus, '/com/fluendo/Elisa/Plugins/Database/Photo',
                self.bus_name)
        self.music = Music(bus, '/com/fluendo/Elisa/Plugins/Database/Music',
                self.bus_name)

    def _stop_dbus(self):
        bus = dbus.SessionBus()

        self.photo.remove_from_connection(bus,
                '/com/fluendo/Elisa/Plugins/Database/Photo')
        self.music.remove_from_connection(bus,
                '/com/fluendo/Elisa/Plugins/Database/Music')

        # BusName implements __del__, eew
        del self.bus_name

    def _register_player_status_changes(self):
        if PigmentFrontendLoadedMessage is None:
            self.warning("You don't seem to run the pigment frontend." \
                         " Can't connect to the player.")
            return

        bus = common.application.bus
        bus.register(self._frontend_loaded, PigmentFrontendLoadedMessage)

    def _frontend_loaded(self, msg, sender):
        frontend = sender
        music_player = \
                frontend.retrieve_controllers('/poblesec/music_player')[0]
        music_player.player.connect('status-changed',self._music_player_status_cb)
        slideshow_player = \
                frontend.retrieve_controllers('/poblesec/slideshow_player')[0]
        slideshow_player.player.connect('current-picture-changed',
                self._slideshow_player_current_picture_changed_cb)

    def _music_player_status_cb(self, player, status):
        if status != player.PLAYING:
            return defer.succeed(None)

        model = player.get_current_model()
        if isinstance(model, MusicTrack):
            current_track_path = model.file_path
        elif isinstance(model, PlayableModel) and \
                model.uri.scheme == 'file':
            current_track_path = model.uri.path
        else:
            self.log('not emitting last_played_track for %s' % model)
            return defer.succeed(None)

        if self._last_played_track_path is None or \
                current_track_path != self._last_played_track_path:
            self._last_played_track_path = current_track_path

            if isinstance(model, MusicTrack):
                dfr = defer.succeed(model)
            else:
                dfr = common.application.store.find(MusicTrack,
                        MusicTrack.file_path == current_track_path)
                dfr.addCallback(result_set_one)

            dfr.addCallback(build_last_track_played_tuple)
            dfr.addCallback(self._emit_last_played_track)
            def log_failure(failure):
                self.info('not emitting last_played_track signal for %s: %s'
                        % (current_track_path, getFailureMessage(failure)))
            dfr.addErrback(log_failure)

            return dfr

        return defer.succeed(None)
    
    def _emit_last_played_track(self, last_played_track_tuple):
        self.log('emitting last_played_track %s' % str(last_played_track_tuple))
        self.music.last_played_track(*last_played_track_tuple)

    def _slideshow_player_current_picture_changed_cb(self, player, picture,
                                                     index):
        if picture == None:
            return defer.succeed(None)

        current_picture_path = picture.references[-1].path

        if self._last_displayed_picture_path is None or \
                current_picture_path != self._last_displayed_picture_path:
            self._last_displayed_picture_path = current_picture_path
           
            dfr = common.application.store.find(Image,
                    Image.file_path == current_picture_path)
            dfr.addCallback(result_set_one)
            dfr.addCallback(build_last_picture_played_tuple)
            dfr.addCallbacks(self._emit_last_displayed_picture,
                             self._ignore_non_db_picture)

            return dfr
        
        return defer.succeed(None)
    
    def _emit_last_displayed_picture(self, last_displayed_picture_tuple):
        self.info('Emitting last_displayed_picture %s' % \
                    str(last_displayed_picture_tuple))
        self.photo.last_displayed_picture(*last_displayed_picture_tuple)

    def _ignore_non_db_picture(self, failure):
        r = failure.trap(PictureNotFoundError)
        self.info('Not a picture from the database: not emitting' \
                 ' last_displayed_picture')


if __name__ == '__main__':
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    loop = gobject.MainLoop()

    def service_provider_created_cb(service_provider):
        service_provider.start()

    dfr = DatabaseDBusServiceProvider.create({})
    dfr.addCallback(service_provider_created_cb)

    loop.run()
