# -*- 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: Benjamin Kampmann <benjamin@fluendo.com>

from twisted.trial.unittest import TestCase, SkipTest
from twisted.internet import defer, task, reactor

import gobject
import platform

try:
    import dbus
except ImportError:
    dbus = None
    PlayerImpl = object
    Player = object
    InformationNotAvailableError = Exception
    ArtistNotFoundError = Exception
    MusicAlbumNotFoundError = Exception
    DeferredDBusProxyMeta = type
else:
    from elisa.plugins.poblesec.dbus_player import PlayerImpl, \
        Player, InformationNotAvailableError, ArtistNotFoundError, \
        MusicAlbumNotFoundError

    # FIXME: refactor the DBus-Part so that we don't have to depend on the
    # DatabasePlugin in this case (and multiple others):
    from elisa.plugins.database.tests.test_dbus import DeferredDBusProxyMeta

    from elisa.plugins.database.tests.test_dbus import ArtistName, MusicAlbumName, \
        MusicTrackTitle, MusicAlbumCoverUri, MusicTrackTitle, MusicTrackFilePath, \
        PhotoAlbumName, ImageFilePath

# the different models that can be in the playlist
from elisa.plugins.database.models import MusicTrack, Video, Artist, \
        MusicAlbum, File
from elisa.plugins.base.models.audio import TrackModel, AlbumModel
from elisa.plugins.base.models.media import PlayableModel
from elisa.plugins.base.models.video import VideoModel

from elisa.core.media_uri import MediaUri
from elisa.core import common


# for the database setup
from elisa.extern.storm_wrapper import store
from storm.locals import create_database
from elisa.plugins.database.media_scanner import SCHEMA


METHODS_TO_DECORATE = ['get_volume', 'set_volume', 'get_metadata',
            'toggle_pause', 'get_position', 'stop', 'next', 'previous',
            'get_duration', 'play_music_album', 'play_file']


class DeferredPlayerProxy(object):
    __metaclass__ = DeferredDBusProxyMeta

    method_names = METHODS_TO_DECORATE

    def __init__(self, dbus_proxy):
        self.dbus_proxy = dbus_proxy

class SimpleProxy(object):
    # for the tests we want deferred results always, so wrap around the methods
    def __init__(self, player_impl):
        self.impl = player_impl
        for method in METHODS_TO_DECORATE:
            remote_method = getattr(self.impl, method)
            local_method = self._decorate(remote_method)
            setattr(self, method, local_method)

    def _decorate(self, method):
        def wrapper(*args, **kw):
            try:
                return defer.maybeDeferred(method, *args, **kw)
            except:
                return defer.fail()
        return wrapper

class TestMusicTrack(MusicTrack):
    def get_artists(self):
        return defer.succeed(self.artist_list)

class TestTrackModel(TrackModel):
    def get_album(self):
        return defer.succeed(self.album)
    def get_playable_model(self):
        return defer.succeed(self.playable_model)

