# -*- coding: utf-8 -*-
"""Back-port compiler for Python 3.8 positional-only parameter syntax."""

import argparse
import os
import pathlib
import re
import sys
import traceback
from typing import Generator, List, Optional, Union

import f2format
import parso.python.tree
import parso.tree
import tbtrim
from bpc_utils import (BaseContext, BPCSyntaxError, Config, TaskLock, archive_files,
                       detect_encoding, detect_files, detect_indentation, detect_linesep,
                       first_non_none, get_parso_grammar_versions, map_tasks, parse_boolean_state,
                       parse_indentation, parse_linesep, parse_positive_integer, parso_parse,
                       recover_files)
from bpc_utils.typing import Linesep
from typing_extensions import Literal, final

__all__ = ['main', 'poseur', 'convert', 'decorator']  # pylint: disable=undefined-all-variable

# version string
__version__ = '0.4.3.post1'

###############################################################################
# Typings


class PoseurConfig(Config):
    indentation = ''  # type: str
    linesep = '\n'  # type: Literal[Linesep]
    pep8 = True  # type: bool
    filename = None  # Optional[str]
    source_version = None  # Optional[str]
    decorator = 'decorator'  # type: str
    dismiss = False  # type: bool


##############################################################################
# Auxiliaries

#: Get supported source versions.
#:
#: .. seealso:: :func:`bpc_utils.get_parso_grammar_versions`
POSEUR_SOURCE_VERSIONS = get_parso_grammar_versions(minimum='3.8')

# option default values
#: Default value for the ``quiet`` option.
_default_quiet = False
#: Default value for the ``concurrency`` option.
_default_concurrency = None  # auto detect
#: Default value for the ``do_archive`` option.
_default_do_archive = True
#: Default value for the ``archive_path`` option.
_default_archive_path = 'archive'
#: Default value for the ``source_version`` option.
_default_source_version = POSEUR_SOURCE_VERSIONS[-1]
#: Default value for the ``linesep`` option.
_default_linesep = None  # auto detect
#: Default value for the ``indentation`` option.
_default_indentation = None  # auto detect
#: Default value for the ``pep8`` option.
_default_pep8 = True
#: Default value for the ``dismiss-runtime`` option.
_default_dismiss = False
#: Default value for the ``decorator-name`` option.
_default_decorator = '_poseur_decorator'

# option getter utility functions
# option value precedence is: explicit value (CLI/API arguments) > environment variable > default value


def _get_quiet_option(explicit: Optional[bool] = None) -> Optional[bool]:
    """Get the value for the ``quiet`` option.

    Args:
        explicit (Optional[bool]): the value explicitly specified by user,
            :data:`None` if not specified

    Returns:
        bool: the value for the ``quiet`` option

    :Environment Variables:
        :envvar:`POSEUR_QUIET` -- the value in environment variable

    See Also:
        :data:`_default_quiet`

    """
    # We need short circuit evaluation, so first_non_none(a, b, c) does not work here
    # with PEP 505 we can simply write a ?? b ?? c
    def _option_layers() -> Generator[Optional[bool], None, None]:
        yield explicit
        yield parse_boolean_state(os.getenv('POSEUR_QUIET'))
        yield _default_quiet
    return first_non_none(_option_layers())


def _get_concurrency_option(explicit: Optional[int] = None) -> Optional[int]:
    """Get the value for the ``concurrency`` option.

    Args:
        explicit (Optional[int]): the value explicitly specified by user,
            :data:`None` if not specified

    Returns:
        Optional[int]: the value for the ``concurrency`` option;
        :data:`None` means *auto detection* at runtime

    :Environment Variables:
        :envvar:`POSEUR_CONCURRENCY` -- the value in environment variable

    See Also:
        :data:`_default_concurrency`

    """
    return parse_positive_integer(explicit or os.getenv('POSEUR_CONCURRENCY') or _default_concurrency)


def _get_do_archive_option(explicit: Optional[bool] = None) -> Optional[bool]:
    """Get the value for the ``do_archive`` option.

    Args:
        explicit (Optional[bool]): the value explicitly specified by user,
            :data:`None` if not specified

    Returns:
        bool: the value for the ``do_archive`` option

    :Environment Variables:
        :envvar:`POSEUR_DO_ARCHIVE` -- the value in environment variable

    See Also:
        :data:`_default_do_archive`

    """
    def _option_layers() -> Generator[Optional[bool], None, None]:
        yield explicit
        yield parse_boolean_state(os.getenv('POSEUR_DO_ARCHIVE'))
        yield _default_do_archive
    return first_non_none(_option_layers())


def _get_archive_path_option(explicit: Optional[str] = None) -> str:
    """Get the value for the ``archive_path`` option.

    Args:
        explicit (Optional[str]): the value explicitly specified by user,
            :data:`None` if not specified

    Returns:
        str: the value for the ``archive_path`` option

    :Environment Variables:
        :envvar:`POSEUR_ARCHIVE_PATH` -- the value in environment variable

    See Also:
        :data:`_default_archive_path`

    """
    return explicit or os.getenv('POSEUR_ARCHIVE_PATH') or _default_archive_path


def _get_source_version_option(explicit: Optional[str] = None) -> Optional[str]:
    """Get the value for the ``source_version`` option.

    Args:
        explicit (Optional[str]): the value explicitly specified by user,
            :data:`None` if not specified

    Returns:
        str: the value for the ``source_version`` option

    :Environment Variables:
        :envvar:`POSEUR_SOURCE_VERSION` -- the value in environment variable

    See Also:
        :data:`_default_source_version`

    """
    return explicit or os.getenv('POSEUR_SOURCE_VERSION') or _default_source_version


def _get_linesep_option(explicit: Optional[str] = None) -> Optional[Linesep]:
    r"""Get the value for the ``linesep`` option.

    Args:
        explicit (Optional[str]): the value explicitly specified by user,
            :data:`None` if not specified

    Returns:
        Optional[Literal['\\n', '\\r\\n', '\\r']]: the value for the ``linesep`` option;
        :data:`None` means *auto detection* at runtime

    :Environment Variables:
        :envvar:`POSEUR_LINESEP` -- the value in environment variable

    See Also:
        :data:`_default_linesep`

    """
    return parse_linesep(explicit or os.getenv('POSEUR_LINESEP') or _default_linesep)


