"""This is a python implementation of clip()."""
from __future__ import annotations

# std imports
import os
import sys
import enum
from itertools import islice

from typing import Literal, Callable, Optional, NamedTuple

# local
from ._width import width
from .grapheme import iter_graphemes
from .hyperlink import Hyperlink, HyperlinkParams
from .sgr_state import (_SGR_PATTERN,
                        _SGR_STATE_DEFAULT,
                        _SGRState,
                        _sgr_state_update,
                        _sgr_state_is_active,
                        _sgr_state_to_sequence)
from ._constants import _clamp_ambiguous_width
from .text_sizing import TextSizing, TextSizingParams
from .escape_sequences import (_SEQUENCE_CLASSIFY,
                               _HORIZONTAL_CURSOR_MOVEMENT,
                               INDETERMINATE_EFFECT_SEQUENCE)

_c_clip: Optional[Callable[..., Optional[str]]]  # pylint: disable=invalid-name
if os.environ.get('WCWIDTH_PYTHON', ''):
    _c_clip = None
else:
    try:
        # local
        from ._wcwidth_c import clip as _c_clip_impl
    except ImportError:
        _c_clip = None
    else:
        _c_clip = _c_clip_impl


class _HyperlinkAction(enum.Enum):
    """Outcome of processing an OSC 8 hyperlink unit."""

    NO_CLOSE = enum.auto()   # open sequence without matching close
    EMPTY = enum.auto()       # hyperlink with no visible inner text
    OUTSIDE = enum.auto()     # hyperlink entirely outside the clip window
    VISIBLE = enum.auto()     # hyperlink overlaps the clip window


class _HyperlinkResult(NamedTuple):
    """
    Result of processing an OSC 8 hyperlink.

    Only the fields relevant to each action are populated.
    """

    action: _HyperlinkAction
    close_end: int = 0
    inner_width: int = 0
    open_seq: str = ''
    clipped_inner: str = ''
    close_seq: str = ''
    clipped_width: int = 0
    hl_col_end: int = 0


def _apply_sgr_wrap(result: str, captured_style: Optional[_SGRState],
                    end_style: Optional[_SGRState] = None) -> str:
    """
    Apply SGR prefix/suffix around *result*.

    If an SGR state was captured at the first visible character, prefix the result with the
    corresponding SGR sequence, and suffix with a reset if any styles remain active at the end
    of the clipped region.

    *end_style* is the style in effect after the final SGR sequence emitted within the clip window,
    or ``None`` when no such sequence was emitted, the style at the first visible character is still
    in effect.  This matches :func:`wcwidth.propagate_sgr`, which decides the trailing reset by SGR
    state at the end of each line.
    """
    if captured_style is not None:
        if prefix := _sgr_state_to_sequence(captured_style):
            result = prefix + result
        if _sgr_state_is_active(captured_style if end_style is None else end_style):
            result += '\x1b[0m'
    return result


def _process_hyperlink(
    text: str,
    start: int,
    end: int,
    fillchar: str,
    tabsize: int,
    ambiguous_width: int,
    term_program: bool | str,
    control_codes: Literal['parse', 'strict', 'ignore'],
    *,
    params: HyperlinkParams,
    match_end: int,
    col: int,
) -> _HyperlinkResult:
    """
    Process an OSC 8 hyperlink unit.

    Finds the matching close sequence, measures the inner text width, and determines whether the
    hyperlink is empty, outside the clip window, or visible (requiring inner-text clipping).
    """
    # pylint: disable=too-many-locals,too-many-positional-arguments,too-many-arguments
    close_start, close_end = Hyperlink.find_close(text, match_end)
    if (close_start, close_end) == (-1, -1):
        return _HyperlinkResult(_HyperlinkAction.NO_CLOSE)
    inner_text = text[match_end:close_start]
    inner_width = width(
        inner_text, control_codes=control_codes,
        tabsize=tabsize, ambiguous_width=ambiguous_width,
        term_program=term_program,
    )

    if inner_width == 0:
        return _HyperlinkResult(_HyperlinkAction.EMPTY, close_end=close_end)

    hl_col_end = col + inner_width

    if hl_col_end <= start or col >= end:
        return _HyperlinkResult(_HyperlinkAction.OUTSIDE, close_end=close_end,
                                inner_width=inner_width)

    inner_clip_start = max(0, start - col)
    inner_clip_end = end - col

    clipped_inner = clip(
        inner_text, inner_clip_start, inner_clip_end,
        fillchar=fillchar, tabsize=tabsize,
        ambiguous_width=ambiguous_width,
        term_program=term_program,
        propagate_sgr=False,
        control_codes=control_codes,
    )

    clipped_width = width(
        clipped_inner, control_codes=control_codes,
        tabsize=tabsize, ambiguous_width=ambiguous_width,
        term_program=term_program,
    )

    return _HyperlinkResult(
        _HyperlinkAction.VISIBLE,
        close_end=close_end,
        inner_width=inner_width,
        open_seq=params.make_open(),
        clipped_inner=clipped_inner,
        close_seq=params.make_close(),
        clipped_width=clipped_width,
        hl_col_end=hl_col_end,
    )


