import itertools
import math
from collections.abc import Callable, Iterator, Sequence
from dataclasses import dataclass, field
from typing import Optional, TypeVar, Union, cast

from black.brackets import COMMA_PRIORITY, DOT_PRIORITY, BracketTracker
from black.mode import Mode, Preview
from black.nodes import (
    BRACKETS,
    CLOSING_BRACKETS,
    OPENING_BRACKETS,
    STANDALONE_COMMENT,
    TEST_DESCENDANTS,
    child_towards,
    is_docstring,
    is_import,
    is_multiline_string,
    is_one_sequence_between,
    is_type_comment,
    is_type_ignore_comment,
    is_with_or_async_with_stmt,
    make_simple_prefix,
    replace_child,
    syms,
    whitespace,
)
from black.strings import str_width
from blib2to3.pgen2 import token
from blib2to3.pytree import Leaf, Node

# types
T = TypeVar("T")
Index = int
LeafID = int
LN = Union[Leaf, Node]


@dataclass
class Line:
    """Holds leaves and comments. Can be printed with `str(line)`."""

    mode: Mode = field(repr=False)
    depth: int = 0
    leaves: list[Leaf] = field(default_factory=list)
    # keys ordered like `leaves`
    comments: dict[LeafID, list[Leaf]] = field(default_factory=dict)
    bracket_tracker: BracketTracker = field(default_factory=BracketTracker)
    inside_brackets: bool = False
    should_split_rhs: bool = False
    magic_trailing_comma: Leaf | None = None

    def append(
        self, leaf: Leaf, preformatted: bool = False, track_bracket: bool = False
    ) -> None:
        """Add a new `leaf` to the end of the line.

        Unless `preformatted` is True, the `leaf` will receive a new consistent
        whitespace prefix and metadata applied by :class:`BracketTracker`.
        Trailing commas are maybe removed, unpacked for loop variables are
        demoted from being delimiters.

        Inline comments are put aside.
        """
        has_value = (
            leaf.type in BRACKETS
            # empty fstring and tstring middles must not be truncated
            or leaf.type in (token.FSTRING_MIDDLE, token.TSTRING_MIDDLE)
            or bool(leaf.value.strip())
        )
        if not has_value:
            return

        if leaf.type == token.COLON and self.is_class_paren_empty:
            del self.leaves[-2:]
        if self.leaves and not preformatted:
            # Note: at this point leaf.prefix should be empty except for
            # imports, for which we only preserve newlines.
            leaf.prefix += whitespace(
                leaf,
                complex_subscript=self.is_complex_subscript(leaf),
                mode=self.mode,
            )
        if self.inside_brackets or not preformatted or track_bracket:
            self.bracket_tracker.mark(leaf)
            if self.mode.magic_trailing_comma:
                if self.has_magic_trailing_comma(leaf):
                    self.magic_trailing_comma = leaf
            elif self.has_magic_trailing_comma(leaf):
                self.remove_trailing_comma()
        if not self.append_comment(leaf):
            self.leaves.append(leaf)

    def append_safe(self, leaf: Leaf, preformatted: bool = False) -> None:
        """Like :func:`append()` but disallow invalid standalone comment structure.

        Raises ValueError when any `leaf` is appended after a standalone comment
        or when a standalone comment is not the first leaf on the line.
        """
        if (
            self.bracket_tracker.depth == 0
            or self.bracket_tracker.any_open_for_or_lambda()
        ):
            if self.is_comment:
                raise ValueError("cannot append to standalone comments")

            if self.leaves and leaf.type == STANDALONE_COMMENT:
                raise ValueError(
                    "cannot append standalone comments to a populated line"
                )

        self.append(leaf, preformatted=preformatted)

    @property
    def is_comment(self) -> bool:
        """Is this line a standalone comment?"""
        return len(self.leaves) == 1 and self.leaves[0].type == STANDALONE_COMMENT

    @property
    def is_decorator(self) -> bool:
        """Is this line a decorator?"""
        return bool(self) and self.leaves[0].type == token.AT

    @property
    def is_import(self) -> bool:
        """Is this an import line?"""
        return bool(self) and is_import(self.leaves[0])

    @property
    def is_with_or_async_with_stmt(self) -> bool:
        """Is this a with_stmt line?"""
        return bool(self) and is_with_or_async_with_stmt(self.leaves[0])

    @property
    def is_class(self) -> bool:
        """Is this line a class definition?"""
        return (
            bool(self)
            and self.leaves[0].type == token.NAME
            and self.leaves[0].value == "class"
        )

    @property
    def is_stub_class(self) -> bool:
        """Is this line a class definition with a body consisting only of "..."?"""
        return self.is_class and self.leaves[-3:] == [
            Leaf(token.DOT, ".") for _ in range(3)
        ]

    @property
    def is_def(self) -> bool:
        """Is this a function definition? (Also returns True for async defs.)"""
        try:
            first_leaf = self.leaves[0]
        except IndexError:
            return False

        try:
            second_leaf: Leaf | None = self.leaves[1]
        except IndexError:
            second_leaf = None
        return (first_leaf.type == token.NAME and first_leaf.value == "def") or (
            first_leaf.type == token.ASYNC
            and second_leaf is not None
            and second_leaf.type == token.NAME
            and second_leaf.value == "def"
        )

    @property
    def is_stub_def(self) -> bool:
        """Is this line a function definition with a body consisting only of "..."?"""
        return self.is_def and self.leaves[-4:] == [Leaf(token.COLON, ":")] + [
            Leaf(token.DOT, ".") for _ in range(3)
        ]

    @property
    def is_class_paren_empty(self) -> bool:
        """Is this a class with no base classes but using parentheses?

        Those are unnecessary and should be removed.
        """
        return (
            bool(self)
            and len(self.leaves) == 4
            and self.is_class
            and self.leaves[2].type == token.LPAR
            and self.leaves[2].value == "("
            and self.leaves[3].type == token.RPAR
            and self.leaves[3].value == ")"
        )

    @property
    def _is_triple_quoted_string(self) -> bool:
        """Is the line a triple quoted string?"""
        if not self or self.leaves[0].type != token.STRING:
            return False
        value = self.leaves[0].value
        if value.startswith(('"""', "'''")):
            return True
        if value.startswith(("r'''", 'r"""', "R'''", 'R"""')):
            return True
        return False

    @property
    def is_docstring(self) -> bool:
        """Is the line a docstring?"""
        return bool(self) and is_docstring(self.leaves[0])

    @property
    def is_chained_assignment(self) -> bool:
        """Is the line a chained assignment"""
        return [leaf.type for leaf in self.leaves].count(token.EQUAL) > 1

    @property
    def opens_block(self) -> bool:
        """Does this line open a new level of indentation."""
        if len(self.leaves) == 0:
            return False
        return self.leaves[-1].type == token.COLON

    def is_fmt_pass_converted(
        self, *, first_leaf_matches: Callable[[Leaf], bool] | None = None
    ) -> bool:
        """Is this line converted from fmt off/skip code?

        If first_leaf_matches is not None, it only returns True if the first
        leaf of converted code matches.
        """
        if len(self.leaves) != 1:
            return False
        leaf = self.leaves[0]
        if (
            leaf.type != STANDALONE_COMMENT
            or leaf.fmt_pass_converted_first_leaf is None
        ):
            return False
        return first_leaf_matches is None or first_leaf_matches(
            leaf.fmt_pass_converted_first_leaf
        )

    def contains_standalone_comments(self) -> bool:
        """If so, needs to be split before emitting."""
        for leaf in self.leaves:
            if leaf.type == STANDALONE_COMMENT:
                return True

        return False

    def contains_implicit_multiline_string_with_comments(self) -> bool:
        """Chck if we have an implicit multiline string with comments on the line"""
        for leaf_type, leaf_group_iterator in itertools.groupby(
            self.leaves, lambda leaf: leaf.type
        ):
            if leaf_type != token.STRING:
                continue
            leaf_list = list(leaf_group_iterator)
            if len(leaf_list) == 1:
                continue
            for leaf in leaf_list:
                if self.comments_after(leaf):
                    return True
        return False

    def contains_uncollapsable_type_comments(self) -> bool:
        ignored_ids = set()
        try:
            last_leaf = self.leaves[-1]
            ignored_ids.add(id(last_leaf))
            if last_leaf.type == token.COMMA or (
                last_leaf.type == token.RPAR and not last_leaf.value
            ):
                # When trailing commas or optional parens are inserted by Black for
                # consistency, comments after the previous last element are not moved
                # (they don't have to, rendering will still be correct).  So we ignore
                # trailing commas and invisible.
                last_leaf = self.leaves[-2]
                ignored_ids.add(id(last_leaf))
        except IndexError:
            return False

        # A type comment is uncollapsable if it is attached to a leaf
        # that isn't at the end of the line (since that could cause it
        # to get associated to a different argument) or if there are
        # comments before it (since that could cause it to get hidden
        # behind a comment.
        comment_seen = False
        for leaf_id, comments in self.comments.items():
            for comment in comments:
                if is_type_comment(comment, mode=self.mode):
                    if comment_seen or (
                        not is_type_ignore_comment(comment, mode=self.mode)
                        and leaf_id not in ignored_ids
                    ):
                        return True

                comment_seen = True

        return False

    def contains_unsplittable_type_ignore(self) -> bool:
        if not self.leaves:
            return False

        # If a 'type: ignore' is attached to the end of a line, we
        # can't split the line, because we can't know which of the
        # subexpressions the ignore was meant to apply to.
        #
        # We only want this to apply to actual physical lines from the
        # original source, though: we don't want the presence of a
        # 'type: ignore' at the end of a multiline expression to
        # justify pushing it all onto one line. Thus we
        # (unfortunately) need to check the actual source lines and
        # only report an unsplittable 'type: ignore' if this line was
        # one line in the original code.

        # Grab the first and last line numbers, skipping generated leaves
        first_line = next((leaf.lineno for leaf in self.leaves if leaf.lineno != 0), 0)
        last_line = next(
            (leaf.lineno for leaf in reversed(self.leaves) if leaf.lineno != 0), 0
        )

        if first_line == last_line:
            # We look at the last two leaves since a comma or an
            # invisible paren could have been added at the end of the
            # line.
            for node in self.leaves[-2:]:
                for comment in self.comments.get(id(node), []):
                    if is_type_ignore_comment(comment, mode=self.mode):
                        return True

        return False

    def contains_multiline_strings(self) -> bool:
        return any(is_multiline_string(leaf) for leaf in self.leaves)

    def has_magic_trailing_comma(self, closing: Leaf) -> bool:
        """Return True if we have a magic trailing comma, that is when:
        - there's a trailing comma here
        - it's not from single-element square bracket indexing
        - it's not a one-tuple
        """
        if not (
            closing.type in CLOSING_BRACKETS
            and self.leaves
            and self.leaves[-1].type == token.COMMA
        ):
            return False

        if closing.type == token.RBRACE:
            return True

        if closing.type == token.RSQB:
            if (
                closing.parent is not None
                and closing.parent.type == syms.trailer
                and closing.opening_bracket is not None
                and is_one_sequence_between(
                    closing.opening_bracket,
                    closing,
                    self.leaves,
                    brackets=(token.LSQB, token.RSQB),
                )
            ):
                assert closing.prev_sibling is not None
                assert closing.prev_sibling.type == syms.subscriptlist
                return False

            return True

        if self.is_import:
            return True

        if closing.opening_bracket is not None and not is_one_sequence_between(
            closing.opening_bracket, closing, self.leaves
        ):
            return True

        return False

    def append_comment(self, comment: Leaf) -> bool:
        """Add an inline or standalone comment to the line."""
        if (
            comment.type == STANDALONE_COMMENT
            and self.bracket_tracker.any_open_brackets()
        ):
            comment.prefix = ""
            return False

        if comment.type != token.COMMENT:
            return False

        if not self.leaves:
            comment.type = STANDALONE_COMMENT
            comment.prefix = ""
            return False

        last_leaf = self.leaves[-1]
        if (
            last_leaf.type == token.RPAR
            and not last_leaf.value
            and last_leaf.parent
            and len(list(last_leaf.parent.leaves())) <= 3
            and not is_type_comment(comment, mode=self.mode)
        ):
            # Comments on an optional parens wrapping a single leaf should belong to
            # the wrapped node except if it's a type comment. Pinning the comment like
            # this avoids unstable formatting caused by comment migration.
            if len(self.leaves) < 2:
                comment.type = STANDALONE_COMMENT
                comment.prefix = ""
                return False

            last_leaf = self.leaves[-2]
        self.comments.setdefault(id(last_leaf), []).append(comment)
        return True

    def comments_after(self, leaf: Leaf) -> list[Leaf]:
        """Generate comments that should appear directly after `leaf`."""
        return self.comments.get(id(leaf), [])

    def remove_trailing_comma(self) -> None:
        """Remove the trailing comma and moves the comments attached to it."""
        trailing_comma = self.leaves.pop()
        trailing_comma_comments = self.comments.pop(id(trailing_comma), [])
        self.comments.setdefault(id(self.leaves[-1]), []).extend(
            trailing_comma_comments
        )

    def is_complex_subscript(self, leaf: Leaf) -> bool:
        """Return True iff `leaf` is part of a slice with non-trivial exprs."""
        open_lsqb = self.bracket_tracker.get_open_lsqb()
        if open_lsqb is None:
            return False

        subscript_start = open_lsqb.next_sibling

        if isinstance(subscript_start, Node):
            if subscript_start.type == syms.listmaker:
                return False

            if subscript_start.type == syms.subscriptlist:
                subscript_start = child_towards(subscript_start, leaf)

        return subscript_start is not None and any(
            n.type in TEST_DESCENDANTS for n in subscript_start.pre_order()
        )

    def enumerate_with_length(
        self, is_reversed: bool = False
    ) -> Iterator[tuple[Index, Leaf, int]]:
        """Return an enumeration of leaves with their length.

        Stops prematurely on multiline strings and standalone comments.
        """
        op = cast(
            Callable[[Sequence[Leaf]], Iterator[tuple[Index, Leaf]]],
            enumerate_reversed if is_reversed else enumerate,
        )
        for index, leaf in op(self.leaves):
            length = len(leaf.prefix) + len(leaf.value)
            if "\n" in leaf.value:
                return  # Multiline strings, we can't continue.

            for comment in self.comments_after(leaf):
                length += len(comment.value)

            yield index, leaf, length

    def clone(self) -> "Line":
        return Line(
            mode=self.mode,
            depth=self.depth,
            inside_brackets=self.inside_brackets,
            should_split_rhs=self.should_split_rhs,
            magic_trailing_comma=self.magic_trailing_comma,
        )

    def __str__(self) -> str:
        """Render the line."""
        if not self:
            return "\n"

        indent = "    " * self.depth
        leaves = iter(self.leaves)
        first = next(leaves)
        res = f"{first.prefix}{indent}{first.value}"
        res += "".join(str(leaf) for leaf in leaves)
        comments_iter = itertools.chain.from_iterable(self.comments.values())
        comments = [str(comment) for comment in comments_iter]
        res += "".join(comments)

        return res + "\n"

    def __bool__(self) -> bool:
        """Return True if the line has leaves or comments."""
        return bool(self.leaves or self.comments)


