# Copyright (C) Patrick Totzke <patricktotzke@gmail.com>
# This file is released under the GNU GPL, version 3 or a later revision.
# For further details see the COPYING file
import argparse
import glob
import logging
import os
import re

from ..settings.const import settings
from ..helper import split_commandstring, string_decode


class Command:

    """base class for commands"""
    repeatable = False

    def __init__(self):
        self.prehook = None
        self.posthook = None
        self.undoable = False
        self.help = self.__doc__

    def apply(self, ui):
        """code that gets executed when this command is applied"""
        pass


class CommandCanceled(Exception):
    """ Exception triggered when an interactive command has been cancelled
    """
    pass


class SequenceCanceled(Exception):
    """ Exception triggered when a command sequence has been cancelled by the
    confirmsequence command
    """
    pass


COMMANDS = {
    'search': {},
    'envelope': {},
    'bufferlist': {},
    'taglist': {},
    'namedqueries': {},
    'thread': {},
    'global': {},
}


def lookup_command(cmdname, mode):
    """
    returns commandclass, argparser and forced parameters used to construct
    a command for `cmdname` when called in `mode`.

    :param cmdname: name of the command to look up
    :type cmdname: str
    :param mode: mode identifier
    :type mode: str
    :rtype: (:class:`Command`, :class:`~argparse.ArgumentParser`,
            dict(str->dict))
    """
    if cmdname in COMMANDS[mode]:
        return COMMANDS[mode][cmdname]
    elif cmdname in COMMANDS['global']:
        return COMMANDS['global'][cmdname]
    else:
        return None, None, None


def lookup_parser(cmdname, mode):
    """
    returns the :class:`CommandArgumentParser` used to construct a
    command for `cmdname` when called in `mode`.
    """
    return lookup_command(cmdname, mode)[1]


class CommandParseError(Exception):

    """could not parse commandline string"""
    pass


class CommandArgumentParser(argparse.ArgumentParser):

    """
    :class:`~argparse.ArgumentParser` that raises :class:`CommandParseError`
    instead of printing to `sys.stderr`"""
    def exit(self, message):
        raise CommandParseError(message)

    def error(self, message):
        raise CommandParseError(message)


class registerCommand:
    """
    Decorator used to register a :class:`Command` as
    handler for command `name` in `mode` so that it
    can be looked up later using :func:`lookup_command`.

    Consider this example that shows how a :class:`Command` class
    definition is decorated to register it as handler for
    'save' in mode 'thread' and add boolean and string arguments::

    .. code-block::

        @registerCommand('thread', 'save', arguments=[
            (['--all'], {'action': 'store_true', 'help':'save all'}),
            (['path'], {'nargs':'?', 'help':'path to save to'})],
            help='save attachment(s)')
        class SaveAttachmentCommand(Command):
            pass

    """
    def __init__(self, mode, name, help=None, usage=None,
                 forced=None, arguments=None):
        """
        :param mode: mode identifier
        :type mode: str
        :param name: command name to register as
        :type name: str
        :param help: help string summarizing what this command does
        :type help: str
        :param usage: overides the auto generated usage string
        :type usage: str
        :param forced: keyword parameter used for commands constructor
        :type forced: dict (str->str)
        :param arguments: list of arguments given as pairs (args, kwargs)
                          accepted by
                          :meth:`argparse.ArgumentParser.add_argument`.
        :type arguments: list of (list of str, dict (str->str)
        """
        self.mode = mode
        self.name = name
        self.help = help
        self.usage = usage
        self.forced = forced or {}
        self.arguments = arguments or []

    def __call__(self, klass):
        helpstring = self.help or klass.__doc__
        argparser = CommandArgumentParser(description=helpstring,
                                          usage=self.usage,
                                          prog=self.name, add_help=False)
        for args, kwargs in self.arguments:
            argparser.add_argument(*args, **kwargs)
        COMMANDS[self.mode][self.name] = (klass, argparser, self.forced)
        return klass


def commandfactory(cmdline, mode='global'):
    """
    parses `cmdline` and constructs a :class:`Command`.

    :param cmdline: command line to interpret
    :type cmdline: str
    :param mode: mode identifier
    :type mode: str
    """
    # split commandname and parameters
    if not cmdline:
        return None
    logging.debug('mode:%s got commandline "%s"', mode, cmdline)
    # allow to shellescape without a space after '!'
    if cmdline.startswith('!'):
        cmdline = 'shellescape \'%s\'' % cmdline[1:]
    cmdline = re.sub(r'"(.*)"', r'"\\"\1\\""', cmdline)
    try:
        args = split_commandstring(cmdline)
    except ValueError as e:
        raise CommandParseError(str(e))
    args = [string_decode(x, 'utf-8') for x in args]
    logging.debug('ARGS: %s', args)
    cmdname = args[0]
    args = args[1:]

    # unfold aliases
    # TODO: read from settingsmanager

    # get class, argparser and forced parameter
    (cmdclass, parser, forcedparms) = lookup_command(cmdname, mode)
    if cmdclass is None:
        msg = 'unknown command: %s' % cmdname
        logging.debug(msg)
        raise CommandParseError(msg)

    parms = vars(parser.parse_args(args))
    parms.update(forcedparms)

    logging.debug('cmd parms %s', parms)

    # create Command
    cmd = cmdclass(**parms)

    # set pre and post command hooks
    get_hook = settings.get_hook
    cmd.prehook = get_hook('pre_%s_%s' % (mode, cmdname)) or \
        get_hook('pre_global_%s' % cmdname)
    cmd.posthook = get_hook('post_%s_%s' % (mode, cmdname)) or \
        get_hook('post_global_%s' % cmdname)

    return cmd


pyfiles = glob.glob1(os.path.dirname(__file__), '*.py')
__all__ = list(filename[:-3] for filename in pyfiles)
