class_name Persistence extends Node

@export var path = "user://saves/"
@export var group_name = "persist"

const SaveMethod = "on_save"
const OnBeforeLoadMethod = "on_before_load"
const LoadMethod = "on_load"

static func get_instance_data(node: Node):
    return {
        path = node.get_path(),
        scene = node.scene_file_path,
        data = node.call(SaveMethod)
    }

func get_file(filename: String) -> Result:
    if not filename.is_valid_filename():
        return Result.err('invalid filename "%s"' % filename)
    else:
        var file_path = path.path_join(filename)
        return Result.ok(file_path)

func save(filename: String) -> Result:
    DirAccess.make_dir_recursive_absolute(path.get_base_dir())
    var file_result = get_file(filename)
    if file_result.is_err():
        push_error(file_result.unwrap_err())
        return file_result
    var file_path = file_result.unwrap()
    var file = FileAccess.open(file_path, FileAccess.WRITE)
    var nodes = get_tree().get_nodes_in_group(group_name)

    for node in nodes:
        if node.scene_file_path.is_empty():
            push_warning('"%s" is not instanced, skipping' % node.name)
            continue

        if not node.has_method("on_save"):
            push_warning('"%s" does not have a "%s" method, skipping' % [node.name, SaveMethod])
            continue

        var data = get_instance_data(node)
        var serial = BinarySerializer.serialize_var(data)
        var bytes = var_to_bytes(serial)
        var length = bytes.size()
        file.store_32(length)
        file.store_buffer(var_to_bytes(serial))

    return Result.Unit

func _get_or_add(instance_data: Dictionary) -> Variant:
    var node_path: NodePath = instance_data['path']
    var scene_path = instance_data['scene']
    var node = get_node_or_null(node_path)

    if node.is_queued_for_deletion():
        await node.tree_exited
        node = null

    if node == null:
        node = load(scene_path).instantiate()
        var parent = node_path.slice(0, node_path.get_name_count() - 1)
        get_node(parent).call_deferred('add_child', node)

    return node

func load(filename: String) -> Result:
    var file_result = get_file(filename)
    if file_result.is_err():
        push_error(file_result.unwrap_err())
        return file_result
    var file_path = file_result.unwrap()

    if not FileAccess.file_exists(file_path):
        var err = 'ENOENT: "%s" does not exist' % path
        push_error(err)
        return Result.err(err)

    get_tree().call_group(group_name, OnBeforeLoadMethod)

    var file = FileAccess.open(file_path, FileAccess.READ)

    var length = file.get_length()
    while file.get_position() < length:
        var byte_len = file.get_32()
        var bytes = file.get_buffer(byte_len)
        var result = bytes_to_var(bytes)
        var save_data = BinarySerializer.deserialize_var(result)
        var instance = await _get_or_add(save_data)

        if not instance.has_method(LoadMethod):
            push_warning('"%s" does not have a "%s" method' % [instance.name, LoadMethod])
            instance.queue_free()
            continue

        instance.call(LoadMethod, save_data.data)

    return Result.Unit
