"""Safely print to streams, avoiding Unicode errors.

It ensures compatibility with various encodings and allows customization of
padding, separators, and output formatting. A global `safe_print` instance is
also provided for convenience.
"""

# Standard Library Imports
from __future__ import annotations

import os
import sys
from typing import BinaryIO, Callable

# Local Imports
from DisplayCAL.encoding import get_encodings

ORIGINAL_CODEPAGE = None
ENC, FS_ENC = get_encodings()
_CONWIDTH = None


def _get_console_width() -> int:
    """Get the width of the console in characters.

    Returns:
        int: The width of the console.
    """
    global _CONWIDTH
    if _CONWIDTH is None:
        _CONWIDTH = 80
        try:
            if sys.platform == "win32":
                import struct
                from ctypes import create_string_buffer, windll

                # Use stderr handle so that pipes don't affect the reported size
                stderr_handle = windll.kernel32.GetStdHandle(-12)
                buf = create_string_buffer(22)
                consinfo = windll.kernel32.GetConsoleScreenBufferInfo(
                    stderr_handle, buf
                )
                if consinfo:
                    _CONWIDTH = struct.unpack("hhhhHhhhhhh", buf.raw)[0]
            else:
                _CONWIDTH = int(os.getenv("COLUMNS"))
        except Exception:
            pass
    return _CONWIDTH


class SafePrinter:
    r"""A class for safely printing to a stream, avoiding Unicode errors.

    Write safely, avoiding any UnicodeDe-/EncodingErrors on strings and
    converting all other objects to safe string representations.

    sprint = SafePrinter(pad=False, padchar=' ', sep=' ', end=r'\n',
                            file=sys.stdout, fn=None)
    sprint(value, ..., pad=False, padchar=' ', sep=' ', end=r'\n',
            file=sys.stdout, fn=None)

    Writes the values to a stream (default sys.stdout), honoring its
    encoding and replacing characters not present in the encoding with
    question marks silently.

    Args:
        pad (bool | int, optional): Pad the lines to n chars, or
            os.getenv('COLUMNS') if True.
        padchar (str, optional): Character to use for padding, default a space.
        sep (str, optional): String inserted between values, default a space.
        end (str, optional): String appended after the last value, default a
            newline.
        file (BinaryIO, optional): A file-like object (stream); defaults to
            the sys.stdout.
        fn (Callable, optional): A function to execute instead of printing.
    """

    def __init__(
        self,
        pad: bool | int = False,
        padchar: str = " ",
        sep: str = " ",
        end: str = "\n",
        file_: BinaryIO = sys.stdout,
        fn: None | Callable = None,
        encoding: None | str = None,
    ) -> None:
        self.pad = pad
        self.padchar = padchar
        self.sep = sep
        self.end = end
        self.file = file_
        self.fn = fn
        self.encoding = encoding or "utf-8"

    def __call__(self, *args, **kwargs) -> None:
        """Call the instance to write the given arguments to the stream."""
        # TODO: Why calling the instance writes it, this is not a good practice.
        self.write(*args, **kwargs)

    def flush(self) -> None:
        """Flush the output stream."""
        self.file and self.file.flush()

    def write(self, *args, **kwargs) -> None:
        """Write the given arguments to the stream, formatted and encoded."""
        pad = kwargs.get("pad", self.pad)
        padchar = kwargs.get("padchar", self.padchar)
        sep = kwargs.get("sep", self.sep)
        end = kwargs.get("end", self.end)
        file_ = kwargs.get("file_", self.file)
        fn = kwargs.get("fn", self.fn)
        encoding = self.encoding

        # convert everything to bytes
        if isinstance(padchar, str):
            padchar = padchar.encode(self.encoding)
        if isinstance(sep, str):
            sep = sep.encode(self.encoding)
        if isinstance(end, str):
            end = end.encode(self.encoding)

        strargs = []
        for arg in args:
            if not isinstance(arg, bytes):
                arg = bytes(arg, encoding, "asciize")
            strargs.append(arg)
        line = sep.join(strargs).rstrip(end)
        if pad is not False:
            width = _get_console_width() if pad is True else int(pad)
            line = line.ljust(width, padchar)
        if fn:
            fn(line)
        else:
            try:
                if "b" not in file_.mode:
                    line = line.decode(self.encoding)
                    if end:
                        end = end.decode(self.encoding)
                file_.write(line)
                if end:
                    file_.write(end)
            except OSError:
                pass


safe_print = SafePrinter()


if __name__ == "__main__":
    for arg in sys.argv[1:]:
        print(arg)
