# -*- coding: utf-8 -*-
# Elisa - Home multimedia server
# Copyright (C) 2006-2008 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 Elisa with Fluendo's plugins.
#
# The GPL part of Elisa is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Elisa" in the root directory of this distribution package
# for details on that license.


__maintainer__ = 'Benjamin Kampmann <benjamin@fluendo.com>'


from elisa.core.bus import bus
from elisa.base_components import message
from elisa.core import common, log
from elisa.extern import enum

from elisa.core.utils import classinit

from elisa.core.player_engine_registry import NoEngineFound

#import pygst
#pygst.require('0.10')
import gst

from gobject import GError

class PlayerPlaying(message.Message):
    """
    Sent over the message bus when the first frame or buffer of a media has
    been outputted.
    """

class PlayerStopping(message.Message):
    """
    Sent over the message bus when the playing is stopped.

    @ivar percent: how much of the media has been shown.
    @type percent: float
    """

    def __init__(self, percent=100):
        """
        Initialize the PlayerStopping Message.

        @param percent: the percent of the media, that was been watched yet
        @type percent: float
        """
        message.Message.__init__(self)
        self.percent = percent

    # FIXME: the reason why it was stopped could be useful (like EOS/EOF,
    #        Error, StreamBroke or Requested)

class PlayerLoading(message.Message):
    """
    Sent over the message bus when the player is asked to play. When the
    playback B{really} starts, that is when the first buffer reach the sink,
    the L{elisa.core.player.PlayerPlaying} message is sent over the bus.

    That message can be understood as the first answer after doing
    L{elisa.core.player.Player.play}.

    Warning: this message is B{not} sent when the uri is set.
    """

class PlayerBuffering(message.Message):
    """
    Sent if the player is loading before playing or if it is prebuffering during
    playing. This message might be sent very often and is mainly useful for
    user interface updates.

    This message is optional and the frontend should not expect that every
    player sends it or is able to send it.

    @ivar progress: the progress in percent
    @type progress: float
    """

    __metaclass__ = classinit.ClassInitMeta
    __classinit__ = classinit.build_properties

    def __init__(self,progress):
        message.Message.__init__(self)
        self._progress = 0
        self.progress = progress

    def progress__get(self):
        return self._progress

    def progress__set(self, progress):
        if progress > 100:
            self._progress = 100
        elif progress < 0:
            self._progress = 0
        else:
            self._progress = progress

class PlayerPausing(message.Message):
    """
    Sent over the message bus when the playing is paused.
    """

class NewClock(message.Message):
    """
    This message is triggered everytime the engine gets a new Clock.
    It is independent from any trigger_message-settings. It is simply always
    send, when gstreamer finds a new one.
    Even this message is send by the engine, please think, that the
    message_sender is maybe overritten.

    @ivar clock: the new clock that was found
    @type clock: L{gst.Clock}
    """
    clock = None
    def __init__(self, clock):
        """
        set the clock to clock. This is the new clock, the engine is using.
        """
        message.Message.__init__(self)
        self.clock = clock

class NewBaseTime(message.Message):
    """
    This message is triggered everytime, the engine got a new BaseTime. This
    message simply is send everytime a gstreamer change-state message is
    coming to the engine.
    This message is used for synchronization.
    
    @ivar base_time: the new base time
    @type base_time: L{gst.ClockTime}
    """

    def __init__(self, base_time):
        message.Message.__init__(self)
        self.base_time = base_time

class PlayerError(message.Message):
    """
    Sent over the message bus if the player encountered an issue.

    @ivar error: the error that occured
    @type error: str
    """
    def __init__(self, error):
        message.Message.__init__(self)
        self.error = error

# FIXME: need for a "codec missing" message

STATES = enum.Enum("LOADING", "PLAYING", "PAUSED", "STOPPED")
"""
STATES.LOADING : The media is still loading. When it is finished the state
                 is switched to playing.
STATES.PLAYING : The player is currently playing a media.
STATES.PAUSED  : The player is paused. It means the media is paused at the last
                 played position and will continue to play from there.
STATES.STOPPED : The player is stopped. It will restart playing from the
                 beginning of the media.
"""

