# -*- 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: Florian Boucault <florian@fluendo.com>

import os.path
import time

from twisted.internet import reactor
import gobject
import pgm

from elisa.core.input_event import *
from elisa.core.log import Loggable
from elisa.core.utils import defer, caching
from elisa.core.utils.i18n import install_translation
from elisa.core import common

from elisa.plugins.base.models import image

from elisa.plugins.poblesec import constants
from elisa.plugins.poblesec.player_base import BasePlayerController
from elisa.plugins.pigment.graph.group import Group
from elisa.plugins.pigment.graph.image import Image

from elisa.plugins.poblesec.player_video import PlayerOsd as VideoPlayerOsd
from elisa.plugins.poblesec.widgets.player.status_display import \
                                                      PictureStatusDisplay
from elisa.plugins.poblesec.widgets.player.counter import Counter
from elisa.plugins.poblesec.videoplayer_controls import PlayPauseControl
from elisa.plugins.poblesec.slideshow.controls import ShowNextControl, \
                                                      ShowPreviousControl, \
                                                      RotationControl, \
                                                      StopControl, \
                                                      ToggleSlideshowControl
from elisa.plugins.poblesec.slideshow.transition_slideshow import Slideshow, \
                                                               CrossfadeSlideshow

from elisa.plugins.database.models import File as DBFile

_ = install_translation('poblesec')


