# Copyright (c) 2008-2015 testtools developers. See LICENSE for details.

"""Compatibility support - kept for backwards compatibility."""

__all__ = [
    "BytesIO",
    "StringIO",
    "text_repr",
    "unicode_output_stream",
]

import codecs
import io
import sys
import types
import unicodedata
import warnings
from io import BytesIO, StringIO  # for backwards-compat
from typing import IO, Any, NoReturn


def reraise(
    exc_class: type[BaseException],
    exc_obj: BaseException,
    exc_tb: types.TracebackType,
    _marker: Any = object(),
) -> NoReturn:
    """Re-raise an exception received from sys.exc_info() or similar."""
    warnings.warn(
        "This is not necessary in Python 3.",
        DeprecationWarning,
        stacklevel=2,
    )
    raise exc_obj.with_traceback(exc_tb)


def _u(s: str) -> str:
    warnings.warn(
        "This is not necessary in Python 3.",
        DeprecationWarning,
        stacklevel=2,
    )
    return s


def _b(s: str) -> bytes:
    """A byte literal."""
    warnings.warn(
        "This is not necessary in Python 3.",
        DeprecationWarning,
        stacklevel=2,
    )
    return s.encode("latin-1")


def _slow_escape(text: str) -> str:
    """Escape unicode ``text`` leaving printable characters unmodified

    The behaviour emulates the Python 3 implementation of repr, see
    unicode_repr in unicodeobject.c and isprintable definition.

    Because this iterates over the input a codepoint at a time, it's slow, and
    does not handle astral characters correctly on Python builds with 16 bit
    rather than 32 bit unicode type.
    """
    output: list[str | bytes] = []
    for c in text:
        o = ord(c)
        if o < 256:
            if o < 32 or 126 < o < 161:
                output.append(c.encode("unicode-escape"))
            elif o == 92:
                # Separate due to bug in unicode-escape codec in Python 2.4
                output.append("\\\\")
            else:
                output.append(c)
        else:
            # To get correct behaviour would need to pair up surrogates here
            if unicodedata.category(c)[0] in "CZ":
                output.append(c.encode("unicode-escape"))
            else:
                output.append(c)
    return "".join(output)  # type: ignore[arg-type]


def text_repr(text: str | bytes, multiline: bool | None = None) -> str:
    """Rich repr for ``text`` returning unicode, triple quoted if ``multiline``."""
    nl = (isinstance(text, bytes) and bytes((0xA,))) or "\n"
    if multiline is None:
        multiline = nl in text  # type: ignore[operator]
    if not multiline:
        # Use normal repr for single line of unicode
        return repr(text)
    prefix = repr(text[:0])[:-2]
    if multiline:
        # To escape multiline strings, split and process each line in turn,
        # making sure that quotes are not escaped.
        offset = len(prefix) + 1
        lines = []
        for line in text.split(nl):  # type: ignore[arg-type]
            r = repr(line)
            q = r[-1]
            lines.append(r[offset:-1].replace("\\" + q, q))
        # Combine the escaped lines and append two of the closing quotes,
        # then iterate over the result to escape triple quotes correctly.
        _semi_done = "\n".join(lines) + "''"
        p = 0
        while True:
            p = _semi_done.find("'''", p)
            if p == -1:
                break
            _semi_done = "\\".join([_semi_done[:p], _semi_done[p:]])
            p += 2
        return "".join([prefix, "'''\\\n", _semi_done, "'"])
    escaped_text = _slow_escape(text)
    # Determine which quote character to use and if one gets prefixed with a
    # backslash following the same logic Python uses for repr() on strings
    quote = "'"
    if "'" in text:
        if '"' in text:
            escaped_text = escaped_text.replace("'", "\\'")
        else:
            quote = '"'
    return "".join([prefix, quote, escaped_text, quote])


def unicode_output_stream(stream: IO[str]) -> IO[str]:
    """Get wrapper for given stream that writes any unicode without exception

    Characters that can't be coerced to the encoding of the stream, or 'ascii'
    if valid encoding is not found, will be replaced. The original stream may
    be returned in situations where a wrapper is determined unneeded.

    The wrapper only allows unicode to be written, not non-ascii bytestrings,
    which is a good thing to ensure sanity and sanitation.
    """
    warnings.warn(
        "This is not necessary in Python 3.",
        DeprecationWarning,
        stacklevel=2,
    )

    if sys.platform == "cli" or isinstance(stream, (io.TextIOWrapper, io.StringIO)):
        # Best to never encode before writing in IronPython, or if it is
        # already a TextIO [which in the io library has no encoding
        # attribute).
        return stream

    try:
        writer = codecs.getwriter(stream.encoding or "")  # type: ignore[attr-defined]
    except (AttributeError, LookupError):
        return codecs.getwriter("ascii")(stream, "replace")  # type: ignore[arg-type, return-value]

    if writer.__module__.rsplit(".", 1)[1].startswith("utf"):
        # The current stream has a unicode encoding so no error handler is needed
        return stream

    # Python 3 doesn't seem to make this easy, handle a common case
    try:
        return stream.__class__(  # type: ignore[call-arg]
            stream.buffer,  # type: ignore[attr-defined]
            stream.encoding,  # type: ignore[attr-defined]
            "replace",
            stream.newlines,  # type: ignore[attr-defined]
            stream.line_buffering,  # type: ignore[attr-defined]
        )
    except AttributeError:
        pass

    return writer(stream, "replace")  # type: ignore[arg-type, return-value]
