#
# SPDX-FileCopyrightText: Copyright (c) 1993-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
#
# 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.
#
import copy
import enum
import functools
import logging
import os
import sys
import time
import traceback

COLORED_MODULE_PRESENT = None


def has_colors():
    global COLORED_MODULE_PRESENT
    if COLORED_MODULE_PRESENT is None:
        try:
            import colored

            COLORED_MODULE_PRESENT = True
        except:
            COLORED_MODULE_PRESENT = False
            print(
                "[W] 'colored' module is not installed, will not use colors when logging. "
                "To enable colors, please install the 'colored' module: python3 -m pip install colored"
            )
    return COLORED_MODULE_PRESENT


# Context manager to apply indentation to messages
class LoggerIndent:
    def __init__(self, logger, indent):
        self.logger = logger
        self.old_indent = self.logger.logging_indent
        self.indent = indent

    def __enter__(self):
        self.logger.logging_indent = self.indent
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.logger.logging_indent = self.old_indent


# Context manager to temporarily set verbosity
class LoggerVerbosity:
    def __init__(self, logger, severity):
        self.logger = logger
        self.old_severity = copy.copy(self.logger.module_severity)
        self.module_severity = severity

    def __enter__(self):
        self.logger.module_severity = self.module_severity
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.logger.module_severity = self.old_severity


class LogMode(enum.IntEnum):
    """
    Specifies how messages should be logged.
    """

    EACH = 0
    """Log the message each time"""
    ONCE = 1
    """Log the message only once. The same message will not be logged again."""


class SeverityTrie:
    """
    A trie that represents per-path logging verbosities.
    """

    def _split_path(self, path):
        # Leading or duplicate slashes can create empty elements in the path components. We ignore those.
        return list(filter(lambda x: x, path.split(os.path.sep)))

    def __init__(self, severity_dict):
        assert "" in severity_dict, "severity_dict must include default severity!"

        self.trie = {}
        for path, severity in severity_dict.items():
            cur_dict = self.trie
            for path_component in self._split_path(path):
                if path_component not in cur_dict:
                    cur_dict[path_component] = {}
                cur_dict = cur_dict[path_component]
            cur_dict[""] = severity

        # Skip path checking if we don't have any path entries.
        self.has_non_default_entries = len(self.trie) > 1

    def get(self, path=None):
        """
        Get the logging verbosity for the given path.

        Args:
            path (str): The path

        Returns:
            int: The logging verbosity.
        """
        default_severity = self.trie[""]
        if path is None or not self.has_non_default_entries:
            return default_severity

        cur_dict = self.trie

        def get_value(dct):
            return dct.get("", default_severity)

        for path_component in self._split_path(path):
            if path_component not in cur_dict:
                return get_value(cur_dict)
            cur_dict = cur_dict[path_component]
        return get_value(cur_dict)

    def __str__(self):
        return str(self.trie)


