"""
Command-line completion mechanisms, executed by the core ``--complete`` flag.
"""

from typing import List
import glob
import os
import re
import shlex
from typing import TYPE_CHECKING

from ..exceptions import Exit, ParseError
from ..util import debug, task_name_sort_key

if TYPE_CHECKING:
    from ..collection import Collection
    from ..parser import Parser, ParseResult, ParserContext


def complete(
    names: List[str],
    core: "ParseResult",
    initial_context: "ParserContext",
    collection: "Collection",
    parser: "Parser",
) -> Exit:
    # Strip out program name (scripts give us full command line)
    # TODO: this may not handle path/to/script though?
    invocation = re.sub(r"^({}) ".format("|".join(names)), "", core.remainder)
    debug("Completing for invocation: {!r}".format(invocation))
    # Tokenize (shlex will have to do)
    tokens = shlex.split(invocation)
    # Handle flags (partial or otherwise)
    if tokens and tokens[-1].startswith("-"):
        tail = tokens[-1]
        debug("Invocation's tail {!r} is flag-like".format(tail))
        # Gently parse invocation to obtain 'current' context.
        # Use last seen context in case of failure (required for
        # otherwise-invalid partial invocations being completed).

        contexts: List[ParserContext]
        try:
            debug("Seeking context name in tokens: {!r}".format(tokens))
            contexts = parser.parse_argv(tokens)
        except ParseError as e:
            msg = "Got parser error ({!r}), grabbing its last-seen context {!r}"  # noqa
            debug(msg.format(e, e.context))
            contexts = [e.context] if e.context is not None else []
        # Fall back to core context if no context seen.
        debug("Parsed invocation, contexts: {!r}".format(contexts))
        if not contexts or not contexts[-1]:
            context = initial_context
        else:
            context = contexts[-1]
        debug("Selected context: {!r}".format(context))
        # Unknown flags (could be e.g. only partially typed out; could be
        # wholly invalid; doesn't matter) complete with flags.
        debug("Looking for {!r} in {!r}".format(tail, context.flags))
        if tail not in context.flags:
            debug("Not found, completing with flag names")
            # Long flags - partial or just the dashes - complete w/ long flags
            if tail.startswith("--"):
                for name in filter(
                    lambda x: x.startswith("--"), context.flag_names()
                ):
                    print(name)
            # Just a dash, completes with all flags
            elif tail == "-":
                for name in context.flag_names():
                    print(name)
            # Otherwise, it's something entirely invalid (a shortflag not
            # recognized, or a java style flag like -foo) so return nothing
            # (the shell will still try completing with files, but that doesn't
            # hurt really.)
            else:
                pass
        # Known flags complete w/ nothing or tasks, depending
        else:
            # Flags expecting values: do nothing, to let default (usually
            # file) shell completion occur (which we actively want in this
            # case.)
            if context.flags[tail].takes_value:
                debug("Found, and it takes a value, so no completion")
                pass
            # Not taking values (eg bools): print task names
            else:
                debug("Found, takes no value, printing task names")
                print_task_names(collection)
    # If not a flag, is either task name or a flag value, so just complete
    # task names.
    else:
        debug("Last token isn't flag-like, just printing task names")
        print_task_names(collection)
    raise Exit


def print_task_names(collection: "Collection") -> None:
    for name in sorted(collection.task_names, key=task_name_sort_key):
        print(name)
        # Just stick aliases after the thing they're aliased to. Sorting isn't
        # so important that it's worth bending over backwards here.
        for alias in collection.task_names[name]:
            print(alias)


def print_completion_script(shell: str, names: List[str]) -> None:
    # Grab all .completion files in invoke/completion/. (These used to have no
    # suffix, but surprise, that's super fragile.
    completions = {
        os.path.splitext(os.path.basename(x))[0]: x
        for x in glob.glob(
            os.path.join(
                os.path.dirname(os.path.realpath(__file__)), "*.completion"
            )
        )
    }
    try:
        path = completions[shell]
    except KeyError:
        err = 'Completion for shell "{}" not supported (options are: {}).'
        raise ParseError(err.format(shell, ", ".join(sorted(completions))))
    debug("Printing completion script from {}".format(path))
    # Choose one arbitrary program name for script's own internal invocation
    # (also used to construct completion function names when necessary)
    binary = names[0]
    with open(path, "r") as script:
        print(
            script.read().format(binary=binary, spaced_names=" ".join(names))
        )
