#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import collections
from poppy.core.logger import logger

from poppy.core.generic.signals import Signal
from poppy.core.tools.poppy_argparse import argparse
from poppy.core.generic.metaclasses import ManagerMeta
from poppy.core.pipeline import Pipeline
__all__ = ["Command"]


def after_creation(cls, name):
    """
    Some extra things to do after the creation of command classes (and not
    instances, this is not an error).
    """
    # check that the class has the good attributes
    if not hasattr(cls, "__command__"):
        logger.error(
            "The class {0} has no attribute __command__".format(name)
        )
        return
    if not hasattr(cls, "__parent__"):
        logger.error(
            "The class {0} has no attribute __parent__".format(name)
        )
        return


class CommandManager(object):
    """
    A class to manage the available defined commands by the user through the
    plugin command classes.
    """

    def __init__(self):
        """
        Create containers where to store defined commands.
        """
        # containers
        self.commands = {}
        self.instancesByCommand = {}

        # tree of dependencies for commands
        self.tree = collections.defaultdict(list)

        # define the arguments specific to the pipeline
        self.pipeline_arguments = {
            'settings': (
                ['--settings'],
                {'help': 'The path to the settings file.'},
            ),

            'log_level': (
                ['-ll', '--log-level'],
                {
                    'help': 'Specifies the amount information that the pipeline should print to the console ',
                    'choices': ('DEBUG', 'INFO', 'WARNING', 'ERROR')
                }
            )
        }

        # signals
        self.added = Signal()
        self.created = Signal()
        self.generation_start = Signal()
        self.generation_stop = Signal()
        self.run_start = Signal()
        self.run_end = Signal()

    def add(self, name, cls):
        """
        Adds the command and its class to the manager.
        """
        # register
        logger.debug("Register the command {0}".format(name))
        self.commands[cls.__command__] = cls

        # add its dependence in the tree (none parent is the root tree)
        self.tree[cls.__parent__].append(cls.__command__)

        # send a signal of a new definition
        self.added(name, cls)

    def create(self, instance):
        """
        Register the instances of the commands by their name.
        """
        # register
        logger.debug(
            "Register command instance {0}".format(instance.__command__)
        )
        self.instancesByCommand[instance.__command__] = instance

        # send the signal of a new instantiation
        self.created(instance)

    def generate(self, options):
        """
        Given a first base subparser and the parents parser, generate the
        parsers for children commands recursively.
        """
        # send signal that the generation is starting
        self.generation_start(options)

        # generate commands
        logger.debug("Generating commands hierarchy")
        self._generate()

        # send signal that the generation stopped
        self.generation_stop(options)

    def _generate(self, subparser=None, start=None, parsers=None):
        # keep trace of the parsers generated by commands
        if parsers is None:
            self.parsers = {}

        # loop over children
        for command in self.tree[start]:
            # create an instance of the command
            instance = self.commands[command]()

            # generate the list of parent parsers used by the command if
            # provided by the user
            if hasattr(instance, "__parent_arguments__"):
                parents = [
                    self.parsers[name]
                    for name in instance.__parent_arguments__
                ]
            else:
                parents = []

            # get the parser for the given command
            parser = instance.parser(subparser, parents)

            # add arguments to the parser from the user
            instance.add_arguments(parser)

            # by default run the instance when the command is called
            parser.set_defaults(run=instance)

            # keep trace of the parser
            self.parsers[instance.__command__] = parser

            # if the command has children, create a subparser and recall the
            # method recursively
            if instance.has_children():
                # generate the subcommands for this command
                self._generate(
                    instance.subparser(parser),
                    command,
                    self.parsers,
                )

    def launch(self, argv=None):
        """
        Launch the commands by parsing the input of the program and then
        calling the good command with the good arguments.
        """

        # get arguments
        args, unknown = self.parse(argv)

        if unknown != []:
            logger.warning(f'There may be wrong/unused arguments : {unknown}')

        # a signal/hook to do something before running the command with the
        # parsed arguments
        self.run_start(args)

        # call the good command
        args.run(args)

        # a signal/hook to do something after running the command with the
        # parsed arguments
        self.run_end(args)

    def parse(self, argv=None):
        """
        Responsible to parse the command line, and also check the consistency
        of the tree of commands for the base command.
        """
        # first check that the number of commands linked to the root that does
        # nothing is not greater than one. If it is not the case, several
        # parsers are defined and thus we are unable to define which one to
        # use.
        if len(self.tree[None]) > 1:
            logger.error(
                "Multiple parsers are defined for the commands. Cannot " +
                "decide which one to use."
            )
            raise SystemExit(-1)
        elif len(self.tree[None]) <= 0:
            logger.error("No commands defined for the root command.")
            raise SystemExit(-1)

        # else parse the root parser
        return self.parsers[self.tree[None][0]].parse_known_args(args=argv)

    def preprocess_args(self, argv):
        """
        Preprocess options like --settings and --config.

        These options could affect the pipeline behavior, so they must be processed early.

        :param argv:
        :return: options, args
        """
        logger.debug('Preprocessing of settings, log-level, etc. arguments')

        parser = argparse.ArgumentParser(description='Preprocess some arguments.', add_help=False, allow_abbrev=False)

        for argument in self.pipeline_arguments.values():
            parser.add_argument(*argument[0], **argument[1])

        return parser.parse_known_args(argv)

    def __getitem__(self, name):
        """
        Get instance of command by its name.
        """
        return self.instancesByCommand[name]

    def __contains__(self, name):
        """
        Check if the instance of a command is present in the manager.
        """
        return name in self.instancesByCommand


