# Elisa - Home multimedia server
# Copyright (C) 2006-2008 Fluendo, S.A. (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 import log, config
from elisa.core.frontend import Frontend
from elisa.core.backend import Backend
from elisa.core import common
from elisa.core.utils import locale_helper

from twisted.internet import defer
import os

class UndefinedMVCAssociation(Exception):
    """
    Raised by L{InterfaceController} when no Model/View or
    Model/Controller association is found in the MVC mappings of the
    frontend or backend managed by the interface controller.
    """
    pass

class InterfaceController(log.Loggable):
    """ InterfaceController is responsible for creating, managing and deleting
    L{elisa.core.frontend.Frontend}s and L{elisa.core.backend.Backend}s
    defined by the user, and to associate them together. It also links the
    chosen L{elisa.base_components.input_provider.InputProvider}s with them.
    """

    # Backends dictionary:
    # key : name defined in config
    # value : backend instance
    _backends = {}

    # Frontends dictionary:
    # key : name defined in config
    # value : frontend instance
    _frontends = {}

    # Activities dictionary:
    # key : Activity path
    # value : Activity instance
    _activities = {}

    def __init__(self):
        log.Loggable.__init__(self)
        self.debug("Creating")

    def _initialize_mvc_mappings(self, backend_paths, frontend_paths):
        self._mvc_mappings = {}
        
        app_config = common.application.config

        paths = backend_paths + frontend_paths
        for path in paths:
            mvc_mappings = app_config.get_option('mvc_mappings',
                                                 section=path,
                                                 default='')
            if mvc_mappings:
                config_file = self._get_mvc_config_file(mvc_mappings)
                self._mvc_mappings[path] = config_file

    def _get_mvc_config_file(self, mvc_mappings):
        app_config_dir = common.application.config.get_config_dir()
        plugin_registry = common.application.plugin_registry
        
        if ':' not in mvc_mappings:
            mvc_mappings_config_file = os.path.join(app_config_dir,
                                                    mvc_mappings)
        else:
            plugin_name, rel_path = mvc_mappings.split(':')
            plugin = plugin_registry.get_plugin_with_name(plugin_name)
            mvc_mappings_config_file = plugin.get_resource_file(rel_path)
            
        return mvc_mappings_config_file

    def _get_mvc_config_from_frontend(self, backend_name):
        mvc_config = None
        config = common.application.config
        frontend_paths = config.get_option('frontends', section='general',
                                           default=[])
        for frontend_path in frontend_paths:
            backend = config.get_option('backend', section=frontend_path)
            if backend and backend_name == backend:
                mvc_config = self._mvc_mappings.get(frontend_path)
                if mvc_config:
                    break
                
        return mvc_config

    def initialize(self):
        """ Initialize various variables internal to the InterfaceController
        such as the backends and the frontends.

        This method is called after the application's configuration is loaded.
        """
        self.info("Initializing")
        application = common.application
     
        self._backends = {}
        self._frontends = {}
        self._activities = {}

        backend_paths = application.config.get_option('backends',
                                                      section='general',
                                                      default=[])
        frontend_paths = application.config.get_option('frontends',
                                                       section='general',
                                                       default=[])

        self._initialize_mvc_mappings(backend_paths, frontend_paths)

        # Create backend instances declared in the configuration
        for backend_path in backend_paths:
            try:
                self._create_backend(backend_path)
            except Exception, e:
                path = application.log_traceback()
                self.warning("An error occured causing backend '%s' creation " \
                             "to fail. A full traceback can be found here: %s"
                             % (backend_path, path))
                if backend_path in self._backends:
                    self._backends.pop(backend_path)

        # Create frontend instances declared in the configuration
        for frontend_path in frontend_paths:
            try:
                self._create_frontend(frontend_path)
            except Exception, exc:
                path = application.log_traceback()
                self.warning("An error occured causing frontend '%s' creation " \
                             "to fail. A full traceback can be found here: %s"
                             % (frontend_path, path))
                if frontend_path in self._frontends:
                    self._frontends.pop(frontend_path)
                    
    def _create_backend(self, backend_name):
        self.debug("Initializing backend %s" % backend_name)
        application = common.application
        plugin_registry = application.plugin_registry
        
        master_backend = application.config.get_option('master', backend_name,
                                                       default='')

        mvc_config = None
        mvc_mappings_config_file = self._mvc_mappings.get(backend_name)
        if not mvc_mappings_config_file:
            mvc_mappings_config_file = self._get_mvc_config_from_frontend(backend_name)
        mvc_config = config.Config(mvc_mappings_config_file)
            
        backend = Backend(mvc_config)
        
        self._backends[backend_name] = backend

        # Create Activity and retrieve the root Model from it
        if master_backend == '':
            activity_path = application.config.get_option('activity',
                                                          backend_name,
                                                          default=[])
            try:
                activity = plugin_registry.create_component(activity_path)
            except Exception, exc:
                msg = "Cannot create activity %r for backend %r" % (activity_path,
                                                                    backend_name)
                self.warning(msg)
                raise 

            root_model = activity.get_model()
            activity_key = "%s/%s" % (backend_name, activity.path)
            self._activities[activity_key] = activity
        else:
            root_model = self._backends[master_backend].root_controller.model
            backend.mvc_config = self._backends[master_backend].mvc_config
            
        # Create root controller
        controller_path = application.config.get_option('controller',
                                                        backend_name)
        controller_path = backend.get_controller_path(root_model.path)

        try:
            root_controller = plugin_registry.create_component(controller_path)
        except:
            self.warning("Cannot create controller %r for backend %r",
                         controller_path, backend_name)
            raise
            
        root_controller.backend = backend

        backend.root_controller = root_controller

        # Create and connect InputProviders found in the config to root
        # Controller
        # FIXME: no checking is done for duplicated InputProviders
        providers_paths = self._create_input_providers(backend_name, backend)
        for provider in providers_paths:
            application.input_manager.subscribe(provider,
                                                backend.dispatch_input)

        root_controller.model = root_model
        root_controller.focus()
        
    def _create_frontend(self, frontend_name):
        self.debug("Initializing frontend %s" % frontend_name)
        application = common.application
        plugin_registry = application.plugin_registry


        # Retrieving the backend the new frontend will connect to
        # FIXME: this has to be changed to support remote backends
        backend_name = application.config.get_option('backend', frontend_name,
                                                     default=[])
        if backend_name in self._backends:
            backend = self._backends[backend_name]
            root_controller = backend.root_controller
        else:
            msg = "Backend %r not existing for frontend %r" % (backend_name,
                                                               frontend_name)
            self.warning(msg)
            raise

        if root_controller == None:
            msg = "Invalid backend %r for frontend %r" % (backend_name,
                                                          frontend_name)
            self.warning(msg)
            raise
            
        mvc_mappings = self._mvc_mappings.get(frontend_name)
        if mvc_mappings:
            mvc_config = config.Config(mvc_mappings)
        else:
            mvc_config = backend.mvc_config
            
        # create the frontend
        frontend = Frontend(mvc_config)
        frontend.name = frontend_name

        model_path = root_controller.model.path
        view_path = frontend.get_view_path(model_path)
        root_view = plugin_registry.create_component(view_path)
        root_view.parent = None

        # Create theme
        theme_path = application.config.get_option('theme', frontend_name,
                                                   default=[])
        try:
            theme = plugin_registry.create_component(theme_path)
        except:
            msg = "Cannot create theme %r for frontend %r" % (theme_path,
                                                              frontend_name)
            self.warning(msg)
            raise

        # Create a Context needed by the root View to render
        try:
            context = plugin_registry.create_component(root_view.context_path)
        except:
            msg = "Cannot create context %r for frontend %r" % (root_view.context_path,
                                                                frontend_name)
            self.warning(msg)
            raise
    
        # do the translation things
        languages = application.config.get_option('languages', frontend_name,
                                                  None)
        if not languages:
            languages = []
            locale = locale_helper.get_from_system_locale()
            if locale:
                languages.append(locale)

        # update the frontend
        frontend.view = root_view
        frontend.context = context
        frontend.theme = theme
        frontend.languages = languages
        frontend.backend = backend
        self._frontends[frontend_name] = frontend

        # Create and connect InputProviders found in the config to root Controller
        # FIXME: no checking is done for duplicated InputProviders
        providers_paths = self._create_input_providers(frontend_name,
                                                       frontend,
                                                       context.viewport_handle)
        for provider in providers_paths:
            application.input_manager.subscribe(provider,
                                                backend.dispatch_input)

        # connect the root controller to the root view
        root_view.frontend = frontend
        root_view.controller = root_controller

        return True


    def _create_input_providers(self, path, origin=None, viewport=None):
        application = common.application
        plugin_registry = application.plugin_registry
        providers_paths = application.config.get_option("input_providers",
                                                         section=path,
                                                         default=[])

        count = 0
        for provider_path in providers_paths:
            try:
                provider = plugin_registry.create_component(provider_path)
                provider.origin = origin
                provider.viewport = viewport
                application.input_manager.register_component(provider)
                count += 1
            except Exception, error:
                self.warning(error)

        self.debug("Loaded and connected %s input_providers" % count)
        return providers_paths


    def start(self):
        """
        Start refreshing the frontends.
        """
        self.info("Starting")

    def stop(self):
        """
        Stop the frontends and terminate the backends.

        @rtype: L{twisted.internet.defer.Deferred}
        """
        dfrs = []
        self.info("Stopping")

        for frontend in self._frontends.values():
            dfr = defer.maybeDeferred(frontend.clean)
            dfrs.append(dfr)

        # FIXME: unsubscribe InputProviders ?

        def all_done(result):
            application = common.application

            for activity_path, activity in self._activities.iteritems():
                activity.clean()

            return result

        dfr = defer.DeferredList(dfrs)
        dfr.addCallback(all_done)
        return dfr