@dataclass
class RHSResult:
    """Intermediate split result from a right hand split."""

    head: Line
    body: Line
    tail: Line
    opening_bracket: Leaf
    closing_bracket: Leaf


@dataclass
class LinesBlock:
    """Class that holds information about a block of formatted lines.

    This is introduced so that the EmptyLineTracker can look behind the standalone
    comments and adjust their empty lines for class or def lines.
    """

    mode: Mode
    previous_block: Optional["LinesBlock"]
    original_line: Line
    before: int = 0
    content_lines: list[str] = field(default_factory=list)
    after: int = 0
    form_feed: bool = False

    def all_lines(self) -> list[str]:
        empty_line = str(Line(mode=self.mode))
        prefix = make_simple_prefix(self.before, self.form_feed, empty_line)
        return [prefix] + self.content_lines + [empty_line * self.after]


@dataclass
class EmptyLineTracker:
    """Provides a stateful method that returns the number of potential extra
    empty lines needed before and after the currently processed line.

    Note: this tracker works on lines that haven't been split yet.  It assumes
    the prefix of the first leaf consists of optional newlines.  Those newlines
    are consumed by `maybe_empty_lines()` and included in the computation.
    """

    mode: Mode
    previous_line: Line | None = None
    previous_block: LinesBlock | None = None
    previous_defs: list[Line] = field(default_factory=list)
    semantic_leading_comment: LinesBlock | None = None

    def maybe_empty_lines(self, current_line: Line) -> LinesBlock:
        """Return the number of extra empty lines before and after the `current_line`.

        This is for separating `def`, `async def` and `class` with extra empty
        lines (two on module-level).
        """
        form_feed = (
            current_line.depth == 0
            and bool(current_line.leaves)
            and "\f\n" in current_line.leaves[0].prefix
        )
        before, after = self._maybe_empty_lines(current_line)
        previous_after = self.previous_block.after if self.previous_block else 0
        before = max(0, before - previous_after)
        if Preview.fix_module_docstring_detection in self.mode:
            # Always have one empty line after a module docstring
            if self._line_is_module_docstring(current_line):
                before = 1
        else:
            if (
                # Always have one empty line after a module docstring
                self.previous_block
                and self.previous_block.previous_block is None
                and len(self.previous_block.original_line.leaves) == 1
                and self.previous_block.original_line.is_docstring
                and not (current_line.is_class or current_line.is_def)
            ):
                before = 1

        block = LinesBlock(
            mode=self.mode,
            previous_block=self.previous_block,
            original_line=current_line,
            before=before,
            after=after,
            form_feed=form_feed,
        )

        # Maintain the semantic_leading_comment state.
        if current_line.is_comment:
            if self.previous_line is None or (
                not self.previous_line.is_decorator
                # `or before` means this comment already has an empty line before
                and (not self.previous_line.is_comment or before)
                and (self.semantic_leading_comment is None or before)
            ):
                self.semantic_leading_comment = block
        # `or before` means this decorator already has an empty line before
        elif not current_line.is_decorator or before:
            self.semantic_leading_comment = None

        self.previous_line = current_line
        self.previous_block = block
        return block

    def _line_is_module_docstring(self, current_line: Line) -> bool:
        previous_block = self.previous_block
        if not previous_block:
            return False
        if (
            len(previous_block.original_line.leaves) != 1
            or not previous_block.original_line.is_docstring
            or current_line.is_class
            or current_line.is_def
        ):
            return False
        while previous_block := previous_block.previous_block:
            if not previous_block.original_line.is_comment:
                return False
        return True

    def _maybe_empty_lines(self, current_line: Line) -> tuple[int, int]:
        max_allowed = 1
        if current_line.depth == 0:
            max_allowed = 1 if self.mode.is_pyi else 2

        if current_line.leaves:
            # Consume the first leaf's extra newlines.
            first_leaf = current_line.leaves[0]
            before = first_leaf.prefix.count("\n")
            before = min(before, max_allowed)
            first_leaf.prefix = ""
        else:
            before = 0

        user_had_newline = bool(before)
        depth = current_line.depth

        # Mutate self.previous_defs, remainder of this function should be pure
        previous_def = None
        while self.previous_defs and self.previous_defs[-1].depth >= depth:
            previous_def = self.previous_defs.pop()
        if current_line.is_def or current_line.is_class:
            self.previous_defs.append(current_line)

        if self.previous_line is None:
            # Don't insert empty lines before the first line in the file.
            return 0, 0

        if current_line.is_docstring:
            if self.previous_line.is_class:
                return 0, 1
            if self.previous_line.opens_block and self.previous_line.is_def:
                return 0, 0

        if previous_def is not None:
            assert self.previous_line is not None
            if self.mode.is_pyi:
                if previous_def.is_class and not previous_def.is_stub_class:
                    before = 1
                elif depth and not current_line.is_def and self.previous_line.is_def:
                    # Empty lines between attributes and methods should be preserved.
                    before = 1 if user_had_newline else 0
                elif depth:
                    before = 0
                else:
                    before = 1
            else:
                if depth:
                    before = 1
                elif (
                    not depth
                    and previous_def.depth
                    and current_line.leaves[-1].type == token.COLON
                    and (
                        current_line.leaves[0].value
                        not in ("with", "try", "for", "while", "if", "match")
                    )
                ):
                    # We shouldn't add two newlines between an indented function and
                    # a dependent non-indented clause. This is to avoid issues with
                    # conditional function definitions that are technically top-level
                    # and therefore get two trailing newlines, but look weird and
                    # inconsistent when they're followed by elif, else, etc. This is
                    # worse because these functions only get *one* preceding newline
                    # already.
                    before = 1
                else:
                    before = 2

        if current_line.is_decorator or current_line.is_def or current_line.is_class:
            return self._maybe_empty_lines_for_class_or_def(
                current_line, before, user_had_newline
            )

        if (
            self.previous_line.is_import
            and self.previous_line.depth == 0
            and current_line.depth == 0
            and not current_line.is_import
            and not current_line.is_fmt_pass_converted(first_leaf_matches=is_import)
            and Preview.always_one_newline_after_import in self.mode
        ):
            return 1, 0

        if (
            self.previous_line.is_import
            and not current_line.is_import
            and not current_line.is_fmt_pass_converted(first_leaf_matches=is_import)
            and depth == self.previous_line.depth
        ):
            return (before or 1), 0

        return before, 0

    def _maybe_empty_lines_for_class_or_def(
        self, current_line: Line, before: int, user_had_newline: bool
    ) -> tuple[int, int]:
        assert self.previous_line is not None

        if self.previous_line.is_decorator:
            if self.mode.is_pyi and current_line.is_stub_class:
                # Insert an empty line after a decorated stub class
                return 0, 1
            return 0, 0

        if self.previous_line.depth < current_line.depth and (
            self.previous_line.is_class or self.previous_line.is_def
        ):
            if self.mode.is_pyi:
                return 0, 0
            return 1 if user_had_newline else 0, 0

        comment_to_add_newlines: LinesBlock | None = None
        if (
            self.previous_line.is_comment
            and self.previous_line.depth == current_line.depth
            and before == 0
        ):
            slc = self.semantic_leading_comment
            if (
                slc is not None
                and slc.previous_block is not None
                and not slc.previous_block.original_line.is_class
                and not slc.previous_block.original_line.opens_block
                and slc.before <= 1
            ):
                comment_to_add_newlines = slc
            else:
                return 0, 0

        if self.mode.is_pyi:
            if current_line.is_class or self.previous_line.is_class:
                if self.previous_line.depth < current_line.depth:
                    newlines = 0
                elif self.previous_line.depth > current_line.depth:
                    newlines = 1
                elif current_line.is_stub_class and self.previous_line.is_stub_class:
                    # No blank line between classes with an empty body
                    newlines = 0
                else:
                    newlines = 1
            # Don't inspect the previous line if it's part of the body of the previous
            # statement in the same level, we always want a blank line if there's
            # something with a body preceding.
            elif self.previous_line.depth > current_line.depth:
                newlines = 1
            elif (
                current_line.is_def or current_line.is_decorator
            ) and not self.previous_line.is_def:
                if current_line.depth:
                    # In classes empty lines between attributes and methods should
                    # be preserved.
                    newlines = min(1, before)
                else:
                    # Blank line between a block of functions (maybe with preceding
                    # decorators) and a block of non-functions
                    newlines = 1
            else:
                newlines = 0
        else:
            newlines = 1 if current_line.depth else 2
            # If a user has left no space after a dummy implementation, don't insert
            # new lines. This is useful for instance for @overload or Protocols.
            if self.previous_line.is_stub_def and not user_had_newline:
                newlines = 0
        if comment_to_add_newlines is not None:
            previous_block = comment_to_add_newlines.previous_block
            if previous_block is not None:
                comment_to_add_newlines.before = (
                    max(comment_to_add_newlines.before, newlines) - previous_block.after
                )
                newlines = 0
        return newlines, 0


