#     Copyright 2025, Kay Hayen, mailto:kay.hayen@gmail.com find license text at end of file


"""Outputs to the user.

Printing with intends or plain, mostly a compensation for the print strangeness.

We want to avoid "from __future__ import print_function" in every file out
there, which makes adding another debug print rather tedious. This should
cover all calls/uses of "print" we have to do, and the make it easy to simply
to "print for_debug" without much hassle (braces).

"""

from __future__ import print_function

import inspect
import os
import struct
import sys
import textwrap
import traceback

from nuitka.__past__ import (  # pylint: disable=redefined-builtin
    BrokenPipeError,
    raw_input,
)
from nuitka.utils.Utils import isWin32Windows

# Written by Options module.
is_quiet = False

# We have to interact with displayed progress bars when doing out trace outputs.
progress = None


def setQuiet():
    # singleton, pylint: disable=global-statement
    global is_quiet
    is_quiet = True


def isQuiet():
    return is_quiet


def printIndented(level, *what):
    print("    " * level, *what)


def printSeparator(level=0):
    print("    " * level, "*" * 10)


def printLine(*what, **kwargs):
    is_atty = sys.stdout.isatty()

    if progress and is_atty:
        progress.close()

    print(*what, **kwargs)


def printError(message):
    my_print(message, file=sys.stderr)


def flushStandardOutputs():
    sys.stdout.flush()
    sys.stderr.flush()


def getEnableStyleCode(style):
    style = _aliasStyle(style)

    if style == "pink":
        style = "\033[95m"
    elif style == "blue":
        style = "\033[94m"
    elif style == "green":
        style = "\033[92m"
    elif style == "yellow":
        style = "\033[93m"
    elif style == "red":
        style = "\033[91m"
    elif style == "bold":
        style = "\033[1m"
    elif style == "underline":
        style = "\033[4m"
    else:
        style = None

    return style


def wrapWithStyles(value, styles):
    assert type(styles) is not str
    result = (
        "".join(getEnableStyleCode(style) for style in styles)
        + value
        + getDisableStylesCode()
    )

    assert type(result) is str
    return result


_enabled_ansi = False


def _enableAnsi():
    # singleton, pylint: disable=global-statement
    global _enabled_ansi
    if not _enabled_ansi:
        # Only necessary on Windows, as a side effect of this, ANSI colors get enabled
        # for the terminal and never deactivated, so we are free to use them after
        # this.
        if os.name == "nt":
            os.system("")

        _enabled_ansi = True


def hasTerminalLinkSupport():
    if not sys.stdout.isatty():
        return False

    terminal_name = os.getenv("TERM_PROGRAM")

    if terminal_name is not None:
        if terminal_name == "vscode":
            return True

    # Windows terminal, in some cases, but not on Windows 11 unfortunately there
    # is no difference between CMD with its own terminal and CMD in Windows
    # Terminal, not even parent processes.
    if isWin32Windows():
        if "WT_SESSION" in os.environ or "WT_PROFILE_ID" in os.environ:
            return True

    # Powershell, should be used in new terminals only. Also hard to detect,
    # although here we could search parent process names to determine it.
    if isWin32Windows():
        if "PSModulePath" in os.environ:
            return True

    return False


def formatTerminalLink(text, link):
    if link is not None and hasTerminalLinkSupport():
        return "\033]8;;%s\033\\%s\033]8;;\033\\" % (link, text)
    else:
        return text


def getDisableStylesCode():
    return "\033[0m"


def _getIoctlGWINSZ(fd):
    # On non-Windows, this might work, # spell-checker: ignore GWINSZ,TIOCGWINSZ
    try:
        # pylint: disable=I0021,import-error
        import fcntl
        import termios

        return struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
    except BaseException:  # Catch all the things, pylint: disable=broad-except
        return None


def _getTerminalSizeWin32():
    try:
        from ctypes import create_string_buffer, windll

        # handles are -10 to -12
        h = windll.kernel32.GetStdHandle(-12)
        buffer = create_string_buffer(22)

        res = windll.kernel32.GetConsoleScreenBufferInfo(h, buffer)
        if res:
            # TODO: Could also define the C structure
            (
                _,
                _,
                _,
                _,
                _,
                left,
                top,
                right,
                bottom,
                _,
                _,
            ) = struct.unpack("hhhhHhhhhhh", buffer.raw)

            columns = right - left + 1
            lines = bottom - top + 1

            return lines, columns
    except BaseException:  # Catch all the things, pylint: disable=broad-except
        return None