class FakePlayer(gobject.GObject):
    """
    This Class is similar but not like the real player
    """
    STOPPED = 0
    PLAYING = 1
    PAUSED = 2
    BUFFERING = 3

    __gsignals__ = {'status-changed':
                              (gobject.SIGNAL_RUN_LAST,
                              gobject.TYPE_BOOLEAN,
                              (gobject.TYPE_INT,)),
                    # this signal is not yet in the player
                    'volume-changed':
                              (gobject.SIGNAL_RUN_LAST,
                              gobject.TYPE_BOOLEAN,
                              (gobject.TYPE_FLOAT,))
                              }

    def __init__(self):
        gobject.GObject.__init__(self)
        self.status = self.STOPPED
        self.current_index = -1
        self.volume_max = 2.0
        self.playlist = []

        self._play_deferreds = []

        # log the calls the dbus thing is doing
        self.call_log = []

    def enqueue_to_playlist(self, playable_model):
        self.call_log.append( ('enqueue', playable_model) )

    def play_next(self):
        self.call_log.append('next')

    def play_previous(self):
        self.call_log.append('previous')

    def play_model(self, playable_model):
        self.call_log.append('play_model')
        self.playlist.append(playable_model)

    def play_at_index(self, index):
        self.call_log.append( ('play_at_index', index))

    def stop(self):
        self.call_log.append('stop')

    def pause(self):
        self.call_log.append('pause')

    def play(self):
        self.call_log.append('play')

    def get_position(self):
        self.call_log.append('get_position')
        if hasattr(self, 'position'):
            return self.position
        return -1

    def set_position(self, value):
        # not relevant ATM
        raise NotImplementedError()

    def get_duration(self):
        self.call_log.append('get_duration')
        if hasattr(self, 'duration'):
            return self.duration
        return -1

    def get_volume(self):
        self.call_log.append('get_volume')
        return self.volume

    def set_volume(self, volume):
        self.call_log.append(('set_volume', volume))
        self.volume = volume

    def volume_up(self):
        # not relevant ATM
        raise NotImplementedError()

    def volume_down(self):
        # not relevant ATM
        raise NotImplementedError()

    def seek_backward(self):
        # not relevant ATM
        raise NotImplementedError()

    def seek_forward(self):
        # not relevant ATM
        raise NotImplementedError()


