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


from elisa.core.component import Component, ComponentError
from elisa.base_components.model import Model
from elisa.core.observers.observer import Observer
from elisa.core.observers.observable import Observable
from elisa.core import common
from elisa.core.input_event import *
from elisa.core.interface_controller import UndefinedMVCAssociation


class ModelNotSupported(Exception):
    pass


class Controller(Component, Observer, Observable):
    """
    Connects to a compatible L{elisa.base_components.model.Model} and
    holds extra information useful for a L{elisa.base_components.view.View} to
    render the model.
    It is notified of all the changes occuring to L{model}. Part of these
    changes can be treated automatically using the bindings system.
    A Controller can be part of a hierarchy of Controllers thus potentially
    having a reference to its L{parent} and to its children defined in
    L{bindings}.

    DOCME: focus, focused, branch of focused controllers, backend.

    @cvar supported_models:     list of models that are compatible with the
                                controller identified by their path: 'plugin:model'
    @type supported_models:     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 model:                model to which the controller is connected;
                                setting it to an incompatible model will fail
    @type model:                L{elisa.base_components.model.Model}
    @ivar parent:               parent controller; None if the controller is a root
    @type parent:               L{elisa.base_components.controller.Controller}
    @ivar focused:              DOCME
    @type focused:              boolean
    @ivar backend:              backend to which the controller belongs
    @type backend:              L{elisa.core.backend.Backend}
    """

    supported_models = ('base:model',)
    bindings = ()

    def __init__(self):
        Component.__init__(self)
        Observer.__init__(self)
        Observable.__init__(self)

        self._model = None
        self._backend = None
        self._focused = False
        self._root = None
        self._parent = None
        self._supported_model_classes = ()

    def initialize(self):
        plugin_registry = common.application.plugin_registry
        classes = [ plugin_registry.get_component_class(model_path) for
                    model_path in self.supported_models ]
        self._supported_model_classes = tuple(classes)
        
    def focus(self):
        """
        Grab the focus. The controller previously owning the focus loses it.
        """
        self.backend.focused_controller = self

    def focused__get(self):
        return self._focused

    def focused__set(self, focused):
        # FIXME: how/why is it different from focus? This is confusing.
        # Apparently, it is wrong to do controller.focused = True/False since
        # it does not have the same effect as doing controller.focus

        old_focused = self._focused
        self._focused = focused
        self.focused_changed(old_focused, focused)

    def focused_changed(self, old_focused, new_focused):
        """
        Called when L{focused} 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_focused: value of focused before
        @type old_focused:  bool
        @param new_focused: value of focused now
        @type new_focused:  bool
        """
        pass

    def backend__get(self):
        return self._backend

    def backend__set(self, new_backend):
        old_backend, self._backend = self._backend, new_backend
        if old_backend != new_backend:
            self.backend_changed(old_backend, new_backend)

    def backend_changed(self, old_backend, new_backend):
        """
        Called when L{backend} 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_backend: value of backend before
        @type old_backend:  L{elisa.core.backend.Backend}
        @param new_backend: value of backend now
        @type new_backend:  L{elisa.core.backend.Backend}
        """
        pass

    def parent__set(self, parent):
        self._parent = parent
        if parent is not None:
            self.backend = parent.backend
        
    def parent__get(self):
        return self._parent

    def model__get(self):
        return self._model

    def model__set(self, model):
        if self._model == model:
            return

        if model != None and \
           not isinstance(model, self._supported_model_classes):
            msg = "%r does not support %r, but supports %r" % \
                  (self, model.path, self.supported_models)
            self.warning(msg)
            raise ModelNotSupported(msg)

        old_model = self._model
        self._model = model

        # update bindings
        for model_attribute, controller_attributes in self.bindings:
            self._update_bound_attributes(model_attribute,
                                          controller_attributes)

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

        self.model_changed(old_model, model)

    def model_changed(self, old_model, new_model):
        """
        Called when L{model} 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_model: value of model before
        @type old_model:  L{elisa.base_components.model.Model}
        @param new_model: value of model now
        @type new_model:  L{elisa.base_components.model.Model}
        """
        pass

    def handle_input(self, input_event):
        """
        Process an input event.
        It can decide that no further processing should be done by returning
        True or let its parent controller process it by returning False.

        @param input_event:  the input event to translate
        @type input_event:   L{elisa.core.input_event.InputEvent}
        @rtype: bool
        """
        if input_event.action == EventAction.EXIT:
            common.application.stop()
            return True

        return False

    def attribute_set(self, origin, key, old_value, new_value):
        """
        Called when an attribute of the model 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 model_attribute, controller_attributes in self.bindings:
            if model_attribute == key:
                self._update_bound_attributes(model_attribute,
                                              controller_attributes)

    def _delete_bound_attributes(self, model_attribute, child_attributes):
        """
        Delete controller's attributes bound to L{model_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, model_attribute, child_attributes):
        """
        Update (create or delete) controller's attribute bound to
        L{model_attribute}.
        """
        try:
            model_child_value = getattr(self._model, model_attribute)
        except AttributeError:
            self._delete_bound_attributes(model_attribute, child_attributes)
            return

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

        if isinstance(model_child_value, Model):
            for attribute in child_attributes:
                self._update_bound_controller(attribute, model_child_value)
        else:
            for attribute in child_attributes:
                setattr(self, attribute, model_child_value)

    def _update_bound_controller(self, attribute, model):
        """
        Update (create or delete) a child controller stored as L{attribute}
        which is connected to L{model}.
        """
        if hasattr(self, attribute):
            # reconnect the corresponding controller
            child_controller = getattr(self, attribute)
            child_controller.model = model
        else:
            try:
                # create the corresponding controller
                path = self.backend.get_controller_path(model.path)
                ctrl = self._create_child_controller(attribute, path)
                child_controller = ctrl

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

    def _create_child_controller(self, attribute, path):
        """
        Create a Controller 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 controller
        @type attribute:  string
        @param path:      path to the type of controller which is to be
                          created; syntax: plugin:component
        @type path:       string
        """
        # create the new child controller
        registry = common.application.plugin_registry
        new_controller = registry.create_component(path)

        # associate the new child controller
        self._add_child_controller(attribute, new_controller)

        return new_controller

    def _add_child_controller(self, attribute, controller):
        """
        Make L{controller} 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 controller
        @type attribute:   string
        @param controller: controller to be made child
        @type path:        L{elisa.base_components.controller.Controller}
        """
        controller.parent = self
        setattr(self, attribute, controller)
