# -*- 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>

"""
Box widgets.
"""

from elisa.core.input_event import EventValue

from elisa.extern import enum

from elisa.plugins.pigment.widgets.widget import Widget
from elisa.plugins.pigment.widgets.theme import ValueWithUnit
from elisa.plugins.pigment.graph.group import Group
from elisa.plugins.pigment.graph.drawable import Drawable

import pgm

from twisted.internet import reactor

PACKING = enum.Enum("START", "END")
ALIGNMENT = enum.Enum("START", "CENTER", "END")


class Child(object):
    """
    Child encapsulates all the necessary information for a L{Box} to render
    one of its children.

    @ivar widget:  widget to render in the parent box
    @type widget:  L{elisa.plugins.pigment.widgets.widget.Widget}
    @ivar packing: START if child was packed using pack_start, END if child
                   was packed using pack_end
    @type packing: L{PACKING}
    @ivar expand:  value of expand passed at packing time
    @type expand:  bool
    """

    def __init__(self, widget, packing, expand):
        self.widget = widget
        self.packing = packing
        self.expand = expand

class Box(Widget):
    """
    Box is an abstract widget that defines a specific kind of container that
    layouts a variable number of widgets into a rectangular area. The former
    is organized into either a single row or a single column of child widgets
    depending upon whether the box is of type L{HBox} or L{VBox}, respectively.

    Use repeated calls to gtk_box_pack_start to pack widgets from start to end.
    Use gtk_box_pack_end to add widgets from end to start.

    @ivar spacing:   amount of space between children
    @type spacing:   L{elisa.plugins.pigment.widgets.theme.ValueWithUnit}
    @ivar padding:   amount of space between the first and last children and
                     the border of the box
    @type padding:   L{elisa.plugins.pigment.widgets.theme.ValueWithUnit}
    @ivar alignment: defines where the children are positioned in the
                     rectangular area of the box
    @type alignment: L{ALIGNMENT}

    @ivar navigable: Whether the box can be navigated. Typically, a box that
                     packs focusable widgets (i.e. not just used for graphical
                     layout) should be navigable. C{False} by default.
    @type navigable: C{bool}

    @cvar prev_event_value: An event value to navigate to the previous widget
    @type prev_event_value: L{elisa.core.input_event.EventValue}
    @cvar next_event_value: An event value to navigate to the next widget
    @type next_event_value: L{elisa.core.input_event.EventValue}
    """

    prev_event_value = None
    next_event_value = None

    _style_to_enum = {"start": ALIGNMENT.START,
                      "center": ALIGNMENT.CENTER,
                      "end": ALIGNMENT.END}

    def __init__(self):
        super(Box, self).__init__()
        self._spacing = ValueWithUnit(0.0)
        self._padding = ValueWithUnit(0.0)
        self._start_packed_children = []
        self._end_packed_children = []
        self._alignment = ALIGNMENT.CENTER

        # False by default for backward compatibility. Most boxes are currently
        # used for graphical layout, without any navigation involved.
        self.navigable = False

        # Track the last widget that accepted the focus so as to restore it when
        # getting the focus back (more intuitive navigation).
        self._last_focused_widget = None

        # dictionary of signal ids per child to be disconnected on cleanup
        # key:  child widget
        # value: list of signal ids
        self._children_signal_ids = {}

        self.create_widgets()

        self.update_style_properties(self.style.get_items())

    def create_widgets(self):
        """
        Override this method in subclasses to create subwidgets.
        """
        pass

    # Overridden from Group

    def regenerate(self):
        self._queue_layout()
        super(Box, self).regenerate()

    def do_mapped(self):
        self._layout()

    def _queue_layout(self):
        if not self.is_mapped:
            return
        self._layout()

    def _child_resized_callback(self, notifier, width, height):
        self._queue_layout()

    def _child_changed_callback(self, notifier, property):
        # FIXME: I could not find the enum value pgm.DRAWABLE_SIZE of
        # PgmDrawableProperty; it is therefore hardcoded here
        # As to revision 1296, Pigment Python has the value and we are waiting
        # for a release before making the modification here.
        PGM_DRAWABLE_SIZE = 1
        if property == PGM_DRAWABLE_SIZE:
            self._queue_layout()

    def _disconnect_child(self, child):
        for id in self._children_signal_ids.get(child, []):
            child.widget.disconnect(id)

    def clean(self):
        super(Box, self).clean()

        # disconnect from signals of the children
        for child in self._children_signal_ids:
            self._disconnect_child(child)

    def pack_start(self, widget, expand=False):
        """
        Add L{widget} to the box packed after any other widget packed using
        pack_start. Visually L{widget} will be positioned after any other
        widget packed that way.

        @param widget: widget to pack in the box
        @type widget:  L{elisa.plugins.pigment.widgets.widget.Widget}
        @param expand: True if widget is to be given extra space allocated to box.
                       The extra space will be divided evenly between all
                       widgets of box that use this option.
        @type expand:  bool
        """
        child = Child(widget, PACKING.START, expand)
        self._start_packed_children.append(child)
        self._insert_new_child(child)

    def pack_end(self, widget, expand=False):
        """
        Add L{widget} to the box packed after any other widget packed using
        pack_end. Visually L{widget} will be positioned before any other
        widget packed that way.

        @param widget: widget to pack in the box
        @type widget:  L{elisa.plugins.pigment.widgets.widget.Widget}
        @param expand: True if widget is to be given extra space allocated to box.
                       The extra space will be divided evenly between all
                       widgets of box that use this option.
        @type expand:  bool
        """
        child = Child(widget, PACKING.END, expand)
        self._end_packed_children.insert(0, child)
        self._insert_new_child(child)

    def remove(self, widget):
        super(Box, self).remove(widget)

        for i, child in enumerate(self._start_packed_children):
            if child.widget == widget:
                self._remove_packed_child(i, self._start_packed_children)
                self._queue_layout()
                return

        for i, child in enumerate(self._end_packed_children):
            if child.widget == widget:
                self._remove_packed_child(i, self._end_packed_children)
                self._queue_layout()
                return

    def pop(self):
         child = None

         if self._end_packed_children:
             child = self._remove_packed_child(0, self._end_packed_children)
         elif self._start_packed_children:
             child = self._remove_packed_child(-1, self._start_packed_children)

         if child:
             super(Box, self).remove(child.widget)
             self._queue_layout()

         return child

    def _remove_packed_child(self, idx, child_list):
        child = child_list.pop(idx)

        if child in self._children_signal_ids:
            self._disconnect_child(child)
            del self._children_signal_ids[child]

        if child.widget is self._last_focused_widget:
            self._last_focused_widget = None

        return child

    def __len__(self):
        return len(self._start_packed_children) + len(self._end_packed_children)

    def __contains__(self, widget):
        for child in self._start_packed_children:
            if child.widget == widget:
                return True

        for child in self._end_packed_children:
            if child.widget == widget:
                return True

        return False

    def _insert_new_child(self, child):
        child.widget.position = (0.0, 0.0, 0.0)
        self.add(child.widget)
        self._queue_layout()
        if not child.expand:
            self._connect_to_child_signals(child)

    def _connect_to_child_signals(self, child):
        # connect to 'resized' signal of the child in order to relayout when
        # the child's size changes
        # FIXME: deals with two different cases because signals API are
        # different for Groups and Drawables
        widget = child.widget
        if isinstance(widget, Group):
            id = widget.connect("resized", self._child_resized_callback)
            self._children_signal_ids[child] = [id]
        elif isinstance(widget, Drawable):
            id = widget.connect("changed", self._child_changed_callback)
            self._children_signal_ids[child] = [id]

    def spacing__get(self):
        return self._spacing

    def spacing__set(self, spacing):
        if not isinstance(spacing, ValueWithUnit):
            spacing = ValueWithUnit(spacing)
        self._spacing = spacing
        self._queue_layout()

    spacing = property(spacing__get, spacing__set)

    def padding__get(self):
        return self._padding

    def padding__set(self, padding):
        if not isinstance(padding, ValueWithUnit):
            padding = ValueWithUnit(padding)
        self._padding = padding
        self._queue_layout()

    padding = property(padding__get, padding__set)

    def alignment__get(self):
        return self._alignment

    def alignment__set(self, alignment):
        if isinstance(alignment, basestring):
            alignment = self._style_to_enum[alignment]

        self._alignment = alignment
        self._queue_layout()

    alignment = property(alignment__get, alignment__set)

    def _layout(self):
        raise NotImplementedError("To be implemented by classes inheriting of \
                                   Box")

    def _prelayout_children(self, children, property, spacing, padding):
        """
        Compute the size of children packed with expand set to True and the
        coordinate at which the first widget should be positioned depending on
        the number of children, their expand mode and the box alignment.

        @param children: children for which to compute the value 
        @type children:  list of L{Child}
        @param property: one of 'width', 'height'
        @type property:  str
        @param spacing:  distance between two children in relative coordinates
        @type spacing:   float
        @param padding:  distance between the first/last children and the
                         border of the box in relative coordinates
        @type padding:   float

        @rtype: tuple of float
        """
        # total amount of spacing needed
        total_spacing = spacing*(len(children)-1)
        total_spacing += padding*2.0

        # compute size of children in expand mode = remaining space divided
        # equally among them after taking off other children and spacing
        non_expand_children = []
        non_expand_children_size = 0
        for child in children:
            if child.expand:
                continue

            non_expand_children.append(child)
            non_expand_children_size += getattr(child.widget, property)

        len_expand_children = len(children)-len(non_expand_children)
        if len_expand_children > 0:
            expand_children_size = 1.0-non_expand_children_size-total_spacing
            expand_child_size = expand_children_size/len_expand_children
        else:
            expand_children_size = 0.0
            expand_child_size = 0.0

        # compute 'start_coordinate' initial value depending on the alignment of the
        # box
        if self.alignment == ALIGNMENT.START:
            start_coordinate = 0
        else:
            children_size = non_expand_children_size+expand_children_size

            if self.alignment == ALIGNMENT.END:
                start_coordinate = 1.0-children_size-total_spacing
            elif self.alignment == ALIGNMENT.CENTER:
                start_coordinate = (1.0-children_size-total_spacing)/2.0

        start_coordinate += padding

        return start_coordinate, expand_child_size

    def set_focus(self):
        # Overridden from elisa.plugins.pigment.widgets.widget.Widget
        if not self.navigable:
            return super(Box, self).set_focus()
        else:
            if self._last_focused_widget is not None and \
                self._last_focused_widget.set_focus():
                return True

            children = self._start_packed_children + self._end_packed_children
            for child in children:
                widget = child.widget
                if isinstance(widget, Widget) and widget.set_focus():
                    self._last_focused_widget = widget
                    return True

            # No child widget accepted the focus, accept it anyway.
            self._accept_focus()
            return True

    def _get_child_widgets_and_focused_index(self):
        """
        Return the list of widgets associated to the children as well as the
        index of the one that is focused.
        Some children of the box are not widgets but drawables thus not having
        the focus attribute. They are excluded from the returned list.
        """
        child_widgets = [child.widget for child in \
                         self._start_packed_children + self._end_packed_children \
                         if isinstance(child.widget, Widget)]

        children_focus = [widget.focus for widget in child_widgets]
        return child_widgets, children_focus.index(True)

    def _focus_previous_widget(self):
        """
        Focus the child widget coming before the currently focused widget.
        """
        child_widgets, focused_index = self._get_child_widgets_and_focused_index()
        for widget in reversed(child_widgets[:focused_index]):
            if widget.set_focus():
                self._last_focused_widget = widget
                return True

        return False

    def _focus_next_widget(self):
        """
        Focus the child widget coming after the currently focused widget.
        """
        child_widgets, focused_index = self._get_child_widgets_and_focused_index()
        for widget in child_widgets[focused_index+1:]:
            if widget.set_focus():
                self._last_focused_widget = widget
                return True

        return False

    def cycle_focus(self, reverse=False, excludes=[]):
        """
        Try to pass the focus to the next child widget in a cyclic way.

        @param reverse:  whether to cycle backwards the child widgets
                         (default: C{False})
        @type reverse:   C{bool}
        @param excludes: an optional list of child widgets to ignore
                         (default: C{[]})
        @type excludes:  C{list} of
                         L{elisa.plugins.pigment.widgets.widget.Widget}

        @return: whether a child widget accepted the focus
        @rtype:  C{bool}
        """
        child_widgets, focused_index = self._get_child_widgets_and_focused_index()
        widgets = child_widgets[focused_index+1:] + child_widgets[:focused_index]
        if reverse:
            widgets = reversed(widgets)
        for widget in widgets:
            if widget in excludes:
                continue
            if widget.set_focus():
                self._last_focused_widget = widget
                return True

        return False

    def handle_input(self, manager, event):
        if self.navigable:
            accepted = False
            if event.value == self.prev_event_value:
                accepted = self._focus_previous_widget()
            elif event.value == self.next_event_value:
                accepted = self._focus_next_widget()

            if accepted:
                return True

        return super(Box, self).handle_input(manager, event)

    @classmethod
    def _demo_widget(cls, *args, **kwargs):
        from elisa.plugins.pigment.graph.image import Image

        widget = cls()
        widget.spacing = 0.01
        widget.alignment = ALIGNMENT.END
        widget.visible = True

        widget.position = (100.0, 50.0, 0.0)
        widget.size = (100.0, 100.0)

        # red debugging background
        background = Image()
        background.bg_color = (255, 0, 0, 255)
        widget.add(background)
        background.position = (0.0, 0.0, 0.0)
        background.size = (1.0, 1.0)
        background.visible = True

        # children start packed with expand set to True
        # their background is green
        for i in xrange(2):
            image = Image()
            image.bg_color = (55, 255, 55, 255)
            image.visible = True
            widget.pack_start(image, expand=True)

        # children end packed with expand set to True
        # their background is blue
        for i in xrange(1):
            image = Image()
            image.bg_color = (55, 55, 255, 255)
            image.visible = True
            widget.pack_end(image, expand=True)

        # children start packed with expand set to False
        # their background is white
        for i in xrange(4):
            image = Image()
            image.bg_color = (255, 255, 255, 255)
            image.width /= 10.0
            image.height /= 10.0
            image.visible = True
            widget.pack_start(image)

        return widget