def _getTerminalSize():
    # For Python2, we get to do it ourselves.

    if str is bytes:
        if isWin32Windows():
            columns = _getTerminalSizeWin32()
        else:
            # Try all file handles.
            columns = _getIoctlGWINSZ(0) or _getIoctlGWINSZ(1) or _getIoctlGWINSZ(2)

        if columns:
            return columns[1]

        try:
            return int(os.getenv("COLUMNS", "1000"))
        except ValueError:
            return 1000
    else:
        try:
            return os.get_terminal_size()[0]
        except OSError:
            return 1000


def _aliasStyle(style):
    """Map named styles to concrete colors."""

    # return driven, pylint: disable=too-many-return-statements

    if style == "test-prepare":
        return "pink"
    if style == "test-execution":
        return "green"
    if style == "test-progress":
        return "blue"
    if style == "test-debug":
        return "bold"
    if style == "link":
        return "blue"
    if style == "warning":
        return "yellow"
    if style == "scons-unexpected":
        return "yellow"
    if style == "error":
        return "red"
    else:
        return style


def _my_print(is_atty, args, kwargs):
    file_output = kwargs.get("file", sys.stdout)

    try:
        _my_print2(is_atty, args, kwargs)

        # Flush the output.
        file_output.flush()
    except BrokenPipeError:
        pass


def _my_print2(is_atty, args, kwargs):
    if "style" in kwargs:
        style = kwargs["style"]
        del kwargs["style"]

        if "end" in kwargs:
            end = kwargs["end"]
            del kwargs["end"]
        else:
            end = "\n"

        if style is not None and is_atty:
            enable_style = getEnableStyleCode(style)

            if enable_style is None:
                raise ValueError(
                    "%r is an invalid value for keyword argument style" % style
                )

            _enableAnsi()

            print(enable_style, end="", **kwargs)

        if style is not None and is_atty:
            print(*args, end="", **kwargs)
            print(getDisableStylesCode(), end=end, **kwargs)
        else:
            print(*args, end=end, **kwargs)
    else:
        print(*args, **kwargs)


def canUseColor(file_output):
    """Can use color with the given file handle.

    This function is intended for globally disabling the feature
    potentially, but for that we would need to use it everywhere
    first.
    """
    return file_output.isatty()


def my_print(*args, **kwargs):
    """Make sure we flush after every print.

    Not even the "-u" option does more than that and this is easy enough.

    Use kwarg style=[option] to print in a style listed below
    """

    file_output = kwargs.get("file", sys.stdout)

    is_atty = file_output.isatty()

    if progress and is_atty:
        with progress.withExternalWritingPause():
            _my_print(is_atty, args, kwargs)
    else:
        _my_print(is_atty, args, kwargs)


class ReportingSystemExit(SystemExit):
    """Our own system exit, after which a report should be written."""

    def __init__(self, exit_code, exit_message):
        SystemExit.__init__(self, exit_code)

        self.exit_message = exit_message


