import argparse
import os
import sys
import warnings
from gettext import gettext
from typing import Any
from typing import Callable
from typing import cast
from typing import Dict
from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union

import _pytest._io
from _pytest.compat import final
from _pytest.config.exceptions import UsageError
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
from _pytest.deprecated import ARGUMENT_TYPE_STR
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE

if TYPE_CHECKING:
    from typing import NoReturn
    from typing_extensions import Literal

FILE_OR_DIR = "file_or_dir"


@final
class Parser:
    """Parser for command line arguments and ini-file values.

    :ivar extra_info: Dict of generic param -> value to display in case
        there's an error processing the command line arguments.
    """

    prog: Optional[str] = None

    def __init__(
        self,
        usage: Optional[str] = None,
        processopt: Optional[Callable[["Argument"], None]] = None,
    ) -> None:
        self._anonymous = OptionGroup("custom options", parser=self)
        self._groups: List[OptionGroup] = []
        self._processopt = processopt
        self._usage = usage
        self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {}
        self._ininames: List[str] = []
        self.extra_info: Dict[str, Any] = {}

    def processoption(self, option: "Argument") -> None:
        if self._processopt:
            if option.dest:
                self._processopt(option)

    def getgroup(
        self, name: str, description: str = "", after: Optional[str] = None
    ) -> "OptionGroup":
        """Get (or create) a named option Group.

        :name: Name of the option group.
        :description: Long description for --help output.
        :after: Name of another group, used for ordering --help output.

        The returned group object has an ``addoption`` method with the same
        signature as :py:func:`parser.addoption
        <_pytest.config.argparsing.Parser.addoption>` but will be shown in the
        respective group in the output of ``pytest. --help``.
        """
        for group in self._groups:
            if group.name == name:
                return group
        group = OptionGroup(name, description, parser=self)
        i = 0
        for i, grp in enumerate(self._groups):
            if grp.name == after:
                break
        self._groups.insert(i + 1, group)
        return group

    def addoption(self, *opts: str, **attrs: Any) -> None:
        """Register a command line option.

        :opts: Option names, can be short or long options.
        :attrs: Same attributes which the ``add_argument()`` function of the
           `argparse library <https://docs.python.org/library/argparse.html>`_
           accepts.

        After command line parsing, options are available on the pytest config
        object via ``config.option.NAME`` where ``NAME`` is usually set
        by passing a ``dest`` attribute, for example
        ``addoption("--long", dest="NAME", ...)``.
        """
        self._anonymous.addoption(*opts, **attrs)

    def parse(
        self,
        args: Sequence[Union[str, "os.PathLike[str]"]],
        namespace: Optional[argparse.Namespace] = None,
    ) -> argparse.Namespace:
        from _pytest._argcomplete import try_argcomplete

        self.optparser = self._getparser()
        try_argcomplete(self.optparser)
        strargs = [os.fspath(x) for x in args]
        return self.optparser.parse_args(strargs, namespace=namespace)

    def _getparser(self) -> "MyOptionParser":
        from _pytest._argcomplete import filescompleter

        optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
        groups = self._groups + [self._anonymous]
        for group in groups:
            if group.options:
                desc = group.description or group.name
                arggroup = optparser.add_argument_group(desc)
                for option in group.options:
                    n = option.names()
                    a = option.attrs()
                    arggroup.add_argument(*n, **a)
        file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*")
        # bash like autocompletion for dirs (appending '/')
        # Type ignored because typeshed doesn't know about argcomplete.
        file_or_dir_arg.completer = filescompleter  # type: ignore
        return optparser

    def parse_setoption(
        self,
        args: Sequence[Union[str, "os.PathLike[str]"]],
        option: argparse.Namespace,
        namespace: Optional[argparse.Namespace] = None,
    ) -> List[str]:
        parsedoption = self.parse(args, namespace=namespace)
        for name, value in parsedoption.__dict__.items():
            setattr(option, name, value)
        return cast(List[str], getattr(parsedoption, FILE_OR_DIR))

    def parse_known_args(
        self,
        args: Sequence[Union[str, "os.PathLike[str]"]],
        namespace: Optional[argparse.Namespace] = None,
    ) -> argparse.Namespace:
        """Parse and return a namespace object with known arguments at this point."""
        return self.parse_known_and_unknown_args(args, namespace=namespace)[0]

    def parse_known_and_unknown_args(
        self,
        args: Sequence[Union[str, "os.PathLike[str]"]],
        namespace: Optional[argparse.Namespace] = None,
    ) -> Tuple[argparse.Namespace, List[str]]:
        """Parse and return a namespace object with known arguments, and
        the remaining arguments unknown at this point."""
        optparser = self._getparser()
        strargs = [os.fspath(x) for x in args]
        return optparser.parse_known_args(strargs, namespace=namespace)