# Copyright 2014 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2

from __future__ import print_function

import os

from layman.output import Message

class InvalidModuleName(Exception):
    '''An invalid or unknown module name.'''

class Module(object):
    '''
    Class to define and hold our plugin-module

    @type name: string
    @param name: The module name
    @type namepath: string
    @param namepath: The path to the new module
    @param output: The output for any information
    '''
    def __init__(self, name, namepath, output):
        self.name = name
        self._namepath = namepath
        self.kids_names = []
        self.kids = {}
        self.output = output
        self.initialized = self._initialize()


    def _initialize(self):
        '''
        Initializes the plug-in module

        @rtype bool: reflects success or failure to initialize the module
        '''
        self.valid = False
        try:
            mod_name = '.'.join([self._namepath, self.name])
            self._module = __import__(mod_name, [], [], ['not empty'])
            self.valid = True
        except ImportError as e:
            self.output.error('Module._initialize(); failed to import %(mod) '\
                'error was: %(err)' % ({'err': e, 'mod': mod_name}))
            return False
        self.module_spec = self._module.module_spec
        for submodule in self.module_spec['provides']:
            kid = self.module_spec['provides'][submodule]
            kidname = kid['name']
            try:
                kid['module_name'] = '.'.join([mod_name, kid['sourcefile']])
            except KeyError:
                kid['module_name'] = '.'.join([mod_name, kidname])
                f = self._module.__file__
                msg = 'Module.__initialize(); %(module)s module spec is old, missing '\
                      'attribute: \'sourcefile\'.\nBackward compatibility '\
                      'may be removed in the future.\nFile: %(f)s' % {
                      'module': self.name, 'f': f}
                self.output.warn(msg)
            kid['is_imported'] = False
            self.kids[kidname] = kid
            self.kids_names.append(kidname)
        return True


    def get_class(self, name):
        '''
        Retrieves a module class desired

        @type name: string
        @param name: the plug-in module's name
        @rtype mod_class: instance of plug-in module's class
        '''
        if not name or name not in self.kids_names:
            raise InvalidModuleName('Module name "%(name)" was invalid or not'\
                    'part of the module "%(mod_name)"' ({'mod_name':self.name,
                                                         'name': name}))
        kid = self.kids[name]
        if kid['is_imported']:
            module = kid['instance']
        else:
            try:
                module = __import__(kid['module_name'], [], [], ["not empty"])
                kid['instance'] = module
                kid['is_imported'] = True
            except ImportError:
                raise
        mod_class = getattr(module, kid['class'])
        return mod_class


class Modules(object):
    '''
    Dynamic module system for loading and retrieving any of the
    installed layman modules and/or provided class'

    @param path: Optional path to the "modules" directory or defaults to
                 the directory of this file + "/modules"
    @param namepath: Optional python import path to the "modules" directory or
                     defaults to the directory name of this file + ".modules"
    @param output: Optional output, defaults to layman.output.Message object
    '''
    def __init__(self, path=None, namepath=None, output=None):
        if path:
            self._module_path = path
        else:
            self._module_path = os.path.join(
                (os.path.dirname(os.path.realpath(__file__))), 'modules')
        if namepath:
            self._namepath = namepath
        else:
            self._namepath = '.'.join(os.path.dirname(
                os.path.realpath(__file__)), 'modules')
        if output:
            self.output = output
        else:
            self.output = Message()
        self._modules = self._get_all_modules()
        self.module_names = sorted(self._modules)


    def _get_all_modules(self):
        '''
        Scans the overlay modules dir for loadable modules

        @rtype dict of module_plugins
        '''
        module_dir = self._module_path
        importables = []
        names = os.listdir(module_dir)
        for entry in names:
            if entry.startswith('__'):
                continue
            try:
                os.lstat(os.path.join(module_dir, entry, '__init__.py'))
                importables.append(entry)
            except EnvironmentError:
                pass

        kids = {}
        for entry in importables:
            new_module = Module(entry, self._namepath, self.output)
            for module_name in new_module.kids:
                kid = new_module.kids[module_name]
                kid['parent'] = new_module
                kids[kid['name']] = kid
        return kids


    def get_module_names(self):
        '''
        Retrieves all available module names

        @rtype: list of installed module names available
        '''
        return self.module_names


    def get_class(self, modname):
        '''
        Retrieves a module class desired

        @type modname: string
        @param modname: the module class name
        '''
        if modname and modname in self.module_names:
            mod = self._modules[modname]['parent'].get_class(modname)
        else:
            raise InvalidModuleName('Module name "%(name)s" was invalid or'\
                ' not found.' % ({'name': modname}))
        return mod


    def get_description(self, modname):
        '''
        Retrieves the module class decription

        @type modname: string
        @param modname: the module class name
        @rtype: string of modules class decription
        '''
        if modname and modname in self.module_names:
            mod = self._modules[modname]['description']
        else:
            raise InvalidModuleName('Module name "%(name)s" was invalid or'\
                ' not found.' % ({'name': modname}))
        return mod


    def get_functions(self, modname):
        '''
        Retrieves the module class exported function names

        @type modname: string
        @param modname: the module class name
        @rtype: list of the modules class exported function names
        '''
        if modname and modname in self.module_names:
            mod = self._modules[modname]['functions']
        else:
            raise InvalidModuleName('Module name "%(name)s" was invalid or'\
                ' not found.' % ({'name': modname}))
        return mod


    def get_func_descriptions(self, modname):
        '''
        Retrieves the module class  exported functions descriptions

        @type modname: string
        @param modname: the module class name
        @rtype: dict of the modules class exported functions descriptions
        '''
        if modname and modname in self.module_names:
            desc = self._modules[modname]['func_desc']
        else:
            raise InvalidModuleName('Module name "%(name)s" was invalid or'\
                ' not found.' % ({'name': modname}))
        return desc
