# Copyright (c) 2020-2023 Mansur Isaev and contributors - MIT License
# See `LICENSE.md` included in the source distribution for details.

## Logger class.
class_name Logger
extends Node

## Emitted when the logger create a message.
signal logged(message: Dictionary)


enum {
    INFO = 1, ## Info level.
    DEBUG = 1<<1, ## Warning level.
    WARNING = 1<<2, ## Debug level.
    ERROR = 1<<3, ## Error level.
    FATAL = 1<<4, ## Fatal level.
    MAX = FATAL, ## Bitwise left shift for custom levels.
}


var _level : int

var _log_enabled : bool
var _default_output_enabled : bool
var _file_write_enabled : bool

var _file : FileAccess
var _file_path : String

var _format_default_output : String
var _format_file : String

var _names = {
    INFO: "ИНФО",
    DEBUG: "ОТЛАДКА",
    WARNING: "ВНИМАНИЕ",
    ERROR: "ОШИБКА",
    FATAL: "АВАРИЯ"
}
var _msg_array: Array
var _filters: Array

# Can be overridden to define custom default values.
func _enter_tree() -> void:
    set_log_enabled(ProjectSettings.get_setting("plugins/logger/log_enabled", true))
    set_default_output_enabled(ProjectSettings.get_setting("plugins/logger/default_output", false))

    _level = ProjectSettings.get_setting("plugins/logger/level", INFO | DEBUG | WARNING | ERROR | FATAL)

    set_file_path(ProjectSettings.get_setting("plugins/logger/file/file_path", "user://uarep-ctl.log"))
    set_file_write_enabled(ProjectSettings.get_setting("plugins/logger/file/log_file_write", true))

    _format_default_output = ProjectSettings.get_setting("plugins/logger/default_output_format", "[{year}-{month}-{day} {hour}:{minute}:{second}][{level}]{text}")
    _format_file = ProjectSettings.get_setting("plugins/logger/file_format", "[{year}-{month}-{day} {hour}:{minute}:{second}][{level}]{text}")


func _exit_tree() -> void:
    _close_file()

## Return [param true] if logger has level.
func has_level(level: int) -> bool:
    return _names.has(level)

## Set the level enabled.
func set_level(level: int, enabled: bool) -> void:
    assert(has_level(level), "Invalid level.")
    if not has_level(level):
        return

    if enabled:
        _level |= level
    else:
        _level &= ~level

## Add a custom log level. The level value must be unique and be greater than [member MAX].
## To call the custom level use [method message] method.
func add_level(level: int, nm: String) -> void:
    assert(not has_level(level), "Has level.")
    assert(level > MAX and not level % 2, "Invalid level.")
    assert(name, "Invalid name.")

    if not has_level(level) and level > MAX and not level % MAX and not name.is_empty():
        _names[level] = nm
        set_level(level, true)

## Return the level name.
func get_level_name(level: int) -> String:
    return _names[level]

## Set Log enabled. Disable ALL messages.
func set_log_enabled(enabled: bool) -> void:
    _log_enabled = enabled

## Return [param true] if Log enabled.
func is_log_enabled() -> bool:
    return _log_enabled

## Set default output enabled.
func set_default_output_enabled(enabled: bool) -> void:
    _default_output_enabled = enabled

## Returns [param true] if default output is enabled.
func is_default_output_enabled() -> bool:
    return _default_output_enabled

## Set file write enabled.
func set_file_write_enabled(enabled: bool) -> void:
    if _file_write_enabled == enabled:
        return

    _file_write_enabled = enabled

    if enabled:
        _open_file()
    else:
        _close_file()

## Returns [param true] if write to a file is enabled.
func is_file_write_enabled() -> bool:
    return _file_write_enabled

## Set the path to the log file.
func set_file_path(path: String) -> void:
    assert(path.is_absolute_path(), "Invalid path.")
    if not path.is_absolute_path() or _file_path == path:
        return

    _file_path = path

    if is_file_write_enabled():
        _open_file()

## Return path to log file.
func get_file_path() -> String:
    return _file_path

## Create an info message.
func info(text: String) -> void:
    message(INFO, text)

## Create a debug message. Debug build only.
func debug(text: String) -> void:
    if OS.is_debug_build():
        message(DEBUG, text)

## Create a warning message.
func warning(text: String) -> void:
    message(WARNING, text)

## Create an error message.
func error(text: String) -> void:
    message(ERROR, text)

## Create a fatal error message.
func fatal(text: String) -> void:
    message(FATAL, text)

## Format string for log file.
func format_file(msg: Dictionary) -> String:
    return _format_file.format(
        {
            "year": "%02d" % msg["year"],
            "month": "%02d" % msg["month"],
            "day": "%02d" % msg["day"],
            "hour": "%02d" % msg["hour"],
            "minute":"%02d" % msg["minute"],
            "second":"%02d" % msg["second"],
            "level": _names[msg["level"]],
            "text": msg["text"],
        }
    )

## Format string for editor output.
func format_stdout(msg: Dictionary) -> String:
    return _format_default_output.format(
        {
            "year": "%02d" % msg["year"],
            "month": "%02d" % msg["month"],
            "day": "%02d" % msg["day"],
            "hour": "%02d" % msg["hour"],
            "minute":"%02d" % msg["minute"],
            "second":"%02d" % msg["second"],
            "level": _names[msg["level"]],
            "text": msg["text"],
        }
    )

## Create a message with a custom level.
func message(level: int, text: String) -> void:
    assert(has_level(level), "Has not level.")
    assert(text, "Invalid message text.")

    if _log_enabled and _level & level:
        var msg : Dictionary = Time.get_datetime_dict_from_system()
        msg["level"] = level
        msg["level_name"] = _names[level]
        msg["text"] = " " + text

        _msg_array.append(msg)
        if (_flt_msg(msg)):
            logged.emit(msg)


        if _file_write_enabled:
            _file.store_line(format_file(msg))
            _file.call_deferred('flush')

        if _default_output_enabled:
            print(format_stdout(msg))


func _open_file() -> void:
    if is_instance_valid(_file) and _file.is_open():
        _file.close()

    _file = FileAccess.open(get_file_path(), FileAccess.READ_WRITE)

    var err := FileAccess.get_open_error()
    if err:
        _file = FileAccess.open(get_file_path(), FileAccess.WRITE)
        _file.close()
        _file = FileAccess.open(get_file_path(), FileAccess.READ_WRITE)
        return print_debug(error_string(err))

    err = _file.get_error()
    if err:
        return print_debug(error_string(err))
    _file.seek_end(0)


func _close_file() -> void:
    if is_instance_valid(_file) and _file.is_open():
        _file = null


func save_file(path):
    var f = FileAccess.open(path, FileAccess.WRITE)
    var err := FileAccess.get_open_error()
    if err:
        print_debug(error_string(err))
    var log_text = _file.get_as_text()
    f.store_line(log_text)
    f.close()


func set_filtrs(flt_array: Array):
    _filters = flt_array


func _flt_msg(msg):
    var flt_arr: Array = []
    for item in _filters:
        if item.button_pressed:
            flt_arr.append(item.get_meta('filter'))
            if msg['text'].find(item.get_meta('filter')) != -1:
                return true
    if msg['level_name'] in flt_arr:
        return true
    return false


func add_msgs():
    for msg in _msg_array:
        if (_flt_msg(msg)):
            logged.emit(msg)
