# -*- 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 gobject
from twisted.internet import reactor

import pgm
from elisa.plugins.pigment.animation.implicit import AnimatedObject, SMOOTH

from elisa.core.utils import defer
from elisa.core.utils.cancellable_queue import CancellableQueue
from elisa.core.utils.i18n import install_translation

from elisa.plugins.pigment.graph.image import Image
from elisa.plugins.pigment.graph.group import NodeNotInGroup
from elisa.plugins.poblesec.slideshow.transitions import CrossfadeTransition, \
                                                         FlipTransition, \
                                                         FadeToRedTransition, \
                                                         SlideTransition, \
                                                         CubeTransition, \
                                                         PostcardTransition

_ = install_translation('poblesec')


class Slideshow(gobject.GObject):
    """
    A L{Slideshow} is responsible for displaying a set of pictures sequentially
    in automated fashion.
    It is interruptible at any time and may also be controlled manually while it
    is playing.

    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]

     - end-reached: when the slideshow was playing and it displayed all the
                    pictures available

    @cvar name:   user displayable name of the slideshow
    @type name:   C{str}
    @ivar status: whether the slideshow was started or not
    @type status: one of [Slideshow.STOPPED, Slideshow.PLAYING]
    """

    STOPPED = 0
    PLAYING = 1

    name = _("Abstract Slideshow")

    __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,)),
                    'status-changed': (gobject.SIGNAL_RUN_LAST,
                                  gobject.TYPE_BOOLEAN,
                                 (gobject.TYPE_INT,)),
                    'end-reached': (gobject.SIGNAL_RUN_LAST,
                                  gobject.TYPE_NONE, ()),
                   }

    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(Slideshow, self).__init__()
        self._playground = playground
        self._status = self.STOPPED

    def clean(self):
        """
        Cleanup the slideshow as to prepare it for destruction.
        """
        self._playground = None

    def set_playlist(self, playlist, 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 index:    index in the playlist of the picture to display
                         immediately
        @type  index:    C{int}
        """
        raise NotImplementedError()

    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}
        """
        raise NotImplementedError()

    def start(self, index=None):
        """
        Start the automated slideshow.

        @param index: index in the playlist of the picture to start the
                      slideshow with
        @type  index: C{int}
        """
        raise NotImplementedError()

    def stop(self):
        """
        Stop the slideshow.
        """
        raise NotImplementedError()

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

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

    def _get_status(self):
        return self._status

    def _set_status(self, status):
        self._status = status
        self.emit('status-changed', status)

    status = property(fget=_get_status, fset=_set_status)


class TransitionSlideshow(Slideshow):
    """
    Specialised slideshow that uses 2-pictures transitions every interval
    seconds to swap the current and next pictures.

    @cvar transition_class: transition to use in the slideshow
    @type transition_class: L{elisa.plugins.poblesec.slideshow.transitions.Transition}
    """
    transition_class = None

    def __init__(self, *args, **kwargs):
        super(TransitionSlideshow, self).__init__(*args, **kwargs)

        # a visual transition will be applied every so often; _looping_call
        # stores the delayed call
        self._looping_call = None

        # queue of picture loading requests
        self._loading_queue = CancellableQueue()

    # Public methods

    def clean(self):
        super(TransitionSlideshow, self).clean()
        self._cancel_looping_call()
        self._loading_queue.empty()
        self._playlist = None
        self._current_picture = None
        self._loaded_result = None

    def set_playlist(self, playlist, index=0):
        self._playlist = playlist
        self._current_index = index
        self._current_picture = None

        self._loaded_result = None

        self.stop()
        self._loading_queue.empty()
        self._playground.empty()

        if len(playlist) != 0:
            self._load_and_display_picture(index)

    def set_interval(self, interval):
        self._interval = interval

    def start(self, index=None):
        if self.status == self.PLAYING:
            return

        self.status = self.PLAYING

        # start at index if specified otherwise at the next picture
        if index == None:
            index = self._current_index + 1
        self._load_and_display_picture(index)
        self._schedule_transition(index+1)

    def stop(self):
        if self.status == self.STOPPED:
            return

        self.status = self.STOPPED
        self._cancel_looping_call()
        self._loading_queue.empty()

    def next(self):
        index = self._current_index + 1
        if index >= len(self._playlist):
            return

        restart = False
        if self.status == self.PLAYING:
            self._cancel_looping_call()
            restart = True

        self._load_and_display_picture(index)

        # restart it if it was started
        if restart:
            self._schedule_transition(index+1)

    def previous(self):
        index = self._current_index - 1
        if index < 0:
            return

        self.stop()
        self._load_and_display_picture(index)


    # Private methods

    def _load_and_display_picture(self, index):
        # cancel any other loading in progress and display the picture specified
        # by index as soon as possible
        self._set_current_index(index)
        self._loading_queue.empty()
        dfr = self._load_picture(index)
        dfr.addCallback(self._do_transition)
        dfr.addErrback(self._trap_cancelled)

    def _set_current_index(self, index):
        if index < 0 or index >= len(self._playlist):
            raise IndexError()

        model = self._playlist[index]
        self._current_index = index
        self.emit('current-picture-changed', model, index)

    def _trap_cancelled(self, failure):
        failure.trap(defer.CancelledError)

    def _load_picture(self, index):
        if index < 0 or index >= len(self._playlist):
            raise IndexError()

        model = self._playlist[index]
        return self._loading_queue.enqueue(self._load_model_into_animated, model)

    def _cancel_looping_call(self):
        if self._looping_call != None and self._looping_call.active():
            self._looping_call.cancel()
            self._looping_call = None

    def _schedule_transition(self, index):
        # start loading picture at index and display it when 'interval' seconds
        # are elapsed and the picture is loaded
        # two scenarii can happen:
        # 1) the picture has finished loading before 'interval' seconds
        # 2) the picture has not finished loading yet when 'interval' seconds
        #    are elapsed
        self._loaded_result = None

        try:
            dfr = self._load_picture(index)
        except IndexError:
            # there is no more pictures to load and display
            # keep the current picture displayed for another cycle
            self._looping_call = reactor.callLater(self._interval,
                                                   self._end_of_slideshow)
        else:
            dfr.addCallback(self._do_transition_if_timeout, index)
            dfr.addErrback(self._trap_cancelled)

            # schedule call to _do_transition_if_loaded in self._interval seconds
            self._looping_call = reactor.callLater(self._interval,
                                                   self._do_transition_if_loaded,
                                                   index)

    def _do_transition_if_loaded(self, new_index):
        self._looping_call = None
        self._set_current_index(new_index)

        if self._loaded_result != None:
            # picture finished loading
            new_picture = self._loaded_result
            self._do_transition(new_picture)
            self._schedule_transition(new_index+1)

    def _load_model_into_animated(self, model):
        dfr = model.get_cached_data_path(-1)
        dfr.addCallback(Image.new_from_file_deferred, 1024)
        dfr.addCallback(self._activate_antialiasing)
        dfr.addCallback(self._wrap_into_animated)
        return dfr

    def _activate_antialiasing(self, drawable):
        drawable.set_wrapping(pgm.IMAGE_TRANSPARENT, pgm.IMAGE_TRANSPARENT)
        drawable.set_interp(pgm.IMAGE_BILINEAR)
        return drawable

    def _wrap_into_animated(self, drawable):
        animated = AnimatedObject(drawable)
        animated.setup_next_animations(transformation=SMOOTH)
        return animated


    def _do_transition_if_timeout(self, new_picture, new_index):
        if self._looping_call == None:
            self._do_transition(new_picture)
            self._schedule_transition(new_index+1)
        else:
            self._loaded_result = new_picture

    def _do_transition(self, new_picture):
        self._insert_into_playground(new_picture.object)
        self.transition_class.apply(self._current_picture, new_picture,
                                    self._interval)

        if self._current_picture != None:
            # we assume that after self._interval the transition is finished
            reactor.callLater(self._interval, self._remove_from_playground,
                              self._current_picture.object)

        self._current_picture = new_picture
        self.emit('current-picture-loaded', self._current_picture)

    def _insert_into_playground(self, drawable):
        self._playground.add(drawable)
        drawable.opacity = 0
        drawable.bg_color = (0, 0, 0, 0)
        drawable.visible = True

    def _remove_from_playground(self, drawable):
        if self._playground != None:
            try:
                self._playground.remove(drawable)
            except NodeNotInGroup:
                pass

    def _end_of_slideshow(self):
        self.stop()
        self.emit("end-reached")


class CrossfadeSlideshow(TransitionSlideshow):
    name = _("Crossfade")
    transition_class = CrossfadeTransition

class FlipSlideshow(TransitionSlideshow):
    name = _("Flip")
    transition_class = FlipTransition

class FadeToRedSlideshow(TransitionSlideshow):
    name = _("Fade to Red")
    transition_class = FadeToRedTransition

class SlideSlideshow(TransitionSlideshow):
    name = _("Slide")
    transition_class = SlideTransition

class CubeSlideshow(TransitionSlideshow):
    name = _("Cube")
    transition_class = CubeTransition

class PostcardSlideshow(TransitionSlideshow):
    name = _("Postcard")
    transition_class = PostcardTransition

slideshows = [FlipSlideshow,
              CubeSlideshow,
              PostcardSlideshow]

def register_slideshows(slideshow_controller):
    for slideshow in slideshows:
        slideshow_controller.player.register_slideshow(slideshow)
    return defer.succeed(None)