def _reconstruct_painter(
    cells: dict[int, tuple[str, int]],
    sequences: list[tuple[int, int, str]],
    start: int,
    end: int,
    fillchar: str,
) -> str:
    """
    Reconstruct the output string from painter's algorithm state.

    Walks columns left-to-right, interleaving escape sequences and cell content, filling gaps with
    *fillchar*.
    """
    # pylint: disable=too-many-locals
    # Group and sort sequences by column, preserving insertion order within each.
    seqs_by_col: dict[int, list[tuple[int, str]]] = {}
    for col_pos, order, seq_text in sequences:
        seqs_by_col.setdefault(col_pos, []).append((order, seq_text))
    for entries in seqs_by_col.values():
        entries.sort()

    max_cell_col = max(cells.keys()) if cells else -1
    max_seq_col = max(seqs_by_col.keys()) if seqs_by_col else -1
    max_col = max(max_cell_col, max_seq_col)

    parts: list[str] = []
    walk_col = 0
    col_limit = min(max_col, end)
    while walk_col <= col_limit:
        # Emit any sequences anchored at this column.
        for _, seq_text in seqs_by_col.get(walk_col, ()):
            parts.append(seq_text)

        if walk_col >= end:
            walk_col += 1
            continue

        if walk_col in cells:
            cell_text, cell_w = cells[walk_col]
            parts.append(cell_text)
            walk_col += cell_w or 1
        else:
            if start <= walk_col <= max_cell_col:
                parts.append(fillchar)
            walk_col += 1

    # Emit sequences anchored beyond the visible region.
    for c in sorted(seqs_by_col.keys()):
        if c > col_limit:
            for _, seq_text in seqs_by_col[c]:
                parts.append(seq_text)

    return ''.join(parts)


