# -*- coding: utf-8 -*-
#
##########################################################################
# pyeole.command_line - automatic command line creator
# Copyright © 2012-2013 Pôle de compétences EOLE <eole@ac-dijon.fr>
#
# License CeCILL:
#  * in french: http://www.cecill.info/licences/Licence_CeCILL_V2-fr.html
#  * in english http://www.cecill.info/licences/Licence_CeCILL_V2-en.html
##########################################################################

"""Dynamic command line utility that parses the arity of a given library
function, and launches the function with params that typically comes from the
command line

Example:

CommandLine content:

::

    from importlib import import_module
    from sys import argv
    from pyeole.command_line import ArgumentParser
    arguments = ArgumentParser(import_module('pyeole.module'), ('func1', 'func2'), argv[0])
    arguments.parse_args(argv[1:])
    arguments.trigger_callback()


Launch command:

::
    # CommandLine func1 --help
"""

from sys import exit
from inspect import getmembers, isfunction, getfullargspec
import argparse

class ArgumentParser(object):
    """argument parser **and** automatic callback launcher

    - dynamically parses the callback's arity
    - builds an argument parser with the correspondent params
    - launches the calback with his correspondent params
    """

    def __init__(self, module, allowed_functions, command_name):
        # et ces keywords apportent des indices sur les types des variables
        # (donc pas None)
        self.allowed_functions = allowed_functions
        self.module = module
        self.command_name = command_name
        # building function specifications
        self.specs = self._inspect_functions()
        self.function_name = None
        self.function_args = None

    def _inspect_functions(self):
        """Dynamically retrieves the signature of self.module in a
        self.allowed_functions

        :returns: function_name: param specifications
        """
        all_functions = getmembers(self.module, isfunction)
        funcs = dict()
        for func, value in all_functions:
            if func in self.allowed_functions:
                funcs[func] = []
                specs = getfullargspec(value)
                len_args = len(specs.args)
                defaults = list(specs.defaults)
                len_defaults = len(defaults)
                if len_args != len_defaults:
                    for i in range(len_args - len_defaults):
                        defaults.insert(0, '')
                for indice in range(len(specs.args)):
                    funcs[func].append( (specs.args[indice],
                                     type(defaults[indice]),
                                     defaults[indice]))
        return funcs

    def print_help(self):
        """build default help command"""
        print ("usage: {} [{}] [options|--help]".format(self.command_name,
                '|'.join(self.allowed_functions)))
        exit(1)

    def parse_args(self, args):
        """Parse given argument

        param args: typically sys.argv[1:]
        """
        if args == [] or args[0] not in self.allowed_functions:
            self.print_help()
        self._parse_callback_function_name(args[0])
        self._parse_callback_function_args(args[1:])

    def _parse_callback_function_name(self, function_name):
        """parses the first cmdline argument, wich is the function name

        :param function_name: function name to add in argparse
        :type function_name: `str`
        """
        # yes, have to create a parser only for this...
        parser = argparse.ArgumentParser()
        parser.add_argument("__callback_function_name__",
                            help="function name to be called",
                            type=str)
        args = parser.parse_args([function_name])
        if function_name not in self.allowed_functions:
            raise Exception("unknow callback function named: {}".format(
                    self.function_name))
        self.function_name = args.__callback_function_name__

    def _parse_callback_function_args(self, args):
        """must have called _parse_callback_function_name before"""
        argparser = argparse.ArgumentParser(usage='%(prog)s {} [options]'
                            ''.format(self.function_name))
        if '__help__' in dir(self.module):
            mhelp = self.module.__help__
        else:
            mhelp = {}
        for name, argtype, default in self.specs[self.function_name]:
            doc = mhelp.get(self.function_name, {}).get(name, '')
            arg = Argument(name, argtype=argtype, default=default, doc=doc)
            arg.add_argparse_option(argparser)
        arguments = argparser.parse_args(args)
        self.function_args = vars(arguments)

    def trigger_callback(self):
        """launches the callback with the appropriate arguments"""
        callback = getattr(self.module, self.function_name)
        # now let's triggers the callback
        try:
            ret = callback(**self.function_args)
            if isinstance(ret, bool):
                if ret is False:
                    exit(1)
        except Exception as e:
            print (e)
            exit(1)

class Argument(object):
    "represents an argument that is to be passed to argparse"
    def __init__(self, name, argtype=None, default=None, doc=""):
        self.name = name
        self.argtype = argtype
        self.default = default
        self.doc = doc

    def add_argparse_option(self, parser):
        if self.argtype == str:
            parser.add_argument("--"+self.name, help=self.doc,
                    type=str, default=self.default)
        elif self.argtype == bool:
            if self.default == True:
                action = "store_false"
                name = 'no-' + self.name
            else:
                action = "store_true"
                name = self.name
            parser.add_argument("--"+name, help=self.doc, dest=self.name,
                    action=action, default=self.default)
        else:
            raise Exception("unknom argument type : {}".format(str(
                                                        self.argtype)))