def _get_indentation_option(explicit: Optional[Union[str, int]] = None) -> Optional[str]:
    """Get the value for the ``indentation`` option.

    Args:
        explicit (Optional[Union[str, int]]): the value explicitly specified by user,
            :data:`None` if not specified

    Returns:
        Optional[str]: the value for the ``indentation`` option;
        :data:`None` means *auto detection* at runtime

    :Environment Variables:
        :envvar:`POSEUR_INDENTATION` -- the value in environment variable

    See Also:
        :data:`_default_indentation`

    """
    return parse_indentation(explicit or os.getenv('POSEUR_INDENTATION') or _default_indentation)


def _get_pep8_option(explicit: Optional[bool] = None) -> Optional[bool]:
    """Get the value for the ``pep8`` option.

    Args:
        explicit (Optional[bool]): the value explicitly specified by user,
            :data:`None` if not specified

    Returns:
        bool: the value for the ``pep8`` option

    :Environment Variables:
        :envvar:`POSEUR_PEP8` -- the value in environment variable

    See Also:
        :data:`_default_pep8`

    """
    def _option_layers() -> Generator[Optional[bool], None, None]:
        yield explicit
        yield parse_boolean_state(os.getenv('POSEUR_PEP8'))
        yield _default_pep8
    return first_non_none(_option_layers())


def _get_dismiss_option(explicit: Optional[bool] = None) -> Optional[bool]:
    """Get the value for the ``dismiss-runtime`` option.

    Args:
        explicit (Optional[bool]): the value explicitly specified by user,
            :data:`None` if not specified

    Returns:
        bool: the value for the ``dismiss-runtime`` option

    :Environment Variables:
        :envvar:`POSEUR_DISMISS` -- the value in environment variable

    See Also:
        :data:`_default_dismiss`

    """
    def _option_layers() -> Generator[Optional[bool], None, None]:
        yield explicit
        yield parse_boolean_state(os.getenv('POSEUR_DISMISS'))
        yield _default_dismiss
    return first_non_none(_option_layers())


def _get_decorator_option(explicit: Optional[str] = None) -> Optional[str]:
    """Get the value for the ``decorator`` option.

    Args:
        explicit (Optional[str]): the value explicitly specified by user,
            :data:`None` if not specified

    Returns:
        str: the value for the ``decorator`` option

    :Environment Variables:
        :envvar:`POSEUR_DECORATOR` -- the value in environment variable

    See Also:
        :data:`_default_decorator`

    """
    return explicit or os.getenv('POSEUR_DECORATOR') or _default_decorator


###############################################################################
# Traceback Trimming (tbtrim)

# root path
ROOT = pathlib.Path(__file__).resolve().parent


def predicate(filename: str) -> bool:
    return pathlib.Path(filename).parent == ROOT


tbtrim.set_trim_rule(predicate, strict=True, target=BPCSyntaxError)

###############################################################################
# Main convertion implementations

# cf. https://mail.python.org/pipermail/python-ideas/2017-February/044888.html
DECORATOR_TEMPLATE = '''\
def %(decorator)s(*poseur):
%(indentation)s"""Positional-only parameters runtime checker.
%(indentation)s
%(indentation)s    Args:
%(indentation)s        *poseur: Name list of positional-only parameters.
%(indentation)s
%(indentation)s    Raises:
%(indentation)s        TypeError: If any position-only parameters were passed as
%(indentation)s            keyword parameters.
%(indentation)s
%(indentation)s    The decorator function may decorate regular :term:`function` and/or
%(indentation)s    :term:`lambda` function to provide runtime checks on the original
%(indentation)s    positional-only parameters.
%(indentation)s
%(indentation)s"""
%(indentation)simport functools
%(indentation)sdef caller(func):
%(indentation)s%(indentation)s@functools.wraps(func)
%(indentation)s%(indentation)sdef wrapper(*args, **kwargs):
%(indentation)s%(indentation)s%(indentation)sposeur_args = set(poseur).intersection(kwargs)
%(indentation)s%(indentation)s%(indentation)sif poseur_args:
%(indentation)s%(indentation)s%(indentation)s%(indentation)sraise TypeError('%%s() got some positional-only arguments passed as keyword arguments: %%r' %% (func.__name__, ', '.join(poseur_args)))
%(indentation)s%(indentation)s%(indentation)sreturn func(*args, **kwargs)
%(indentation)s%(indentation)sreturn wrapper
%(indentation)sreturn caller
'''.splitlines()  # `str.splitlines` will remove trailing newline


