# -*- 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__ = 'Florian Boucault <florian@fluendo.com>'
__maintainer2__ = 'Lionel Martin <lionel@fluendo.com>'


from elisa.core.component import Component, ComponentError
from elisa.base_components.controller import Controller
from elisa.core.observers.observer import Observer
from elisa.core import common
from elisa.core.interface_controller import UndefinedMVCAssociation


class ControllerNotSupported(Exception):
    pass

class View(Component, Observer):
    """
    A View is responsible for rendering a Model. It can be of various nature
    such as graphics, audio or even network output. A View does not hold a
    direct reference to the Model it renders but accesses it through
    L{controller}.
    It is notified of all the changes occuring to L{controller}. Part of these
    changes can be treated automatically using the bindings system.
    A View can be part of a hierarchy of Views thus potentially having a
    reference to its L{parent} and to its children defined in L{bindings}.
    A View belongs to its L{frontend} which encapsulates objects necessary for
    a View to be rendered (rendering context, theme, etc.)

    @cvar supported_controllers: list of controllers that are compatible with
                                 the view identified by their path:
                                 'plugin:controller'
    @type supported_controllers: tuple of strings
    @cvar bindings:             associations between L{model}'s attribute and
                                Controller's attributes; if an attribute
                                contained in L{bindings}' keys is changed or
                                created in L{model}, it is replicated in all
                                the corresponding attributes of the
                                controller defined in L{bindings} values
    @type bindings:             tuple of tuples with for each tuple:
                                key (1st value): string
                                value (2nd value): list of strings or string
    @ivar controller:           controller to which the view is connected in
                                order to access the model; setting it to an
                                incompatible controller will fail
    @type controller:           L{elisa.base_components.controller.Controller}
    @ivar parent:               parent view; None if the view is a root
    @type parent:               L{elisa.base_components.view.View}
    @ivar frontend:             frontend to which the view belongs
    @type frontend:             L{elisa.core.frontend.Frontend}
    @ivar context_handle:       DOCME
    @type context_handle:       any
    """

    supported_controllers = ('base:controller',)
    bindings = ()

    context_path = None

    def __init__(self):
        super(View, self).__init__()
        self._controller = None
        self.context_handle = None
        self._frontend = None
        self._parent_view = None
        self._supported_controller_classes = ()


    def initialize(self):
        super(View, self).initialize()
        plugin_registry = common.application.plugin_registry
        classes = [ plugin_registry.get_component_class(controller_path)
                    for controller_path in self.supported_controllers ]
        self._supported_controller_classes = tuple(classes)

    def clean(self):
        super(View, self).clean()
        self.controller = None
        self.parent = None
        self.frontend = None

    def frontend__get(self):
        return self._frontend

    def frontend__set(self, new_frontend):
        previous_frontend, self._frontend = self._frontend, new_frontend
        if previous_frontend != new_frontend:
            self.frontend_changed(previous_frontend, new_frontend)

    def frontend_changed(self, previous_frontend, new_frontend):
        """
        Called when L{frontend} is set to a new value.

        Override if you wish to react to that change. Do not forget to call
        the parent class method.

        @param old_frontend: value of frontend before
        @type old_frontend:  L{elisa.core.frontend.Frontend}
        @param new_frontend: value of frontend now
        @type new_frontend:  L{elisa.core.frontend.Frontend}
        """
        pass

    def controller__get(self):
        return self._controller

    def controller__set(self, controller):
        if self._controller == controller:
            return

        if controller != None and \
           not isinstance(controller, self._supported_controller_classes):
                msg = "%r does not support %r but supports %r" % \
                      (self, controller, self.supported_controllers)
                self.warning(msg)
                raise ControllerNotSupported(msg)

        old_controller = self._controller
        self._controller = controller

        # update bindings
        for controller_attribute, view_attributes in self.bindings:
            self._update_bound_attributes(controller_attribute,
                                          view_attributes)

        # stop observing the old model and start observing the new one
        if old_controller != None:
            old_controller.remove_observer(self)
        if controller != None:
            controller.add_observer(self)

        self.controller_changed(old_controller, controller)

    def controller_changed(self, old_controller, new_controller):
        """
        Called when L{controller} is set to a new value.

        Override if you wish to react to that change. Do not forget to call
        the parent class method.

        @param old_controller: value of controller before
        @type old_controller:  L{elisa.base_components.controller.Controller}
        @param new_controller: value of controller now
        @type new_controller:  L{elisa.base_components.controller.Controller}
        """
        pass

    def update(self):
        """
        Refresh completely the rendering of the associated Controller. This should
        be called when the Controller has changed.
        """
        pass

    def parent__set(self, parent):
        self._parent_view = parent
        if parent is not None:
            self.frontend = parent.frontend
        
    def parent__get(self):
        # FIXME: This collapses with Group.parent__get when doing
        #        multiple inheritance
        return self._parent_view

    def attribute_set(self, origin, key, old_value, new_value):
        """
        Called when an attribute of the controller to which it is connected
        changes.

        @param key:       attribute changed
        @type key:        string
        @param old_value: value of the attribute before being set; None if
                          attribute was not existing
        @type old_value:  any
        @param new_value: value of the attribute after being set
        @type new_value:  any
        """
        # update bindings
        for controller_attribute, view_attributes in self.bindings:
            if controller_attribute == key:
                self._update_bound_attributes(controller_attribute,
                                              view_attributes)

    def _delete_bound_attributes(self, controller_attribute, child_attributes):
        """
        Delete view's attributes bound to L{controller_attribute}.
        """
        if isinstance(child_attributes, str):
            child_attributes = [child_attributes]

        for child_attribute in child_attributes:
            try:
                delattr(self, child_attribute)
            except AttributeError:
                pass

    def _update_bound_attributes(self, controller_attribute, child_attributes):
        """
        Update (create or delete) view's attribute bound to
        L{controller_attribute}.
        """
        try:
            controller_child_value = getattr(self._controller,
                                             controller_attribute)
        except AttributeError:
            self._delete_bound_attributes(controller_attribute, child_attributes)
            return

        if isinstance(child_attributes, str):
            child_attributes = [child_attributes]

        if isinstance(controller_child_value, Controller):
            for attribute in child_attributes:
                self._update_bound_view(attribute, controller_child_value)
        else:
            for child_attribute in child_attributes:
                setattr(self, child_attribute, controller_child_value)

    def _update_bound_view(self, attribute, controller):
        """
        Update (create or delete) a child view stored as L{attribute}
        which is connected to L{controller}.
        """
        if hasattr(self, attribute):
            child_view = getattr(self, attribute)

            # reconnect the corresponding view
            child_view.controller = controller
        else:
            try:
                # create the corresponding view
                path = self.frontend.get_view_path(controller.model.path)
                child_view = self._create_child_view(attribute, path)

                # connect the corresponding view
                child_view.controller = controller
            except UndefinedMVCAssociation, e:
                self.warning(e)

    def _create_child_view(self, attribute, path):
        """
        Create a View of type L{path} and make it a child of
        self stored as an instance variable named after the content of
        L{attribute}.

        @param attribute:  attribute of self where to store the newly
                           created view
        @type attribute:   string
        @param path:       path to the type of view which is to be
                           created; syntax: plugin:component
        @type path:        string
        """
        # create the new child view
        registry = common.application.plugin_registry
        new_view = registry.create_component(path)

        # associate the new child view
        self._add_child_view(attribute, new_view)

        return new_view

    def _add_child_view(self, attribute, view):
        """
        Make L{view} a child of self stored as an instance variable named
        after the content of L{attribute}.

        @param attribute:  attribute of self where to store the view
        @type attribute:   string
        @param view:       view to be made child
        @type path:        L{elisa.base_components.view.View}
        """
        view.parent = self
        setattr(self, attribute, view)