class PicturePlayer(gobject.GObject, Loggable):
    """
    PicturePlayer is the main interface to slideshow handling. It is responsible
    for registration, creation and switching of slideshows. It acts as a stable
    proxy to the currently selected slideshow forwarding user requests to it.

    Emit the signals:
     - current-picture-changed: when the current picture displayed/in focus is
                                changed; at that point the actual picture file
                                starts being loaded
         params: L{elisa.plugins.base.models.image.ImageModel} of the picture
                 C{int} index of the picture in the playlist

     - current-picture-loaded: when the current picture is finally loaded
         params: L{elisa.plugins.pigment.animation.implicit.AnimatedObject} wrapping the image
                 drawable containing the loaded picture

     - status-changed: when L{status} changes
         params: one of [Slideshow.STOPPED, Slideshow.PLAYING]

     - playlist-size-changed: when the size of the playlist changes
         params: size of the playlist

     - available-slideshows-changed: when a slideshow has been added or removed
                                     from the list of available slideshows
         params: the list of available slideshows

     - slideshow-changed: when the currently selected slideshow has changed
         params: the newly selected slideshow type

     - current-picture-rotated: when the currently displayed/in focus picture
                                has been rotated
         params: final orientation of the picture; rotation constant from
                 L{elisa.plugins.base.models.image}


    @ivar status: whether the current slideshow was started or not
    @type status: one of [Slideshow.STOPPED, Slideshow.PLAYING]
    """

    PLAYING = Slideshow.PLAYING
    STOPPED = Slideshow.STOPPED

    __gsignals__ = {'current-picture-changed': (gobject.SIGNAL_RUN_LAST,
                                  gobject.TYPE_NONE,
                                 (gobject.TYPE_PYOBJECT, gobject.TYPE_INT)),
                    'current-picture-loaded': (gobject.SIGNAL_RUN_LAST,
                                  gobject.TYPE_NONE,
                                 (gobject.TYPE_PYOBJECT,)),
                    'current-picture-rotated': (gobject.SIGNAL_RUN_LAST,
                                  gobject.TYPE_NONE,
                                 (gobject.TYPE_PYOBJECT,)),
                    'playlist-size-changed': (gobject.SIGNAL_RUN_LAST,
                                  gobject.TYPE_NONE,
                                 (gobject.TYPE_INT,)),
                    'available-slideshows-changed': (gobject.SIGNAL_RUN_LAST,
                                  gobject.TYPE_NONE,
                                 (gobject.TYPE_PYOBJECT,)),
                    'slideshow-changed': (gobject.SIGNAL_RUN_LAST,
                                  gobject.TYPE_BOOLEAN,
                                 (gobject.TYPE_PYOBJECT,)),
                    'status-changed': (gobject.SIGNAL_RUN_LAST,
                                  gobject.TYPE_BOOLEAN,
                                 (gobject.TYPE_INT,)),
                   }

    def __init__(self, playground):
        """
        @param playground: node of the scene where the pictures will be
                           displayed
        @type playground:  L{elisa.plugins.pigment.graph.group.Group}
        """
        super(PicturePlayer, self).__init__()

        # playlist that will contain all the pictures to be displayed by the
        # slideshows
        self._playlist = []

        # the playground is a container that the slideshows will use to
        # display the pictures
        self._playground = playground

        # animated drawable containing the picture currently displayed
        # it is _not_ an Image drawable but an AnimatedObject proxying an
        # Image drawable
        self._displayed_drawable = None

        # index of the picture in the playlist that is to be displayed; it
        # might be already loaded and in which case _displayed_drawable contains
        # the corresponding animated drawable
        self._current_index = 0

        # time interval for which a picture is to be displayed when a
        # slideshow is playing
        self._interval = 5.0

        # reference to the slideshow currently chosen
        self._slideshow = None

        # list of slideshow classes from which the user can choose from; one
        # may register a new slideshow calling 'register_slideshow'
        self._available_slideshows = [CrossfadeSlideshow]

        # FIXME: default hardcoded slideshow
        self.set_slideshow(CrossfadeSlideshow)


    # Public API

    def set_playlist(self, playlist, picture_index=0):
        """
        Set the list of pictures to display during the slideshow and display one
        of them immediately, by default the first one.

        @param playlist: list of pictures to display during the slideshow
        @type  playlist: C{list} of L{elisa.plugins.base.models.image.ImageModel}
        @param picture_index: index in the playlist of the picture to display
                              immediately
        @type  picture_index: C{int}
        """
        self._playlist[:] = playlist
        self._slideshow.set_playlist(self._playlist, picture_index)
        self.emit('playlist-size-changed', len(self._playlist))

    def clear_playlist(self):
        """
        Empty the playlist and removes any displayed picture from the playground.
        """
        self.set_playlist([])

    # FIXME: here for backwards compatibility only
    stop = clear_playlist

    def get_playlist_size(self):
        """
        Return the size of the playlist.

        @rtype: C{int}
        """
        return len(self._playlist)

    def set_interval(self, interval):
        """
        Set the time during which a picture should be displayed/in focus for the
        user.

        @param interval: time in seconds
        @type  interval: C{float}
        """
        self._interval = interval
        self._slideshow.set_interval(interval)

    def start_slideshow(self, index=None):
        """
        Start the currently selected automated slideshow.

        @param index: index in the playlist of the picture to start the
                      slideshow with
        @type  index: C{int}
        """
        if self.status == self.PLAYING:
            return

        self._slideshow.start(index)

    def stop_slideshow(self):
        """
        Stop the currently selected slideshow.
        """
        if self.status == self.STOPPED:
            return

        self._slideshow.stop()

    def next(self):
        """
        Display the next picture in the playlist.
        """
        self._slideshow.next()

    def previous(self):
        """
        Display the previous picture in the playlist.
        """
        self._slideshow.previous()

    def get_current_picture(self):
        """
        Return the model of the current picture in the playlist.

        @rtype: L{elisa.plugins.base.models.image.ImageModel}
        """
        return self._playlist[self._current_index]

    def rotate_current_picture(self, orientation):
        """
        Visually rotate the current picture.
    
        @param orientation: final orientation of the picture
        @type orientation:  rotation constant from L{elisa.plugins.base.models.image}
        """
        # FIXME: affects the drawable that is displayed _not_ the
        # current_picture probably shown in the OSD
        self._apply_rotation(self._displayed_drawable, orientation)
        self.emit('current-picture-rotated', orientation)

    def set_slideshow(self, slideshow_class):
        """
        Switch the currently selected slideshow for another one.
        
        @param slideshow_class: class of the slideshow that should become the
                                current one
        @type slideshow_class:  a class inheriting from
             L{elisa.plugins.poblesec.slideshow.transition_slideshow.Slideshow}
        """
        # will restart the slideshow if it was playing before the switch
        restart = False

        # clean up the current slideshow
        if self._slideshow != None:
            restart = self.status == self.PLAYING
            slideshow = self._slideshow
            slideshow.stop()
            slideshow.disconnect_by_func(self._on_slideshow_picture_changed)
            slideshow.disconnect_by_func(self._on_slideshow_picture_loaded)
            slideshow.disconnect_by_func(self._on_slideshow_status_changed)
            slideshow.disconnect_by_func(self._on_slideshow_end_reached)
            slideshow.clean()

        # create the requested slideshow and pass it the current playground,
        # interval and playlist
        slideshow = slideshow_class(self._playground)
        slideshow.set_interval(self._interval)
        slideshow.set_playlist(self._playlist, self._current_index)
        slideshow.connect('current-picture-changed',
                          self._on_slideshow_picture_changed)
        slideshow.connect('current-picture-loaded',
                          self._on_slideshow_picture_loaded)
        slideshow.connect('status-changed',
                          self._on_slideshow_status_changed)
        slideshow.connect('end-reached',
                          self._on_slideshow_end_reached)

        if restart:
            slideshow.start(self._current_index)

        self._slideshow = slideshow
        self.emit('slideshow-changed', slideshow_class)

    def get_slideshow(self):
        """
        Return the type of the currently selected slideshow.

        @rtype: a class inheriting from
              L{elisa.plugins.poblesec.slideshow.transition_slideshow.Slideshow}
        """
        return type(self._slideshow)

    def get_available_slideshows(self):
        """
        Return the list of available slideshows.

        @rtype: a list of classes inheriting from
              L{elisa.plugins.poblesec.slideshow.transition_slideshow.Slideshow}
        """
        return self._available_slideshows

    def register_slideshow(self, slideshow_class):
        """
        Register a slideshow in the list of available slideshows.

        @param slideshow_class: class of the slideshow that is to be registered
        @type slideshow_class:  a class inheriting from
             L{elisa.plugins.poblesec.slideshow.transition_slideshow.Slideshow}
        """
        self._available_slideshows.append(slideshow_class)
        self.emit('available-slideshows-changed', self._available_slideshows)

    @property
    def playlist(self):
        """
        Make _playlist a public read-only property. This is needed for
        consistency with other Players APIs (audio/video).
        """
        return self._playlist

    # Private Methods

    def _get_status(self):
        return self._slideshow.status

    status = property(fget=_get_status)

    def _apply_rotation(self, drawable, orientation):
        rotation_parameters = image.IMAGE_ORIENTATIONS[orientation][1]

        if orientation in (image.ROTATED_90_CW, image.ROTATED_90_CCW):
            pixbuf_width, pixbuf_height = drawable.aspect_ratio
            pixbuf_ratio = float(pixbuf_height)/float(pixbuf_width)
            scale = pixbuf_ratio
        else:
            scale = 1.0

        drawable.s = scale
        drawable.rx = rotation_parameters['rx']
        drawable.ry = rotation_parameters['ry']
        drawable.rz = rotation_parameters['rz']


    # Slideshow signal handlers

    def _on_slideshow_picture_changed(self, slideshow, model, index):
        self._current_index = index
        if model != None:
            self._update_playcount_and_timestamp(model)

        self.emit('current-picture-changed', model, index)

    def _update_playcount_and_timestamp(self, picture):
        store = common.application.store
        if not store:
            # a database is needed
            return defer.succeed(None)

        uri = picture.references[-1]
        if uri.scheme != 'file':
            # the database schema only support files for now
            return defer.succeed(None)

        def update_playcount(result):
            if result == None:
                # the picture is not referenced in the database
                return
            self.debug("updating playcount and timestamp")
            result.playcount += 1
            result.last_played = time.time()

        dfr = store.get(DBFile, uri.path)
        dfr.addCallback(update_playcount)
        return dfr

    def _on_slideshow_picture_loaded(self, slideshow, animated):
        drawable = animated.object

        # apply rotation to drawable respecting model's orientation
        model = self._playlist[self._current_index]
        self._apply_rotation(drawable, model.orientation)

        self._displayed_drawable = animated

        self.emit('current-picture-loaded', animated.object)

    def _on_slideshow_status_changed(self, slideshow, status):
        self.emit('status-changed', status)

    def _on_slideshow_end_reached(self, slideshow):
        pass