class Context(BaseContext):
    """General conversion context.

    Args:
        node (parso.tree.NodeOrLeaf): parso AST
        config (Config): conversion configurations

    Keyword Args:
        clx_ctx (Optional[str]): class context name
        indent_level (int): current indentation level
        raw (bool): raw processing flag

    Important:
        ``raw`` should be :data:`True` only if the ``node`` is in the clause of another *context*,
        where the converted wrapper functions should be inserted.

    For the :class:`Context` class of :mod:`poseur` module,
    it will process nodes with following methods:

    * :token:`suite`

      - :meth:`Context._process_suite_node`

    * :token:`funcdef`

      - :meth:`Context._process_funcdef`

    * :token:`lambdef`

      - :meth:`Context._process_lambdef`

    * :token:`async_funcdef`

      - :meth:`Context._process_async_funcdef`

    * :token:`async_stmt`

      - :meth:`Context._process_async_stmt`

    * :token:`classdef`

      - :meth:`Context._process_classdef`

    * :token:`if_stmt`

      - :meth:`Context._process_if_stmt`

    * :token:`while_stmt`

      - :meth:`Context._process_while_stmt`

    * :token:`for_stmt`

      - :meth:`Context._process_for_stmt`

    * :token:`with_stmt`

      - :meth:`Context._process_with_stmt`

    * :token:`try_stmt`

      - :meth:`Context._process_try_stmt`

    * :token:`stringliteral`

      * :meth:`Context._process_strings`
      * :meth:`Context._process_string_context`

    * :token:`f_string`

      * :meth:`Context._process_fstring`

    """
    #: re.Pattern: Pattern to find the function definition line.
    pattern_funcdef = re.compile(r'^\s*(async\s+)?def\s', re.ASCII)

    @final
    @property
    def decorator(self) -> str:
        """Name of the ``poseur`` decorator.

        :rtype: str
        """
        return self._decorator

    def __init__(self, node: parso.tree.NodeOrLeaf, config: PoseurConfig, *,
                 clx_ctx: Optional[str] = None, indent_level: int = 0, raw: bool = False):
        #: bool: Dismiss runtime checks for positional-only parameters.
        self._dismiss = config.dismiss  # type: bool
        #: str: Decorator name.
        self._decorator = config.decorator  # type: str
        #: Optional[str]: Class context name. This is rather useful as
        #: we might need to *mangle* and/or *normalize* variable names
        #: in certain scenario.
        self._cls_ctx = clx_ctx  # type: Optional[str]

        super().__init__(node, config, indent_level=indent_level, raw=raw)

    def _process_suite_node(self, node: parso.tree.NodeOrLeaf, *, cls_ctx: Optional[str] = None) -> None:
        """Process indented suite (:token:`suite` or others).

        Args:
            node (parso.tree.NodeOrLeaf): suite node

        Keyword Args:
            cls_ctx (Optional[str]): class context name

        This method first checks if ``node`` contains positional-only parameters.
        If not, it will not perform any processing, rather just append the
        original source code to context buffer.

        If ``node`` contains positional-only parameters, then it will initiate
        another Context` instance to perform the conversion process on such
        ``node``.

        """
        if not self.has_expr(node):
            self += node.get_code()
            return

        indent = self._indent_level + 1
        self += self._linesep + self._indentation * indent

        if cls_ctx is None:
            cls_ctx = self._cls_ctx

        # initialize new context
        ctx = Context(node=node, config=self.config, clx_ctx=cls_ctx,  # type: ignore[arg-type]
                      indent_level=indent, raw=True)
        self += ctx.string.lstrip()

    def _process_funcdef(self, node: parso.python.tree.Function, *,
                         async_ctx: Optional[parso.python.tree.Keyword] = None) -> None:
        """Process function definition (:token:`funcdef`).

        Args:
            node (parso.python.tree.Function): function node

        Keyword Args:
            async_ctx (Optional[parso.python.tree.Keyword]): ``async`` keyword AST node

        """
        if not self.has_expr(node):
            self += node.get_code()
            return

        posonly = []  # positional-only parameters
        funcdef = '' if async_ctx is None else async_ctx.get_code()

        # 'def' NAME '(' PARAM ')' [ '->' NAME ] ':' SUITE
        for child in node.children[:-1]:
            if child.type == 'parameters':
                # <Operator: (>
                funcdef += child.children[0].get_code()

                parameters = ''
                param_list = []  # type: List[parso.python.tree.Param]
                for grandchild in child.children[1:-1]:
                    # <Operator: />
                    if grandchild.type == 'operator' and grandchild.value == '/':
                        parameters += grandchild.get_code().replace('/', '')
                        posonly.extend(param_list)
                        continue

                    # <Param: ...>
                    if grandchild.type == 'param':
                        param_list.append(grandchild)

                        if grandchild.default is not None:
                            # initiate new context
                            ctx = Context(grandchild, self.config, raw=True,  # type: ignore[arg-type]
                                          indent_level=self._indent_level)
                            parameters += ctx.string
                            continue

                    # <Param: ...> / <Operator: *> / <Operator: ,>
                    parameters += grandchild.get_code()

                if self._pep8:
                    funcdef += ', '.join(filter(None, map(lambda s: s.strip(), parameters.split(','))))
                else:
                    funcdef += ','.join(filter(lambda s: s.strip(), parameters.split(',')))

                # <Operator: )>
                funcdef += child.children[-1].get_code()
                continue

            funcdef += child.get_code()

        # decorate the function
        if not self._dismiss and posonly:
            prefix = ''
            suffix = ''
            deflag = False  # function definition line

            for line in funcdef.splitlines(True):
                if self.pattern_funcdef.match(line) is not None:
                    deflag = True
                if deflag:
                    suffix += line
                else:
                    prefix += line

            posonly_args = ', '.join(map(lambda param: repr(self.normalizer(param.name.value)), posonly))
            indentation = self._indentation * self._indent_level
            self += ('%(prefix)s'
                     '%(indentation)s@%(decorator)s(%(posonly)s)%(linesep)s'
                     '%(suffix)s') % dict(
                         prefix=prefix, suffix=suffix,
                         linesep=self._linesep, indentation=indentation,
                         decorator=self._decorator, posonly=posonly_args,
                     )
        else:
            self += funcdef

        # SUITE
        self._process_suite_node(node.children[-1])

    def _process_async_stmt(self, node: parso.python.tree.PythonNode) -> None:
        """Process ``async`` statement (:token:`async_stmt`).

        Args:
            node (parso.python.tree.PythonNode): ``async`` statement node

        This method processes an ``async`` statement node. If such statement is an
        *async* :term:`function`, then it will pass on the processing to
        :meth:`self._process_funcdef <Context._process_funcdef>`.

        """
        child_1st = node.children[0]
        child_2nd = node.children[1]

        flag_1st = child_1st.type == 'keyword' and child_1st.value == 'async'
        flag_2nd = child_2nd.type == 'funcdef'

        if flag_1st and flag_2nd:
            self._process_funcdef(child_2nd, async_ctx=child_1st)
            return

        self._process(child_1st)
        self._process(child_2nd)

    def _process_async_funcdef(self, node: parso.python.tree.PythonNode) -> None:
        """Process ``async`` function definition (:token:`async_funcdef`).

        Args:
            node (parso.python.tree.PythonNode): ``async`` function node

        This method processes an ``async`` function node. It will extract
        the ``async`` keyword node (:class:`parso.python.tree.Keyword`)
        and the :term:`function` node (:class:`parso.python.tree.Function`)
        then pass on the processing to
        :meth:`self._process_funcdef <Context._process_funcdef>`.

        """
        async_ctx, funcdef = node.children
        self._process_funcdef(funcdef, async_ctx=async_ctx)

    def _process_lambdef(self, node: parso.python.tree.Lambda) -> None:
        """Process lambda definition (:token:`lambdef`).

        Args:
            node (parso.python.tree.Lambda): lambda node

        """
        if not self.has_expr(node):
            self += node.get_code()
            return

        pos_only = []
        children = iter(node.children)

        # string buffers
        params = ''
        prefix = ''
        suffix = ''

        # <Keyword: lambda>
        prefix += next(children).get_code()

        # vararglist
        param_list = []  # type: List[parso.python.tree.Param]
        for child in children:
            if child.type == 'operator':
                # <Operator: />
                if child.value == '/':
                    params += child.get_code().replace('/', '')
                    pos_only.extend(param_list)
                    continue

                # <Operator: :>
                if child.value == ':':
                    suffix += child.get_code()
                    break

            # <Param: ...>
            if child.type == 'param':
                param_list.append(child)

                if child.default is not None:
                    # initialize new context
                    ctx = Context(node=child, config=self.config,  # type: ignore[arg-type]
                                  indent_level=self._indent_level, raw=True)
                    params += ctx.string
                    continue

            # <Param: ...> / <Operator: *> / <Operator: ,>
            params += child.get_code()

        # test_nocond | test
        ctx = Context(node=next(children), config=self.config,  # type: ignore[arg-type]
                      indent_level=self._indent_level, raw=True)
        suffix += ctx.string

        whitespace_prefix, whitespace_suffix = self.extract_whitespaces(params)
        if self._pep8:
            params = ', '.join(filter(None, map(lambda s: s.strip(), params.split(','))))
        else:
            params = ','.join(filter(lambda s: s.strip(), params.split(',')))
        lambdef = prefix + whitespace_prefix + params.strip() + suffix.lstrip()

        if self._dismiss or not pos_only:
            self += lambdef
            return

        # decorate lambda definition
        whitespace_prefix, whitespace_suffix = self.extract_whitespaces(lambdef)
        posonly_args = ', '.join(map(lambda param: repr(self.normalizer(param.name.value).strip()), pos_only))
        self += ('%(prefix)s'
                 '%(decorator)s(%(posonly)s)'
                 '(%(lambdef)s)'
                 '%(suffix)s') % dict(
                     prefix=whitespace_prefix, suffix=whitespace_suffix,
                     lambdef=lambdef.strip(),
                     decorator=self._decorator, posonly=posonly_args,
                 )

    def _process_string_context(self, node: parso.python.tree.PythonNode) -> None:
        """Process string contexts (:token:`stringliteral`).

        Args:
            node (parso.python.tree.PythonNode): string literals node

        This method first checks if ``node`` contains position-only parameters.
        If not, it will not perform any processing, rather just append the
        original source code to context buffer. Later it will check if
        ``node`` contains *debug f-string*. If not, it will process the
        *regular* processing on each child of such ``node``.

        See Also:
            The method calls :meth:`f2format.Context.has_debug_fstring`
            to detect *debug f-strings*.

        Otherwise, it will initiate a new :class:`StringContext` instance
        to perform the conversion process on such ``node``, which will first
        use :mod:`f2format` to convert those formatted string literals.

        Important:
            When initialisation, ``raw`` parameter **must** be set to :data:`True`;
            as the converted wrapper functions should be inserted in the *outer*
            context, rather than the new :class:`StringContext` instance.

        """
        if not self.has_expr(node):
            self += node.get_code()
            return

        if not f2format.Context.has_debug_fstring(node):
            for child in node.children:
                self._process(child)
            return

        # initiate new context
        ctx = StringContext(node=node, config=self.config,  # type: ignore[arg-type]
                            indent_level=self._indent_level, raw=True)
        self += ctx.string

    def _process_classdef(self, node: parso.python.tree.Class) -> None:
        """Process class definition (:token:`classdef`).

        Args:
            node (parso.python.tree.Class): class node

        This method converts the whole class suite context with
        :meth:`~Context._process_suite_node` through :class:`Context`
        respectively.

        """
        # <Name: ...>
        name = node.name

        # <Keyword: class>
        # <Name: ...>
        # [<Operator: (>, PythonNode(arglist, [...]]), <Operator: )>]
        # <Operator: :>
        for child in node.children[:-1]:
            self._process(child)

        # PythonNode(suite, [...]) / PythonNode(simple_stmt, [...])
        suite = node.children[-1]
        self._process_suite_node(suite, cls_ctx=name.name)

    def _process_if_stmt(self, node: parso.python.tree.IfStmt) -> None:
        """Process if statement (:token:`if_stmt`).

        Args:
            node (parso.python.tree.IfStmt): if node

        This method processes each indented suite under the *if*, *elif*,
        and *else* statements.

        """
        children = iter(node.children)

        # <Keyword: if>
        self._process(next(children))
        # namedexpr_test
        self._process(next(children))
        # <Operator: :>
        self._process(next(children))
        # suite
        self._process_suite_node(next(children))

        while True:
            try:
                # <Keyword: elif | else>
                key = next(children)
            except StopIteration:
                break
            self._process(key)

            if key.value == 'elif':
                # namedexpr_test
                self._process(next(children))
                # <Operator: :>
                self._process(next(children))
                # suite
                self._process_suite_node(next(children))
                continue
            if key.value == 'else':
                # <Operator: :>
                self._process(next(children))
                # suite
                self._process_suite_node(next(children))
                continue

    def _process_while_stmt(self, node: parso.python.tree.WhileStmt) -> None:
        """Process while statement (:token:`while_stmt`).

        Args:
            node (parso.python.tree.WhileStmt): while node

        This method processes the indented suite under the *while* and optional
        *else* statements.

        """
        children = iter(node.children)

        # <Keyword: while>
        self._process(next(children))
        # namedexpr_test
        self._process(next(children))
        # <Operator: :>
        self._process(next(children))
        # suite
        self._process_suite_node(next(children))

        try:
            key = next(children)
        except StopIteration:
            return

        # <Keyword: else>
        self._process(key)
        # <Operator: :>
        self._process(next(children))
        # suite
        self._process_suite_node(next(children))

    def _process_for_stmt(self, node: parso.python.tree.ForStmt) -> None:
        """Process for statement (:token:`for_stmt`).

        Args:
            node (parso.python.tree.ForStmt): for node

        This method processes the indented suite under the *for* and optional
        *else* statements.

        """
        children = iter(node.children)

        # <Keyword: for>
        self._process(next(children))
        # exprlist
        self._process(next(children))
        # <Keyword: in>
        self._process(next(children))
        # testlist
        self._process(next(children))
        # <Operator: :>
        self._process(next(children))
        # suite
        self._process_suite_node(next(children))

        try:
            key = next(children)
        except StopIteration:
            return

        # <Keyword: else>
        self._process(key)
        # <Operator: :>
        self._process(next(children))
        # suite
        self._process_suite_node(next(children))

    def _process_with_stmt(self, node: parso.python.tree.WithStmt) -> None:
        """Process with statement (:token:`with_stmt`).

        Args:
            node (parso.python.tree.WithStmt): with node

        This method processes the indented suite under the *with* statement.

        """
        children = iter(node.children)

        # <Keyword: with>
        self._process(next(children))

        while True:
            # with_item | <Operator: ,>
            item = next(children)
            self._process(item)

            # <Operator: :>
            if item.type == 'operator' and item.value == ':':
                break

        # suite
        self._process_suite_node(next(children))

    def _process_try_stmt(self, node: parso.python.tree.TryStmt) -> None:
        """Process try statement (:token:`try_stmt`).

        Args:
            node (parso.python.tree.TryStmt): try node

        This method processes the indented suite under the *try*, *except*,
        *else*, and *finally* statements.

        """
        children = iter(node.children)

        while True:
            try:
                key = next(children)
            except StopIteration:
                break

            # <Keyword: try | else | finally> | PythonNode(except_clause, [...]
            self._process(key)
            # <Operator: :>
            self._process(next(children))
            # suite
            self._process_suite_node(next(children))

    def _process_strings(self, node: parso.python.tree.PythonNode) -> None:
        """Process concatenable strings (:token:`stringliteral`).

        Args:
            node (parso.python.tree.PythonNode): concatentable strings node

        As in Python, adjacent string literals can be concatenated in certain
        cases, as described in the `documentation`_. Such concatenable strings
        may contain formatted string literals (:term:`f-string`) within its scope.

        .. _documentation: https://docs.python.org/3/reference/lexical_analysis.html#string-literal-concatenation

        """
        self._process_string_context(node)

    def _process_fstring(self, node: parso.python.tree.PythonNode) -> None:
        """Process formatted strings (:token:`f_string`).

        Args:
            node (parso.python.tree.PythonNode): formatted strings node

        """
        self._process_string_context(node)

    def _concat(self) -> None:
        """Concatenate final string.

        This method tries to inserted the runtime check decorator function
        at the very location where starts to contain positional-only parameters, i.e.
        between the converted code as :attr:`self._prefix <Context._prefix>` and
        :attr:`self._suffix <Context._suffix>`.

        The inserted code is rendered from :data:`DECORATOR_TEMPLATE`. If
        :attr:`self._pep8 <Context._pep8>` is :data:`True`, it will insert the code
        in compliance with :pep:`8`.

        """
        if self._dismiss or not self.has_expr(self._root):
            self._buffer += self._prefix + self._suffix
            return

        # strip suffix comments
        prefix, suffix = self.split_comments(self._suffix, self._linesep)
        match = re.match(r'^(?P<linesep>(%s)*)' % self._linesep, suffix, flags=re.ASCII)
        suffix_linesep = match.group('linesep') if match is not None else ''

        # first, the prefix code
        self._buffer += self._prefix + prefix + suffix_linesep
        if self._pep8 and self._buffer:
            if (self._node_before_expr is not None
                    and self._node_before_expr.type in ('funcdef', 'classdef')
                    and self._indent_level == 0):
                blank = 2
            else:
                blank = 1
            self._buffer += self._linesep * self.missing_newlines(prefix=self._buffer, suffix='',
                                                                  expected=blank, linesep=self._linesep)

        # then, the decorator function
        self._buffer += self._linesep.join(DECORATOR_TEMPLATE) % dict(
            decorator=self._decorator,
            indentation=self._indentation,
        ) + self._linesep

        # finally, the suffix code
        if self._pep8:
            self._buffer += self._linesep * self.missing_newlines(prefix=self._buffer, suffix='',
                                                                  expected=2, linesep=self._linesep)
        self._buffer += suffix.lstrip(self._linesep)

    @final
    @classmethod
    def has_expr(cls, node: parso.tree.NodeOrLeaf) -> bool:
        """Check if node has positional-only parameters.

        Args:
            node (parso.tree.NodeOrLeaf): parso AST

        Returns:
            bool: if ``node`` has positional-only parameters

        """
        if node.type == 'funcdef':
            return cls._check_funcdef(node)  # type: ignore[arg-type]
        if node.type == 'lambdef':
            return cls._check_lambdef(node)  # type: ignore[arg-type]
        if hasattr(node, 'children'):
            return any(map(cls.has_expr, node.children))  # type: ignore[attr-defined]
        return False

    # backward compatibility and auxiliary alias
    has_poseur = has_expr

    @final
    @classmethod
    def _check_funcdef(cls, node: parso.python.tree.Function) -> bool:
        """Check if :term:`function` definition contains positional-only parameters.

        Args:
            node (parso.python.tree.Function): function definition

        Returns:
            bool: if :term:`function` definition contains positional-only parameters

        """
        for child in node.children:
            if child.type == 'parameters':
                for param in child.children[1:-1]:
                    if param.type == 'operator':
                        if param.value == '/':
                            return True
                        continue
                    if param.default is not None and cls.has_expr(param.default):
                        return True
            elif cls.has_expr(child):  # suite / ...
                return True
        return False

    @final
    @classmethod
    def _check_lambdef(cls, node: parso.python.tree.Lambda) -> bool:
        """Check if :term:`lambda` definition contains positional-only parameters.

        Args:
            node (parso.python.tree.Lambda): lambda definition

        Returns:
            bool: if :term:`lambda` definition contains positional-only parameters

        """
        param = False
        suite = False
        for child in node.children:
            if child.type == 'param':
                if child.default is not None:
                    if cls.has_expr(child.default):
                        return True
                param = True
            elif child.type == 'operator' and child.value == ':':
                param = False
                suite = True
            elif param and child.type == 'operator' and child.value == '/':
                return True
            elif suite and cls.has_expr(child):
                return True
        return False

    @final
    def normalizer(self, name: str) -> str:
        """Variable name normalizer.

        If :attr:`self._cls_ctx <poseur.Context._cls_ctx>` is :data:`None`,
        the methold will simply class :meth:`~bpc_utils.context.BaseContext.normalize`
        on the variable name; otherwise, it will call
        :meth:`~bpc_utils.context.BaseContext.mangle` on the variable name.

        Args:
            name (str): variable name

        Returns:
            str: *normalized* variable name

        """
        if self._cls_ctx is None:
            return self.normalize(name)
        return self.mangle(self._cls_ctx, name)


