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

import sys
import gobject
import weakref

from twisted.trial.unittest import TestCase
from pkg_resources import resource_filename

from elisa.core.utils import defer
from elisa.plugins.pigment.widgets.widget import Widget
from elisa.plugins.pigment.widgets.style import Style
from elisa.plugins.pigment.widgets.theme import Theme
from elisa.plugins.pigment.widgets.const import *
from elisa.plugins.pigment.tests.graph.test_group import GenericTestGroup

import pgm
       

class ThemeMock(object):
    """
    Mock elisa.plugins.pigment.widgets.theme.Theme object used by Widget test
    cases to retrieve state dependent styles.
    """

    styles = {None: {"property_a": "value_a",
                     "property_b": "value_b"},
              STATE_NORMAL: {},
              STATE_PRESSED: {"property_a": "value_a_pressed",
                              "property_c": "value_c_pressed"},
              STATE_HOVERED: {},
              STATE_SELECTED: {},
              STATE_DISABLED: {},
              STATE_LOADING: {},
             }

    styles_named_widget = {None: {"named_a": "named_value_a",
                                  "property_a": "named_value_a",
                                  "normal_named": "normal_named"},
                           STATE_NORMAL: {},
                           STATE_PRESSED: {"named_a": "named_value_a_pressed",
                                           "property_a": "named_value_a_pressed",
                                           "property_c": "value_c_pressed"},
                           STATE_HOVERED: {},
                           STATE_SELECTED: {},
                           STATE_DISABLED: {},
                          }

    def _is_named_widget(self, widget_path):
        return not widget_path.find("#") == -1

    def get_style_for_widget(self, widget_path, state):
        """
        Simplistic implementation of Theme.get_style_for_widget
        """
        if self._is_named_widget(widget_path):
            return self.styles_named_widget[state]
        else:
            return self.styles[state]

    def clean(self):
        pass

