@icon("res://components/Persister/hard-drive.svg")
extends Base
## Data persister to persist data through locations
##
## A data persister that is used to persist data through the game, run, round, 
## or room. A function is called to set data with a key in one of the categories 
## and then other functions can be called to clear the data for a key or all
## data in a category. 

signal data_persisted(key: String, value, category: PersisterEnums.Scope)

var _persisted = {}


func _spawned():
	if _triggerer:
		_info("Connecting to data scope triggers")
		_triggerer.listen("game", _on_game_triggered)
		_triggerer.listen("run", _on_run_triggered)
		_triggerer.listen("round", _on_round_triggered)
		_triggerer.listen("room", _on_room_triggered)
	_load()


## Store data in a category
func persist_data(key: String, value, category := PersisterEnums.Scope.RUN) -> void:
	# Only allow ints, bools or strings to be persisted and as Strings
	var original_pass = value
	
	if value is int:
		value = str(value)
	
	if value is bool:
		value = str(value)
	
	if not value is String:
		_warn("Attempted to persist data for key <%s> with an invalid data type |%s|" % [key, type_string(typeof(value))])
		return
	
	# Create category if does not exist
	if not _persisted.has(category):
		_info("Created new persister category {%s}" % [_get_category_name(category)])
		_persisted[category] = {}
	
	# If key is already set to value exit early
	if _persisted[category].has(key) and _persisted[category][key] == value:
		return
	
	_info("Set key <%s> in category {%s} to value |%s|" % [key, _get_category_name(category), value])
	_persisted[category][key] = value
	
	if _triggerer:
		_triggerer.trigger(key, {"value": original_pass})
	
	_emit_change(key, value, category)


## Get the value associated with a key in the highest priority or specified category 
func get_value(key: String, category: PersisterEnums.Scope = PersisterEnums.Scope.UNKNOWN, default = null):
	if category == PersisterEnums.Scope.UNKNOWN:
		category = _get_key_category(key)
	
	if category == PersisterEnums.Scope.UNKNOWN:
		return default
	
	if _persisted[category][key].is_valid_int():
		return int(_persisted[category][key])
	
	if _persisted[category][key] == "true" || _persisted[category][key] == "false":
		return _persisted[category][key] == "true"
	
	return _persisted[category][key]


## Delete data associated with a key from a certain category
func clear_data(key: String, category := PersisterEnums.Scope.RUN) -> void:
	if not _persisted.has(category):
		_info("Attempted to clear key <%s> in category {%s} but the category did not exist"  % [key, _get_category_name(category)])
		return
	
	if not _persisted[category].has(key):
		_info("Attempted to clear key <%s> in category {%s} but it did not exist"  % [key, _get_category_name(category)])
		return
	
	_info("Cleared key <%s> in category {%s}" % [key, _get_category_name(category)])
	_persisted[category].erase(key)
	
	_emit_removal(key, category)


## Delete a category including all data within it
func clear_category(category := PersisterEnums.Scope.RUN) -> void:
	if not _persisted.has(category):
		_error("Attempted to clear category {%s} but it did not exist" % [_get_category_name(category)])
		return
	
	var keys = _persisted[category].keys()
	
	_info("Cleared category {%s}" % [_get_category_name(category)])
	_persisted.erase(category)
	
	for key in keys:
		_emit_removal(key, category)


## Add a number to a number value with the given key from a certain category
func change_value(key: String, value: int, category := PersisterEnums.Scope.RUN) -> void:
	if not _persisted.has(category):
		_persisted[category] = {}
	
	if not _persisted[category].has(key):
		persist_data(key, value, category)
		return
	
	if is_nan(int(_persisted[category][key])):
		_error("Attempted to add number |%d| to key <%s> in category {%s} that is not a number (value: |%s|)" % [value, key, category, _persisted[category][key]])
		return
	
	var old_value = int(_persisted[category][key])
	_info("Added number |%d| to key <%s> in category {%s} (old: |%d|) (new: |%d|)" % [value, key, _get_category_name(category), old_value, old_value + value])
	persist_data(key, value + old_value, category)


func change_value_clamp_min(key: String, value: int, min: int, category := PersisterEnums.Scope.RUN) -> void:
	change_value_clamp(key, value, min, INF, category)


func change_value_clamp_max(key: String, value: int, max: int, category := PersisterEnums.Scope.RUN) -> void:
	change_value_clamp(key, value, -INF, max, category)


func change_value_clamp(key: String, value: int, min: int, max: int, category := PersisterEnums.Scope.RUN) -> void:
	if min > max and not min == -9223372036854775808 and not max == -9223372036854775808:
		_warn("Attempted to change clamp value %s with higher min %s than max %s" % [key, min, max])
		return
	
	if not _persisted.has(category):
		_persisted[category] = {}
	
	if not _persisted[category].has(key):
		if value < min and not min == -9223372036854775808:
			value = min
		
		if value > max and not max == -9223372036854775808:
			value = max
		
		persist_data(key, value, category)
		return
	
	if is_nan(int(_persisted[category][key])):
		_error("Attempted to add number |%d| to key <%s> in category {%s} that is not a number (value: |%s|)" % [value, key, category, _persisted[category][key]])
		return
	
	var old_value = int(_persisted[category][key])
	var new_value = value + old_value
	
	if new_value < min and not min == -9223372036854775808:
		new_value = min
	
	if new_value > max and not max == -9223372036854775808:
		new_value = max
	
	if old_value == new_value:
		return
	
	_info("Added clamped number |%d| to key <%s> in category {%s} (old: |%d|) (new: |%d|)" % [value, key, _get_category_name(category), old_value, new_value])
	persist_data(key, new_value, category)