class StringContext(Context):
    """String (f-string) conversion context.

    This class is mainly used for converting **formatted strings**.

    Args:
        node (parso.python.tree.PythonNode): parso AST
        config (Config): conversion configurations

    Keyword Args:
        clx_ctx (Optional[str]): class context name
        indent_level (int): current indentation level
        raw (Literal[True]): raw processing flag

    Note:
        * ``raw`` should always be :data:`True`.

    As the conversion in :class:`Context` changes the original expression,
    which may change the content of *debug f-string*.

    """

    def __init__(self, node: parso.python.tree.PythonNode, config: PoseurConfig, *,
                 clx_ctx: Optional[str] = None, indent_level: int = 0, raw: Literal[True] = True):
        # convert using f2format first
        prefix, suffix = self.extract_whitespaces(node.get_code())
        code = f2format.convert(node.get_code().strip())
        node = parso_parse(code, filename=config.filename, version=config.source_version)

        # call super init
        super().__init__(node=node, config=config, clx_ctx=clx_ctx,
                         indent_level=indent_level, raw=raw)
        self._buffer = prefix + self._buffer + suffix


###############################################################################
# Public Interface

exec(os.linesep.join(DECORATOR_TEMPLATE) % dict(decorator='decorator', indentation='    '))  # nosec: B102; pylint: disable=exec-used