class TestMixin(object):

    def setUp(self):
        if not dbus:
            raise SkipTest("Not supported on %s platform" % platform.system())

        self.player = FakePlayer()

        #FIXME: we should not connect to the Controller but the controller
        # should act on changes of the player on its own
        class Dummy(object): pass

        class MainController(Dummy):
            def __init__(self):
                self.called = []
            def show_music_player(self):
                self.called.append('show_music_player')
            def hide_current_player(self):
                self.called.append('hide_current_player')

        fake_main = MainController()
        fake_main.music_player = Dummy()
        fake_main.music_player.player = self.player

        self.dbus_player = self.get_dbus_player(fake_main)
        self._setup_fake_playlist()

        # create and start the database
        self.db = create_database('sqlite:')
        self.store = store.DeferredStore(self.db, False)
        self.patch_application()
        dfr = self.store.start()
        dfr.addCallback(self._populate_database)

        return  dfr

    def tearDown(self):
        self.unpatch_application()
        self.store.stop()

    def patch_application(self):
        class Application(object):pass

        self.application = common.application
        common.application = Application()
        common.application.store = self.store

    def unpatch_application(self):
        common.application = self.application

    # copy-paste from the dbus_service test
    def _populate_database(self, result):
        store = self.store

        def iterator():
            for statement in SCHEMA:
                yield store.execute(statement)
            yield store.commit()

            for a in xrange(5):
                artist = Artist()
                artist.name = ArtistName(a)
                yield store.add(artist)

                for i in xrange(5):
                    album = MusicAlbum()
                    album.name = MusicAlbumName(artist.name, i)
                    album.cover_uri = MusicAlbumCoverUri(album.name)
                    yield store.add(album)

                    for x in xrange(5):
                        track = MusicTrack()
                        track.title = MusicTrackTitle(album.name, x)
                        track.file_path = MusicTrackFilePath(track.title)
                        track.album_name = album.name
                        yield store.add(track)
                        yield track.artists.add(artist)

                        dbfile = File()
                        dbfile.path = track.file_path
                        dbfile.last_played = (a * 100) + (i * 10) + x
                        yield store.add(dbfile)

            # add one test video
            video = Video()
            video.title = u"test"
            video.file_path = u"/tmp/test/video"
            yield store.add(video)

            video_file = File()
            video_file.path = video.file_path
            yield store.add(video_file)

            yield store.commit()

            self._last_played_track_path = dbfile.path

        dfr = task.coiterate(iterator())
        return dfr

    def _setup_fake_playlist(self):
        # 0-2:     PlayableModel (one audio, one video but no metadata)
        # 3-5:     MusicTrack (containing the informations)
        # 6-8:     TrackModel (assuming they come from daap)
        # 9-11:    VideoModel (no data)
        # 11-14:   Video (no data)

        playlist = []
        for i in xrange(3):
            model = PlayableModel()
            model.uri = MediaUri('file:///test/%s' % i)
            playlist.append(model)

        for path, artists, title, num, album in ( \
                            (u'sure', [u'a', u'b'], u'why', 4, u'not'),
                            (u'yes', [u'c', u'd'], u'we', None, u'do'),
                            (u'sometimes', [u'e'], u'we', 0, u'don\'t')):
            music_track = TestMusicTrack()
            music_track.file_path = path
            music_track.title = title
            music_track.track_number = num
            music_track.album_name = album
            music_track.artist_list = artists

            playlist.append(music_track)

        for title, artist, track, album in (\
                    ('x&y', 'coldplay', 1, 'X&Y'),
                    ('american pie', 'madonna', None, 'music'),
                    ('hefty fine', 'bloodhound gang', 1, 'balade')):
            model = TestTrackModel()
            model.title = title
            model.artist = artist
            model.track_number = track

            # the album naming is weird
            album_model = AlbumModel()
            album_model.album = album
            model.album = album_model

            model.playable_model = PlayableModel()
            model.playable_model.uri = MediaUri('file:///audio/%s' % title)

            playlist.append(model)


        for i in xrange(3):
            model = Video()
            model.file_path = u'/test/%s' % i
            playlist.append(model)

        for i in xrange(3):
            model = VideoModel()
            playlist.append(model)


        assert len(playlist) == 15

        self.player.playlist = playlist    

    def test_get_metadata(self):

        def check(result, expected_data):
            self.failUnless(result)
            for x, item in enumerate(result):
                self.assertEquals(item, expected_data[x])
        
        def iterate(metadata):
            for index, expected_data in metadata:
                self.player.current_index = index
                dfr = self.dbus_player.get_metadata()
                dfr.addCallback(check, expected_data)
                yield dfr

        # syntax according to the API Docs:
        #  file_path, title, num, artist, album_name
        metadata = { 
                     3 : ('sure', 'why', '4', 'a, b', 'not'),
                     4 : ('yes', 'we', '', 'c, d', 'do'),
                     5 : ('sometimes', 'we', '0', 'e', 'don\'t'),
                     6 : ('/audio/x&y', 'x&y', '1', 'coldplay', 'X&Y'),
                     7 : ('/audio/american pie', 'american pie', '', \
                            'madonna', 'music'),
                     8 : ('/audio/hefty fine', 'hefty fine', '1', \
                            'bloodhound gang', 'balade'),
                    }

        dfr = task.coiterate(iterate(metadata.iteritems()))
        return dfr

    def test_play_music_album(self):

        def iterate(metadata):
            for key, titles in metadata.iteritems():
                artist, album = key
                dfr = self.dbus_player.play_music_album(artist, album)
                dfr.addCallback(self.check_music_album_playing, titles)
                dfr.addCallback(self.check_call_log, ['play_model'])
                yield dfr

                # clear the call log again
                self.player.call_log[:] = []

        metadata = {}
        for x in xrange(5):
            artist = ArtistName(x)
            for y in xrange(5):
                album = MusicAlbumName(artist, y)
                tracks = []
                for z in xrange(5):
                    tracks.append(MusicTrackTitle(album, z))
                metadata[ (artist, album) ] = tracks

        dfr = task.coiterate(iterate(metadata))
        return dfr


    def test_play_music_album_wrong_arguments(self):
        artist_name = ArtistName(0)
        album_name = MusicAlbumName(artist_name, 0)

        def test_wrong_artist_name(result):
            dfr = self.dbus_player.play_music_album(
                    u'not in the library', album_name)
            self.failUnlessFailure(dfr, ArtistNotFoundError)
            return dfr

        def test_wrong_album_name(result):
            dfr = self.dbus_player.play_music_album(
                    artist_name, u'not in the library')
            self.failUnlessFailure(dfr, MusicAlbumNotFoundError)
            return dfr

        dfr = defer.Deferred()
        dfr.addCallback(test_wrong_artist_name)
        dfr.addCallback(test_wrong_album_name)
        dfr.callback(None)

        return dfr

    def test_play_file_audio(self):

        def request(track):
            dfr = self.dbus_player.play_file(track.file_path)
            dfr.addCallback(check, track)
            return dfr

        def check(result, reference_track):
            model = self.player.playlist.pop()
            self.failUnless(model is reference_track)
        
        dfr = self._get_any(MusicTrack)
        dfr.addCallback(request)
        dfr.addCallback(self.check_call_log, ['play_model'])
        return dfr

    def test_play_file_video(self):

        def request(video):
            dfr = self.dbus_player.play_file(video.file_path)
            dfr.addCallback(check, video)
            return dfr

        def check(result, reference_video):
            model = self.player.playlist.pop()
            self.failUnless(model is reference_video)
        
        dfr = self._get_any(Video)
        dfr.addCallback(request)
        dfr.addCallback(self.check_call_log, ['play_model'])
        return dfr

    test_play_file_video.todo = "video playback disabled"

    def test_play_file_not_in_db(self):

        def check(result, path):
            model = self.player.playlist.pop()
            self.failUnless(isinstance(model, PlayableModel))
            self.assertEquals(model.uri.path, path)

        path = u'/this/does/not/exist/for/sure'

        dfr = self.dbus_player.play_file(path)
        dfr.addCallback(check, path)
        dfr.addCallback(self.check_call_log, ['play_model'])
        return dfr


    # simple getters
    def test_get_volume(self):
        # the internal player uses a scale from 0 to 2 but the public API should
        # be in percent. So let's check if that is the case
        self.player.volume = 0.5
        dfr = self.dbus_player.get_volume()
        dfr.addCallback(self.assertEquals, 25)
        return dfr

    def test_get_position(self):
        self.player.position = 20744512471L
        dfr = self.dbus_player.get_position()
        dfr.addCallback(self.assertEquals, 20744512471)
        return dfr

    def test_get_position_fails(self):
        self.assertFalse(hasattr(self.player, 'position'))
        dfr = self.dbus_player.get_position()
        self.assertFailure(dfr, InformationNotAvailableError)
        return dfr

    def test_get_duration(self):
        self.player.duration = 227395918367L 
        dfr = self.dbus_player.get_duration()
        dfr.addCallback(self.assertEquals, 227395918367L)
        return dfr

    def test_get_duration_fails(self):
        self.assertFalse(hasattr(self.player, 'duration'))
        dfr = self.dbus_player.get_duration()
        self.assertFailure(dfr, InformationNotAvailableError)
        return dfr

    # method calls
    def test_call_stop(self):
        self.check_call_log(reference=[])

        dfr = self.dbus_player.stop()
        dfr.addCallback(self.check_call_log, ['stop'])
        return dfr

    def test_call_toggle_pause(self):
        self.check_call_log(reference=[])

        def iterate():
            for i in xrange(5):
                if self.player.status == self.player.PLAYING:
                    self.player.status = self.player.PAUSED
                else:
                    self.player.status = self.player.PLAYING
                yield self.dbus_player.toggle_pause()

        # we have toggled a lot
        ref = ['pause', 'play', 'pause', 'play', 'pause']

        deferred = task.coiterate(iterate())
        deferred.addCallback(self.check_call_log, ref)
        return deferred
    
    def test_call_set_volume(self):
        self.check_call_log(reference=[])

        dfrs = []
        # the API is in percentage
        dfrs.append(self.dbus_player.set_volume(25))
        dfrs.append(self.dbus_player.set_volume(50))
        dfrs.append(self.dbus_player.set_volume(100))

        ref = [ ('set_volume', 0.5), ('set_volume', 1),
                ('set_volume', 2.0) ]

        deferred_list = defer.DeferredList(dfrs)
        deferred_list.addCallback(self.check_call_log, ref)
        return deferred_list

    def test_call_next(self):
        self.check_call_log(reference=[])

        refs = []
        dfrs = []
        for i in xrange(3):
            dfrs.append(self.dbus_player.next())
            refs.append('next')

        deferred_list = defer.DeferredList(dfrs)
        deferred_list.addCallback(self.check_call_log, refs)
        return deferred_list

    def test_call_previous(self):
        self.check_call_log(reference=[])

        dfrs = []
        refs = []
        for i in xrange(4):
            dfrs.append(self.dbus_player.previous())
            refs.append('previous')

        deferred_list = defer.DeferredList(dfrs)
        deferred_list.addCallback(self.check_call_log, refs)
        return deferred_list

    # check helpers
    def check_call_log(self, result=None, reference=None):
        self.assertEquals(reference, self.player.call_log)
    
    def check_music_album_playing(self, result, expected_titles):
        added_entries = self.player.playlist[-len(expected_titles):]
        for index, title in enumerate(expected_titles):
            model = added_entries.pop(0)
            self.assertEquals(model.title, title)

    # other helpers
    def _get_any(self, klass):
        def get_any(result_set):
            return result_set.any()

        store = common.application.store
        dfr = store.find(klass)
        dfr.addCallback(get_any)
        return dfr