def _clip_simple(
    text: str,
    start: int,
    end: int,
    *,
    propagate_sgr: bool,
    ambiguous_width: int,
    term_program: bool | str,
    fillchar: str,
    tabsize: int,
    control_codes: Literal['parse', 'strict', 'ignore'],
) -> tuple[str, Optional[_SGRState], Optional[_SGRState]]:
    """
    Clip text without cursor movement (simple append-to-output path).

    Returns ``(result, captured_style, end_style)``.  The caller applies SGR wrapping.
    """
    # pylint: disable=too-complex,too-many-locals,too-many-branches,too-many-statements
    # pylint: disable=too-many-nested-blocks
    # code length and complexity traded for performance, to allow this to be used as a "hot path"

    strict = control_codes == 'strict'

    output: list[str] = []
    col = 0
    idx = 0
    captured_style: Optional[_SGRState] = None
    end_style: Optional[_SGRState] = None
    current_style = _SGR_STATE_DEFAULT if propagate_sgr else None

    while idx < len(text):
        char = text[idx]

        # Early exit: past visible region.
        if col >= end and char not in '\r\x08\t\x1b':
            if captured_style is not None:
                break
            # propagate_sgr is always False here: with propagate_sgr=True,
            # captured_style is set on the first visible emission in the
            # clip window and we would have broken above.  The skip-ahead
            # optimization is only needed (and safe) when SGR tracking is off.
            next_esc = text.find('\x1b', idx + 1)
            if next_esc == -1:
                break
            idx = next_esc
            continue

        if char == '\x1b':
            m = _SEQUENCE_CLASSIFY.match(text, idx)
            if not m:
                output.append(char)
                idx += 1
                continue

            # SGR: update current_style.  Sequences before the first visible
            # emission are folded into the prefix synthesized by
            # _apply_sgr_wrap(); those inside the clip window are emitted at
            # their original position; those at or beyond *end* are dropped.
            if m.group('sgr_params') is not None and propagate_sgr and current_style is not None:
                current_style = _sgr_state_update(current_style, m.group())
                if captured_style is not None and col < end:
                    output.append(m.group())
                    end_style = current_style
                idx = m.end()
                continue

            # OSC 8 hyperlink.
            if hl_state := HyperlinkParams.parse(m.group()):
                r = _process_hyperlink(
                    text, start, end, fillchar, tabsize, ambiguous_width,
                    term_program,
                    control_codes,
                    params=hl_state, match_end=m.end(), col=col,
                )
                if r.action is _HyperlinkAction.NO_CLOSE:
                    output.append(m.group())
                    idx = m.end()
                elif r.action is _HyperlinkAction.EMPTY:
                    idx = r.close_end
                elif r.action is _HyperlinkAction.OUTSIDE:
                    col += r.inner_width
                    idx = r.close_end
                else:
                    output.append(r.open_seq)
                    output.append(r.clipped_inner)
                    output.append(r.close_seq)
                    if propagate_sgr and captured_style is None:
                        captured_style = current_style
                    # Inner text is clipped with propagate_sgr=False, so any SGR
                    # sequences it emits are verbatim: fold them into our state.
                    if propagate_sgr and current_style is not None:
                        for sgr_m in _SGR_PATTERN.finditer(r.clipped_inner):
                            current_style = _sgr_state_update(current_style, sgr_m.group())
                            end_style = current_style
                    col += r.inner_width
                    idx = r.close_end
                continue

            # OSC 66 Text Sizing.
            if (ts_meta := m.group('ts_meta')) is not None:
                ts_text = m.group('ts_text') or ''
                ts_term = m.group('ts_term')
                assert ts_term is not None
                ts = TextSizing(
                    TextSizingParams.from_params(ts_meta, control_codes=control_codes),
                    ts_text, ts_term)
                ts_width = ts.display_width(ambiguous_width)

                if col >= start and col + ts_width <= end:
                    output.append(ts.make_sequence())
                    if propagate_sgr and captured_style is None:
                        captured_style = current_style
                    col += ts_width
                elif col < end and col + ts_width > start:
                    ts_parts: list[str] = []

                    def _ts_write(s: str, _w: int, _col: int) -> None:
                        ts_parts.append(s)
                    col = _text_sizing_clip(
                        ts, col, start, end, fillchar, ambiguous_width,
                        term_program,
                        _ts_write)
                    output.extend(ts_parts)
                    if propagate_sgr and captured_style is None:
                        captured_style = current_style
                else:
                    col += ts_width
                idx = m.end()
                continue

            # Indeterminate-effect sequences: raise in strict mode.
            seq = m.group()
            if strict and INDETERMINATE_EFFECT_SEQUENCE.match(seq):
                raise ValueError(
                    f"Indeterminate cursor sequence at position {idx}, "
                    f"{seq!r}"
                )

            # Any other recognized sequence: preserve as-is.
            output.append(seq)
            idx = m.end()
            continue

        if char == '\t':
            # Expand tab, filling clip window with spaces.
            if tabsize > 0:
                next_tab = col + (tabsize - (col % tabsize))
                while col < next_tab:
                    if start <= col < end:
                        output.append(' ')
                        if propagate_sgr and captured_style is None:
                            captured_style = current_style
                    col += 1
            elif start <= col < end:
                output.append('\t')
            idx += 1
            continue

        grapheme = next(iter_graphemes(text, start=idx))
        grapheme_w = width(grapheme, ambiguous_width=ambiguous_width,
                           term_program=term_program)

        # Emit grapheme or fillchar depending on visibility within clip window.
        if grapheme_w == 0:
            if start <= col < end:
                output.append(grapheme)
        elif col >= start and col + grapheme_w <= end:
            output.append(grapheme)
            if propagate_sgr and captured_style is None:
                captured_style = current_style
        elif col < end and col + grapheme_w > start:
            output.append(fillchar * (min(end, col + grapheme_w) - max(start, col)))
            if propagate_sgr and captured_style is None:
                captured_style = current_style

        col += grapheme_w
        idx += len(grapheme)

    return ''.join(output), captured_style, end_style