def enumerate_reversed(sequence: Sequence[T]) -> Iterator[tuple[Index, T]]:
    """Like `reversed(enumerate(sequence))` if that were possible."""
    index = len(sequence) - 1
    for element in reversed(sequence):
        yield (index, element)
        index -= 1


def append_leaves(
    new_line: Line, old_line: Line, leaves: list[Leaf], preformatted: bool = False
) -> None:
    """
    Append leaves (taken from @old_line) to @new_line, making sure to fix the
    underlying Node structure where appropriate.

    All of the leaves in @leaves are duplicated. The duplicates are then
    appended to @new_line and used to replace their originals in the underlying
    Node structure. Any comments attached to the old leaves are reattached to
    the new leaves.

    Pre-conditions:
        set(@leaves) is a subset of set(@old_line.leaves).
    """
    for old_leaf in leaves:
        new_leaf = Leaf(old_leaf.type, old_leaf.value)
        replace_child(old_leaf, new_leaf)
        new_line.append(new_leaf, preformatted=preformatted)

        for comment_leaf in old_line.comments_after(old_leaf):
            new_line.append(comment_leaf, preformatted=True)


def is_line_short_enough(line: Line, *, mode: Mode, line_str: str = "") -> bool:
    """For non-multiline strings, return True if `line` is no longer than `line_length`.
    For multiline strings, looks at the context around `line` to determine
    if it should be inlined or split up.
    Uses the provided `line_str` rendering, if any, otherwise computes a new one.
    """
    if not line_str:
        line_str = line_to_string(line)

    if Preview.multiline_string_handling not in mode:
        return (
            str_width(line_str) <= mode.line_length
            and "\n" not in line_str  # multiline strings
            and not line.contains_standalone_comments()
        )

    if line.contains_standalone_comments():
        return False
    if "\n" not in line_str:
        # No multiline strings (MLS) present
        return str_width(line_str) <= mode.line_length

    first, *_, last = line_str.split("\n")
    if str_width(first) > mode.line_length or str_width(last) > mode.line_length:
        return False

    # Traverse the AST to examine the context of the multiline string (MLS),
    # tracking aspects such as depth and comma existence,
    # to determine whether to split the MLS or keep it together.
    # Depth (which is based on the existing bracket_depth concept)
    # is needed to determine nesting level of the MLS.
    # Includes special case for trailing commas.
    commas: list[int] = []  # tracks number of commas per depth level
    multiline_string: Leaf | None = None
    # store the leaves that contain parts of the MLS
    multiline_string_contexts: list[LN] = []

    max_level_to_update: int | float = math.inf  # track the depth of the MLS
    for i, leaf in enumerate(line.leaves):
        if max_level_to_update == math.inf:
            had_comma: int | None = None
            if leaf.bracket_depth + 1 > len(commas):
                commas.append(0)
            elif leaf.bracket_depth + 1 < len(commas):
                had_comma = commas.pop()
            if (
                had_comma is not None
                and multiline_string is not None
                and multiline_string.bracket_depth == leaf.bracket_depth + 1
            ):
                # Have left the level with the MLS, stop tracking commas
                max_level_to_update = leaf.bracket_depth
                if had_comma > 0:
                    # MLS was in parens with at least one comma - force split
                    return False

        if leaf.bracket_depth <= max_level_to_update and leaf.type == token.COMMA:
            # Inside brackets, ignore trailing comma
            # directly after MLS/MLS-containing expression
            ignore_ctxs: list[LN | None] = [None]
            ignore_ctxs += multiline_string_contexts
            if (line.inside_brackets or leaf.bracket_depth > 0) and (
                i != len(line.leaves) - 1 or leaf.prev_sibling not in ignore_ctxs
            ):
                commas[leaf.bracket_depth] += 1
        if max_level_to_update != math.inf:
            max_level_to_update = min(max_level_to_update, leaf.bracket_depth)

        if is_multiline_string(leaf):
            if leaf.parent and (
                leaf.parent.type == syms.test
                or (leaf.parent.parent and leaf.parent.parent.type == syms.dictsetmaker)
            ):
                # Keep ternary and dictionary values parenthesized
                return False
            if len(multiline_string_contexts) > 0:
                # >1 multiline string cannot fit on a single line - force split
                return False
            multiline_string = leaf
            ctx: LN = leaf
            # fetch the leaf components of the MLS in the AST
            while str(ctx) in line_str:
                multiline_string_contexts.append(ctx)
                if ctx.parent is None:
                    break
                ctx = ctx.parent

    # May not have a triple-quoted multiline string at all,
    # in case of a regular string with embedded newlines and line continuations
    if len(multiline_string_contexts) == 0:
        return True

    return all(val == 0 for val in commas)