def convert(code: Union[str, bytes], filename: Optional[str] = None, *,
            source_version: Optional[str] = None, linesep: Optional[Linesep] = None,
            indentation: Optional[Union[int, str]] = None, pep8: Optional[bool] = None,
            dismiss: Optional[bool] = None, decorator: Optional[str] = None) -> str:
    """Convert the given Python source code string.

    Args:
        code (Union[str, bytes]): the source code to be converted
        filename (Optional[str]): an optional source file name to provide a context in case of error

    Keyword Args:
        source_version (Optional[str]): parse the code as this Python version (uses the latest version by default)
        linesep (Optional[str]): line separator of code (``LF``, ``CRLF``, ``CR``) (auto detect by default)
        indentation (Optional[Union[int, str]]): code indentation style, specify an integer for the number of spaces,
            or ``'t'``/``'tab'`` for tabs (auto detect by default)
        pep8 (Optional[bool]): whether to make code insertion :pep:`8` compliant

    :Environment Variables:
     - :envvar:`POSEUR_SOURCE_VERSION` -- same as the ``source_version`` argument and the ``--source-version`` option
        in CLI
     - :envvar:`POSEUR_LINESEP` -- same as the `linesep` `argument` and the ``--linesep`` option in CLI
     - :envvar:`POSEUR_INDENTATION` -- same as the ``indentation`` argument and the ``--indentation`` option in CLI
     - :envvar:`POSEUR_PEP8` -- same as the ``pep8`` argument and the ``--no-pep8`` option in CLI (logical negation)
     - :envvar:`POSEUR_DISMISS` -- same as the ``--dismiss-runtime`` option in CLI
     - :envvar:`POSEUR_DECORATOR` -- same as the ``--decorator-name`` option in CLI

    Returns:
        str: converted source code

    Raises:
        ValueError: if ``decorator`` is not a valid identifier name or starts with double underscore

    """
    # parse source string
    source_version = _get_source_version_option(source_version)
    module = parso_parse(code, filename=filename, version=source_version)

    # get linesep, indentation and pep8 options
    linesep = _get_linesep_option(linesep)
    indentation = _get_indentation_option(indentation)
    if linesep is None:
        linesep = detect_linesep(code)
    if indentation is None:
        indentation = detect_indentation(code)
    pep8 = _get_pep8_option(pep8)
    dismiss = _get_dismiss_option(dismiss)
    decorator = _get_decorator_option(decorator)

    # validate that decorator name is valid identifier
    if not decorator.isidentifier():
        raise ValueError('name of decorator for runtime checks is not a valid identifier name: %r' % decorator)

    # prevent using class-private names and dunder names
    if decorator.startswith('__'):
        raise ValueError('name of decorator for runtime checks should not start with double underscore')

    # pack conversion configuration
    config = Config(linesep=linesep, indentation=indentation, pep8=pep8,
                    filename=filename, source_version=source_version,
                    dismiss=dismiss, decorator=decorator)

    # convert source string
    result = Context(module, config).string  # type: ignore[arg-type]

    # return conversion result
    return result