class TestWidget(TestCase):

    def setUp(self):
        mock = ThemeMock()
        Theme.set_default(mock)

    def tearDown(self):
        Theme.set_default(None)

    def signal_connect_after(self, emitter, signal, callback, *args, **kwargs):
        """
        Connect L{callback} to L{signal} emitted by L{emitter} object and
        return a deferred that will fail if the callback raises an exception
        and succeed otherwise.
        L{callback} is added to the signal handler list after the default class
        signal handler; see gobject.GObject.connect_object_after.
        Disconnection is performed automatically when the signal has been
        emitted once.
        """
        dfr = defer.Deferred()
        id = None
        def callback_wrapper(*args, **kwargs):
            emitter.disconnect(id)
            try:
                callback(*args, **kwargs)
            except Exception, e:
                dfr.errback(e)
            else:
                dfr.callback(None)

        id = emitter.connect_after(signal, callback_wrapper, *args, **kwargs)
        return dfr

    def assertIsProxy(self, obj, proxy):
        return self.assertTrue(weakref.ref(proxy) in weakref.getweakrefs(obj))

    def test_simple(self):
        widget = Widget()

    def test_clean(self):
        widget = Widget()
        widget.clean()

    def test_hierarchy(self):
        parent = Widget()
        parent.name = 'parent'
        children = []
        grandchildren = []

        for x in range(3):
            child = Widget()
            child.name = 'child_' + str(x)
            parent.add(child)
            children.append(child)

            for x in range(2):
                grandchild = Widget()
                grandchild.name = 'grandchild_' + str(x)
                child.add(grandchild)
                grandchildren.append(grandchild)

                self.assertIsProxy(child, grandchild.get_parent())
                self.assertIsProxy(parent, grandchild.get_root())
                self.assertEquals(len(grandchild.get_children()), 0)
                self.assertEquals(len(grandchild.get_descendants()), 0)

            self.assertIsProxy(parent, child.get_parent())
            self.assertIsProxy(parent, child.get_root())
            self.assertEquals(len(child.get_descendants()), 2)
            self.assertEquals(len(child.get_descendants()),
                              len(child.get_children()))

        self.assertEquals(len(parent.get_children()), 3)
        self.assertEquals(len(parent.get_descendants()), 3 + (3 * 2))

        # focus related tests

        # dictionary holding signal connection ids
        # key:   widget
        # value: id of the gobject signal connection
        signal_ids = {}

        def focus_changed(widget, focus):
            # make sure the signal sends the right focus value
            self.assertEquals(widget.focus, focus)

        for item in parent.get_descendants():
            self.assertEquals(item.focus, False)
            id = item.connect("focus", focus_changed)
            signal_ids[item] = id

        # Test that setting the focus on a child recursively forwards it
        # until its parent
        children[0].set_focus()
        self.failUnless(parent.focus)
        focused = children[0]
        self.failUnless(focused.focus)
        self.assertEquals(focused, Widget._focused)

        self.assertOnlyAscendantsFocused(focused)

        all_widgets = [parent] + parent.get_descendants()
        for widget in all_widgets:
            self.failUnless(widget.set_focus())
            self.assertOnlyAscendantsFocused(Widget._focused)

        # disconnect from 'focus' signal of all widgets
        for item, id in signal_ids.iteritems():
            item.disconnect(id)

    def assertOnlyAscendantsFocused(self, focused):
        root = focused.get_root()
        for widget in root.get_descendants():
            if widget == focused or focused in widget.get_descendants():
                self.failUnless(widget.focus)
            else:
                self.failIf(widget.focus)

    def test_state(self):
        widget = Widget()

        self.assertEquals(widget.state, STATE_NORMAL)

        self.emitted = False
        def on_state_changed(widget, previous_state):
            self.emitted = True
            self.assertEquals(previous_state, STATE_NORMAL)
            self.assertEquals(widget.state, STATE_PRESSED)

        dfr = self.signal_connect_after(widget, 'state-changed', on_state_changed)
        widget.state = STATE_PRESSED
        self.assertEquals(self.emitted, True)
        return dfr

    def test_state_change_style(self):
        widget = Widget()

        widget.state = STATE_PRESSED

        merged_style = Style(ThemeMock.styles[None])
        merged_style.update(ThemeMock.styles[widget.state])
        self.assertEquals(widget.style, merged_style)

    def test_style(self):
        widget = Widget()

        self.assertEquals(widget.style, widget._styles[STATE_NORMAL])

        new_style = Style({"property": "value"})

        self.emitted = False
        def on_style_set(widget, style):
            self.emitted = True
            self.assertEquals(style, new_style)

        dfr = self.signal_connect_after(widget, 'style-set', on_style_set)
        widget.style = new_style
        self.assertEquals(self.emitted, True)
        return dfr

    def test_set_name(self):
        widget = Widget()

        # default name is None
        self.assertEquals(widget.name, None)

        widget.set_name("widget_name")
        self.assertEquals(widget.name, "widget_name")

    def test_set_name_change_style(self):
        widget = Widget()

        widget.set_name("widget_name")

        merged_style = Style(ThemeMock.styles[None])
        merged_style.update(ThemeMock.styles_named_widget[None])
        merged_style.update(ThemeMock.styles[STATE_NORMAL])
        merged_style.update(ThemeMock.styles_named_widget[STATE_NORMAL])
        self.assertEquals(widget.style, merged_style)

    def test_named_widget_state_change_style(self):
        widget = Widget()

        widget.set_name("widget_name")
        widget.state = STATE_PRESSED

        # merging order is important:
        # 1) default style of the widget class
        # 2) default style of the widget named instance
        # 3) style for STATE_PRESSED of the widget class
        # 4) style for STATE_PRESSED of the widget named instance
        merged_style = Style(ThemeMock.styles[None])
        merged_style.update(ThemeMock.styles_named_widget[None])
        merged_style.update(ThemeMock.styles[widget.state])
        merged_style.update(ThemeMock.styles_named_widget[widget.state])
        self.assertEquals(widget.style, merged_style)

    def test_parse_style_key(self):
        widget = Widget()
        widget.attribute = None
        widget.subwidget = Widget()
        widget.subwidget.attribute = None
        widget.subwidget.subwidget = Widget()
        widget.subwidget.subwidget.attribute = None

        key = 'attribute'
        subwidget, attribute, logstr = widget._parse_style_key(key, '')
        self.failUnlessIdentical(subwidget, widget)
        self.failUnlessEqual(attribute, 'attribute')

        key = 'subwidget-attribute'
        subwidget, attribute, logstr = widget._parse_style_key(key, '')
        self.failUnlessIdentical(subwidget, widget.subwidget)
        self.failUnlessEqual(attribute, 'attribute')

        key = 'subwidget-subwidget'
        subwidget, attribute, logstr = widget._parse_style_key(key, '')
        self.failUnlessIdentical(subwidget, widget.subwidget)
        self.failUnlessEqual(attribute, 'subwidget')

        key = 'subwidget-subwidget-attribute'
        subwidget, attribute, logstr = widget._parse_style_key(key, '')
        self.failUnlessIdentical(subwidget, widget.subwidget.subwidget)
        self.failUnlessEqual(attribute, 'attribute')

        wrong_keys = ('attribute_not_found', 'subwidget_not-found',
                      'subwidget-attribute_not_found')
        for key in wrong_keys:
            self.failUnlessRaises(AttributeError,
                                  widget._parse_style_key, key, '')

    def test_parse_style_key_for_properties(self):
        class WidgetWriteOnlyProperty(Widget):
            """
            Widget with properties:
            - 'rw_property': read-write
            - 'ro_property': read only
            - 'wo_property': write only
            """
            def set_rw_property(self, value):
                pass

            def get_rw_property(self):
                return None

            rw_property = property(fget=get_rw_property, fset=set_rw_property)

            def set_wo_property(self, value):
                pass

            wo_property = property(fset=set_wo_property)

            def get_ro_property(self):
                return None

            ro_property = property(fget=get_ro_property)

        widget = WidgetWriteOnlyProperty()

        for attribute in ('rw_property', 'ro_property', 'wo_property'):
            widget._parse_style_key(attribute, '')

    def test_parse_style_value(self):
        widget = Widget()

        value = widget._parse_style_value(1.0, None, '', '')
        self.failUnlessEqual(value, 1.0)

        value = widget._parse_style_value((0, 0, 0, 255), None, '', '')
        self.failUnlessEqual(value, (0, 0, 0, 255))

        value = widget._parse_style_value('something', None, '', '')
        self.failUnlessEqual(value, 'something')

        value = widget._parse_style_value('center', pgm.Image(), 'alignment', '')
        self.failUnlessEqual(value, pgm.IMAGE_CENTER)

        value = widget._parse_style_value('right', pgm.Text(), 'alignment', '')
        self.failUnlessEqual(value, pgm.TEXT_ALIGN_RIGHT)

        value = widget._parse_style_value('bold', pgm.Text(), 'weight', '')
        self.failUnlessEqual(value, pgm.TEXT_WEIGHT_BOLD)

        self.failUnlessRaises(AttributeError, widget._parse_style_value,
                              'nada', pgm.Text(), 'weight', '')


