#
#    Metrix++, Copyright 2009-2013, Metrix++ Project
#    Link: http://metrixplusplus.sourceforge.net
#    
#    This file is a part of Metrix++ Tool.
#    
#    Metrix++ is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, version 3 of the License.
#    
#    Metrix++ is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#    GNU General Public License for more details.
#    
#    You should have received a copy of the GNU General Public License
#    along with Metrix++.  If not, see <http://www.gnu.org/licenses/>.
#

import mpp.api

import os
import sys
import ConfigParser
import re
import optparse

class MultiOptionParser(optparse.OptionParser):
    
    class MultipleOption(optparse.Option):
        ACTIONS = optparse.Option.ACTIONS + ("multiopt",)
        STORE_ACTIONS = optparse.Option.STORE_ACTIONS + ("multiopt",)
        TYPED_ACTIONS = optparse.Option.TYPED_ACTIONS + ("multiopt",)
        ALWAYS_TYPED_ACTIONS = optparse.Option.ALWAYS_TYPED_ACTIONS + ("multiopt",)
    
        def take_action(self, action, dest, opt, value, values, parser):
            if action == "multiopt":
                values.ensure_value(dest, []).append(value)
            else:
                optparse.Option.take_action(self, action, dest, opt, value, values, parser)

    
    def __init__(self, *args, **kwargs):
        optparse.OptionParser.__init__(self, *args, option_class=self.MultipleOption, **kwargs)

class Loader(object):

    def __init__(self):
        self.plugins = []
        self.hash    = {}
        self.action = None
    
    def get_action(self):
        return self.action
    
    def get_plugin(self, name):
        return self.hash[name]['instance']
    
    def iterate_plugins(self, is_reversed = False):
        if is_reversed == False:
            for item in self.plugins:
                yield item['instance']
        else:
            for item in reversed(self.plugins):
                yield item['instance']
            
    def load(self, command, directories, args):

        class IniContainer(object):
            def __init__(self):
                self.plugins = []
                self.actions = []
                self.hash    = {}
                
        def load_recursively(inicontainer, directory):
            active_plugins = []
            if os.path.exists(directory) == False or os.path.isdir(directory) == False:
                return active_plugins

            pattern = re.compile(r'.*[.]ini$', flags=re.IGNORECASE)
            dirList = os.listdir(directory)
            for fname in dirList:
                fname = os.path.join(directory, fname)
                if os.path.isdir(fname):
                    active_plugins += load_recursively(inicontainer, fname)
                elif re.match(pattern, fname):
                    config = ConfigParser.ConfigParser()
                    config.read(fname)
                    item = {'package': config.get('Plugin', 'package'),
                            'module': config.get('Plugin', 'module'),
                            'class': config.get('Plugin', 'class'),
                            'version': config.get('Plugin', 'version'),
                            'depends': config.get('Plugin', 'depends'),
                            'actions': config.get('Plugin', 'actions'),
                            'enabled': config.getboolean('Plugin', 'enabled'),
                            'instance': None}
                    if item['enabled']:
                        item['actions'] = item['actions'].split(',')
                        for (ind, action) in enumerate(item['actions']):
                            action = action.strip()
                            item['actions'][ind] = action
                            if action not in inicontainer.actions + ['*', 'None', 'none', 'False', 'false']:
                                inicontainer.actions.append(action)
                            if action == '*' or action == command:
                                active_plugins.append(item['package'] + '.' + item['module'])
                        inicontainer.plugins.append(item)
                        inicontainer.hash[item['package'] + '.' + item['module']] = item
            return active_plugins
                        
        def list_dependants_recursively(inicontainer, required_plugin_name):
            assert required_plugin_name in inicontainer.hash.keys(), \
                "depends section requires unknown plugin: " + required_plugin_name
            item = inicontainer.hash[required_plugin_name]
            if item['depends'] in ('None', 'none', 'False', 'false'):
                return []
            result = []
            for child in item['depends'].split(','):
                child = child.strip()
                result += list_dependants_recursively(inicontainer, child)
                result.append(child)
            return result

        # configure python path for loading
        std_ext_dir = os.path.join(os.environ['METRIXPLUSPLUS_INSTALL_DIR'], 'ext')
        std_ext_priority_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        for each in [std_ext_dir] + directories:
            sys.path.append(each)

        inicontainer = IniContainer()
        # load available plugin ini files
        required_plugins = []
        for each in ([std_ext_priority_dir, std_ext_dir] + directories):
            required_plugins += load_recursively(inicontainer, each)
            
        # upgrade the list of required plugins
        required_and_dependant_plugins = []
        for name in required_plugins:
            for each in list_dependants_recursively(inicontainer, name):
                if each not in required_and_dependant_plugins:
                    required_and_dependant_plugins.append(each)
            required_and_dependant_plugins.append(name)
            
        # load
        for plugin_name in required_and_dependant_plugins:
            item = inicontainer.hash[plugin_name]
            plugin = __import__(item['package'], globals(), locals(), [item['module']], -1)
            module_attr = plugin.__getattribute__(item['module'])
            class_attr = module_attr.__getattribute__(item['class'])
            item['instance'] = class_attr.__new__(class_attr)
            item['instance'].__init__()
            item['instance'].set_name(item['package'] + "." + item['module'])
            item['instance'].set_version(item['version'])
            item['instance']._set_plugin_loader(self)
            self.plugins.append(item)
            self.hash[plugin_name] = item

        optparser = MultiOptionParser(
            usage = "Usage: python %prog --help\n" +
                    "       python %prog <action> --help\n" +
                    "       python %prog <action> [options] -- [path 1] ... [path N]\n" +
                    "\n" +
                    "Actions: \n  " + "\n  ".join(inicontainer.actions))
        if command in ['--help', '--h', '-h']:
            optparser.print_help()
            exit(0)
        if command.strip() == "":
            optparser.error("Mandatory action argument required")
        if command not in inicontainer.actions:
            optparser.error("action {action}: Unknown command".format(action=command))

        self.action = command

        optparser = MultiOptionParser(
            usage="Usage: python %prog --help\n"
                  "       python %prog {command} --help\n"
                  "       python %prog {command} [options] -- [path 1] ... [path N]".format(command=command))

        for item in self.iterate_plugins():
            if (isinstance(item, mpp.api.IConfigurable)):
                item.declare_configuration(optparser)

        (options, args) = optparser.parse_args(args)
        for item in self.iterate_plugins():
            if (isinstance(item, mpp.api.IConfigurable)):
                item.configure(options)

        for item in self.iterate_plugins():
            item.initialize()
            
        return args

    def unload(self):
        for item in self.iterate_plugins(is_reversed = True):
            item.terminate()

    def run(self, args):
        exit_code = 0
        for item in self.iterate_plugins():
            if (isinstance(item, mpp.api.IRunable)):
                exit_code += item.run(args)
        return exit_code

    def __repr__(self):
        result = object.__repr__(self) + ' with loaded:'
        for item in self.iterate_plugins():
            result += '\n\t' + item.__repr__()
            if isinstance(item, mpp.api.Parent):
                result += ' with subscribed:'
                for child in item.iterate_children():
                    result += '\n\t\t' + child.__repr__()
        return result