class PlayerOsd(VideoPlayerOsd):

    status_widget = PictureStatusDisplay


class SlideshowController(BasePlayerController):
    """
    Main entry point for the picture player and slideshow user interface.

    Responsible for:
     - the creation of the user interface and user interaction
     - reacting to input events
     - updating the on screen display

    @ivar player: picture player responsible for slideshow handling
    @type player: L{elisa.plugins.poblesec.slideshow.player.PicturePlayer}
    @ivar player_osd: OSD containing the control ribbon and status display
    @type player_osd: L{elisa.plugins.poblesec.slideshow.player.PlayerOsd}
    @ivar counter: OSD displayed over the playground with loading information
    @type counter: L{elisa.plugins.poblesec.widgets.player.counter.Counter}
    @ivar playground: node of the scene containing the pictures displayed by the
                      picture player
    @type playground:  L{elisa.plugins.pigment.graph.group.Group}
    """
    PlayerOsdClass = PlayerOsd
    needs = [constants.PLAYER_PROVIDES_IMAGE,]

    def __init__(self):
        super(SlideshowController, self).__init__()

        # create a playground: an empty group the player will use to display
        # pictures
        playground = Group()
        self.widget.add(playground)
        playground.visible = True

        # create the picture player
        self.player = PicturePlayer(playground)

        self.player.connect('current-picture-changed',
                            self._player_picture_changed)
        self.player.connect('current-picture-loaded',
                            self._player_picture_loaded)
        self.player.connect('current-picture-rotated',
                            self._player_picture_rotated)
        self.player.connect('playlist-size-changed',
                            self._playlist_size_changed)
        self.player.connect('status-changed',
                            self._player_status_changed)

        # create the counter that will be used to give some user feedback when
        # loading a picture
        self.counter = Counter()
        self.widget.add(self.counter)
        self.counter.visible = True

        # add the controls to the OSD's ribbon
        ribbon = self.player_osd.control_ribbon

        ribbon.add_control(ShowPreviousControl(self))
        ribbon.add_control(PlayPauseControl(self))
        ribbon.add_control(StopControl(self))
        ribbon.add_control(ShowNextControl(self))
        ribbon.add_control(ToggleSlideshowControl(self))
        # TODO: add fit-screen control

        # make sure toggle play/pause control is selected by default
        ribbon.select_next_control()

        # create but do not add the rotation control: it will be added/removed
        # dynamically depending on whether or not the picture allows to be
        # rotated
        self._rotation_control = RotationControl(self)


        # placeholder for logo, optionally used by plugins to indicate the
        # origin of the pictures
        self.logo = Image()
        self.widget.add(self.logo)
        self.logo.alignment = pgm.IMAGE_BOTTOM_RIGHT
        self.logo.size = (0.1, 0.1)
        # logo is located:
        # - at the bottom right corner of the player
        # - on top of the player but below the on screen display
        self.logo.position = (1.0-self.logo.width, 1.0-self.logo.height,
                              self.player_osd.z/2.0)
        self.logo.bg_color = (0, 0, 0, 0)
        self.logo.opacity = 150
        self.logo.visible = True

    def set_frontend(self, frontend):
        super(SlideshowController, self).set_frontend(frontend)

        # FIXME: it would be better to connect to a similar signal on
        # self.widget; cf. signal 'motion' on Pigment's Drawables.
        self.frontend.viewport.connect('motion-notify-event',
                                       self._mouse_motion_cb)

        # initialize interaction with the music player:
        # the picture player displays the volume of the music player and lets
        # the user modify it
        music = self.frontend.retrieve_controllers('/poblesec/music_player')[0]
        self.music_player = music.player

        self.volume_display_factor = 10.0

        volume_max = self.music_player.volume_max * self.volume_display_factor
        self.player_osd.status.set_volume_max(volume_max)
        volume = self.music_player.get_volume() * self.volume_display_factor
        self.player_osd.status.set_volume_position(volume)

    def clean(self):
        # disconnect from all signals it connected to
        self.player.disconnect_by_func(self._player_picture_changed)
        self.player.disconnect_by_func(self._player_picture_loaded)
        self.player.disconnect_by_func(self._player_picture_rotated)
        self.player.disconnect_by_func(self._playlist_size_changed)
        self.player.disconnect_by_func(self._player_status_changed)
        self.player_osd.disconnect_by_func(self._player_osd_shown_cb)
        self.player_osd.disconnect_by_func(self._player_osd_hidden_cb)
        self.frontend.viewport.disconnect_by_func(self._mouse_motion_cb)
        self.player_osd.status.disconnect_by_func(self._volume_bar_cb)

        dfr = super(SlideshowController, self).clean()
        return dfr

    def exit(self):
        """
        Exit the player by asking poblesec's main controller to hide it.
        """
        controllers = self.frontend.retrieve_controllers('/poblesec')
        main = controllers[0]
        self.player_osd.hide()
        self.player.stop_slideshow()
        main.hide_current_player()

    def toggle_play_pause(self):
        """
        Start the slideshow if it is not playing, pause it otherwise.
        """
        if self.player.status == self.player.PLAYING:
            self.player.stop_slideshow()
        else:
            self.player.start_slideshow()

    def _player_osd_shown_cb(self, player_osd):
        self.counter.hide()

    def _player_osd_hidden_cb(self, player_osd):
        if self.counter.loading:
            self.counter.show()

    def _volume_bar_cb(self, widget, position):
        self.music_player.set_volume(position / self.volume_display_factor)

    def _mouse_motion_cb(self, *args):
        if self.has_focus():
            self.player_osd.show()
            self.player_osd.control_ribbon.show()
            self.player_osd.mouse_osd.show()

    def _player_picture_changed(self, player, picture, index):
        if picture != None:
            self._update_status(picture, index)
            self._update_ribbon(picture, index)
            self._update_logo(picture, index)
            self.counter.set_index(index)
            self.counter.set_loading(True)
            if not self.player_osd.is_shown:
                self.counter.show(delay=0.2)
        else:
            self._clear_osd()

    def _player_picture_loaded(self, player, drawable):
        self.player_osd.status.preview.set_from_image(drawable)
        picture = player.get_current_picture()
        self._update_preview_orientation(picture.orientation)
        self.counter.hide()
        self.counter.set_loading(False)

    def _player_picture_rotated(self, player, orientation):
        self._update_preview_orientation(orientation)

    def _playlist_size_changed(self, player, size):
        self.player_osd.status.set_seeking_duration(size)
        self.counter.set_max(size)

    def _player_status_changed(self, player, status):
        if status == player.PLAYING:
            self.player_osd.hide()

    def _update_status(self, picture, index):
        status = self.player_osd.status

        # update title display
        status.status_display_details.title.label = picture.title

        # update thumbnail
        # 1) if a thumbnail is available and already downloaded, load it
        # 2) load a generic photo icon and wait for the full resolution picture
        #    to be loaded in order to clone it

        icon = 'elisa.plugins.poblesec.player.thumbnail.default_image'
        self.frontend.load_from_theme(icon, status.preview)
        self._update_preview_orientation(image.ROTATED_0)

        path = None
        thumbnail_uri = picture.references[0]
        if thumbnail_uri.scheme == 'file':
            path = thumbnail_uri.path
        else:
            cached_path = caching.get_cached_image_path(thumbnail_uri)
            if os.path.exists(cached_path):
                path = cached_path

        if path != None:
            status.preview.set_from_file(path)
            # FIXME: should apply orientation of picture to preview once
            # the thumbnail is loaded. It is not the case here because
            # waiting for the loading of a file in a pgm.Image to be
            # completed is complicated.

        # update index display
        status.set_seeking_position(index)

    def _update_preview_orientation(self, orientation):
        preview = self.player_osd.status.preview

        # FIXME: accessing private API of PicturePlayer
        self.player._apply_rotation(preview, orientation)

        if orientation == image.ROTATED_0:
            preview.alignment = pgm.IMAGE_RIGHT
            preview.x = 0.0
        elif orientation == image.ROTATED_180:
            preview.alignment = pgm.IMAGE_LEFT
            preview.x = 0.0
        else:
            preview.alignment = pgm.IMAGE_CENTER
            # FIXME: ugly repositioning
            scale = preview.get_scale()
            delta = (preview.absolute_width-preview.absolute_height*scale)/2.0
            preview.x = delta/preview.parent.absolute_width

    def _update_ribbon(self, picture, index):
        ribbon = self.player_osd.control_ribbon

        if picture.can_rotate:
            if self._rotation_control not in ribbon.controls:
                ribbon.add_control(self._rotation_control)
        else:
            if self._rotation_control in ribbon.controls:
                ribbon.remove_control(self._rotation_control)

    def _update_logo(self, picture, index):
        # if the current picture does not have a 'logo' attribute
        # display nothing otherwise load the 'logo' resource
        try:
            self.frontend.load_from_theme(picture.logo, self.logo)
        except AttributeError:
            self.logo.clear()

    def _clear_osd(self):
        status = self.player_osd.status
        status.status_display_details.title.label = ""

        icon = 'elisa.plugins.poblesec.player.thumbnail.default_image'
        self.frontend.load_from_theme(icon, status.preview)

        self.logo.clear()

    def handle_input(self, manager, input_event):
        ribbon = self.player_osd.control_ribbon

        if input_event.value == EventValue.KEY_MENU:
            if self.player_osd.is_shown and ribbon.is_shown:
                self.exit()
            else:
                self.player_osd.show(with_ribbon=True)
            return True

        elif input_event.value == EventValue.KEY_OK or \
             input_event.value == EventValue.KEY_SPACE:
            if self.player_osd.is_shown and ribbon.is_shown:
                self.player_osd.show(with_ribbon=True)
                ribbon.selected_control.activate()
            else:
                self.toggle_play_pause()
            return True

        elif input_event.value == EventValue.KEY_GO_LEFT:
            if self.player_osd.is_shown and ribbon.is_shown:
                ribbon.select_previous_control()
                self.player_osd.show(with_ribbon=True)
            else:
                self.player.previous()
            return True

        elif input_event.value == EventValue.KEY_GO_RIGHT:
            if self.player_osd.is_shown and ribbon.is_shown:
                ribbon.select_next_control()
                self.player_osd.show(with_ribbon=True)
            else:
                self.player.next()
            return True

        elif input_event.value == EventValue.KEY_GO_UP:
            # display the volume bar
            self.player_osd.status.display_volume_bar()
            self.player_osd.show()

            # set the volume
            volume = self.music_player.volume_up() * self.volume_display_factor
            self.player_osd.status.set_volume_position(int(volume))
            return True

        elif input_event.value == EventValue.KEY_GO_DOWN:
            # display the volume bar
            self.player_osd.status.display_volume_bar()
            self.player_osd.show()

            # set the volume
            volume = self.music_player.volume_down() * self.volume_display_factor
            self.player_osd.status.set_volume_position(int(volume))
            return True

        return False