def _text_sizing_clip(
    ts: TextSizing,
    col: int,
    start: int,
    end: int,
    fillchar: str,
    ambiguous_width: int,
    term_program: bool | str,
    write_cells: Callable[[str, int, int], None],
) -> int:
    """
    Emit tokens for a text-sizing (OSC 66) sequence, clipped to (start, end).

    Calls *write_cells(text, width, col)* for each emitted cell or sequence. Returns new column
    position.
    """
    # pylint: disable=too-many-locals,too-many-branches,too-many-positional-arguments,too-complex
    ts_width = ts.display_width(ambiguous_width)

    # Fully visible: emit entire sequence
    if col >= start and col + ts_width <= end:
        write_cells(ts.make_sequence(), ts_width, col)
        return col + ts_width
    # Fully outside: just advance column
    if col >= end or col + ts_width <= start:
        return col + ts_width

    # Partial overlap: decompose
    rel_start = max(0, start - col)
    rel_end = min(end, col + ts_width) - col
    scale = ts.params.scale

    units: list[tuple[str, int]] = []
    if ts.params.width > 0:
        for g in islice(iter_graphemes(ts.text), ts.params.width):
            units.append((g, scale))
        for _ in range(ts.params.width - len(units)):
            units.append(('', scale))
    else:
        for g in iter_graphemes(ts.text):
            units.append(
                (g, width(g, ambiguous_width=ambiguous_width,
                          term_program=term_program) * scale))

    pending_units: list[tuple[str, int]] = []

    def flush(flush_col: int) -> None:
        if not pending_units:
            return
        texts = [u[0] for u in pending_units]
        total_w = sum(u[1] for u in pending_units)
        params = TextSizingParams(
            scale,
            len(texts) if ts.params.width > 0 else 0,
            ts.params.numerator, ts.params.denominator,
            ts.params.vertical_align, ts.params.horizontal_align)
        write_cells(
            TextSizing(params, ''.join(texts), ts.terminator).make_sequence(),
            total_w,
            flush_col)
        pending_units.clear()

    flush_col_pos = col + rel_start
    unit_pos = 0
    for unit_text, unit_w in units:
        unit_end = unit_pos + unit_w
        if unit_end <= rel_start:
            unit_pos = unit_end
            continue
        if unit_pos >= rel_end:
            break

        overlap = min(unit_end, rel_end) - max(unit_pos, rel_start)
        if overlap == unit_w and unit_w > 0:
            if not pending_units:
                flush_col_pos = col + max(unit_pos, rel_start)
            pending_units.append((unit_text, unit_w))
        else:
            flush(flush_col_pos)
            abs_start = col + max(unit_pos, rel_start)
            for i in range(overlap):
                write_cells(fillchar, 1, abs_start + i)
        unit_pos = unit_end

    flush(flush_col_pos)
    return col + ts_width