def poseur(filename: str, *, source_version: Optional[str] = None, linesep: Optional[Linesep] = None,
           indentation: Optional[Union[int, str]] = None, pep8: Optional[bool] = None,
           dismiss: Optional[bool] = None, decorator: Optional[str] = None,
           quiet: Optional[bool] = None, dry_run: bool = False) -> None:
    """Convert the given Python source code file. The file will be overwritten.

    Args:
        filename (str): the file to convert

    Keyword Args:
        source_version (Optional[str]): parse the code as this Python version (uses the latest version by default)
        linesep (Optional[str]): line separator of code (``LF``, ``CRLF``, ``CR``) (auto detect by default)
        indentation (Optional[Union[int, str]]): code indentation style, specify an integer for the number of spaces,
            or ``'t'``/``'tab'`` for tabs (auto detect by default)
        pep8 (Optional[bool]): whether to make code insertion :pep:`8` compliant
        quiet (Optional[bool]): whether to run in quiet mode
        dry_run (bool): if :data:`True`, only print the name of the file to convert but do not perform any conversion

    :Environment Variables:
     - :envvar:`POSEUR_SOURCE_VERSION` -- same as the ``source-version`` argument and the ``--source-version`` option
        in CLI
     - :envvar:`POSEUR_LINESEP` -- same as the ``linesep`` argument and the ``--linesep`` option in CLI
     - :envvar:`POSEUR_INDENTATION` -- same as the ``indentation`` argument and the ``--indentation`` option in CLI
     - :envvar:`POSEUR_PEP8` -- same as the ``pep8`` argument and the ``--no-pep8`` option in CLI (logical negation)
     - :envvar:`POSEUR_QUIET` -- same as the ``quiet`` argument and the ``--quiet`` option in CLI
     - :envvar:`POSEUR_DISMISS` -- same as the ``--dismiss-runtime`` option in CLI
     - :envvar:`POSEUR_DECORATOR` -- same as the ``--decorator-name`` option in CLI

    """
    quiet = _get_quiet_option(quiet)
    if not quiet:
        with TaskLock():
            print('Now converting: %r' % filename, file=sys.stderr)
    if dry_run:
        return

    # read file content
    with open(filename, 'rb') as file:
        content = file.read()

    # detect source code encoding
    encoding = detect_encoding(content)

    # get linesep and indentation
    linesep = _get_linesep_option(linesep)
    indentation = _get_indentation_option(indentation)
    if linesep is None or indentation is None:
        with open(filename, 'r', encoding=encoding) as file:
            if linesep is None:
                linesep = detect_linesep(file)
            if indentation is None:
                indentation = detect_indentation(file)

    # do the dirty things
    result = convert(content, filename=filename, source_version=source_version,
                     linesep=linesep, indentation=indentation, pep8=pep8,
                     dismiss=dismiss, decorator=decorator)

    # overwrite the file with conversion result
    with open(filename, 'w', encoding=encoding, newline='') as file:
        file.write(result)