class OurLogger(object):
    def __init__(self, name, quiet=False, base_style=None):
        self.name = name
        self.base_style = base_style
        self.is_quiet = quiet

    def my_print(self, message, **kwargs):
        # For overload, pylint: disable=no-self-use
        my_print(message, **kwargs)

    @staticmethod
    def _warnMnemonic(mnemonic, style, output_function):
        if mnemonic.startswith("http"):
            url = mnemonic
            extra_prefix = ""
        else:
            url = "https://nuitka.net/info/%s.html" % mnemonic
            extra_prefix = "Complex topic! "

        output_function(
            """    %sMore information can be found at %s%s"""
            % (extra_prefix, getEnableStyleCode("link"), url),
            style=style,
        )

    def _printFormatted(self, prefix, leader, message, style, keep_format, file_handle):
        style = style or self.base_style

        if sys.stderr.isatty() and not keep_format:
            width = _getTerminalSize() or 10000
        else:
            width = 10000

        if leader is not None:
            message_prefix = prefix + "  "
        else:
            message_prefix = prefix

        if width - len(message_prefix) < 20:
            width = 80

        formatted_message = textwrap.fill(
            message,
            width=width,
            initial_indent=message_prefix,
            subsequent_indent=message_prefix,
            break_on_hyphens=False,
            break_long_words=False,
            expand_tabs=False,
            replace_whitespace=False,
        )

        formatted_message = _removeNotBreakingSpaces(formatted_message)

        if leader is not None:
            formatted_message = prefix + leader + "\n" + formatted_message

        self.my_print(formatted_message, style=style, file=file_handle)

    def warning(
        self, message, style="yellow", mnemonic=None, keep_format=False, leader=None
    ):
        if mnemonic is not None:
            from .options.Options import shallDisplayWarningMnemonic

            if not shallDisplayWarningMnemonic(mnemonic):
                return

        if self.name:
            prefix = "%s:WARNING: " % self.name
        else:
            prefix = "WARNING: "

        self._printFormatted(
            prefix=prefix,
            leader=leader,
            message=message,
            style=style,
            keep_format=keep_format,
            file_handle=sys.stderr,
        )

        if mnemonic is not None:
            self._warnMnemonic(mnemonic, style=style, output_function=self.warning)

    def sysexit(
        self, message="", style=None, mnemonic=None, exit_code=1, reporting=False
    ):
        from nuitka.Progress import closeProgressBar

        closeProgressBar()

        if exit_code != 0 and style is None:
            style = "red"

        if message:
            if exit_code != 0:
                self.my_print(
                    "FATAL: %s" % message,
                    style=style,
                    file=sys.stderr,
                )
            else:
                self.my_print(
                    message,
                    style=style,
                    file=sys.stderr,
                )

        if mnemonic is not None:
            self._warnMnemonic(mnemonic, style=style, output_function=self.warning)

        if reporting:
            raise ReportingSystemExit(exit_code=exit_code, exit_message=message)

        sys.exit(exit_code)

    def sysexit_exception(self, message, exception, exit_code=1):
        self.my_print("FATAL: %s" % message, style="red", file=sys.stderr)

        traceback.print_exc()
        self.sysexit("FATAL:" + repr(exception), exit_code=exit_code, reporting=True)

    def isQuiet(self):
        return is_quiet or self.is_quiet

    def info(
        self,
        message,
        style=None,
        mnemonic=None,
        prefix=None,
        keep_format=False,
        leader=None,
    ):
        if not self.isQuiet():
            self._printFormatted(
                prefix=(
                    "%s: " % self.name
                    if prefix is None
                    else "%s:%s: " % (self.name, prefix)
                ),
                leader=leader,
                message=message,
                style=style,
                keep_format=keep_format,
                file_handle=sys.stderr,
            )

            if mnemonic is not None:
                self._warnMnemonic(mnemonic, style=style, output_function=self.info)


class FileLogger(OurLogger):
    def __init__(self, name, quiet=False, base_style=None, file_handle=None):
        OurLogger.__init__(self, name=name, quiet=quiet, base_style=base_style)

        self.file_handle = file_handle

    def __del__(self):
        if self.file_handle is not None:
            self.file_handle.close()

    def my_print(self, message, **kwargs):
        if "file" not in kwargs:
            kwargs["file"] = self.file_handle or sys.stdout

        my_print(message, **kwargs)
        kwargs["file"].flush()

    def setFileHandle(self, file_handle):
        self.file_handle = file_handle

    def isFileOutput(self):
        return self.file_handle is not None

    def info(
        self,
        message,
        style=None,
        mnemonic=None,
        prefix=None,
        keep_format=False,
        leader=None,
    ):
        if not self.isQuiet() or self.file_handle:
            self._printFormatted(
                prefix=(
                    "%s: " % self.name
                    if prefix is None
                    else "%s:%s: " % (self.name, prefix)
                ),
                leader=leader,
                message=message,
                style=style,
                keep_format=keep_format,
                file_handle=self.file_handle or sys.stderr,
            )

            if mnemonic is not None:
                self._warnMnemonic(mnemonic, style=style, output_function=self.info)

    def debug(self, message, style=None):
        if self.file_handle:
            message = "%s:DEBUG: %s" % (self.name, message)

            style = style or self.base_style
            self.my_print(message, style=style)

    def info_to_file_only(self, message, style=None):
        if self.file_handle:
            self.info(message, style=style)

    def info_if_file(self, message, other_logger, style=None):
        if self.file_handle:
            self.info(message, style=style)
        else:
            other_logger.info(message, style=style)


