"""
Module containing code for creating the command line argument parser.
"""
import argparse
from itertools import chain
import logging

from gisht import __version__
from gisht.args.autocomplete import gist_completer
from gisht.args.data import GistCommand


__all__ = ['create_argv_parser']


def create_argv_parser():
    """Create a :class:`argparse.ArgumentParser` object
    for parsing command line arguments passed by the user.
    """
    parser = argparse.ArgumentParser(
        description="Download & run GitHub gists straight from the terminal",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        add_help=False)

    add_gist_group(parser)
    add_gist_command_group(parser)
    add_logging_group(parser)

    misc_group = add_misc_group(parser)

    # get the autogenerated usage string and tweak it a little
    # to include gist arguments that are handled separately
    # and exclude the miscellaneous flags which aren't part of a normal usage
    usage = parser.format_usage()
    usage = usage[usage.find(parser.prog):].rstrip("\n")  # remove cruft
    for misc_flag in chain.from_iterable(a.option_strings
                                         for a in misc_group._group_actions):
        usage = usage.replace(" [%s]" % misc_flag, "")
    parser.usage = usage + " [-- GIST_ARGS]"
    return parser


# Gist specification

def add_gist_group(parser):
    """Include an argument group that allows to specify the gist.
    :param parser: :class:`argparse.ArgumentParser`
    :return: Resulting argument group
    """
    group = parser.add_argument_group(
        "Gist", "Specifies the gist, optionally with flags")

    group.add_argument('gist', type=gist,
                       help="GitHub gist, specified as <owner>/<name> "
                            "(e.g. Octocat/foo)",
                       metavar="GIST").completer = gist_completer

    fetch_group = group.add_mutually_exclusive_group()
    fetch_group.set_defaults(local=None)
    fetch_group.add_argument('-l', '--local', '--cached',
                             action='store_true', dest='local',
                             help="operate only on gists available locally"
                                  " (do not fetch anything from GitHub)")
    fetch_group.add_argument('-f', '--fetch', '--remote',
                             action='store_false', dest='local',
                             help="always fetch the gist from GitHub, "
                                  "possibly updating it to latest version")

    return group


def gist(value):
    """Converter/validator for the GIST command line argument."""
    try:
        owner, gist_name = value.split('/')
    except ValueError:
        raise argparse.ArgumentTypeError(
            "%r is not a valid gist reference; try '<owner>/`<name>`" % value)
    if owner and gist_name:
        return value
    else:
        raise argparse.ArgumentTypeError(
            "neither gist owner or name can be empty (got %r)" % (value,))


# Gist command

def add_gist_command_group(parser):
    """Include an argument group that allows to specify
    the command to be performed on the gist.

    :param parser: :class:`argparse.ArgumentParser`
    :return: Resulting argument group
    """
    # 'command' is our internal wording, intended not to clash with
    # ArgumentParser actions, but the UI shall refer to it still as 'action'
    group = parser.add_argument_group(
        "Actions", "Possible actions to perform on the gist") \
        .add_mutually_exclusive_group()
    group.set_defaults(command=GistCommand.RUN)

    gist_commands = {
        GistCommand.RUN: "run specified gist; this is the default behavior "
                         "if no action was specified explicitly",
        GistCommand.WHICH: "output the path to binary which would be "
                           "ran for given gist; useful for passing it "
                           "to other commands via $( )",
        GistCommand.PRINT: "print gist source on the standard output",
        GistCommand.OPEN: "open the gist's GitHub page "
                          "in the default web browser",
        GistCommand.INFO: "show summary information about specified gist",
    }
    for cmd, help in gist_commands.items():
        group.add_argument(
            *cmd.flags, dest='command', action=GistCommandAction, help=help)

    return group


class GistCommandAction(argparse.Action):
    """Custom argument parser's :class:`Action` for handling
    gist command flags.
    """
    def __init__(self, **kwargs):
        option_strings = kwargs.get('option_strings')
        if not option_strings:
            raise TypeError("GistCommandAction can only be applied to flags")

        # basing upon supplied flag names, find the matching GistCommannd
        commands = set(map(GistCommand.for_flag, option_strings))
        if not commands:
            raise ValueError(
                "no gist command found matching flags %r" % (option_strings,))
        if len(commands) > 1:
            raise ValueError(
                "ambiguous gist command for flags %r -- can be one of: %r" % (
                    option_strings, commands))
        kwargs['const'] = commands.pop()

        super(GistCommandAction, self).__init__(nargs=0, **kwargs)

        #: Whether we have already seen a gist command flag.
        #: Used to prevent the same flag from being supplied twice.
        self._seen = False

    def __call__(self, parser, namespace, values, option_string=None):
        if self._seen:
            raise argparse.ArgumentError(self, "can be specified at most once")
        setattr(namespace, self.dest, self.const)
        self._seen = True


# Logging options

def add_logging_group(parser):
    """Include an argument group that allows to
    control verbosity of the program's logging output.

    :param parser: :class:`argparse.ArgumentParser`
    :return: Resulting argument group
    """
    group = parser.add_argument_group(
        "Verbosity", "Only errors and warnings are printed by default") \
        .add_mutually_exclusive_group()
    group.set_defaults(log_level=LogLevelAction.DEFAULT_LEVEL)

    group.add_argument('-v', '--verbose', dest='log_level',
                       action=LogLevelAction,
                       const=-LogLevelAction.DEFAULT_INCREMENT,
                       help="include finer grained details in the output; "
                            "this option can be specified multiple times")
    group.add_argument('-q', '--quiet', dest='log_level',
                       action=LogLevelAction,
                       const=LogLevelAction.DEFAULT_INCREMENT,
                       help="decrease the verbosity level")

    return group


class LogLevelAction(argparse.Action):
    """Custom argument parser's :class:`Action` for handling
    log level / verbosity flags.
    """
    DEFAULT_LEVEL = logging.WARNING

    DEFAULT_INCREMENT = 10
    DEFAULT_MINIMUM = logging.NOTSET
    DEFAULT_MAXIMUM = logging.CRITICAL

    def __init__(self, *args, **kwargs):
        self.min = kwargs.pop('min', self.DEFAULT_MINIMUM)
        self.max = kwargs.pop('max', self.DEFAULT_MAXIMUM)
        kwargs.setdefault('const', self.DEFAULT_INCREMENT)
        super(LogLevelAction, self).__init__(*args, nargs=0, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        current = getattr(namespace, self.dest, self.DEFAULT_LEVEL)
        new = max(self.min, min(self.max, current + self.const))
        setattr(namespace, self.dest, new)


# Miscellaneous options

def add_misc_group(parser):
    """Include the argument group with miscellaneous options.
    :param parser: :class:`argparse.ArgumentParser`
    :return: Resulting argument group
    """
    group = parser.add_argument_group("Miscellaneous", "Other options")

    group.add_argument('--version', action='version', version=__version__)
    group.add_argument('-h', '--help', action='help',
                       help="show this help message and exit")

    return group
