#!/usr/bin/env python
"""
SGR (Select Graphic Rendition) state tracking for terminal escape sequences.

This module provides functions for tracking and propagating terminal styling (bold, italic, colors,
etc.) via public API propagate_sgr(), and its dependent functions, cut() and wrap(). It only has
attributes necessary to perform its functions, eg 'RED' and 'BLUE' attributes are not defined.
"""

from __future__ import annotations

# std imports
import re
from enum import IntEnum

from typing import TYPE_CHECKING, Iterator, NamedTuple

if TYPE_CHECKING:  # pragma: no cover
    from typing import Sequence


class _SGR(IntEnum):
    """
    SGR (Select Graphic Rendition) parameter codes.

    References:
    - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
    - https://github.com/tehmaze/ansi/tree/master/ansi/colour
    """

    RESET = 0
    BOLD = 1
    DIM = 2
    ITALIC = 3
    UNDERLINE = 4
    BLINK = 5
    RAPID_BLINK = 6
    INVERSE = 7
    HIDDEN = 8
    STRIKETHROUGH = 9
    DOUBLE_UNDERLINE = 21
    BOLD_DIM_OFF = 22
    ITALIC_OFF = 23
    UNDERLINE_OFF = 24
    BLINK_OFF = 25
    INVERSE_OFF = 27
    HIDDEN_OFF = 28
    STRIKETHROUGH_OFF = 29
    FG_BLACK = 30
    FG_WHITE = 37
    FG_EXTENDED = 38
    FG_DEFAULT = 39
    BG_BLACK = 40
    BG_WHITE = 47
    BG_EXTENDED = 48
    BG_DEFAULT = 49
    FG_BRIGHT_BLACK = 90
    FG_BRIGHT_WHITE = 97
    BG_BRIGHT_BLACK = 100
    BG_BRIGHT_WHITE = 107


# SGR sequence pattern: CSI followed by params (digits, semicolons, colons) ending with 'm'
# Colons are used in ITU T.416 (ISO 8613-6) extended color format: 38:2::R:G:B
# This colon format is less common than semicolon (38;2;R;G;B) but supported by kitty,
# iTerm2, and newer VTE-based terminals.
_SGR_PATTERN = re.compile(r'\x1b\[([\d;:]*)m')

# Fast path: quick check if any SGR sequence exists
_SGR_QUICK_CHECK = re.compile(r'\x1b\[[\d;:]*m')

# Reset sequence
_SGR_RESET = '\x1b[0m'


class _SGRState(NamedTuple):
    """
    Track active SGR terminal attributes by category (immutable).

    :param bold: Bold attribute (SGR 1).
    :param dim: Dim/faint attribute (SGR 2).
    :param italic: Italic attribute (SGR 3).
    :param underline: Underline attribute (SGR 4).
    :param blink: Slow blink attribute (SGR 5).
    :param rapid_blink: Rapid blink attribute (SGR 6).
    :param inverse: Inverse/reverse attribute (SGR 7).
    :param hidden: Hidden/invisible attribute (SGR 8).
    :param strikethrough: Strikethrough attribute (SGR 9).
    :param double_underline: Double underline attribute (SGR 21).
    :param foreground: Foreground color as tuple of SGR params, or None for default.
    :param background: Background color as tuple of SGR params, or None for default.
    """

    bold: bool = False
    dim: bool = False
    italic: bool = False
    underline: bool = False
    blink: bool = False
    rapid_blink: bool = False
    inverse: bool = False
    hidden: bool = False
    strikethrough: bool = False
    double_underline: bool = False
    foreground: tuple[int, ...] | None = None
    background: tuple[int, ...] | None = None


# Default state with no attributes set
_SGR_STATE_DEFAULT = _SGRState()


def _sgr_state_is_active(state: _SGRState) -> bool:
    """
    Return True if any attributes are set.

    :param state: The SGR state to check.
    :returns: True if any attribute differs from default.
    """
    return (state.bold or state.dim or state.italic or state.underline
            or state.blink or state.rapid_blink or state.inverse or state.hidden
            or state.strikethrough or state.double_underline
            or state.foreground is not None or state.background is not None)