def _clip_painter(
    text: str,
    start: int,
    end: int,
    *,
    propagate_sgr: bool,
    ambiguous_width: int,
    term_program: bool | str,
    fillchar: str,
    tabsize: int,
    control_codes: Literal['parse', 'strict', 'ignore'],
) -> tuple[str, Optional[_SGRState], Optional[_SGRState]]:
    """
    Clip text, painting cells so that cursor movement can overwrite them.

    Text with no cursor movement to resolve is clipped by _clip_simple() instead.

    Returns ``(result, captured_style, end_style)``.  The caller applies SGR wrapping.
    """
    # pylint: disable=too-complex,too-many-locals,too-many-branches
    # pylint: disable=too-many-statements,too-many-nested-blocks
    # code length and complexity traded for performance, to allow this to be used as a "hot path"

    strict = control_codes == 'strict'
    cells: dict[int, tuple[str, int]] = {}
    hyperlink_cells: set[int] = set()
    sequences: list[tuple[int, int, str]] = []
    seq_order = 0

    col = 0
    idx = 0
    # captured_style is a frozen snapshot of current_style taken at the first
    # visible character emitted within the clip window (start, end); it stays
    # None until that point.  current_style is continuously updated by SGR
    # sequences throughout the scan.
    #
    # When propagate_sgr is False, current_style (and therefore captured_style)
    # remain None, and SGR sequences pass through as literal text.
    captured_style: Optional[_SGRState] = None
    # end_style is the state after the last SGR sequence emitted *within* the
    # clip window; it decides the trailing reset.  None until such a sequence
    # is emitted, meaning captured_style is still in effect at the end.
    end_style: Optional[_SGRState] = None
    current_style = _SGR_STATE_DEFAULT if propagate_sgr else None
    # Next movement at or after the scan position, -1 when none is left.
    next_movement: Optional[int] = None

    def _write_cells(s: str, w: int, write_col: int,
                     is_hyperlink: bool = False) -> None:
        """Write *w* cells of text *s* at *write_col*, handling wide-char splitting."""
        nonlocal captured_style, seq_order
        if w == 0:
            # Zero-width: a cell here would be overwritten by the next write.
            if s:
                sequences.append((write_col, seq_order, s))
                seq_order += 1
                if propagate_sgr and captured_style is None:
                    captured_style = current_style
            return
        for offset in range(w):
            src_col = write_col + offset
            if src_col > 0 and cells.get(src_col - 1, ('', 0))[1] == 2:
                cells[src_col - 1] = (fillchar, 1)
                hyperlink_cells.discard(src_col - 1)
            if cells.get(src_col, ('', 0))[1] == 2:
                cells[src_col + 1] = (fillchar, 1)
                hyperlink_cells.discard(src_col + 1)
            cells.pop(src_col, None)
            hyperlink_cells.discard(src_col)
        cells[write_col] = (s, w)
        if is_hyperlink:
            for offset in range(w):
                hyperlink_cells.add(write_col + offset)
        if propagate_sgr and captured_style is None:
            captured_style = current_style

    while idx < len(text):
        char = text[idx]

        # Early exit: past visible region.
        if col >= end and char not in '\r\x08\t\x1b':
            # Movement right-of the window can still rewinds back into it.
            if next_movement is None or 0 <= next_movement < idx:
                # Any match starts with one of these, and rfind is far cheaper.
                if max(text.rfind('\x08'), text.rfind('\r'), text.rfind('\x1b')) < idx:
                    next_movement = -1
                else:
                    found = _HORIZONTAL_CURSOR_MOVEMENT.search(text, idx)
                    next_movement = -1 if found is None else found.end()
            if next_movement < 0:
                if captured_style is not None:
                    break
                next_esc = text.find('\x1b', idx + 1)
                if next_esc == -1:
                    break
                idx = next_esc
                continue

        if char == '\x1b':
            m = _SEQUENCE_CLASSIFY.match(text, idx)
            if not m:
                # Record lone ESC as a zero-width sequence at current column.
                sequences.append((col, seq_order, char))
                seq_order += 1
                idx += 1
                continue

            # SGR: update current_style.  Sequences before the first visible
            # emission are folded into the prefix synthesized by
            # _apply_sgr_wrap(); those inside the clip window are emitted at
            # their original position; those at or beyond *end* are dropped.
            if m.group('sgr_params') is not None and propagate_sgr and current_style is not None:
                current_style = _sgr_state_update(current_style, m.group())
                if captured_style is not None and col < end:
                    sequences.append((col, seq_order, m.group()))
                    seq_order += 1
                    end_style = current_style
                idx = m.end()
                continue

            # OSC 8 hyperlink.
            if hl_state := HyperlinkParams.parse(m.group()):
                r = _process_hyperlink(
                    text, start, end, fillchar, tabsize, ambiguous_width,
                    term_program,
                    control_codes,
                    params=hl_state, match_end=m.end(), col=col,
                )
                if r.action is _HyperlinkAction.NO_CLOSE:
                    sequences.append((col, seq_order, m.group()))
                    seq_order += 1
                    idx = m.end()
                elif r.action is _HyperlinkAction.EMPTY:
                    idx = r.close_end
                elif r.action is _HyperlinkAction.OUTSIDE:
                    col += r.inner_width
                    idx = r.close_end
                else:
                    sequences.append((col, seq_order, r.open_seq))
                    seq_order += 1
                    _write_cells(r.clipped_inner, r.clipped_width, col,
                                 is_hyperlink=True)
                    col += r.clipped_width
                    sequences.append((col, seq_order, r.close_seq))
                    seq_order += 1
                    # Inner text is clipped with propagate_sgr=False, so any SGR
                    # sequences it emits are verbatim: fold them into our state.
                    if propagate_sgr and current_style is not None:
                        for sgr_m in _SGR_PATTERN.finditer(r.clipped_inner):
                            current_style = _sgr_state_update(current_style, sgr_m.group())
                            end_style = current_style
                    col = r.hl_col_end
                    idx = r.close_end
                continue

            # OSC 66 Text Sizing.
            if (ts_meta := m.group('ts_meta')) is not None:
                ts_text = m.group('ts_text') or ''
                ts_term = m.group('ts_term')
                assert ts_term is not None
                ts = TextSizing(
                    TextSizingParams.from_params(ts_meta, control_codes=control_codes),
                    ts_text, ts_term)
                col = _text_sizing_clip(
                    ts, col, start, end, fillchar, ambiguous_width,
                    term_program,
                    _write_cells)
                idx = m.end()
                continue

            # Indeterminate-effect sequences: raise in strict mode.
            seq = m.group()
            if strict and INDETERMINATE_EFFECT_SEQUENCE.match(seq):
                raise ValueError(
                    f"Indeterminate cursor sequence at position {idx}, "
                    f"{seq!r}"
                )

            # Horizontal Position Absolute (CSI n G).
            if (hpa_n := m.group('hpa_n')) is not None:
                col = int(hpa_n) - 1 if hpa_n else 0
                idx = m.end()
                continue

            # Cursor Forward (CSI n C).
            if (cforward_n := m.group('cforward_n')) is not None:
                n_forward = int(cforward_n) if cforward_n else 1
                move_end = col + n_forward
                if col < end and move_end > start:
                    for i in range(max(col, start), min(move_end, end)):
                        _write_cells(fillchar, 1, i)
                col = move_end
                idx = m.end()
                continue

            # Cursor Backward (CSI n D).
            if (cbackward_n := m.group('cbackward_n')) is not None:
                n_backward = int(cbackward_n) if cbackward_n else 1
                if strict and n_backward > col:
                    raise ValueError(
                        f"Cursor left movement at position {idx} would move "
                        f"{n_backward} cells left from column {col}, "
                        f"exceeding string start"
                    )
                col -= n_backward
                if col < 0:
                    col = 0
                idx = m.end()
                continue

            # Any other recognized sequence: preserve as-is.
            sequences.append((col, seq_order, m.group()))
            seq_order += 1
            idx = m.end()
            continue

        # Carriage return.
        if char == '\r':
            col = 0
            idx += 1
            continue

        # Backspace.
        if char == '\x08':
            if col > 0:
                col -= 1
            idx += 1
            continue

        # Tab expansion.
        if char == '\t':
            if tabsize > 0:
                next_tab = col + (tabsize - (col % tabsize))
                for fill_col in range(max(col, start), min(next_tab, end)):
                    _write_cells(' ', 1, fill_col)
                col = next_tab
            elif start <= col < end:
                sequences.append((col, seq_order, '\t'))
                seq_order += 1
            idx += 1
            continue

        # Grapheme cluster.
        grapheme = next(iter_graphemes(text, start=idx))
        grapheme_w = width(grapheme, ambiguous_width=ambiguous_width,
                           term_program=term_program)

        # Emit grapheme or fillchar depending on visibility within clip window.
        if grapheme_w == 0:
            if start <= col < end:
                sequences.append((col, seq_order, grapheme))
                seq_order += 1
        elif col >= start and col + grapheme_w <= end:
            _write_cells(grapheme, grapheme_w, col)
        elif col < end and col + grapheme_w > start:
            clip_start = max(start, col)
            for offset in range(min(end, col + grapheme_w) - clip_start):
                _write_cells(fillchar, 1, clip_start + offset)

        col += grapheme_w
        idx += len(grapheme)

    return (_reconstruct_painter(cells, sequences, start, end, fillchar),
            captured_style, end_style)


