# -*- 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
import pgm
from elisa.plugins.pigment.animation.animation import DECELERATE

from elisa.core.utils import defer

from elisa.plugins.pigment.graph.image import Image
from elisa.plugins.pigment.graph.text import Text
from elisa.plugins.pigment.widgets.const import *
from elisa.plugins.pigment.widgets.widget import Widget
from elisa.plugins.pigment.widgets.box import VBox, HBox, ALIGNMENT
from elisa.plugins.pigment.widgets.size_enforcer import Square
from elisa.plugins.poblesec.widgets.player.button import Button

class Control(gobject.GObject):
    """
    An abstract class for controls acting on an object. On top of
    encapsulating the code that performs the operation in its L{activate}
    method it also embeds graphical and textual representations of what it
    does. These representations can be used to attach the control to GUI
    elements such as buttons, labels, menus, etc.

    Signals:
     - 'glyphs-changed': emitted when L{glyphs}'s value has been modified, no
                         arguments are passed to the handlers
     - 'caption-changed': emitted when L{caption}'s value has been modified, no
                          arguments are passed to the handlers

    @ivar slave:   object that the control will act on when activated
    @type slave:   object
    @ivar glyphs:  dictionary of image resources indexed by states that
                   graphically represent what the control does
    @type glyphs:  dictionary of states: str
    @ivar caption: label that represents what the control does
    @type caption: str
    """

    __gsignals__ = {
        'glyphs-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'caption-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        }

    def __init__(self, controlled_object):
        """
        Constructor.

        @param controlled_object: object that the control will act on when
                                  activated
        @type controlled_object:  object
        """
        super(Control, self).__init__()
        self.slave = controlled_object
        self._glyphs = {STATE_NORMAL: '',
                        STATE_PRESSED: '',
                        STATE_SELECTED: ''}
        self._caption = ""

    def caption__set(self, caption):
        self._caption = caption
        self.emit("caption-changed")

    def caption__get(self):
        return self._caption

    caption = property(caption__get, caption__set)

    def glyphs__set(self, glyphs):
        self._glyphs = glyphs
        self.emit("glyphs-changed")

    def glyphs__get(self):
        return self._glyphs

    glyphs = property(glyphs__get, glyphs__set)

    def activate(self):
        """
        Activating the action runs the action code attached to the control.
        For example it can be the code executed when a button is clicked.

        Control implementations have to override this method.

        @rtype: L{elisa.core.utils.defer.Deferred}
        """
        return defer.fail(NotImplementedError())

    def pre_activate(self):
        """
        Code run prior to control activation.
        For example it can be the code executed when a button is pressed.

        Control implementations have to override this method.

        @rtype: L{elisa.core.utils.defer.Deferred}
        """
        return defer.succeed(None)

    def de_pre_activate(self):
        """
        Code run after control activation.
        For example it can be the code executed when a button is released.

        Control implementations have to override this method.

        @rtype: L{elisa.core.utils.defer.Deferred}
        """
        return defer.succeed(None)