###############################################################################
# CLI & Entry Point

# option values display
# these values are only intended for argparse help messages
# this shows default values by default, environment variables may override them
__cwd__ = os.getcwd()
__poseur_quiet__ = 'quiet mode' if _get_quiet_option() else 'non-quiet mode'
__poseur_concurrency__ = _get_concurrency_option() or 'auto detect'
__poseur_do_archive__ = 'will do archive' if _get_do_archive_option() else 'will not do archive'
__poseur_archive_path__ = os.path.join(__cwd__, _get_archive_path_option())
__poseur_source_version__ = _get_source_version_option()
__poseur_linesep__ = {
    '\n': 'LF',
    '\r\n': 'CRLF',
    '\r': 'CR',
    None: 'auto detect'
}[_get_linesep_option()]
__poseur_indentation__ = _get_indentation_option()
if __poseur_indentation__ is None:
    __poseur_indentation__ = 'auto detect'
elif __poseur_indentation__ == '\t':
    __poseur_indentation__ = 'tab'
else:
    __poseur_indentation__ = '%d spaces' % len(__poseur_indentation__)
__poseur_pep8__ = 'will conform to PEP 8' if _get_pep8_option() else 'will not conform to PEP 8'
__poseur_dismiss__ = 'will dismiss runtime checks' if _get_dismiss_option() else 'will not dismiss runtime checks'
__poseur_decorator__ = _get_decorator_option() or '_poseur_decorator'


