from __future__ import annotations

import collections.abc as cabc
import textwrap
from contextlib import contextmanager

from ._compat import _ansi_re
from ._compat import term_len


def _truncate_visible(text: str, n: int) -> str:
    """Return the longest prefix of ``text`` containing at most ``n`` visible
    characters.

    ANSI escape sequences inside the prefix are kept intact and do not count
    toward the visible width. A cut is never placed inside an escape sequence.
    """
    if n <= 0:
        return ""

    visible = 0
    i = 0
    cut = 0
    end = len(text)
    while i < end:
        m = _ansi_re.match(text, i)
        if m is not None:
            i = m.end()
            continue
        visible += 1
        i += 1
        cut = i
        if visible >= n:
            break
    return text[:cut]


class TextWrapper(textwrap.TextWrapper):
    """``textwrap.TextWrapper`` variant that measures widths by visible
    character count.

    ANSI escape sequences embedded in chunks, indents, or the placeholder are
    excluded from the width budget. Without this, styled help text (a styled
    ``Usage:`` prefix, a colorized option name, ...) would be wrapped earlier
    than its visible length warrants and tokens would split mid-word.
    """

    def _handle_long_word(
        self,
        reversed_chunks: list[str],
        cur_line: list[str],
        cur_len: int,
        width: int,
    ) -> None:
        space_left = max(width - cur_len, 1)

        if self.break_long_words:
            last = reversed_chunks[-1]
            cut = _truncate_visible(last, space_left)
            res = last[len(cut) :]
            cur_line.append(cut)
            reversed_chunks[-1] = res
        elif not cur_line:
            cur_line.append(reversed_chunks.pop())

    def _wrap_chunks(self, chunks: list[str]) -> list[str]:
        """Wrap chunks counting widths in visible characters.

        Mirrors the algorithm of :meth:`textwrap.TextWrapper._wrap_chunks`
        with every width measurement routed through
        :func:`click._compat.term_len` instead of :func:`len`, so ANSI escape
        bytes in chunks, indents, or the placeholder do not inflate the count.

        .. seealso::
            :class:`textwrap.TextWrapper` in the Python standard library documentation:
            https://docs.python.org/3/library/textwrap.html#textwrap.TextWrapper

            Reference implementation in CPython:
            https://github.com/python/cpython/blob/main/Lib/textwrap.py
        """
        lines: list[str] = []
        if self.width <= 0:
            raise ValueError(f"invalid width {self.width!r} (must be > 0)")
        if self.max_lines is not None:
            if self.max_lines > 1:
                indent = self.subsequent_indent
            else:
                indent = self.initial_indent
            if term_len(indent) + term_len(self.placeholder.lstrip()) > self.width:
                raise ValueError("placeholder too large for max width")

        chunks.reverse()

        while chunks:
            cur_line: list[str] = []
            cur_len = 0

            if lines:
                indent = self.subsequent_indent
            else:
                indent = self.initial_indent

            width = self.width - term_len(indent)

            if self.drop_whitespace and chunks[-1].strip() == "" and lines:
                del chunks[-1]

            while chunks:
                n = term_len(chunks[-1])

                if cur_len + n <= width:
                    cur_line.append(chunks.pop())
                    cur_len += n

                else:
                    break

            if chunks and term_len(chunks[-1]) > width:
                self._handle_long_word(chunks, cur_line, cur_len, width)
                cur_len = sum(map(term_len, cur_line))

            if self.drop_whitespace and cur_line and cur_line[-1].strip() == "":
                cur_len -= term_len(cur_line[-1])
                del cur_line[-1]

            if cur_line:
                if (
                    self.max_lines is None
                    or len(lines) + 1 < self.max_lines
                    or (
                        not chunks
                        or self.drop_whitespace
                        and len(chunks) == 1
                        and not chunks[0].strip()
                    )
                    and cur_len <= width
                ):
                    lines.append(indent + "".join(cur_line))
                else:
                    while cur_line:
                        if (
                            cur_line[-1].strip()
                            and cur_len + term_len(self.placeholder) <= width
                        ):
                            cur_line.append(self.placeholder)
                            lines.append(indent + "".join(cur_line))
                            break
                        cur_len -= term_len(cur_line[-1])
                        del cur_line[-1]
                    else:
                        if lines:
                            prev_line = lines[-1].rstrip()
                            if (
                                term_len(prev_line) + term_len(self.placeholder)
                                <= self.width
                            ):
                                lines[-1] = prev_line + self.placeholder
                                break
                        lines.append(indent + self.placeholder.lstrip())
                    break

        return lines

    @contextmanager
    def extra_indent(self, indent: str) -> cabc.Iterator[None]:
        old_initial_indent = self.initial_indent
        old_subsequent_indent = self.subsequent_indent
        self.initial_indent += indent
        self.subsequent_indent += indent

        try:
            yield
        finally:
            self.initial_indent = old_initial_indent
            self.subsequent_indent = old_subsequent_indent

    def indent_only(self, text: str) -> str:
        rv = []

        for idx, line in enumerate(text.splitlines()):
            indent = self.initial_indent

            if idx > 0:
                indent = self.subsequent_indent

            rv.append(f"{indent}{line}")

        return "\n".join(rv)