class Logger:
    """
    Global logging interface. Do **not** construct a logger manually.
    Instead, use ``G_LOGGER``, the global logger.
    """

    ULTRA_VERBOSE = -20  # Cast it into the flames!
    """Enable unreasonably verbose messages and above"""
    SUPER_VERBOSE = -10
    """Enable extremely verbose messages and above"""
    EXTRA_VERBOSE = 0
    """Enable extra verbose messages and above"""
    VERBOSE = 10
    """Enable verbose messages and above"""
    INFO = 20
    """Enable informative messages and above"""
    START = 22
    """Enable messages indicating when a task is started and above"""
    FINISH = 28
    """Enable messages indicating when a task is finished and above"""
    WARNING = 30
    """Enable only warning messages and above"""
    ERROR = 40
    """Enable only error messages and above"""
    CRITICAL = 50
    """Enable only critical/fatal error messages and above"""

    SEVERITY_LETTER_MAPPING = {
        ULTRA_VERBOSE: "[U]",
        SUPER_VERBOSE: "[S]",
        EXTRA_VERBOSE: "[X]",
        VERBOSE: "[V]",
        INFO: "[I]",
        START: "[I]",
        FINISH: "[I]",
        WARNING: "[W]",
        ERROR: "[E]",
        CRITICAL: "[!]",
    }

    SEVERITY_COLOR_MAPPING = {
        ULTRA_VERBOSE: "dark_gray",
        SUPER_VERBOSE: "medium_violet_red",
        EXTRA_VERBOSE: "medium_purple",
        VERBOSE: "light_magenta",
        INFO: None,
        START: "light_cyan",
        FINISH: "light_green",
        WARNING: "light_yellow",
        ERROR: "light_red",
        CRITICAL: "light_red",
    }

    def __init__(
        self, severity=INFO, colors=True, letter=True, timestamp=False, line_info=False
    ):
        """
        Args:
            severity (Union[int, Dict[str, int]]):
                    Severity below which messages will be ignored.
                    This can be specified on a per-submodule/file basis by providing a dictionary of paths to
                    logging severities. In this case, use the ``""`` to indicate the default severity.
                    Paths should be relative to the `polygraphy/` directory.
                    For example, `polygraphy/backend` can be specified with just `backend/`.
                    For example: ``{"": G_LOGGER.INFO, "backend/trt": G_LOGGER.VERBOSE}``
                    This is converted to a ``SeverityTrie`` on assignment.
                    Defaults to G_LOGGER.INFO.
            colors (bool):
                    Whether to use colored output.
                    Defaults to True.
            letter (bool):
                    Whether to prepend each logging message with a letter indicating it's severity.
                    Defaults to True.
            timestamp (bool):
                    Whether to include a timestamp in the logging output.
                    Defaults to False.
            line_info (bool):
                    Whether to include file and line number information in the logging output.
                    Defaults to False.
        """
        self.logging_indent = 0
        self.once_logged = set()
        self.colors = colors
        self.letter = letter
        self.timestamp = timestamp
        self.line_info = line_info
        self.logger_callbacks = []
        self.log_file = None
        """
        Path to a log file to write logging output from Polygraphy.
        This will not include logging messages from libraries used by Polygraphy, like
        TensorRT or ONNX-Runtime.
        """
        self.module_severity = severity
        """
        Severity below which messages will be ignored.
        This can be specified on a per-submodule/file basis by providing a dictionary of paths to
        logging severities. In this case, use the ``""`` to indicate the default severity.
        Paths should be relative to the `polygraphy/` directory.
        For example, `polygraphy/backend` can be specified with just `backend/`.
        For example: ``{"": G_LOGGER.INFO, "backend/trt": G_LOGGER.VERBOSE}``
        This is converted to a ``SeverityTrie`` on assignment.
        Defaults to G_LOGGER.INFO.
        """
        self._use_python_logging_system = False
        """
        A flag indicating whether to use the Python `logging` module for log emission.
        By default, logs are emitted to `stdout` or `stderr`. When this flag is set to `True`,
        the logger uses the Python `logging` module instead of `stdout`/`stderr`.
        This allows logs to be integrated into a unified logging system which can be helpful
        in advanced cases like using multiprocessing or when the user wants one logging system
        to manage Polygraphy and other libraries logs.
        """

    @property
    def log_file(self):
        return self._log_path

    @log_file.setter
    def log_file(self, value):
        self._log_path = value
        self._log_file = None
        if self._log_path:
            dir_path = os.path.dirname(self._log_path)
            if dir_path:
                dir_path = os.path.realpath(dir_path)
                os.makedirs(dir_path, exist_ok=True)
            self._log_file = open(self._log_path, "w")

    @property
    def module_severity(self):
        return self._module_severity

    @module_severity.setter
    def module_severity(self, value):
        if isinstance(value, SeverityTrie):
            self._module_severity = value
        else:
            if not isinstance(value, dict):
                value = {"": value}
            if "" not in value:
                value[""] = Logger.INFO
            self._module_severity = SeverityTrie(value)

        self._run_callbacks()

    @property
    def severity(self):
        print(
            "Warning: Accessing the `severity` property of G_LOGGER is deprecated and will be removed in v0.50.0. Use `module_severity` instead"
        )
        return self._module_severity.get()

    @severity.setter
    def severity(self, value):
        print(
            "Warning: Accessing the `severity` property of G_LOGGER is deprecated and will be removed in v0.50.0. Use `module_severity` instead"
        )
        self.module_severity = value

    @property
    def use_python_logging_system(self):
        return self._use_python_logging_system

    @use_python_logging_system.setter
    def use_python_logging_system(self, value):
        self._use_python_logging_system = value
        if value:
            self.python_logger = logging.getLogger("Polygraphy")
            self.severity_level_mapping = {
                Logger.ULTRA_VERBOSE: 2,
                Logger.SUPER_VERBOSE: 4,
                Logger.EXTRA_VERBOSE: 6,
            }

    def module_path(self, path):
        """
        Converts a given path to a path relative to the Polygraphy root module.
        If the path is not part of the Polygraphy module, returns a path relative to the common prefix.

        Args:
            path (str): The path

        Returns:
            str: The path relative to the Polygraphy root module or common prefix.
        """
        import polygraphy

        module_root_dir = polygraphy.__path__[0]
        file_path = os.path.relpath(path, module_root_dir)
        if os.pardir in file_path:
            common_path_len = len(os.path.commonpath([module_root_dir, path]))
            file_path = path[common_path_len:].lstrip(os.path.sep)
        return file_path

    def _run_callbacks(self):
        for callback in self.logger_callbacks:
            callback(self._module_severity)

    def register_callback(self, callback):
        """
        Registers a callback with the logger, which will be invoked when the logging severity is modified.
        The callback is guaranteed to be called at least once in the register_callback function.

        Args:
            callback (Callable(SeverityTrie)):
                    A callback that accepts the current logger severity trie.
        """
        callback(self._module_severity)
        self.logger_callbacks.append(callback)

    def indent(self, level=1):
        """
        Returns a context manager that indents all strings logged by the specified amount.

        Args:
            level (int): The indentation level
        """
        return LoggerIndent(self, level + self.logging_indent)

    def verbosity(self, severity=CRITICAL):
        """
        Returns a context manager that temporarily changes the severity of the logger for its duration.

        Args:
            severity (Union[int, Dict[str, int]]):
                    Severity below which messages will be ignored.
                    This can be specified on a per-submodule/file basis by providing a dictionary of paths to
                    logging severities. In this case, use the ``""`` to indicate the default severity.
                    For example: ``{"": G_LOGGER.INFO, "backend/trt": G_LOGGER.VERBOSE}``
                    Defaults to Logger.CRITICAL, which will suppress all messages.
        """
        return LoggerVerbosity(self, severity)

    def log(self, message, severity, mode=LogMode.EACH, stack_depth=2, error_ok=False):
        """
        Logs a message to stdout.

        Args:
            message (Union[str, Callable() -> str]):
                    A string or callable which returns a string of the message to log.
            severity (int):
                    The severity with which to log this message. If the severity is less than
                    the logger's current severity, the message is suppressed. Provided callables
                    will not be called in that case.
            mode (LogMode):
                    Controls how the message is logged.
                    See LogMode for details.
            stack_depth (int):
                    The stack depth to use to determine file and line information.
                    Defaults to 2.
            error_ok (bool):
                    Whether to suppress errors encountered while logging.
                    When this is True, in the event of an error, the message will not be
                    logged, but the logger will recover and resume execution.
                    When False, the logger will re-raise the exception.
        """
        from polygraphy import config, constants

        def get_rel_file_path_and_lineno():
            file_path = sys._getframe(stack_depth).f_code.co_filename
            line_no = sys._getframe(stack_depth).f_lineno
            # If we can't get a valid path, keep walking the stack until we can.
            new_stack_depth = stack_depth
            while not os.path.exists(file_path) and new_stack_depth > 0:
                new_stack_depth -= 1
                file_path = sys._getframe(new_stack_depth).f_code.co_filename
                line_no = sys._getframe(new_stack_depth).f_lineno

            return self.module_path(file_path), line_no

        def process_message(message, file_path, line_no):
            def get_prefix():
                prefix = ""
                if self.letter:
                    prefix += Logger.SEVERITY_LETTER_MAPPING[severity] + " "
                if self.timestamp:
                    prefix += f"({time.strftime('%X')}) "
                if self.line_info:
                    prefix += f"[{file_path}:{line_no}] "
                return prefix

            def apply_indentation(prefix, message):
                message_lines = str(message).splitlines()
                tab = constants.TAB * self.logging_indent
                newline_tab = "\n" + tab + " " * len(prefix)
                return tab + newline_tab.join([line for line in message_lines])

            def apply_color(message):
                if self.colors and has_colors():
                    import colored

                    color = Logger.SEVERITY_COLOR_MAPPING[severity]

                    if color:
                        try:
                            color = colored.fore(color)
                        except:
                            color = [colored.fg(color)]

                        return colored.stylize(message, color)
                return message

            prefix = get_prefix()
            message = apply_indentation(prefix, message)
            return apply_color(f"{prefix}{message}")

        file_path, line_no = None, None
        if self.line_info or self.module_severity.has_non_default_entries:
            file_path, line_no = get_rel_file_path_and_lineno()

        def should_log(message):
            if severity < self.module_severity.get(file_path):
                return False

            if mode == LogMode.ONCE:
                message_hash = hash(message)
                if message_hash in self.once_logged:
                    return False
                self.once_logged.add(message_hash)
            return True

        if not should_log(message):
            return

        if callable(message):
            try:
                message = message()
            except Exception as err:
                if not error_ok or config.INTERNAL_CORRECTNESS_CHECKS:
                    raise
                message = f"<Error while logging this message: {str(err)}>"

        message = str(message)

        # Use the warnings module in correctness checking mode so all warnings are
        # visible in the test result summary.
        if config.INTERNAL_CORRECTNESS_CHECKS and severity == Logger.WARNING:
            import warnings

            warnings.warn(message)

        message = process_message(message, file_path, line_no)

        if self._log_file is not None:
            self._log_file.write(message + "\n")
            self._log_file.flush()

        if self._use_python_logging_system:
            # python logging system does not handle negative levels, map them to positive values
            level = severity if severity > 0 else self.severity_level_mapping[severity]
            self.python_logger.log(level=level, msg=message)
        else:
            print(
                message, file=sys.stdout if severity < Logger.CRITICAL else sys.stderr
            )

    def backtrace(self, depth=0, limit=None, severity=ERROR):
        limit = (
            limit if limit is not None else (3 - self.module_severity.get() // 10) * 2
        )  # Info provides 1 stack frame
        limit = max(limit, 0)
        frame = sys._getframe(depth + 2)
        self.log(
            " ".join(traceback.format_stack(f=frame, limit=limit)), severity=severity
        )

    def ultra_verbose(self, message, mode=LogMode.EACH):
        """
        Logs a message to stdout with ULTRA_VERBOSE severity.

        Args:
            message (Union[str, Callable() -> str]):
                    A string or callable which returns a string of the message to log.
            mode (LogMode):
                    Controls how the message is logged.
                    See LogMode for details.
        """
        self.log(message, Logger.ULTRA_VERBOSE, mode=mode, stack_depth=3, error_ok=True)

    def super_verbose(self, message, mode=LogMode.EACH):
        """
        Logs a message to stdout with SUPER_VERBOSE severity.

        Args:
            message (Union[str, Callable() -> str]):
                    A string or callable which returns a string of the message to log.
            mode (LogMode):
                    Controls how the message is logged.
                    See LogMode for details.
        """
        self.log(message, Logger.SUPER_VERBOSE, mode=mode, stack_depth=3, error_ok=True)

    def extra_verbose(self, message, mode=LogMode.EACH):
        """
        Logs a message to stdout with EXTRA_VERBOSE severity.

        Args:
            message (Union[str, Callable() -> str]):
                    A string or callable which returns a string of the message to log.
            mode (LogMode):
                    Controls how the message is logged.
                    See LogMode for details.
        """
        self.log(message, Logger.EXTRA_VERBOSE, mode=mode, stack_depth=3, error_ok=True)

    def verbose(self, message, mode=LogMode.EACH):
        """
        Logs a message to stdout with VERBOSE severity.

        Args:
            message (Union[str, Callable() -> str]):
                    A string or callable which returns a string of the message to log.
            mode (LogMode):
                    Controls how the message is logged.
                    See LogMode for details.
        """
        self.log(message, Logger.VERBOSE, mode=mode, stack_depth=3, error_ok=True)

    def info(self, message, mode=LogMode.EACH):
        """
        Logs a message to stdout with INFO severity.

        Args:
            message (Union[str, Callable() -> str]):
                    A string or callable which returns a string of the message to log.
            mode (LogMode):
                    Controls how the message is logged.
                    See LogMode for details.
        """
        self.log(message, Logger.INFO, mode=mode, stack_depth=3)

    def start(self, message, mode=LogMode.EACH):
        """
        Logs a message to stdout with START severity.

        Args:
            message (Union[str, Callable() -> str]):
                    A string or callable which returns a string of the message to log.
            mode (LogMode):
                    Controls how the message is logged.
                    See LogMode for details.
        """
        self.log(message, Logger.START, mode=mode, stack_depth=3)

    def finish(self, message, mode=LogMode.EACH):
        """
        Logs a message to stdout with FINISH severity.

        Args:
            message (Union[str, Callable() -> str]):
                    A string or callable which returns a string of the message to log.
            mode (LogMode):
                    Controls how the message is logged.
                    See LogMode for details.
        """
        self.log(message, Logger.FINISH, mode=mode, stack_depth=3)

    def warning(self, message, mode=LogMode.EACH):
        """
        Logs a message to stdout with WARNING severity.

        Args:
            message (Union[str, Callable() -> str]):
                    A string or callable which returns a string of the message to log.
            mode (LogMode):
                    Controls how the message is logged.
                    See LogMode for details.
        """
        self.log(message, Logger.WARNING, mode=mode, stack_depth=3)

    def error(self, message, mode=LogMode.EACH):
        """
        Logs a message to stdout with ERROR severity.

        Args:
            message (Union[str, Callable() -> str]):
                    A string or callable which returns a string of the message to log.
            mode (LogMode):
                    Controls how the message is logged.
                    See LogMode for details.
        """
        self.log(message, Logger.ERROR, mode=mode, stack_depth=3)

    def critical(self, message, ExceptionType=None):
        """
        Logs a message to stdout with CRITICAL severity and raises an exception.

        Args:
            message (Union[str, Callable() -> str]):
                    A string or callable which returns a string of the message to log.
            ExceptionType (type):
                    The type of exception to raise.
                    Defaults to PolygraphyException.

        Raises:
            ExceptionType
        """
        self.log(message, Logger.CRITICAL, stack_depth=3)
        from polygraphy.exception import PolygraphyException

        ExceptionType = ExceptionType or PolygraphyException
        raise ExceptionType(message) from None

    def internal_error(self, message):
        from polygraphy import config

        if not config.INTERNAL_CORRECTNESS_CHECKS:
            return

        self.log(message, Logger.CRITICAL, stack_depth=3)
        from polygraphy.exception import PolygraphyInternalException

        raise PolygraphyInternalException(message)

    def _str_from_module_info(self, module, name=None):
        ret = ""

        def try_append(func):
            nonlocal ret
            try:
                ret += func()
            except:
                pass

        try_append(lambda: name or f"Loaded Module: {module.__name__}")
        try_append(lambda: f" | Version: {module.__version__}")
        try_append(lambda: f" | Path: {list(map(os.path.realpath, module.__path__))}")
        return ret

    def module_info(self, module, name=None, severity=VERBOSE):
        message = self._str_from_module_info(module, name)
        self.log(message, severity=severity, stack_depth=3, mode=LogMode.ONCE)

    def log_exception(self, func):
        """
        Decorator that causes exceptions in a function to be logged.
        This is useful in cases where the exception is caught by a caller, but should
        still be logged.
        """

        @functools.wraps(func)
        def wrapped(*args, **kwargs):
            from polygraphy.exception import PolygraphyException

            try:
                return func(*args, **kwargs)
            except PolygraphyException:
                # `PolygraphyException`s are always logged.
                raise
            except Exception as err:
                G_LOGGER.error(err)
                raise

        return wrapped


G_LOGGER = Logger()
"""The global logger. Use this instead of constructing a logger"""

# For backwards compatibility
G_LOGGER.exit = G_LOGGER.critical