def can_be_split(line: Line) -> bool:
    """Return False if the line cannot be split *for sure*.

    This is not an exhaustive search but a cheap heuristic that we can use to
    avoid some unfortunate formattings (mostly around wrapping unsplittable code
    in unnecessary parentheses).
    """
    leaves = line.leaves
    if len(leaves) < 2:
        return False

    if leaves[0].type == token.STRING and leaves[1].type == token.DOT:
        call_count = 0
        dot_count = 0
        next = leaves[-1]
        for leaf in leaves[-2::-1]:
            if leaf.type in OPENING_BRACKETS:
                if next.type not in CLOSING_BRACKETS:
                    return False

                call_count += 1
            elif leaf.type == token.DOT:
                dot_count += 1
            elif leaf.type == token.NAME:
                if not (next.type == token.DOT or next.type in OPENING_BRACKETS):
                    return False

            elif leaf.type not in CLOSING_BRACKETS:
                return False

            if dot_count > 1 and call_count > 1:
                return False

    return True


def can_omit_invisible_parens(
    rhs: RHSResult,
    line_length: int,
) -> bool:
    """Does `rhs.body` have a shape safe to reformat without optional parens around it?

    Returns True for only a subset of potentially nice looking formattings but
    the point is to not return false positives that end up producing lines that
    are too long.
    """
    line = rhs.body

    # We need optional parens in order to split standalone comments to their own lines
    # if there are no nested parens around the standalone comments
    closing_bracket: Leaf | None = None
    for leaf in reversed(line.leaves):
        if closing_bracket and leaf is closing_bracket.opening_bracket:
            closing_bracket = None
        if leaf.type == STANDALONE_COMMENT and not closing_bracket:
            return False
        if (
            not closing_bracket
            and leaf.type in CLOSING_BRACKETS
            and leaf.opening_bracket in line.leaves
            and leaf.value
        ):
            closing_bracket = leaf

    bt = line.bracket_tracker
    if not bt.delimiters:
        # Without delimiters the optional parentheses are useless.
        return True

    max_priority = bt.max_delimiter_priority()
    delimiter_count = bt.delimiter_count_with_priority(max_priority)
    if delimiter_count > 1:
        # With more than one delimiter of a kind the optional parentheses read better.
        return False

    if delimiter_count == 1:
        if max_priority == COMMA_PRIORITY and rhs.head.is_with_or_async_with_stmt:
            # For two context manager with statements, the optional parentheses read
            # better. In this case, `rhs.body` is the context managers part of
            # the with statement. `rhs.head` is the `with (` part on the previous
            # line.
            return False
        # Otherwise it may also read better, but we don't do it today and requires
        # careful considerations for all possible cases. See
        # https://github.com/psf/black/issues/2156.

    if max_priority == DOT_PRIORITY:
        # A single stranded method call doesn't require optional parentheses.
        return True

    assert len(line.leaves) >= 2, "Stranded delimiter"

    # With a single delimiter, omit if the expression starts or ends with
    # a bracket.
    first = line.leaves[0]
    second = line.leaves[1]
    if first.type in OPENING_BRACKETS and second.type not in CLOSING_BRACKETS:
        if _can_omit_opening_paren(line, first=first, line_length=line_length):
            return True

        # Note: we are not returning False here because a line might have *both*
        # a leading opening bracket and a trailing closing bracket.  If the
        # opening bracket doesn't match our rule, maybe the closing will.

    penultimate = line.leaves[-2]
    last = line.leaves[-1]

    if (
        last.type == token.RPAR
        or last.type == token.RBRACE
        or (
            # don't use indexing for omitting optional parentheses;
            # it looks weird
            last.type == token.RSQB
            and last.parent
            and last.parent.type != syms.trailer
        )
    ):
        if penultimate.type in OPENING_BRACKETS:
            # Empty brackets don't help.
            return False

        if is_multiline_string(first):
            # Additional wrapping of a multiline string in this situation is
            # unnecessary.
            return True

        if _can_omit_closing_paren(line, last=last, line_length=line_length):
            return True

    return False