def _sgr_state_to_sequence(state: _SGRState) -> str:
    """
    Generate minimal SGR sequence to restore this state from reset.

    :param state: The SGR state to convert.
    :returns: SGR escape sequence string, or empty string if no attributes set.
    """
    if not _sgr_state_is_active(state):
        return ''

    # Map boolean attributes to their SGR codes
    bool_attrs = [
        (state.bold, '1'), (state.dim, '2'), (state.italic, '3'),
        (state.underline, '4'), (state.blink, '5'), (state.rapid_blink, '6'),
        (state.inverse, '7'), (state.hidden, '8'), (state.strikethrough, '9'),
        (state.double_underline, '21'),
    ]
    params = [code for active, code in bool_attrs if active]

    # Add color params (already formatted as tuples)
    if state.foreground is not None:
        params.append(';'.join(str(p) for p in state.foreground))
    if state.background is not None:
        params.append(';'.join(str(p) for p in state.background))

    return f'\x1b[{";".join(params)}m'


def _parse_sgr_params(sequence: str) -> list[int | tuple[int, ...]]:
    r"""
    Parse SGR sequence and return list of parameter values.

    Handles compound sequences like ``\x1b[1;31;4m`` -> [1, 31, 4].
    Empty params (e.g., ``\x1b[m``) are treated as [0] (reset).
    Colon-separated extended colors like ``\x1b[38:2::255:0:0m`` are returned
    as tuples: [(38, 2, 255, 0, 0)].

    :param sequence: SGR escape sequence string.
    :returns: List of integer parameters or tuples for colon-separated colors.
    """
    match = _SGR_PATTERN.match(sequence)
    if not match:
        return []
    params_str = match.group(1)
    if not params_str:
        return [0]  # \x1b[m is equivalent to \x1b[0m
    result: list[int | tuple[int, ...]] = []
    for param in params_str.split(';'):
        if ':' in param:
            # Colon-separated extended color (ITU T.416 format)
            # e.g., "38:2::255:0:0" or "38:2:1:255:0:0" (with colorspace)
            parts = [int(p) if p else 0 for p in param.split(':')]
            result.append(tuple(parts))
        else:
            result.append(int(param) if param else 0)
    return result


def _parse_extended_color(
    params: Iterator[int | tuple[int, ...]], base: int
) -> tuple[int, ...] | None:
    """
    Parse extended color (256-color or RGB) from parameter iterator.

    :param params: Iterator of remaining SGR parameters (semicolon-separated format).
    :param base: Base code (38 for foreground, 48 for background).
    :returns: Color tuple like (38, 5, N) or (38, 2, R, G, B), or None if malformed.
    """
    try:
        mode = next(params)
        if isinstance(mode, tuple):
            return None  # Unexpected tuple, colon format handled separately
        if mode == 5:  # 256-color
            n = next(params)
            if isinstance(n, tuple):
                return None
            return (int(base), 5, n)
        if mode == 2:  # RGB
            r, g, b = next(params), next(params), next(params)
            if isinstance(r, tuple) or isinstance(g, tuple) or isinstance(b, tuple):
                return None
            return (int(base), 2, r, g, b)
    except StopIteration:
        pass
    return None


