"""Format colored console output."""

from __future__ import annotations

import os
import re
import shutil
import sys
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from typing import Final

    # fmt: off
    def reset(text: str) -> str: ...  # NoQA: E704
    def bold(text: str) -> str: ...  # NoQA: E704
    def faint(text: str) -> str: ...  # NoQA: E704
    def standout(text: str) -> str: ...  # NoQA: E704
    def underline(text: str) -> str: ...  # NoQA: E704
    def blink(text: str) -> str: ...  # NoQA: E704

    def black(text: str) -> str: ...  # NoQA: E704
    def white(text: str) -> str: ...  # NoQA: E704
    def red(text: str) -> str: ...  # NoQA: E704
    def green(text: str) -> str: ...  # NoQA: E704
    def yellow(text: str) -> str: ...  # NoQA: E704
    def blue(text: str) -> str: ...  # NoQA: E704
    def fuchsia(text: str) -> str: ...  # NoQA: E704
    def teal(text: str) -> str: ...  # NoQA: E704

    def darkgray(text: str) -> str: ...  # NoQA: E704
    def lightgray(text: str) -> str: ...  # NoQA: E704
    def darkred(text: str) -> str: ...  # NoQA: E704
    def darkgreen(text: str) -> str: ...  # NoQA: E704
    def brown(text: str) -> str: ...  # NoQA: E704
    def darkblue(text: str) -> str: ...  # NoQA: E704
    def purple(text: str) -> str: ...  # NoQA: E704
    def turquoise(text: str) -> str: ...  # NoQA: E704
    # fmt: on

try:
    # check if colorama is installed to support color on Windows
    import colorama
except ImportError:
    colorama = None

_CSI: Final[str] = re.escape('\x1b[')  # 'ESC [': Control Sequence Introducer

# Pattern matching ANSI control sequences containing colors.
_ansi_color_re: Final[re.Pattern[str]] = re.compile(r'\x1b\[(?:\d+;){0,2}\d*m')

_ansi_re: Final[re.Pattern[str]] = re.compile(
    _CSI
    + r"""
    (?:
      (?:\d+;){0,2}\d*m     # ANSI color code    ('m' is equivalent to '0m')
    |
      [012]?K               # ANSI Erase in Line ('K' is equivalent to '0K')
    )""",
    re.VERBOSE | re.ASCII,
)
"""Pattern matching ANSI CSI colors (SGR) and erase line (EL) sequences.

See :func:`strip_escape_sequences` for details.
"""

codes: dict[str, str] = {}


def terminal_safe(s: str) -> str:
    """Safely encode a string for printing to the terminal."""
    return s.encode('ascii', 'backslashreplace').decode('ascii')


def get_terminal_width() -> int:
    """Return the width of the terminal in columns."""
    return shutil.get_terminal_size().columns - 1


_tw: int = get_terminal_width()


def term_width_line(text: str) -> str:
    if not codes:
        # if no coloring, don't output fancy backspaces
        return text + '\n'
    else:
        # codes are not displayed, this must be taken into account
        return text.ljust(_tw + len(text) - len(strip_escape_sequences(text))) + '\r'


def color_terminal() -> bool:
    if 'NO_COLOR' in os.environ:
        return False
    if sys.platform == 'win32' and colorama is not None:
        colorama.init()
        return True
    if 'FORCE_COLOR' in os.environ:
        return True
    if not hasattr(sys.stdout, 'isatty'):
        return False
    if not sys.stdout.isatty():
        return False
    if 'COLORTERM' in os.environ:
        return True
    term = os.environ.get('TERM', 'dumb').lower()
    return term in ('xterm', 'linux') or 'color' in term


def nocolor() -> None:
    if sys.platform == 'win32' and colorama is not None:
        colorama.deinit()
    codes.clear()


def coloron() -> None:
    codes.update(_orig_codes)


def colorize(name: str, text: str, input_mode: bool = False) -> str:
    def escseq(name: str) -> str:
        # Wrap escape sequence with ``\1`` and ``\2`` to let readline know
        # it is non-printable characters
        # ref: https://tiswww.case.edu/php/chet/readline/readline.html
        #
        # Note: This hack does not work well in Windows (see #5059)
        escape = codes.get(name, '')
        if input_mode and escape and sys.platform != 'win32':
            return '\1' + escape + '\2'
        else:
            return escape

    return escseq(name) + text + escseq('reset')


def strip_colors(s: str) -> str:
    """Remove the ANSI color codes in a string *s*.

    .. caution::

       This function is not meant to be used in production and should only
       be used for testing Sphinx's output messages.

    .. seealso:: :func:`strip_escape_sequences`
    """
    return _ansi_color_re.sub('', s)


def strip_escape_sequences(text: str, /) -> str:
    r"""Remove the ANSI CSI colors and "erase in line" sequences.

    Other `escape sequences `__ (e.g., VT100-specific functions) are not
    supported and only control sequences *natively* known to Sphinx (i.e.,
    colors declared in this module and "erase entire line" (``'\x1b[2K'``))
    are eliminated by this function.

    .. caution::

       This function is not meant to be used in production and should only
       be used for testing Sphinx's output messages that were not tempered
       with by third-party extensions.

    .. versionadded:: 7.3

       This function is added as an *experimental* feature.

    __ https://en.wikipedia.org/wiki/ANSI_escape_code
    """
    return _ansi_re.sub('', text)


def create_color_func(name: str) -> None:
    def inner(text: str) -> str:
        return colorize(name, text)

    globals()[name] = inner


_attrs = {
    'reset': '39;49;00m',
    'bold': '01m',
    'faint': '02m',
    'standout': '03m',
    'underline': '04m',
    'blink': '05m',
}

for __name, __value in _attrs.items():
    codes[__name] = '\x1b[' + __value

_colors = [
    ('black', 'darkgray'),
    ('darkred', 'red'),
    ('darkgreen', 'green'),
    ('brown', 'yellow'),
    ('darkblue', 'blue'),
    ('purple', 'fuchsia'),
    ('turquoise', 'teal'),
    ('lightgray', 'white'),
]

for __i, (__dark, __light) in enumerate(_colors, 30):
    codes[__dark] = '\x1b[%im' % __i
    codes[__light] = '\x1b[%im' % (__i + 60)

_orig_codes = codes.copy()

for _name in codes:
    create_color_func(_name)