class ControlRibbon(VBox):
    """
    Control ribbon widget for the player. It displays a list of actions and
    keeps track of the selected one.

    @ivar buttons: horizontal box containing the control buttons
    @type buttons: L{elisa.plugins.pigment.widgets.box.HBox}
    @ivar caption: text displaying the meaning of the selected action
    @type caption: L{elisa.plugins.pigment.graph.text.Text}
    @ivar selected_control: control currently selected
    @type selected_control: L{Control}
    """

    def create_widgets(self):
        # FIXME: this widget is full with hardcoded constants that fit well
        # with the player design; it might be interesting to move them over
        # to an external style configuration file

        # buttons ribbon where the actual controls will be displayed
        self.buttons = HBox()
        self.pack_start(self.buttons)
        self.buttons.alignment = ALIGNMENT.CENTER
        self.buttons.height = 0.59
        self.buttons.visible = True

        # decorations around the buttons ribbon expanding horizontally
        background_left = Image()
        background_left.bg_color = (77, 77, 77, 191)
        background_left.visible = True
        self.buttons.pack_start(background_left, expand=True)

        background_right = Image()
        background_right.bg_color = (77, 77, 77, 191)
        background_right.visible = True
        self.buttons.pack_end(background_right, expand=True)

        # selected control caption made of a grey background and of a
        # vertically centered label
        self._caption_centerer = VBox()
        self._caption_centerer.alignment = ALIGNMENT.CENTER
        self._caption_centerer.visible = True
        self.pack_start(self._caption_centerer)
        self._caption_centerer.height = 0.41

        caption_background = Image()
        caption_background.bg_color = (77, 77, 77, 140)
        self._caption_centerer.add(caption_background)
        caption_background.size = (1.0, 1.0)
        caption_background.position = (0.0, 0.0, 0.0)
        caption_background.visible = True

        self.caption = Text()
        self.caption.bg_a = 0
        self._caption_centerer.pack_start(self.caption)
        self.caption.alignment = pgm.TEXT_ALIGN_CENTER
        self.caption.visible = True

        # signal connection ids
        # key:   emitter
        # value: signal handler id
        self._signal_ids = []

        self._controls = []
        self.selected_control = None

        # animation facilities
        settings = {'duration': 350,
                    'transformation': DECELERATE}
        self.animated.setup_next_animations(**settings)

    def clean(self):
        super(ControlRibbon, self).clean()
        # disconnect signal handlers
        for obj, id in self._signal_ids:
            obj.disconnect(id)

        self._signal_ids = None

    def _update_caption(self):
        if self.selected_control != None:
            self.caption.label = self.selected_control.caption
        else:
            self.caption.label = ""

    def _control_clicked(self, notifier, x, y, z, button, time, pressure,
                         control):
        control.activate()
        return True

    def _control_pressed(self, notifier, x, y, z, button, time, pressure,
                         control):
        control.pre_activate()
        return True

    def _control_released(self, notifier, x, y, z, button, time, control):
        control.de_pre_activate()
        return True

    @property
    def controls(self):
        return self._controls

    def add_control(self, control):
        """
        Append a control to the ribbon. As a visual representation of the
        control a button will be added to the ribbon. The ribbon's buttons are
        of type L{elisa.plugins.poblesec.widgets.player.button.Button} and
        constantly reflect their associated control.

        The buttons are forced to be visually square.

        @param control: control to be added to the ribbon
        @type control:  L{Control}
        """
        self._controls.append(control)

        button = Button()
        button.sticky = True
        button.set_backgrounds(
              "elisa.plugins.poblesec.player.ribbon.button_background_normal",
              "elisa.plugins.poblesec.player.ribbon.button_background_selected",
              "elisa.plugins.poblesec.player.ribbon.button_background_active")
        button.visible = True

        # FIXME: add a reference to the button (widget) in the control
        # on-the-fly; that is used in order to update the state of the button
        # when a control is selected or deselected.
        control.button = button

        def update_glyphs(control, button):
            button.set_glyphs(control.glyphs[STATE_NORMAL],
                              control.glyphs[STATE_SELECTED],
                              control.glyphs[STATE_PRESSED])

        def button_state_changed(button, previous_state, control):
            if button.state == STATE_SELECTED:
                previous_control = self.selected_control
                self.selected_control = control
                if previous_control != None and previous_control != control:
                    previous_control.button.state = STATE_NORMAL

            self._update_caption()

        id = control.connect("glyphs-changed", update_glyphs, button)
        self._signal_ids.append((control, id))
        id = control.connect("caption-changed", lambda x: self._update_caption())
        self._signal_ids.append((control, id))
        id = button.connect("state-changed", button_state_changed, control)
        self._signal_ids.append((button, id))
        id = button.connect("clicked", self._control_clicked, control)
        self._signal_ids.append((button, id))
        id = button.connect("pressed", self._control_pressed, control)
        self._signal_ids.append((button, id))
        id = button.connect("released", self._control_released, control)
        self._signal_ids.append((button, id))

        update_glyphs(control, button)

        # we force the button to be square
        s = Square()
        s.add(button)
        button.size = (1.0, 1.0)
        button.square = s
        s.visible = True
        self.buttons.pack_start(s)

        # upon first control insertion, make it selected
        if len(self._controls) == 1:
            self.select_next_control()

    def remove_control(self, control):
        """
        Remove a L{Control} from the ribbon. All signals connected to
        its button are also disconnected and the widget is removed
        from the buttons hbox.

        @param control: control to be removed from the ribbon
        @type control:  L{Control}
        """

        self._controls.remove(control)
        
        if control == self.selected_control:
            if len(self._controls) > 0:
                self.selected_control = self._controls[0]
                self.selected_control.button.state = STATE_SELECTED
            else:
                self.selected_control = None

            self._update_caption()

        # release the gobject signals of the control
        idx = 0
        while idx < len(self._signal_ids):
            obj, handler_id = self._signal_ids[idx]
            if obj == control:
                obj.disconnect(handler_id)
                del self._signal_ids[idx]
            else:
                idx += 1

        # remove the control from the list
        self.buttons.remove(control.button.square)

    @property
    def is_shown(self):
        """
        Is the control ribbon shown or not to the user.
        Note: this is not named "visible" to avoid interfering with the Pigment
        Graph dark magic.

        @returns: whether or not the ribbon is visible
        @rtype:   C{bool}
        """
        return self.animated.opacity > 0

    def hide(self):
        """
        Hide the control ribbon using a smooth fade out.
        """
        self.animated.opacity = 0

    def show(self):
        """
        Show the control ribbon using a smooth fade in.
        """
        if self.animated.opacity != 255:
            self.animated.opacity = 255


    def select_next_control(self):
        """
        Select the next control in the list of controls that were added to
        the ribbon. They are sorted from first added to last added.
        """
        if self.selected_control != None:
            current_index = self._controls.index(self.selected_control)
            next_index = current_index+1
        else:
            next_index = 0

        if next_index < len(self._controls):
            next_control = self._controls[next_index]
            next_control.button.state = STATE_SELECTED

        self._update_caption()

    def select_previous_control(self):
        """
        Select the previous control in the list of controls that were added to
        the ribbon. They are sorted from first added to last added.
        """
        if self.selected_control != None:
            current_index = self._controls.index(self.selected_control)
            next_index = current_index-1
        else:
            next_index = len(self._controls)-1

        if next_index >= 0:
            next_control = self._controls[next_index]
            next_control.button.state = STATE_SELECTED

        self._update_caption()


    @classmethod
    def _demo_widget(cls, *args, **kwargs):
        from elisa.plugins.pigment.widgets.theme import Theme

        theme = Theme.load_from_module("elisa.plugins.poblesec")
        Theme.set_default(theme)

        widget = cls()
        widget.size = (4.0, 0.5)
        widget.visible = True

        class TestControl(Control):
            glyphs = \
            {STATE_NORMAL: 'elisa.plugins.poblesec.player.glyphs.pause_normal',
             STATE_SELECTED: 'elisa.plugins.poblesec.player.glyphs.pause_normal',
             STATE_PRESSED: 'elisa.plugins.poblesec.player.glyphs.pause_active',
            }

            def activate(self):
                print "control \"%s\" activated" % self.caption
                return defer.succeed(None)

        for i in xrange(5):
            control = TestControl(None)
            control.caption = "My Control %s" % i
            widget.add_control(control)

        def key_press_event_cb(self, viewport, event, ribbon):
            if event.keyval == pgm.keysyms.Right:
                ribbon.select_next_control()
            elif event.keyval == pgm.keysyms.Left:
                ribbon.select_previous_control()
            elif event.keyval == pgm.keysyms.Return:
                ribbon.selected_control.activate()

        widget.connect('key-press-event', key_press_event_cb)

        return widget


if __name__ == '__main__':
    import logging
    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)

    i = ControlRibbon.demo()

    try:
        __IPYTHON__
    except NameError:
        pgm.main()