class StubWidget(Widget):

    init_class_styles_called = False

    @classmethod
    def _init_class_styles(cls, theme):
        # calls Widget._init_class_styles with StubWidget as first argument
        super(StubWidget, cls)._init_class_styles(theme)
        cls.init_class_styles_called = True

    @classmethod
    def tearDown(cls):
        try:
            del cls._styles
        except AttributeError:
            pass
        cls.init_class_styles_called = False

class StubInheritingWidget(StubWidget):
    pass

class TestWidgetStylesCaching(TestCase):

    def tearDown(self):
        Theme.set_default(None)

        StubWidget.tearDown()
        StubInheritingWidget.tearDown()
        StubWidget.init_class_styles_called = False
        StubInheritingWidget.init_class_styles_called = False

    def test_styles_caching_in_class(self):
        """
        Make sure that caching happens in the class itself, not in parent
        classes.
        """
        widget = StubInheritingWidget()
        self.assertTrue(StubInheritingWidget.__dict__.has_key("_styles"))
        self.assertFalse(StubWidget.__dict__.has_key("_styles"))

    def test_init_styles_with_inheritance(self):
        """
        Make sure that if a parent class had its styles initialised before the
        inheriting widget class will see its styles initialised anyway.
        """
        StubWidget()
        self.assertTrue(StubWidget.init_class_styles_called)

        StubInheritingWidget()
        self.assertTrue(StubInheritingWidget.init_class_styles_called)

    def test_init_styles_multiple_times(self):
        """
        Initialisation of styles for a widget class only happens when the first
        instance is created.
        """
        widget = StubWidget()
        self.assertTrue(StubWidget.init_class_styles_called)

        StubWidget.init_class_styles_called = False
        another_widget = StubWidget()
        self.assertFalse(StubWidget.init_class_styles_called)

    def test_init_styles_new_theme(self):
        """
        Initialisation of styles for a widget class has to happen again if the
        default theme has changed and still only once when the first instance
        is created after the theme change.
        """
        # No default theme
        widget = StubWidget()
        self.assertTrue(StubWidget.init_class_styles_called)

        # Set a default theme
        mock = ThemeMock()
        Theme.set_default(mock)

        StubWidget.init_class_styles_called = False
        widget = StubWidget()
        self.assertTrue(StubWidget.init_class_styles_called)

        StubWidget.init_class_styles_called = False
        another_widget = StubWidget()
        self.assertFalse(StubWidget.init_class_styles_called)

        # Change the default theme
        mock = ThemeMock()
        Theme.set_default(mock)

        StubWidget.init_class_styles_called = False
        widget = StubWidget()
        self.assertTrue(StubWidget.init_class_styles_called)

        StubWidget.init_class_styles_called = False
        another_widget = StubWidget()
        self.assertFalse(StubWidget.init_class_styles_called)


class GenericTestWidget(GenericTestGroup):
    """
    These are tests that all classes inheriting from Widget should pass.
    You should reimplement this class for anything that inherits from Widget.
    """
    def setUp(self):
        # mock the start_monitoring 
        self.monitoring_mock = Theme.start_monitoring
        Theme.start_monitoring = lambda x: x

    def tearDown(self):
        Theme.start_monitoring = self.monitoring_mock

class GenericTestWidgetImpl(GenericTestWidget, TestCase):
    tested_class = Widget

    def tearDown(self):
        # if the theme was initialized, we have to stop the monitoring or we
        # receive a unclean reactor
        Theme.get_default().stop_monitoring()

