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

## Класс журнал событий.
class_name Logger extends Node

## Выдается, когда журнал событий создаёт сообщение.
signal logged(message: Dictionary)


enum {
    INFO = 1, ## Информационный уровень.
    DEBUG = 1<<1, ## Уровень предупреждения.
    WARNING = 1<<2, ## Уровень отладки.
    ERROR = 1<<3, ## Уровень ошибок.
    FATAL = 1<<4, ## Критический уровень.
    MAX = FATAL, ## Побитовый сдвиг влево для пользовательских уровней.
}


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

## Преопределение пользовательских значений по умолчанию.
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()

## Возвращает [param true], если у журнала события есть уровень.
func has_level(level: int) -> bool:
    return _names.has(level)

## Установить включенный уровень.
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

## Добавить пользовательский уровень журнала. Значение уровня должно быть уникальным и превышать [member MAX].
## Для вызова пользовательского уровня используйте метод - [method message].
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)

## Возвращает имя уровня.
func get_level_name(level: int) -> String:
    return _names[level]

## Установить включенный журнал. Отключить ВСЕ сообщения.
func set_log_enabled(enabled: bool) -> void:
    _log_enabled = enabled

## Возвращает [param true], если журнал включен.
func is_log_enabled() -> bool:
    return _log_enabled

## Установка выхода по умолчанию включена.
func set_default_output_enabled(enabled: bool) -> void:
    _default_output_enabled = enabled

## Возвращает [param true], если вывод по умолчанию включен.
func is_default_output_enabled() -> bool:
    return _default_output_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()

## Возвращает [param true], если запись в файл разрешена.
func is_file_write_enabled() -> bool:
    return _file_write_enabled

## Задать путь к файлу журнала.
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()

## Возвращает путь к файлу журнала.
func get_file_path() -> String:
    return _file_path

## Создать информационное сообщение.
func info(text: String) -> void:
    message(INFO, text)

## Создать отладочное сообщение. Только для отладочной сборки.
func debug(text: String) -> void:
    if OS.is_debug_build():
        message(DEBUG, text)

## Создать предупреждающее сообщение.
func warning(text: String) -> void:
    message(WARNING, text)

## Создать сообщение об ошибке.
func error(text: String) -> void:
    message(ERROR, text)

## Создать сообщение о критической ошибке.
func fatal(text: String) -> void:
    message(FATAL, text)

## Форматировать строку для файла.
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"],
        }
    )

## Форматировать строку для вывода редактора.
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"],
        }
    )

## Создать сообщение с пользовательским уровнем.
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 not Engine.is_editor_hint():
            if _file_write_enabled:
                _file.store_line(format_file(msg))
                _file.call_deferred('flush')

        if _default_output_enabled:
            print_debug(format_stdout(msg))


func _open_file() -> void:
    if Engine.is_editor_hint():
        return

    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)
