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

"""
Plugin base class, the mother of all Elisa plugins
"""

__maintainer__ = 'Philippe Normand <philippe@fluendo.com>'

from elisa.core.utils import classinit, misc, locale_helper
from elisa.core import log
from elisa.core.config import Config
from elisa.core import component

import os
import inspect
import pkg_resources

class Plugin(log.Loggable):
    """
    Plugin Base Class

    This class acts as a L{elisa.core.component.Component}
    factory. All the Plugin's class variables can be stored in a
    plugin.conf file which has the following syntax::

        [general]
        name="base"
        version="0.1"
        plugin_dependencies=["foo"]
        external_dependencies=["pgm"]
        description="base elisa components"

        # that's a component!
        [media_providers.local_media:LocalMedia]
        description="To access file:// media"
        platforms=["linux", "posix"]
        external_dependencies=["gnomevfs",]
        component_dependencies = ["base:coherence_service"]

    Each option of the "general" section will be mapped to it's corresponding
    class variable in the Plugin.

    @cvar config_file:           Plugin's directory relative path to plugin.conf
    @type config_file:           string
    @cvar config:                Config generated by reading L{config_file}
    @type config:                L{elisa.core.config.Config}
    @cvar components:            Component implementations provided by the
                                 Plugin
    @type components:            dict mapping L{elisa.core.component.Component}
                                 names to L{elisa.core.component.Component}
                                 instances
    @cvar name:                  plugin's name. Should be unique
    @type name:                  string
    @cvar external_dependencies: external Python dependencies specified using
                                 dot-syntax
    @type external_dependencies: list
    @cvar plugin_dependencies:   Elisa plugins dependencies specified using
                                 colon-syntax
    @type plugin_dependencies:   list
    @cvar version:               Plugin's version as a dot-syntax string
    @type version:               string
    @cvar description:           one line Plugin's description
    @type description:           string
    @cvar directory:             absolute path to the directory storing the
                                 Plugin's source code file (or Egg)
    @type directory:             string
    @cvar distribution_file:     Absolute path to the Egg distribution file,
                                 if the plugin is packaged inside an egg file
    @type distribution_file:     string
    """

    __metaclass__ = classinit.ClassInitMeta
    __classinit__ = classinit.build_properties

    config = None
    config_file = None
    
    directory = ""
    distribution_file = ""
    components = {}
    name = ""
    plugin_dependencies = []
    external_dependencies = []
    version = ""
    description = ""

    checked = False
    deps_error = None
    
    def __init__(self):
        # configure the log category based on my name
        self.log_category = self.name

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

    @classmethod
    def initialize(cls):
        """
        Check the components class attribute is a dictionary and that
        none of the components of the plugin have the same name as the
        plugin itself.
        
        @raise InitializeFailure: if the Plugin is not well structured
        """
        log.debug(cls.name, "Initializing")

        if not isinstance(cls.components, dict):
            reason = "%s.components must be a dictionnary" % cls.__name__
            raise component.InitializeFailure(cls.name, reason)

        if cls.name in cls.components.keys():
            reason = "Component %s has the same name as its plugin. "\
                     "Please rename it." % cls.name
            raise component.InitializeFailure(cls.name, reason)

    @classmethod
    def check_dependencies(cls):
        """
        Check plugin's python dependencies

        This check is performed at most once.

        @raises: L{elisa.core.component.UnMetDependency}
        """
        if cls.deps_error:
            raise cls.deps_error
        
        if not cls.checked:
            cls.checked = True
            try:
                component.check_python_dependencies(cls.name,
                                                    cls.external_dependencies)
            except component.ComponentError, error:
                cls.deps_error = error
                raise

    @classmethod
    def check_component_dependencies(cls, component_name):
        """
        Check the supported platforms and external dependencies of the
        Component identified by its name.

        This check is performed at most once.
        
        @raises: L{elisa.core.component.UnMetDependency}
        """
        informations = cls.components.get(component_name)

        if not informations.get('checked', False):
            platforms = informations.get('platforms',[])
            deps = informations.get('external_dependencies',[])
            component_path = "%s:%s" % (cls.name, component_name)
            
            try:
                component.check_platforms(component_path, platforms)
                component.check_python_dependencies(component_path, deps)
            finally:
                cls.components[component_name]['checked'] = True
            
    @classmethod
    def load_config(cls):
        """ Load the L{config_file} if defined

        Fill L{config} class variable if L{config_file} points to a
        valid config filename. Also fill L{name}, L{version},
        L{description}, L{plugin_dependencies},
        L{external_dependencies} and L{components}.
        """
        components = {}
                
        if cls.config:
            log.debug(cls.name, "Config already loaded")
        elif cls.config_file:
            config_file = cls.config_file
            if not os.path.exists(config_file):
                plugin_dir = os.path.dirname(inspect.getsourcefile(cls))
                config_file = os.path.join(plugin_dir, cls.config_file)
                
            log.debug(cls.name, "Loading config from file %r", config_file)
            cls.config = Config(config_file)
            general_section = cls.config.get_section('general',{})

            name = general_section.get('name','')
            if name:
                cls.name = name

            version = general_section.get('version','')
            if version:
                cls.version = version

            description = general_section.get('description','')
            if description:
                cls.description = description

            deps = general_section.get('plugin_dependencies',[])
            if deps:
                cls.plugin_dependencies = deps

            ext_deps = general_section.get('external_dependencies',[])
            cls.external_dependencies = ext_deps

            sections = cls.config.as_dict()
            if 'general' in sections:
                del sections['general']

            # scan components
            for component_path, section in sections.iteritems():
                if 'name' not in section:
                    path = component_path.split(':')
                    if len(path) < 2:
                        continue
                    path = path[1]
                    un_camel = misc.un_camelify(path)
                    section['name'] = un_camel
                component_name = section['name']
                section['path'] = component_path

                components.update({component_name:section})

            if components:
                cls.components = components
        else:
            cls.config = Config()

    @classmethod
    def load_translations(cls, translator):
        """ Load the translation files supported by the Plugin into a
        translator, usually the Application's translator. This method
        uses the config class attribute, so be sure to have called
        L{load_config} before calling this method. Translation files
        are loaded from the i18n plugin.conf section (inside general
        section).

        @param translator: The translator to load i18n files into
        @type translator:  L{elisa.extern.translation.Translator}
        """
        i18n = cls.config.get_option('i18n', section='general', default={})

        for key, value in i18n.iteritems():
            trans_path = os.path.join(cls.directory, value)
            translator.addLocaleDir(key, trans_path)
            log.debug(cls.name, "Adding %s to domain %s", key, trans_path)

    @classmethod
    def get_resource_file(cls, path):
        """ Retrieve a data file stored and managed by the plugin.

        If the plugin is stored in a flat directory we build an
        absolute filesystem path of the data file relative to the
        plugin's directory. If the plugin is an egg (most of the time
        zipped), we use pkg_resources which extract the file to a
        temporary directory and use that temporary file.

        @param path: relative path (to the plugin namespace) of the data file
        @type path:  string
        @returns:    absolute path to the data file
        @rtype:      string
        """
        resource = os.path.join(cls.directory, path)
        if not os.path.exists(resource):
            resource = pkg_resources.resource_filename(cls.__module__, path)
        charset = locale_helper.system_encoding()
        resource = resource.decode(charset)
        return resource

    def install(self):
        """ Run code to allow proper installation of the plugin.

        When a plugin is installed for the first time, it will need to change
        Elisa configuration to properly load and configure the components it
        provides.

        It should be fine to run this code multiple times without messing up
        things.

        @returns: whether all went ok
        @rtype: boolean
        """
        return True

    def uninstall(self):
        """ Run code to allow proper uninstallation of the plugin.

        When a plugin is uninstalled, it may want to change Elisa configuration
        or to do other book-keeping staff.

        @returns: whether all went ok
        @rtype: boolean
        """
        return True