def _sgr_state_update(state: _SGRState, sequence: str) -> _SGRState:
    # pylint: disable=too-many-branches,too-complex,too-many-statements
    # NOTE: When minimum Python version is 3.10+, this can be simplified using match/case.
    """
    Parse SGR sequence and return new state with updates applied.

    :param state: Current SGR state.
    :param sequence: SGR escape sequence string.
    :returns: New SGRState with updates applied.
    """
    params_list = _parse_sgr_params(sequence)
    params = iter(params_list)
    for p in params:
        # Handle colon-separated extended colors (ITU T.416 format)
        if isinstance(p, tuple):
            if len(p) >= 2 and p[0] == _SGR.FG_EXTENDED:
                # Foreground: (38, 2, [colorspace,] R, G, B) or (38, 5, N)
                state = state._replace(foreground=p)
            elif len(p) >= 2 and p[0] == _SGR.BG_EXTENDED:
                # Background: (48, 2, [colorspace,] R, G, B) or (48, 5, N)
                state = state._replace(background=p)
            continue
        if p == _SGR.RESET:
            state = _SGR_STATE_DEFAULT
        # Attribute ON codes
        elif p == _SGR.BOLD:
            state = state._replace(bold=True)
        elif p == _SGR.DIM:
            state = state._replace(dim=True)
        elif p == _SGR.ITALIC:
            state = state._replace(italic=True)
        elif p == _SGR.UNDERLINE:
            state = state._replace(underline=True)
        elif p == _SGR.BLINK:
            state = state._replace(blink=True)
        elif p == _SGR.RAPID_BLINK:
            state = state._replace(rapid_blink=True)
        elif p == _SGR.INVERSE:
            state = state._replace(inverse=True)
        elif p == _SGR.HIDDEN:
            state = state._replace(hidden=True)
        elif p == _SGR.STRIKETHROUGH:
            state = state._replace(strikethrough=True)
        elif p == _SGR.DOUBLE_UNDERLINE:
            state = state._replace(double_underline=True)
        # Attribute OFF codes
        elif p == _SGR.BOLD_DIM_OFF:
            state = state._replace(bold=False, dim=False)
        elif p == _SGR.ITALIC_OFF:
            state = state._replace(italic=False)
        elif p == _SGR.UNDERLINE_OFF:
            state = state._replace(underline=False, double_underline=False)
        elif p == _SGR.BLINK_OFF:
            state = state._replace(blink=False, rapid_blink=False)
        elif p == _SGR.INVERSE_OFF:
            state = state._replace(inverse=False)
        elif p == _SGR.HIDDEN_OFF:
            state = state._replace(hidden=False)
        elif p == _SGR.STRIKETHROUGH_OFF:
            state = state._replace(strikethrough=False)
        # Basic colors (30-37, 40-47 standard; 90-97, 100-107 bright)
        elif (_SGR.FG_BLACK <= p <= _SGR.FG_WHITE
              or _SGR.FG_BRIGHT_BLACK <= p <= _SGR.FG_BRIGHT_WHITE):
            state = state._replace(foreground=(p,))
        elif (_SGR.BG_BLACK <= p <= _SGR.BG_WHITE
              or _SGR.BG_BRIGHT_BLACK <= p <= _SGR.BG_BRIGHT_WHITE):
            state = state._replace(background=(p,))
        elif p == _SGR.FG_DEFAULT:
            state = state._replace(foreground=None)
        elif p == _SGR.BG_DEFAULT:
            state = state._replace(background=None)
        # Extended colors (semicolon-separated format)
        elif p == _SGR.FG_EXTENDED:
            if color := _parse_extended_color(params, _SGR.FG_EXTENDED):
                state = state._replace(foreground=color)
        elif p == _SGR.BG_EXTENDED:
            if color := _parse_extended_color(params, _SGR.BG_EXTENDED):
                state = state._replace(background=color)
    return state


def propagate_sgr(lines: Sequence[str]) -> list[str]:
    r"""
    Propagate SGR codes across wrapped lines.

    When text with SGR styling is wrapped across multiple lines, each line
    needs to be self-contained for proper display. This function:

    - Ends each line with ``\x1b[0m`` if styles are active (prevents bleeding)
    - Starts each subsequent line with the active style restored

    :param lines: List of text lines, possibly containing SGR sequences.
    :returns: List of lines with SGR codes propagated.

    Example::

        >>> propagate_sgr(['\x1b[31mhello', 'world\x1b[0m'])
        ['\x1b[31mhello\x1b[0m', '\x1b[31mworld\x1b[0m']

    This is useful in cases of making special editors and viewers, and is used for the
    default modes (propagate_sgr=True) of :func:`wcwidth.wrap` and :func:`wcwidth.clip`.

    When wrapping and clipping text containing SGR sequences, maybe a previous line enabled the BLUE
    color--if we are viewing *only* the line following, we would want the carry over the BLUE color,
    and all lines with sequences should end with terminating reset (``\x1b[0m``).
    """
    # Fast path: check if any line contains SGR sequences
    if not any(_SGR_QUICK_CHECK.search(line) for line in lines) or not lines:
        return list(lines)

    result: list[str] = []
    state = _SGR_STATE_DEFAULT

    for line in lines:
        # Prefix with restoration sequence if state is active
        prefix = _sgr_state_to_sequence(state)

        # Update state by processing all SGR sequences in this line
        for match in _SGR_PATTERN.finditer(line):
            state = _sgr_state_update(state, match.group())

        # Build output line
        output_line = prefix + line if prefix else line
        if _sgr_state_is_active(state):
            output_line = output_line + _SGR_RESET

        result.append(output_line)

    return result