class Player(log.Loggable):
    """
    A player can play one audio or video media at a time. All it needs is a
    L{elisa.core.media_uri.MediaUri} and the sinks for the video and audio
    output. It can also do audio only output and has support for subtitles.
    
    @ivar video_sink:        the video sink that this player outputs to
    @type video_sink:        L{gst.BaseSink}

    @ivar name:              the name of the player instance
    @type name:              string

    @ivar audio_sink:        the audio sink that this player outputs to
    @type audio_sink:        L{gst.BaseSink}

    @ivar volume:            the volume level between 0 and 10
    @type volume:            float

    @ivar position:          the position we are currently playing in
                             nanoseconds; when set, if the value passed is higher
                             than L{duration}, position is set to L{duration}.
                             If the value passed is lower than 0, position is
                             set to 0.
    @type position:          int

    @ivar duration:          (read-only) the total length of the loaded media in
                             nanoseconds
    @type duration:          int

    @ivar speed:             The speed of the current playback:
                                - Normal playback is 1.0
                                - a positive value means forward
                                - a negative one backward
                                - the value 0.0 (equivalent to pause) is not
                                allowed
    @type speed:             float

    @ivar state:             (read-only) The current state. See 
                             L{elisa.core.player.STATES}.
    @type state:             L{elisa.core.player.STATES}

    @ivar playing:           (read-only) is the player currently playing? That
                             also returns False if the player is in LOADING
                             state.
    @type playing:           bool

    @ivar uri:               the uri of the media loaded in the player.
    @type uri:               L{elisa.core.media_uri.MediaUri}

    @ivar subtitle_uri:      the uri for subtitles
    @type subtitle_uri:      L{elisa.core.media_uri.MediaUri}

    @ivar subtitle_callback: the callback, where the timed subtitle texts
                             should be sent to. The callback will get a
                             L{gst.Buffer}, containing the subtitle text to be
                             displayed encoded in text/plain or
                             text/x-pango-markup
    @type subtitle_callback: callable

    @ivar muted:             True if the player is muted, False otherwise. This
                             is independent of the volume attribute (eg. can be
                             False even if volume is 0).
    @type muted:             bool
    """

    __metaclass__ = classinit.ClassInitMeta
    __classinit__ = classinit.build_properties

    def __init__(self, registry):
        """
        @param registry:    the registry to ask for new engines
        @type registry:     L{elisa.core.engine_registry.EngineRegistry}        
        """
        self._engine = None
        self._unmuted_volume = -1
        self._cache = False

        # Subtitle support
        self._make_subtitle_pipeline()
        self._sub_uri = None
        self._sub_callback = None

        self._loaded = False
        self._video_sink = None
        self._audio_sink = None

        self._player_engine_registry = registry

        self._uri = None
        self._visualisation = None

        application = common.application
        self._audiosink = application.config.get_option('audiosink',
                                                        section='player',
                                                        default='autoaudiosink')
        
        self._audiosettings = application.config.get_option(self._audiosink,
                                                        section='player',
                                                        default={})

        application.bus.register(self._new_clock, NewClock)
        application.bus.register(self._new_base_time, NewBaseTime)
        application.bus.register(self._engine_stopped, PlayerStopping)
        application.bus.register(self._engine_error, PlayerError)


    # Main Player Functionality

    def play(self, trigger_message=True):
        """
        Play the media. If trigger_message is set to True, this triggers first
        the message L{elisa.core.player.PlayerLoading} message and if the
        playback is really starting, it triggers
        L{elisa.core.player.PlayerPlaying}. Otherwise it does not trigger any
        messages.

        @param trigger_message: should the player trigger messages here
        @type trigger_message:  bool
        """
        if not self._loaded:
            return

        self._engine.play(trigger_message)
    
    def pause(self, trigger_message=True):
        """
        Pause the playback. If trigger_message is set to True, this triggers
        the L{elisa.core.player.PlayerPausing} message.

        @param trigger_message: should the player trigger a message here
        @type trigger_message:  bool
        """
        if not self._loaded:
            return

        self._engine.pause(trigger_message)
        if self._sub_uri:
            self._sub_pipeline.set_state(gst.STATE_PAUSED)

    def stop(self, trigger_message=True):
        """
        Stop the playback. This is _not_ effecting the subtitles. If
        trigger_message is set, this method triggers the
        L{elisa.core.player.PlayerStopping} message.

        @param trigger_message: should the player trigger a message here
        @type trigger_message:  bool
        """
        if not self._loaded:
            return

        if self._engine.state != STATES.STOPPED:
            self._engine.stop(trigger_message)


    def restart_from_beginning(self):
        """
        Play the uri from the beginning. This is not triggering any
        messages.
        """
        if not self._loaded:
            return

        # FIXME: pause/play/stop are async. That is might not working.

        self._engine.pause(trigger_message=False)
        self._engine.position = 0

        if self._engine.position != 0:
            # This means that the position setting didn't work.
            self._engine.stop(trigger_message=False)

        self._engine.play(trigger_message=False)

 
    def toggle_play_pause(self, trigger_message=True):
        """
        Toggle the player between play and pause state. If it is not playing
        yet, then start it. If trigger_message is set, this method might
        triggers L{elisa.core.player.PlayerPlaying} and
        L{elisa.core.player.PlayerLoading} or
        L{elisa.core.player.PlayerPausing}.


        @param trigger_message: should the player trigger a message here
        @type trigger_message:  bool
        """
        if not self._loaded:
            return

        self.debug("Toggle Play/Pause")

        if self.playing:
            self.pause(trigger_message)
        else:
            self.play(trigger_message)

    def playing__get(self):
        return self.state == STATES.PLAYING 

    # Volume

    def volume__set(self, volume):
        if not self._loaded:
            return

        self.debug("Volume set to %s" % volume)

        if self.muted:
            volume = self._unmuted_volume
            self._unmuted_volume = -1

        self._engine.volume = volume

    def volume__get(self):
        if not self._loaded:
            return

        if self.muted:
            return self._unmuted_volume

        return self._engine.volume
 
    def muted__set(self, value):
        if not self._loaded:
            return

        self.debug("Muting set to %s" % value)

        if value == True:
            old_volume = self.volume
            self.volume = 0 
            self._unmuted_volume = old_volume
        else:
            if self._unmuted_volume >= 0:
                self.volume = self._unmuted_volume
                self._unmuted_volume = -1
                
    def muted__get(self):
        if not self._loaded:
            return
        return self._unmuted_volume != -1

    # For URI Support

    def uri__set(self, uri):

        # FIXME: is that the right way to do it here?
        if uri == self._uri and self.playing:
            return

        self._uri = uri
        if uri == None:
            return
        e_reg = self._player_engine_registry
        media_manager = common.application.media_manager
        bus = common.application.bus
        uri = media_manager.get_real_uri(uri)
        scheme = uri.scheme

        if self._engine:
            # let's test, if this player supports this uri!
            if scheme in self._engine.uri_schemes.keys():
                self._engine.uri = uri
                self._loaded = True
            else:
                self._engine.stop(trigger_message=False)
                try:
                    new_engine = e_reg.create_engine_for_scheme(scheme)
                    self._remove_engine()
                    # Catch Gstreamer-Exceptions here to be sure that the
                    # linking did work and the unlinking was also done correct
                    # maybe?
                    self._engine = new_engine
                    if self._video_sink:
                        self._engine.video_sink = self._video_sink
                    if self._audio_sink:
                        self._engine.audio_sink = self._audio_sink
                    else:
                        self._engine.audio_sink = self._new_audio_sink()
                    self._engine.message_sender = self
                    self._engine.uri = uri
                    self._loaded = True
                except NoEngineFound:
                    # TODO: what if there is no engine found?
                    self.warning('No Engine found for the uri: %s' % uri)
                    self._loaded = False
                    bus.send_message(PlayerError('No engine found'), sender=self)

        else:
            new_engine = e_reg.create_engine_for_scheme(scheme)
            try:
                self._engine = new_engine
                self._engine.message_sender = self
                if self._video_sink:
                    self._engine.video_sink = self._video_sink
                if self._audio_sink:
                    self._engine.audio_sink = self._audio_sink
                else:
                    self._engine.audio_sink = self._new_audio_sink()
                self._engine.uri = uri
                self._engine.visualisation = self.visualisation
                self._loaded = True
            except NoEngineFound:
                self.warning("No Engine found for the uri %s" % uri)
                bus.send_message(PlayerError('No engine found'), sender=self)

    def uri__get(self):
        return self._uri

    def _remove_engine(self):
        self._loaded = False
        if self._engine:
            self._engine.stop(trigger_message=False)
            self._video_sink = self._engine.video_sink
            self._engine.video_sink = None
            self._audio_sink = self._engine.audio_sink
            self._engine.audio_sink = None
            self._engine = None

    def subtitle_uri__set(self, uri):
        self._sub_uri = uri
        self.debug("Setting Subtitle URI to: '%s'" % uri)
        if self._sub_uri == None:
            return self._update_subtitle_handoff()

        self._make_subtitle_pipeline()

        decode = self._sub_pipeline.get_by_name('src')
        if decode.get_factory().get_name() == "filesrc":
            if uri.scheme != 'file':
                self.warning("We only support file-scheme uris for"
                             " subtitles. If you want another scheme"
                             " please install gstreamer >= 0.10.14 with"
                             " uridecodebin!")
                self._sub_uri = None
            else:
                decode.set_property('location', uri.path)
        else:
            # we were able to set uri in decodebin
            decode.set_property('uri', str(uri))

        self._sub_pipeline.set_new_stream_time(gst.CLOCK_TIME_NONE)

        self._update_subtitle_handoff()

        if self.playing:
            self.pause(trigger_message = False)
            self.play(trigger_message = False)

    def subtitle_uri__get(self):
        return self._sub_uri

    def visualisation__get(self):
        return self._visualisation

    def visualisation__set(self, new_visu):
        # That feels bad to me...
        self._visualisation = new_visu
        

    def subtitle_callback__set(self, callback):
        self._sub_callback = callback
        self._update_subtitle_handoff()

    def subtitle_callback__get(self):
        self._sub_callback

    def _engine_error(self, message, sender):
        self._engine_stopped(message, sender)

    def _engine_stopped(self, message, sender):
        if sender == self:
            self._sub_pipeline.set_state(gst.STATE_NULL)

    def _update_subtitle_handoff(self):
        sink = self._sub_pipeline.get_by_name('sink')
        value = (self._sub_callback != None) and (self._sub_uri != None)
        self.debug("Signal handoffs activated: %s" % value)
        sink.set_property('signal-handoffs', value)


    def _new_clock(self, message, sender):
        if sender != self:
            return

        new_clock = message.clock
        self.debug("New clock %s" % new_clock)
        self._sub_pipeline.use_clock(new_clock)
    
    def _new_base_time(self, message, sender):
        if sender != self:
            return

        base_time = message.base_time
        if self._sub_uri != None:
            self._sub_pipeline.set_base_time(base_time)
            self._sub_pipeline.seek(1, gst.FORMAT_TIME,
                                gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT,
                                gst.SEEK_TYPE_SET, self.position,
                                gst.SEEK_TYPE_SET, self.duration)
            self._sub_pipeline.set_state(gst.STATE_PLAYING)
            self.debug("Got new basetime, starting to play!")

    # other states and attributes of the player

    def state__get(self):
        if not self._engine:
            return STATES.STOPPED
        return self._engine.state

    def position__get(self):
        if not self._engine:
            return -1
        return self._engine.position

    def position__set(self, position):
        if not self._engine:
            return

        duration = self.duration
        if position < 0:
            position = 0
        elif duration > 0 and position > duration:
            position = duration

        if self._sub_callback:
            worki = gst.Buffer()
            worki.duration = 1
            self._sub_callback(worki)
        self._engine.position = position

    def duration__get(self):
        if not self._engine:
            return -1

        return self._engine.duration

    def speed__get(self):
        if not self._engine:
            return 1

        return self._engine.speed

    def speed__set(self, speed):
        if not self._engine:
            return

        # FIXME: we have to do this on the subtitles too
        self._engine.speed = speed

    def video_sink__get(self):
        return self._video_sink

    def video_sink__set(self, sink):
        self._video_sink = sink
        if self._engine:
            self._engine.video_sink = self._video_sink

    def audio_sink__get(self):
        return self._audio_sink

    def audio_sink__set(self, sink):
        self._audio_sink = sink
        if self._engine:
            self._engine.audio_sink = self._audio_sink

    # Internal methods

    def _new_handoff(self, element, buffer, pad):
        if self._sub_callback:
            self.debug("calling %s with buffer %s" % (self._sub_callback, buffer)) 
            self._sub_callback(buffer)
   
    def _new_audio_sink(self): 
        # make a new sink
        audio_sink = gst.element_factory_make(self._audiosink)
        for key, value in self._audiosettings.iteritems():
            audio_sink.set_property(key, value)
        return audio_sink
    
    def _make_subtitle_pipeline(self):
        uri_dec = "uridecodebin name=src ! "\
                  "text/plain;text/x-pango-markup ! " \
                  "fakesink name=sink sync=true"
        filesrc = "filesrc name=src ! "\
                  "decodebin ! text/plain;text/x-pango-markup ! "\
                  "fakesink name=sink sync=true"

        try:
           self._sub_pipeline = gst.parse_launch(uri_dec)
        except GError:
            # using uridecode didn't work
            self._sub_pipeline = gst.parse_launch(filesrc)

        sink = self._sub_pipeline.get_by_name('sink')
        sink.set_property('signal-handoffs', False)
        sink.connect('handoff', self._new_handoff)