def clip(
    text: str,
    start: int = 0,
    end: int = -1,
    *,
    fillchar: str = ' ',
    tabsize: int = 8,
    ambiguous_width: int = 1,
    propagate_sgr: bool = True,
    control_codes: Literal['parse', 'strict', 'ignore'] = 'parse',
    overtyping: Optional[bool] = None,
    term_program: bool | str = False,
) -> str:
    r"""
    Clip text to display columns (start, end) while preserving all terminal sequences.

    This function extracts a substring using visible column positions. Terminal escape sequences
    are preserved in output. If a wide character (width of 2) is
    split at a boundary, it is replaced with ``fillchar``.

    TAB characters (``\t``) are expanded to spaces up to the next tab stop, controlled by the
    ``tabsize`` parameter.  When cursor movement is detected, a "painter's algorithm" is used unless
    ``overtyping=False`` is set.  Cursor movement control codes are parsed for their effects instead
    of ignored.  For these operations, it is assumed that ``text`` begins at column 0.

    For text containing **OSC 8 hyperlinks**, the visible text inside a hyperlink is clipped to the
    requested column range and the hyperlink sequence is rebuilt::

        >>> clip('\x1b]8;;http://example.com\x07Click This link\x1b]8;;\x07', 6, 10)
        '\x1b]8;;http://example.com\x07This\x1b]8;;\x07'

    :param text: String to clip, may contain terminal escape sequences.
    :param start: Absolute starting column (inclusive, 0-indexed), default ``0``.
    :param end: Absolute ending column (exclusive).  The default value, ``-1``,
        signifies "to the end of the line", clipping only from *start* without
        requiring the caller to measure the display width of *text*.
    :param fillchar: Character to use when a wide character must be split at
        a boundary (default space). Must have display width of 1.
    :param tabsize: Tab stop width (default 8). Set to 0 to pass tabs through
        as zero-width (preserved in output but don't advance column position).
    :param ambiguous_width: Width to use for East Asian Ambiguous (A)
        characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts.
    :param propagate_sgr: If True (default), SGR (terminal styling) sequences
        are propagated, matching :func:`propagate_sgr`.  The result begins with
        any style active at the start position, retains any style changes
        occurring within the clipped region at their original position, and
        ends with a reset sequence if styles are active at the end position.
    :param control_codes: How to handle control characters and sequences:

        - ``'parse'`` (default): Track horizontal cursor movement and clip
          hyperlink text.  Cursor overwrite is always allowed, with best effort
          results; indeterminate sequences (home, clear, reset, etc.) are
          preserved as zero-width.
        - ``'strict'``: Like ``parse``, but raises :exc:`ValueError` on
          sequences with indeterminate effects (cursor home, clear screen,
          reset, vertical movement, etc.) matching :func:`width` behavior.
          Also raises on out-of-bounds horizontal cursor movement.
        - ``'ignore'``: All control characters are treated as zero-width.
          Cursor movement is not tracked (fastest path).

    :param overtyping: Whether to use the painter's algorithm for cursor
        movement (``\b`` backspace, ``\r`` carriage return, and CSI cursor
        left/right/position sequences).  When ``None`` (default), auto-detects
        by scanning for these characters in *text*.  Set to ``False`` for improved
        performance when the caller knows *text* contains no cursor movement
        characters.  Set to ``True`` to force the painter's algorithm (useful
        for testing).  Has no effect when ``control_codes='ignore'``.
    :param term_program: Terminal software identifier for table correction.
        ``False`` (default) disables override lookup.  ``True`` reads the
        ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection.
        Accepts a canonical terminal name matching :func:`list_term_programs`,
        such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``.

        .. versionadded:: 0.8.0

    :returns: Substring of ``text`` spanning display columns (start, end),
        with all terminal sequences preserved and wide characters at boundaries
        replaced with ``fillchar``.

    :raises ValueError: If ``end`` is negative and not ``-1``, or if
        ``control_codes='strict'`` and an indeterminate-effect sequence or
        out-of-bounds cursor movement is encountered.

    SGR (terminal styling) sequences are propagated by default. The result
    begins with any active style and ends with a reset::

        >>> clip('\x1b[1;34mHello world\x1b[0m', 6, 11)
        '\x1b[1;34mworld\x1b[0m'
        >>> clip('\x1b[1mbold\x1b[m normal', 1, 9)
        '\x1b[1mold\x1b[m norm'

    Set ``propagate_sgr=False`` to disable this behavior.

    .. note::

       Segmentation follows UAX #29 grapheme clusters. Tamil, Kannada and Sinhala may be clipped
       short of ``end`` and leave a bare virama at the boundary. Correct rendering of these
       languages is not specified by unicode.org standards, and no terminal emulator or editor
       handles them legibly, so this library function makes no attempt at accommodating them.

    .. versionadded:: 0.3.0

    .. versionchanged:: 0.5.0
       Added ``propagate_sgr`` parameter (default True).

    .. versionchanged:: 0.7.0
       Added ``control_codes`` parameter (default 'parse').
       OSC 8 hyperlink-aware clipping.  OSC 66 text sizing protocol support.
       Added ``overtyping`` parameter (default None, auto-detect).

    .. versionchanged:: 0.8.4
       ``start`` now defaults to ``0`` and ``end`` to ``-1``, meaning "to the
       end of the line"::

           >>> clip('\x1b[1;34mHello world\x1b[0m', 6)
           '\x1b[1;34mworld\x1b[0m'

    Example::

        >>> clip('hello world', 0, 5)
        'hello'
        >>> clip('中文字', 0, 3)  # Wide char split at column 3
        '中 '
        >>> clip('a\tb', 0, 10)  # Tab expanded to spaces
        'a       b'
    """
    start = max(start, 0)
    if end < 0:
        if end != -1:
            raise ValueError(
                f"end must be -1 (to end of line) or non-negative, got {end}")
        # Unbounded: clip only from *start*, to the end of the line.
        end = sys.maxsize
    if end <= start:
        return ''

    # Fast path: printable ASCII only.
    if text.isascii() and text.isprintable():
        return text[start:end]

    # Offload to libwcwidth, which reports an unsupported sequence rather than
    # answering differently.  Only the options are tested here, never the text.
    if (_c_clip is not None
            and control_codes == 'parse'
            and overtyping is not True
            and tabsize > 0
            and len(fillchar) == 1 and fillchar.isascii()):
        result = _c_clip(
            text, start, end, fillchar=fillchar, tabsize=tabsize,
            ambiguous_width=ambiguous_width, propagate_sgr=propagate_sgr,
            control_codes=control_codes, term_program=term_program)
        if result is not None:
            return result

    ambiguous_width = _clamp_ambiguous_width(ambiguous_width)

    # No escape sequences => no SGR tracking needed.
    has_esc = '\x1b' in text
    if propagate_sgr and not has_esc:
        propagate_sgr = False

    # Determine whether painter's algorithm is needed.
    if overtyping is None:
        # Auto-detect: scan for cursor movement characters.
        overtyping = (
            control_codes != 'ignore' and
            ('\x08' in text or '\r' in text or
             (has_esc and bool(_HORIZONTAL_CURSOR_MOVEMENT.search(text))))
        )
    elif overtyping and control_codes == 'ignore':
        overtyping = False  # control_codes='ignore' overrides

    if overtyping:
        return _apply_sgr_wrap(*_clip_painter(
            text=text,
            start=start,
            end=end,
            propagate_sgr=propagate_sgr,
            ambiguous_width=ambiguous_width,
            term_program=term_program,
            fillchar=fillchar,
            tabsize=tabsize,
            control_codes=control_codes,
        ))

    return _apply_sgr_wrap(*_clip_simple(
        text=text,
        start=start,
        end=end,
        propagate_sgr=propagate_sgr,
        ambiguous_width=ambiguous_width,
        term_program=term_program,
        fillchar=fillchar,
        tabsize=tabsize,
        control_codes=control_codes,
    ))