def get_parser() -> argparse.ArgumentParser:
    """Generate CLI parser.

    Returns:
        argparse.ArgumentParser: CLI parser for poseur

    """
    parser = argparse.ArgumentParser(prog='poseur',
                                     usage='poseur [options] <Python source files and directories...>',
                                     description='Back-port compiler for Python 3.8 position-only parameters.')
    parser.add_argument('-V', '--version', action='version', version=__version__)
    parser.add_argument('-q', '--quiet', action='store_true', default=None,
                        help='run in quiet mode (current: %s)' % __poseur_quiet__)
    parser.add_argument('-C', '--concurrency', action='store', type=int, metavar='N',
                        help='the number of concurrent processes for conversion (current: %s)' % __poseur_concurrency__)
    parser.add_argument('--dry-run', action='store_true',
                        help='list the files to be converted without actually performing conversion and archiving')
    parser.add_argument('-s', '--simple', action='store', nargs='?', dest='simple_args', const='', metavar='FILE',
                        help='this option tells the program to operate in "simple mode"; '
                             'if a file name is provided, the program will convert the file but print conversion '
                             'result to standard output instead of overwriting the file; '
                             'if no file names are provided, read code for conversion from standard input and print '
                             'conversion result to standard output; '
                             'in "simple mode", no file names shall be provided via positional arguments')

    archive_group = parser.add_argument_group(title='archive options',
                                              description="backup original files in case there're any issues")
    archive_group.add_argument('-na', '--no-archive', action='store_false', dest='do_archive', default=None,
                               help='do not archive original files (current: %s)' % __poseur_do_archive__)
    archive_group.add_argument('-k', '--archive-path', action='store', default=__poseur_archive_path__, metavar='PATH',
                               help='path to archive original files (current: %(default)s)')
    archive_group.add_argument('-r', '--recover', action='store', dest='recover_file', metavar='ARCHIVE_FILE',
                               help='recover files from a given archive file')
    archive_group.add_argument('-r2', action='store_true', help='remove the archive file after recovery')
    archive_group.add_argument('-r3', action='store_true', help='remove the archive file after recovery, '
                                                                'and remove the archive directory if it becomes empty')

    # TODO: revise ``--dismiss-runtime`` & ``--decorator-name`` options
    convert_group = parser.add_argument_group(title='convert options', description='conversion configuration')
    convert_group.add_argument('-vs', '-vf', '--source-version', '--from-version', action='store', metavar='VERSION',
                               default=__poseur_source_version__, choices=POSEUR_SOURCE_VERSIONS,
                               help='parse source code as this Python version (current: %(default)s)')
    convert_group.add_argument('-l', '--linesep', action='store',
                               help='line separator (LF, CRLF, CR) to read '
                                    'source files (current: %s)' % __poseur_linesep__)
    convert_group.add_argument('-t', '--indentation', action='store', metavar='INDENT',
                               help='code indentation style, specify an integer for the number of spaces, '
                                    "or 't'/'tab' for tabs (current: %s)" % __poseur_indentation__)
    convert_group.add_argument('-n8', '--no-pep8', action='store_false', dest='pep8', default=None,
                               help='do not make code insertion PEP 8 compliant (current: %s)' % __poseur_pep8__)
    convert_group.add_argument('-nr', '--dismiss-runtime', action='store_true', dest='dismiss', default=None,
                               help='dismiss runtime checks for positional-only parameters (current: %s)' % __poseur_dismiss__)  # pylint: disable=line-too-long
    convert_group.add_argument('-d', '--decorator-name', action='store', dest='decorator', metavar='NAME',
                               default=__poseur_decorator__, help='name of decorator for runtime checks (current: %s)' % __poseur_decorator__)  # pylint: disable=line-too-long

    parser.add_argument('files', action='store', nargs='*', metavar='<Python source files and directories...>',
                        help='Python source files and directories to be converted')

    return parser


def do_poseur(filename: str, **kwargs: object) -> None:
    """Wrapper function to catch exceptions."""
    try:
        poseur(filename, **kwargs)  # type: ignore[arg-type]
    except Exception:  # pylint: disable=broad-except
        with TaskLock():
            print('Failed to convert file: %r' % filename, file=sys.stderr)
            traceback.print_exc()


def main(argv: Optional[List[str]] = None) -> int:
    """Entry point for poseur.

    Args:
        argv (Optional[List[str]]): CLI arguments

    :Environment Variables:
     - :envvar:`POSEUR_QUIET` -- same as the ``--quiet`` option in CLI
     - :envvar:`POSEUR_CONCURRENCY` -- same as the ``--concurrency`` option in CLI
     - :envvar:`POSEUR_DO_ARCHIVE` -- same as the ``--no-archive`` option in CLI (logical negation)
     - :envvar:`POSEUR_ARCHIVE_PATH` -- same as the ``--archive-path`` option in CLI
     - :envvar:`POSEUR_SOURCE_VERSION` -- same as the ``--source-version`` option in CLI
     - :envvar:`POSEUR_LINESEP` -- same as the ``--linesep`` option in CLI
     - :envvar:`POSEUR_INDENTATION` -- same as the ``--indentation`` option in CLI
     - :envvar:`POSEUR_PEP8` -- same as the ``--no-pep8`` option in CLI (logical negation)
     - :envvar:`POSEUR_DISMISS` -- same as the ``--dismiss-runtime`` option in CLI
     - :envvar:`POSEUR_DECORATOR` -- same as the ``--decorator-name`` option in CLI

    """
    parser = get_parser()
    args = parser.parse_args(argv)

    options = {
        'source_version': args.source_version,
        'linesep': args.linesep,
        'indentation': args.indentation,
        'pep8': args.pep8,
        'dismiss': args.dismiss,
        'decorator': args.decorator,
    }

    # check if running in simple mode
    if args.simple_args is not None:
        if args.files:
            parser.error('no Python source files or directories shall be given as positional arguments in simple mode')
        if not args.simple_args:  # read from stdin
            code = sys.stdin.read()
        else:  # read from file
            filename = args.simple_args
            options['filename'] = filename
            with open(filename, 'rb') as file:
                code = file.read()
        sys.stdout.write(convert(code, **options))  # print conversion result to stdout
        return 0

    # get options
    quiet = _get_quiet_option(args.quiet)
    processes = _get_concurrency_option(args.concurrency)
    do_archive = _get_do_archive_option(args.do_archive)
    archive_path = _get_archive_path_option(args.archive_path)

    # check if doing recovery
    if args.recover_file:
        recover_files(args.recover_file)
        if not args.quiet:
            print('Recovered files from archive: %r' % args.recover_file, file=sys.stderr)
        # TODO: maybe implement deletion in bpc-utils?
        if args.r2 or args.r3:
            os.remove(args.recover_file)
            if args.r3:
                archive_dir = os.path.dirname(os.path.realpath(args.recover_file))
                if not os.listdir(archive_dir):
                    os.rmdir(archive_dir)
        return 0

    # fetch file list
    if not args.files:
        parser.error('no Python source files or directories are given')
    filelist = sorted(detect_files(args.files))

    # terminate if no valid Python source files detected
    if not filelist:
        if not args.quiet:
            # TODO: maybe use parser.error?
            print('Warning: no valid Python source files found in %r' % args.files, file=sys.stderr)
        return 1

    # make archive
    if do_archive and not args.dry_run:
        archive_files(filelist, archive_path)

    # process files
    options.update({
        'quiet': quiet,
        'dry_run': args.dry_run,
    })
    map_tasks(do_poseur, filelist, kwargs=options, processes=processes)

    return 0


if __name__ == '__main__':
    sys.exit(main())