class CommandMetaclass(ManagerMeta):
    """
    To manage the definition and instantiation of commands.
    """

    def __call__(cls, *args, **kwargs):
        """
        Make the command instances as singletons.
        """
        # check that the instance is already created or not
        if cls.__command__ in cls.manager:
            return cls.manager[cls.__command__]

        # create the instance as usual
        return super(CommandMetaclass, cls).__call__(*args, **kwargs)


class Command(
    object,
    metaclass=CommandMetaclass,
    manager=CommandManager,
    after_creation=after_creation,
):
    """
    Base class for all accepted commands for the pop command line program.
    """

    def parser(self, subparser, parents):
        """
        Return the parser for the command and options that this command must
        use. Take as argument the subparser from the parent parser.
        """
        return subparser.add_parser(
            self.__command_name__,
            help=self.__help__,
            parents=parents,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            conflict_handler="resolve",
        )

    def add_arguments(self, parser):
        """
        Used by the user to add arguments associated to the given parser.
        """
        pass

    def subparser(self, parser):
        """
        Should return a subparser for this command. Not always called, just
        when the command has possibly subcommands to generate.
        """
        if hasattr(self, "__command_name__"):
            command = self.__command_name__
        else:
            command = self.__command__

        return parser.add_subparsers(
            help="Sub-commands available for command {0}".format(command),
        )

    def has_children(self):
        """
        To know if the command has subcommands.
        """
        # check that the command has subcommands
        return len(self.manager.tree[self.__command__]) > 0

    def print_help(self):
        """
        Print the command help message
        """

        Command.manager.parsers[self.__command__].print_help()

    def __call__(self, args):
        """
        The command is called with its arguments as parameters, in order to
        have the possibility to run the succession of commands necessary.
        """

        # create a pipeline instance
        pipeline = Pipeline(args)

        try:
            # setup the tasks
            self.setup_tasks(pipeline)

            # run the pipeline
            pipeline.run()
        except NotImplementedError:
            # if no tasks are defined, print the help message
            self.print_help()

    def setup_tasks(self, pipeline):
        """
        Set up the task workflow
        """
        raise NotImplementedError()


    @classmethod
    def add_parent_parser(cls, name, parser):
        """
        Used to add a parser with its options and be able to refer from a
        command, since the conflict handler of argparse is not well done, as
        many other things.
        """
        # check that the parser with this name is not already present
        if name in cls.manager.parsers:
            logger.error(
                "Trying to add parser with name {0} while already defined"
            )
            raise SystemExit(-1)

        # add the parser to the collection
        cls.manager.parsers[name] = parser

    def __repr__(self):
        return "Command {0}".format(self.__command__)

    def __str__(self):
        return self.__command__

# vim: set tw=79 :
