"""Utilities for working with command aliases."""

import logging
import re
import shlex
import sys
import subprocess

from rbtools.commands import RB_MAIN


# Regular expression for matching argument replacement
_arg_re = re.compile(r'\$(\d+)')

# Prior to Python 2.7.3, the shlex module could not accept unicode input.
_SHLEX_SUPPORTS_UNICODE = sys.version_info >= (2, 7, 3)


def replace_arguments(cmd, args, posix):
    """Do parameter substitution for the given command.

    Occurrences of variables ``$1``, ``$2``, etc. are replaced with the nth
    element of ``args`` (1-indexed). The special variable ``$*`` is expanded to
    contain all arguments.

    If neither of these are present in the command, all arguments will be
    appended to the end.

    Args:
        cmd (unicode):
            The alias to replace positional variables in.

        args (list of unicode):
            The arguments being provided to the alias.

        posix (boolean):
            Whether or not to use the POSIX processing of commands.

            This should be ``False`` when parsing a command that will be given
            directly to the shell.

    Returns:
        list of unicode:
        The command and its arguments.

    Raises:
        ValueError:
            An invalid escape or missing quote was encountered.
    """
    def arg_sub(m):
        """Replace a positional variable with the appropriate argument."""
        index = int(m.group(1)) - 1

        try:
            return args[index]
        except IndexError:
            return ''

    did_replacement = False

    shlex_convert_text_type = (not _SHLEX_SUPPORTS_UNICODE and
                               isinstance(cmd, str))

    if shlex_convert_text_type:
        # This version of Python does not have a shlex module that supports
        # unicode arguments. We will encode it to a bytestring and then decode
        # it as we are processing the results.
        cmd = cmd.encode('utf-8')

    processed_cmd = []

    # When parsing a command for the shell, we do not operate in POSIX mode so
    # that:
    #
    # 1. Aliases do not have to be double escaped so that they will be properly
    #    be escaped by both us and the shell.
    #
    # 2. Quoted tokens will be returned in their original quotes, which would
    #    be removed in POSIX mode. We need to keep these quotes so aliases that
    #    contain single-quoted shell-specific characters (e.g., `'*'`) are
    #    returned as-is. Otherwise, the shell will try to evaluate these
    #    arguments when that is not the intention.
    for token in shlex.split(cmd, posix=posix):
        if token == '$*':
            did_replacement = True
            processed_cmd += args
        else:
            token, num_subs = _arg_re.subn(arg_sub, token)

            if shlex_convert_text_type:
                # Above we encoded our token into a byte string, so we need
                # to decode it back into a unicode string.
                token = token.decode('utf-8')

            if num_subs != 0:
                did_replacement = True

            if len(token):
                processed_cmd.append(token)

    # We did not do any positional variable replacement, so we will add all
    # arguments to the end of the command.
    if not did_replacement:
        processed_cmd += args

    return processed_cmd


def expand_alias(alias, args):
    """Expand the given alias.

    This function returns a tuple of the list of command line arguments and
    whether or not shell execution is required for the alias.

    Args:
        alias (unicode):
            The alias to expand.

        args (list of unicode):
            The arguments to provide to the command.

    Returns:
        tuple:
        A 2-tuple of:

        * The expanded alias (:py:class:`unicode` or :py:class:`list` of
          :py:class:`unicode`).
        * Whether or not the command should be passed to the shell or not
          (:py:class:`boolean`).
    """
    use_shell = alias.startswith('!')

    if use_shell:
        command = ' '.join(replace_arguments(alias[1:], args, posix=True))
    else:
        command = [RB_MAIN] + replace_arguments(alias, args, posix=False)

    return command, use_shell


def run_alias(alias_name, alias, args):
    """Run the alias with the given arguments after expanding parameters.

    Args:
        alias_name (unicode):
            The name of the alias being run.

        alias (unicode):
            The alias to run.

        args (list of unicode):
            The arguments to the command being run.

    Returns:
        int:
        The exit code of the executed command (if it executed) or ``1`` if the
        alias could not be expanded.
    """
    try:
        cmd, use_shell = expand_alias(alias, args)
    except ValueError as e:
        logging.critical(
            'Could not parse alias "%s" (`%s`): %s',
            alias_name, alias, e)
        return 1

    return subprocess.call(cmd, shell=use_shell)