class TestPlayerNoDBus(TestMixin, TestCase):
    def get_dbus_player(self, player):
        player = SimpleProxy(PlayerImpl(player))
        return player

class TestPlayerDBus(TestMixin, TestCase):

    connection = 'com.fluendo.Elisa'
    path = '/com/fluendo/Elisa/Plugins/Poblesec/AudioPlayer'

    def get_dbus_player(self, player):
        bus = dbus.SessionBus()
        self.bus_name = dbus.service.BusName(self.connection, bus)
        self._real_player = Player(player, bus, self.path)
        bus = dbus.SessionBus()
        self._proxy = bus.get_object(self.connection, self.path)
        deferred = DeferredPlayerProxy(self._proxy)
        return deferred

    def tearDown(self):
        bus = dbus.SessionBus()
        self._real_player.remove_from_connection(bus, self.path)
        # BusName implements __del__, eew
        del self.bus_name

        # remove the reference cycle
        self.dbus_player = None

    # the signals are test for DBus only
    def test_volume_changed(self):

        self.volume = 0
        def volume_changed(volume):
            self.volume = volume

        self._proxy.connect_to_signal('volume_changed', volume_changed)
        
        self.player.emit('volume-changed', 0.4)

        def check(result):
            self.assertEquals(self.volume, 20)

        # as DBus goes over the mainloop to trigger the signals, we have to
        # return to it and then we can check
        dfr = defer.Deferred()
        dfr.addCallback(check)
        reactor.callLater(0.1, dfr.callback, None)
        return dfr

    def test_status_changed(self):

        self.status = FakePlayer.STOPPED
        def status_changed(new_status):
            self.status = new_status

        self._proxy.connect_to_signal('status_changed', status_changed)
    
        def check(result, state):
            self.assertEquals(self.status, state)

        def iterate():
            for id, state in (  (FakePlayer.PLAYING, 'playing'),
                                (FakePlayer.STOPPED, 'stopped'),
                                (FakePlayer.BUFFERING, 'buffering'),
                                (FakePlayer.PAUSED, 'paused')):

                self.player.emit('status-changed', id)


                # as DBus goes over the mainloop to trigger the signals, we have
                # to return to it and then we can check
                dfr = defer.Deferred()
                dfr.addCallback(check, state)
                reactor.callLater(0.1, dfr.callback, None)
                yield dfr
        return task.coiterate(iterate())