class HBox(Box):
    """
    A L{Box} layouting its children horizontally.
    """

    prev_event_value = EventValue.KEY_GO_LEFT
    next_event_value = EventValue.KEY_GO_RIGHT

    def _insert_new_child(self, child):
        child.widget.height = 1.0
        super(HBox, self)._insert_new_child(child)

    def _compute_relative_spacing(self, spacing):
        fx, fy = self.get_factors_relative(spacing.unit)
        return fx*spacing.value

    def _compute_relative_padding(self, padding):
        fx, fy = self.get_factors_relative(padding.unit)
        return fx*padding.value

    def _layout(self):
        # merge all children in layout order
        children = self._start_packed_children + self._end_packed_children
        spacing = self._compute_relative_spacing(self._spacing)
        padding = self._compute_relative_padding(self._padding)
        accumulator, expand_child_width = self._prelayout_children(children,
                                                                   "width",
                                                                   spacing,
                                                                   padding)
        # traverse the children layouting each of them
        # 'accumulator' is used to store the horizontal translation applied to
        # the children as we are traversing them
        for index, child in enumerate(children):
            child.widget.x = accumulator
            if child.expand:
                child.widget.width = expand_child_width
            accumulator += child.widget.width + spacing


class VBox(Box):
    """
    A L{Box} layouting its children vertically.
    """

    prev_event_value = EventValue.KEY_GO_UP
    next_event_value = EventValue.KEY_GO_DOWN

    def _insert_new_child(self, child):
        child.widget.width = 1.0
        super(VBox, self)._insert_new_child(child)

    def _compute_relative_spacing(self, spacing):
        fx, fy = self.get_factors_relative(spacing.unit)
        return fy*spacing.value

    def _compute_relative_padding(self, padding):
        fx, fy = self.get_factors_relative(padding.unit)
        return fy*padding.value

    def _layout(self):
        # merge all children in layout order
        children = self._start_packed_children + self._end_packed_children
        spacing = self._compute_relative_spacing(self._spacing)
        padding = self._compute_relative_padding(self._padding)
        accumulator, expand_child_width = self._prelayout_children(children,
                                                                   "height",
                                                                   spacing,
                                                                   padding)
        # traverse the children layouting each of them
        # 'accumulator' is used to store the vertical translation applied to
        # the children as we are traversing them
        for index, child in enumerate(children):
            child.widget.y = accumulator
            if child.expand:
                child.widget.height = expand_child_width
            accumulator += child.widget.height + spacing

if __name__ == '__main__':
    import logging

    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)

    widget = HBox.demo()
    try:
        __IPYTHON__
    except NameError:
        pgm.main()