def _can_omit_opening_paren(line: Line, *, first: Leaf, line_length: int) -> bool:
    """See `can_omit_invisible_parens`."""
    remainder = False
    length = 4 * line.depth
    _index = -1
    for _index, leaf, leaf_length in line.enumerate_with_length():
        if leaf.type in CLOSING_BRACKETS and leaf.opening_bracket is first:
            remainder = True
        if remainder:
            length += leaf_length
            if length > line_length:
                break

            if leaf.type in OPENING_BRACKETS:
                # There are brackets we can further split on.
                remainder = False

    else:
        # checked the entire string and line length wasn't exceeded
        if len(line.leaves) == _index + 1:
            return True

    return False


def _can_omit_closing_paren(line: Line, *, last: Leaf, line_length: int) -> bool:
    """See `can_omit_invisible_parens`."""
    length = 4 * line.depth
    seen_other_brackets = False
    for _index, leaf, leaf_length in line.enumerate_with_length():
        length += leaf_length
        if leaf is last.opening_bracket:
            if seen_other_brackets or length <= line_length:
                return True

        elif leaf.type in OPENING_BRACKETS:
            # There are brackets we can further split on.
            seen_other_brackets = True

    return False


def line_to_string(line: Line) -> str:
    """Returns the string representation of @line.

    WARNING: This is known to be computationally expensive.
    """
    return str(line).strip("\n")
