from __future__ import annotations

import os
import re
import sys
from enum import Enum
from typing import Final

# Colors are disabled in non-TTY environments such as pipes. This means if output is redirected
# to a file, it won't contain color codes. Colors are enabled by default on continuous integration.

IS_CI: Final[bool] = bool(os.environ.get("CI"))
NO_COLOR: Final[bool] = bool(os.environ.get("NO_COLOR"))
CLICOLOR_FORCE: Final[bool] = bool(os.environ.get("CLICOLOR_FORCE"))
STDOUT_TTY: Final[bool] = bool(sys.stdout.isatty())
STDERR_TTY: Final[bool] = bool(sys.stderr.isatty())


_STDOUT_ORIGINAL: Final[bool] = False if NO_COLOR else CLICOLOR_FORCE or IS_CI or STDOUT_TTY
_STDERR_ORIGINAL: Final[bool] = False if NO_COLOR else CLICOLOR_FORCE or IS_CI or STDERR_TTY
_stdout_override: bool = _STDOUT_ORIGINAL
_stderr_override: bool = _STDERR_ORIGINAL


def is_stdout_color() -> bool:
    return _stdout_override


def is_stderr_color() -> bool:
    return _stderr_override


def force_stdout_color(value: bool) -> None:
    """
    Explicitly set `stdout` support for ANSI escape codes.
    If environment overrides exist, does nothing.
    """
    if not NO_COLOR or not CLICOLOR_FORCE:
        global _stdout_override
        _stdout_override = value


def force_stderr_color(value: bool) -> None:
    """
    Explicitly set `stderr` support for ANSI escape codes.
    If environment overrides exist, does nothing.
    """
    if not NO_COLOR or not CLICOLOR_FORCE:
        global _stderr_override
        _stderr_override = value


class Ansi(Enum):
    """
    Enum class for adding ANSI codepoints directly into strings. Automatically converts values to
    strings representing their internal value.
    """

    RESET = "\x1b[0m"

    BOLD = "\x1b[1m"
    DIM = "\x1b[2m"
    ITALIC = "\x1b[3m"
    UNDERLINE = "\x1b[4m"
    STRIKETHROUGH = "\x1b[9m"
    REGULAR = "\x1b[22;23;24;29m"

    BLACK = "\x1b[30m"
    RED = "\x1b[31m"
    GREEN = "\x1b[32m"
    YELLOW = "\x1b[33m"
    BLUE = "\x1b[34m"
    MAGENTA = "\x1b[35m"
    CYAN = "\x1b[36m"
    WHITE = "\x1b[37m"
    GRAY = "\x1b[90m"

    def __str__(self) -> str:
        return self.value


RE_ANSI = re.compile(r"\x1b\[[=\?]?[;\d]+[a-zA-Z]")


def color_print(*values: object, sep: str | None = " ", end: str | None = "\n", flush: bool = False) -> None:
    """Prints a colored message to `stdout`. If disabled, ANSI codes are automatically stripped."""
    if is_stdout_color():
        print(*values, sep=sep, end=f"{Ansi.RESET}{end}", flush=flush)
    else:
        print(RE_ANSI.sub("", (sep or " ").join(map(str, values))), sep="", end=end, flush=flush)


def color_printerr(*values: object, sep: str | None = " ", end: str | None = "\n", flush: bool = False) -> None:
    """Prints a colored message to `stderr`. If disabled, ANSI codes are automatically stripped."""
    if is_stderr_color():
        print(*values, sep=sep, end=f"{Ansi.RESET}{end}", flush=flush, file=sys.stderr)
    else:
        print(RE_ANSI.sub("", (sep or " ").join(map(str, values))), sep="", end=end, flush=flush, file=sys.stderr)


def print_info(*values: object) -> None:
    """Prints a informational message with formatting."""
    color_print(f"{Ansi.GRAY}{Ansi.BOLD}INFO:{Ansi.REGULAR}", *values)


def print_warning(*values: object) -> None:
    """Prints a warning message with formatting."""
    color_printerr(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR}", *values)


def print_error(*values: object) -> None:
    """Prints an error message with formatting."""
    color_printerr(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR}", *values)


if sys.platform == "win32":

    def _win_color_fix():
        """Attempts to enable ANSI escape code support on Windows 10 and later."""
        from ctypes import POINTER, WINFUNCTYPE, WinError, windll
        from ctypes.wintypes import BOOL, DWORD, HANDLE

        STDOUT_HANDLE = -11
        STDERR_HANDLE = -12
        ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4

        def err_handler(result, func, args):
            if not result:
                raise WinError()
            return args

        GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),))
        GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
            ("GetConsoleMode", windll.kernel32),
            ((1, "hConsoleHandle"), (2, "lpMode")),
        )
        GetConsoleMode.errcheck = err_handler
        SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
            ("SetConsoleMode", windll.kernel32),
            ((1, "hConsoleHandle"), (1, "dwMode")),
        )
        SetConsoleMode.errcheck = err_handler

        for handle_id in [STDOUT_HANDLE, STDERR_HANDLE]:
            try:
                handle = GetStdHandle(handle_id)
                flags = GetConsoleMode(handle)
                SetConsoleMode(handle, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
            except OSError:
                pass

    _win_color_fix()