class OurSconsLogger(OurLogger):
    def sysexit(
        self,
        message,
        style=None,
        mnemonic=None,
        exit_code=1,
        reporting=False,
        env=None,
    ):
        if env is not None:
            from nuitka.utils.FileOperations import getNormalizedPathJoin
            from nuitka.utils.Json import writeJsonToFilename

            writeJsonToFilename(
                getNormalizedPathJoin(env.source_dir, "scons-error.json"),
                {
                    "message": message,
                    "mnemonic": mnemonic,
                    "exit_code": exit_code,
                    "reporting": reporting,
                },
            )

            sys.exit(27)
        else:
            OurLogger.sysexit(
                self,
                message,
                style=style,
                mnemonic=mnemonic,
                exit_code=exit_code,
                reporting=reporting,
            )


general = OurLogger("Nuitka")
plugins_logger = OurLogger("Nuitka-Plugins")
recursion_logger = OurLogger("Nuitka-Inclusion")
progress_logger = OurLogger("Nuitka-Progress", quiet=True)
memory_logger = OurLogger("Nuitka-Memory")
metadata_logger = OurLogger("Nuitka-Metadata")
optimization_logger = FileLogger("Nuitka-Optimization")
pgo_logger = FileLogger("Nuitka-PGO")
code_generation_logger = OurLogger("Nuitka-CodeGen")
inclusion_logger = FileLogger("Nuitka-Inclusion")
scons_logger = OurSconsLogger("Nuitka-Scons")
scons_details_logger = OurLogger("Nuitka-Scons")
postprocessing_logger = OurLogger("Nuitka-Postprocessing")
options_logger = OurLogger("Nuitka-Options")
unusual_logger = OurLogger("Nuitka-Unusual")
data_composer_logger = OurLogger("Nuitka-DataComposer")
onefile_logger = OurLogger("Nuitka-Onefile")
tools_logger = OurLogger("Nuitka-Tools")
wheel_logger = OurLogger("Nuitka-Wheel", base_style="blue")
cache_logger = OurLogger("Nuitka-Cache")
reports_logger = OurLogger("Nuitka-Reports")
offsets_logger = OurLogger("Nuitka-Offsets")


def queryUser(question, choices, default, default_non_interactive):
    assert default in choices, (default, choices)
    assert default_non_interactive in choices, (default, choices)

    prompt = "%s? %s : " % (
        question,
        "/".join(
            "[%s]" % choice.title() if choice == default else choice.title()
            for choice in choices
        ),
    )

    # Integrates with progress bar by closing it.
    printLine(prompt, end="")
    flushStandardOutputs()

    try:
        reply = raw_input() or default
    except EOFError:
        reply = default_non_interactive
        printLine("%s (default non-interactive)" % (reply))

    reply = reply.lower()

    if reply == "y":
        reply = "yes"

    return reply


_non_breaking_space = chr(1)


def doNotBreakSpaces(*args):
    if len(args) == 1 and inspect.isgenerator(args[0]):
        return doNotBreakSpaces(*tuple(args[0]))

    return tuple(element.replace(" ", _non_breaking_space) for element in args)


class LineBufferedStreamRedirector(object):
    def __init__(self, stream):
        self.original_stream = stream
        self.buffer = ""

    def write(self, message):
        if not message:
            return

        self.buffer += message

        if "\n" in self.buffer:
            lines = self.buffer.split("\n")
            # All lines except the last one are complete.
            # The last one is the start of the new line (remainder).
            if lines[-1] == "":
                # Ends with a newline
                to_print = self.buffer
                self.buffer = ""
            else:
                to_print = "\n".join(lines[:-1]) + "\n"
                self.buffer = lines[-1]

            if progress and self.original_stream.isatty():
                with progress.withExternalWritingPause():
                    self.original_stream.write(to_print)
                    self.original_stream.flush()
            else:
                self.original_stream.write(to_print)
                self.original_stream.flush()

    def flush(self):
        # We don't flush the buffer, as that would cause partial lines to be
        # printed, which then get overwritten by the progress bar. We wait
        # for the newline to print it.
        self.original_stream.flush()

    def __del__(self):
        if self.buffer:
            try:
                self.original_stream.write(self.buffer)
                self.original_stream.flush()
            except Exception:  # pylint: disable=broad-except
                pass

    def isatty(self):
        return self.original_stream.isatty()


def _removeNotBreakingSpaces(message):
    #    return message
    return message.replace(_non_breaking_space, " ")


#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the GNU Affero General Public License, Version 3 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        http://www.gnu.org/licenses/agpl.txt
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
