############
#
# Copyright (c) 2022 MIT CSAIL and Joseph DelPreto
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
# IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# See https://action-net.csail.mit.edu for more usage information.
# Created 2021-2022 for the MIT ActionNet project by Joseph DelPreto [https://josephdelpreto.com].
#
############

import numpy as np

from hermes.utils.time_utils import *


# Print a dictionary (recursively as appropriate).
def print_dict(d):
    print(get_dict_str(d))


# Get a string to display a dictionary (recursively as appropriate).
def get_dict_str(d, level=0):
    indent_root = " " * level
    indent_keys = indent_root + " "
    msg = "%s{\n" % indent_root
    for key, value in d.items():
        msg += "%s %s: " % (indent_keys, key)
        if isinstance(value, dict):
            msg += "\n"
            msg += get_dict_str(
                value, level + 2
            )  # one level for the key indent, one for advancing the level
        else:
            msg += "%s\n" % str(value)
    msg += "%s}\n" % indent_root
    return msg


# Print a variable and its type.
def print_var(var, name=None):
    print(get_var_str(var, name=name))


# Get a string to display a variable and its type.
def get_var_str(var, name=None):
    msg = ""
    if name is not None:
        msg += 'Variable "%s" of ' % name
    msg += "Type %s: " % type(var)
    msg += ""
    processed_var = False
    # Dictionary
    if isinstance(var, dict):
        msg += get_dict_str(var, level=3)
        processed_var = True
    # String
    if isinstance(var, str):
        msg += '"%s"' % var
        processed_var = True
    # Numpy array, if numpy has been imported
    try:
        if isinstance(var, np.ndarray):
            msg += "\n shape: %s" % str(var.shape)
            msg += "\n data type: %s" % str(var.dtype)
            msg += "\n %s" % str(var)
            processed_var = True
    except NameError:
        pass
    # Lists and tuples
    if isinstance(var, (list, tuple)):
        contains_non_numbers = False in [isinstance(x, (int, float)) for x in var]
        if contains_non_numbers:
            msg += "["
            for i, x in enumerate(var):
                msg += "\n %d: %s" % (i, get_var_str(x))
            msg += "\n ]"
        else:
            msg += "%s" % str(var)
        processed_var = True
    # Everything else
    if not processed_var:
        msg += "%s" % str(var)
        processed_var = True
    # Done!
    return msg.strip()


# Format (and optionally print) a timestamped log message.
def format_log_message(
    msg: str,
    *extra_msgs,
    source_tag: str | None = None,
    debug: bool = False,
    warning: bool = False,
    error: bool = False,
    userAction: bool = False,
    **kwargs,
):
    # Add a timestamp.
    timestamp_str = get_time_str(format="%Y-%m-%d %H:%M:%S.%f")
    # timestamp_str = get_time_str(format='%H:%M:%S.%f')
    # Add an indicator of the source.
    source_str = "[%s]" % source_tag
    source_str = source_str.ljust(10)
    # Add an indicator of the importance level.
    levels_str = {
        "normal": "[normal]",
        "debug": "[debug]",
        "warning": "[warn]",
        "error": "[error]",
        "userAction": "[prompt]",
    }
    level_str = levels_str["normal"]
    if debug:
        level_str = levels_str["debug"]
    if warning:
        level_str = levels_str["warning"]
    if error:
        level_str = levels_str["error"]
    if userAction:
        level_str = levels_str["userAction"]
    level_str = level_str.ljust(max([len(x) for x in levels_str.values()]))
    # Add the message prefix to each line of the message.
    msg_prefix = "%s %s %s" % (timestamp_str, source_str, level_str)
    msg = "%s: %s" % (msg_prefix, msg.replace("\n", "\n%s: " % msg_prefix))
    # Mimic how print() would print multiple arguments.
    for extra_msg in extra_msgs:
        msg += " %s" % extra_msg
    # Return the formatted message.
    return msg


# Format, optionally print, and optionally write to file a timestamped log message.
def write_log_message(
    msg: str,
    *extra_msgs,
    source_tag: str | None = None,
    debug: bool = False,
    warning: bool = False,
    error: bool = False,
    userAction: bool = False,
    print_message: bool = True,
    filepath: str | None = None,
    **kwargs,
):
    msg = format_log_message(
        msg,
        *extra_msgs,
        source_tag=source_tag,
        debug=debug,
        warning=warning,
        error=error,
        userAction=userAction,
        print_message=print_message,
        **kwargs,
    )
    if filepath is not None:
        # Note that it seems multiple file handles can be opened at the same time,
        #  so it shouldn't get an error in the case of multiple threads calling
        #  this method simultaneously.  But surround with try/except just in case.
        try:
            fout = open(filepath, "a")
            fout.write(msg)
            fout.write("\n")
            fout.close()
        except:
            print(
                "\n\nWARNING: error trying to write a log message to the following file: ",
                filepath,
                "\n\n",
            )
    return msg


######################
###### PRINTING ######
######################


def log_status(source_tag, msg, *extra_msgs, **kwargs) -> str:
    return format_log_message(msg, *extra_msgs, source_tag=source_tag, **kwargs)


def log_debug(source_tag, msg, *extra_msgs, **kwargs) -> str:
    return format_log_message(
        msg, *extra_msgs, source_tag=source_tag, debug=True, **kwargs
    )


def log_error(source_tag, msg, *extra_msgs, **kwargs) -> str:
    return format_log_message(
        msg, *extra_msgs, source_tag=source_tag, error=True, **kwargs
    )


def log_warn(source_tag, msg, *extra_msgs, **kwargs) -> str:
    return format_log_message(
        msg, *extra_msgs, source_tag=source_tag, warning=True, **kwargs
    )