func change_save(index: int):
	_save()
	persist_data("save", index)
	_load()


func save():
	_save()


func _emit_change(key: String, value: String, category: PersisterEnums.Scope) -> void:
	var key_category = _get_key_category(key)
	
	# Only emit change if the category set has the highest priority
	if key_category == category:
		if is_nan(int(value)):
			data_persisted.emit(key, value, category)
		else:
			data_persisted.emit(key, int(value), category)


func _emit_removal(key: String, category: PersisterEnums.Scope) -> void:
	var key_category = _get_key_category(key)
	
	if key_category == PersisterEnums.Scope.UNKNOWN:
		return
	
	var category_priority = _get_category_priority(category)
	var key_category_priority = _get_category_priority(key_category)
	
	if category_priority > key_category_priority:
		var value = _persister[key_category][key]
		
		if is_nan(int(value)):
			data_persisted.emit(key, value, key_category)
		else:
			data_persisted.emit(key, int(value), key_category)


func _get_category_name(category: PersisterEnums.Scope) -> String:
	match category:
		PersisterEnums.Scope.PERMANENT:
			return "permanent"
		PersisterEnums.Scope.SAVE:
			return "save"
		PersisterEnums.Scope.GAME:
			return "game"
		PersisterEnums.Scope.RUN:
			return "run"
		PersisterEnums.Scope.ROUND:
			return "round"
		PersisterEnums.Scope.ROOM:
			return "room"
		_:
			return "unknown"


func _get_category_priority(category: PersisterEnums.Scope) -> int:
	match category:
		PersisterEnums.Scope.PERMANENT:
			return 1
		PersisterEnums.Scope.SAVE:
			return 2
		PersisterEnums.Scope.GAME:
			return 3
		PersisterEnums.Scope.RUN:
			return 4
		PersisterEnums.Scope.ROUND:
			return 5
		PersisterEnums.Scope.ROOM:
			return 6
		_:
			return 0


func _get_key_category(key: String) -> PersisterEnums.Scope:
	var category_order = []
	
	for category in PersisterEnums.Scope:
		var category_value = PersisterEnums.Scope[category]
		
		category_order.push_back(
			{
				"priority": _get_category_priority(category_value),
				"category": category_value
			}
		)
	
	category_order.sort_custom(func(a, b):
		return a.priority > b.priority
	)
	
	for value in category_order:
		var category = value.category
	
		if _persisted.has(category):
			if _persisted[category].has(key):
				return category
	
	return PersisterEnums.Scope.UNKNOWN


func _get_all_category_data(category: PersisterEnums.Scope) -> Dictionary:
	if not _persisted.has(category):
		return {}
	
	return _persisted[category]


func _on_game_triggered(data: Dictionary) -> void:
	clear_category(PersisterEnums.Scope.GAME)


func _on_run_triggered(data: Dictionary) -> void:
	clear_category(PersisterEnums.Scope.RUN)


func _on_round_triggered(data: Dictionary) -> void:
	clear_category(PersisterEnums.Scope.ROUND)


func _on_room_triggered(data: Dictionary) -> void:
	clear_category(PersisterEnums.Scope.ROOM)


func _save():
	_info("Saving")
	_save_permanent_data()
	_save_save_data()


func _load():
	_info("Loading")
	_load_permanent_data()
	
	if not get_value("save"):
		_info("Creating starting save")
		persist_data("save", 1, PersisterEnums.Scope.PERMANENT)
	
	clear_category(PersisterEnums.Scope.SAVE)
	_load_save_data()


func _save_permanent_data():
	var save_file = FileAccess.open("user://data.json", FileAccess.WRITE)
	
	var data = _get_all_category_data(PersisterEnums.Scope.PERMANENT)
	var data_string = JSON.stringify(data)
	
	_info("Saving permanent save data")
	
	save_file.store_line(data_string)


func _save_save_data():
	if not FileAccess.file_exists("user://saves"):
		DirAccess.make_dir_absolute("user://saves")
	
	var save = get_value("save")
	var save_file = FileAccess.open("user://saves/save-%d.json" % [save], FileAccess.WRITE)
	
	var data = _get_all_category_data(PersisterEnums.Scope.SAVE)
	var data_string = JSON.stringify(data)
	
	_info("Saving save save data")
	
	save_file.store_line(data_string)


func _load_permanent_data():
	var save_file = FileAccess.open("user://data.json", FileAccess.READ)
	
	if not save_file:
		return
	
	_info("Loading permanent save data")
	var json_string = ""
	
	while save_file.get_position() < save_file.get_length():
		json_string += save_file.get_line()
	
	var json = JSON.new()
	
	var result = json.parse(json_string)
	var data = json.get_data()
	
	for key in data:
		Persister.persist_data(key, data[key], PersisterEnums.Scope.PERMANENT)


func _load_save_data():
	var save = get_value("save")
	var save_file = FileAccess.open("user://saves/save-%s.json" % [save], FileAccess.READ)
	
	if not save_file:
		return
	
	_info("Loading save save data")
	var json_string = ""
	
	while save_file.get_position() < save_file.get_length():
		json_string += save_file.get_line()
	
	var json = JSON.new()
	
	var result = json.parse(json_string)
	var data = json.get_data()
	
	Persister.clear_category(PersisterEnums.Scope.SAVE)
	
	for key in data:
		Persister.persist_data(key, data[key], PersisterEnums.Scope.SAVE)


func _on_timer_timeout():
	_save()