extends DialogicSubsystem

### Subsystem that manages saving and loading data.


## Emitted when a save was done. Keys of the info dictionary are "slot_name" and "is_autosave".
signal saved(info:Dictionary)

## The directory that will be saved to.
const SAVE_SLOTS_DIR := "user://dialogic/saves/"

## Temporarily stores a taken screen capture when using [take_slot_image()].
enum ThumbnailMode {NONE, TAKE_AND_STORE, STORE_ONLY}
var latest_thumbnail : Image = null


####################################################################################################
##					STATE
####################################################################################################

## Built-in, called by DialogicGameHandler.
func clear_game_state(clear_flag:=Dialogic.ClearFlags.FULL_CLEAR):
	_make_sure_slot_dir_exists()


####################################################################################################
##					MAIN METHODS
####################################################################################################

## Saves the current state to the given slot. 
## If no slot is given the default slot is used (name can be set in the dialogic settings)
## If you want to change to the current slot use save(Dialogic.Save.get_latest_slot())
func save(slot_name:String = '', is_autosave:bool = false, thumbnail_mode:=ThumbnailMode.TAKE_AND_STORE, slot_info :Dictionary = {}):
	# check if to save (if this is an autosave)
	if is_autosave and !ProjectSettings.get_setting('dialogic/save/autosave', false):
		return
	
	if slot_name.is_empty():
		slot_name = get_default_slot()
	
	set_latest_slot(slot_name)
	
	save_file(slot_name, 'state.txt', dialogic.get_full_state())
	
	if thumbnail_mode == ThumbnailMode.TAKE_AND_STORE:
		take_thumbnail()
		save_slot_thumbnail(slot_name)
	elif thumbnail_mode == ThumbnailMode.STORE_ONLY:
		save_slot_thumbnail(slot_name)
	
	if slot_info:
		store_slot_info(slot_name, slot_info)
	
	saved.emit({"slot_name":slot_name, "is_autosave": is_autosave})
	print('[Dialogic] Saved to slot "'+slot_name+'".')


## Loads all info from the given slot in the DialogicGameHandler (Dialogic Autoload).
## If no slot is given, the default slot is used.
## To check if something is saved in that slot use has_slot(). 
## If the slot does not exist, this method will fail. 
func load(slot_name:String=""):
	if slot_name.is_empty(): slot_name = get_default_slot()
	
	if !has_slot(slot_name):
		printerr("[Dialogic Error] Tried loading from invalid save slot '"+slot_name+"'.")
		return
	set_latest_slot(slot_name)
	dialogic.load_full_state(load_file(slot_name, 'state.txt', {}))


# Saves a variable to a file in the given slot.
func save_file(slot_name:String, file_name:String, data:Variant) -> void:
	if slot_name.is_empty(): slot_name = get_default_slot()
	if not slot_name.is_empty():
		if !has_slot(slot_name):
			add_empty_slot(slot_name)
		
		var file = FileAccess.open(SAVE_SLOTS_DIR.path_join(slot_name).path_join(file_name), FileAccess.WRITE)
		file.store_var(data)


## Loads a file from a given list and returns the contained info as a variable.
func load_file(slot_name:String, file_name:String, default:Variant) -> Variant:
	if slot_name.is_empty(): slot_name = get_default_slot()
	
	var path := get_slot_path(slot_name).path_join(file_name)
	
	if FileAccess.file_exists(path):
		var data = FileAccess.open(path, FileAccess.READ).get_var()
		return data
	return default



func set_global_info(key:String, value:Variant) -> void:
	var global_info := ConfigFile.new()
	if global_info.load(SAVE_SLOTS_DIR.path_join('global_info.txt')) == OK:
		global_info.set_value('main', key, value)
		global_info.save(SAVE_SLOTS_DIR.path_join('global_info.txt'))
	else:
		printerr("[Dialogic Error]: Couldn't access global saved info file.")


func get_global_info(key:String, default:Variant) -> Variant:
	var global_info := ConfigFile.new()
	if global_info.load(SAVE_SLOTS_DIR.path_join('global_info.txt')) == OK:
		return global_info.get_value('main', key, default)
	printerr("[Dialogic Error]: Couldn't access global saved info file.")
	return default


####################################################################################################
##					SLOT HELPERS
####################################################################################################
## Returns a list of all available slots. Usefull for iterating over all slots 
## (for example to build a UI list). 
func get_slot_names() -> Array:
	var save_folders := []

	if DirAccess.dir_exists_absolute(SAVE_SLOTS_DIR):
		var directory := DirAccess.open(SAVE_SLOTS_DIR)
		directory.list_dir_begin()
		var file_name := directory.get_next()
		while file_name != "":
			if directory.current_is_dir() and not file_name.begins_with("."):
				save_folders.append(file_name)
			file_name = directory.get_next()
		return save_folders
	return []


## Returns true if the given slot exists.
func has_slot(slot_name:String) -> bool:
	if slot_name.is_empty(): slot_name = get_default_slot()
	return slot_name in get_slot_names()


## Removes all the given slot along with all it's info/files.
func delete_slot(slot_name:String) -> void:
	var path := SAVE_SLOTS_DIR.path_join(slot_name)
	
	if DirAccess.dir_exists_absolute(path):
		var directory := DirAccess.open(path)
		directory.list_dir_begin()
		var file_name := directory.get_next()
		while file_name != "":
			directory.remove(file_name)
			file_name = directory.get_next()
		# then delete folder
		directory.remove(SAVE_SLOTS_DIR.path_join(slot_name))


## this adds a new save folder with the given name
##
func add_empty_slot(slot_name: String) -> void:
	if DirAccess.dir_exists_absolute(SAVE_SLOTS_DIR):
		var directory := DirAccess.open(SAVE_SLOTS_DIR)
		directory.make_dir(slot_name)


## reset the state of the given save folder (or default)
func reset_slot(slot_name: String = '') -> void:
	if slot_name.is_empty(): slot_name = get_default_slot()

	save_file(slot_name, 'state.txt', {})


## Returns the full path to the given slot folder
func get_slot_path(slot_name:String) -> String:
	return SAVE_SLOTS_DIR.path_join(slot_name)


## Returns the default slot name defined in the dialogic settings
func get_default_slot() -> String:
	return ProjectSettings.get_setting('dialogic/save/default_slot', 'Default')


## Returns the latest slot or empty if nothing was saved yet
func get_latest_slot() -> String:
	var latest_slot :String = ""
	if Engine.get_main_loop().has_meta('dialogic_latest_saved_slot'):
		latest_slot = Engine.get_main_loop().get_meta('dialogic_latest_saved_slot', '')
	else:
		latest_slot = get_global_info('latest_save_slot', '')
		Engine.get_main_loop().set_meta('dialogic_latest_saved_slot', latest_slot)
	if !has_slot(latest_slot):
		return ''
	return latest_slot


func set_latest_slot(slot_name:String) -> void:
	Engine.get_main_loop().set_meta('dialogic_latest_saved_slot', slot_name)
	set_global_info('latest_save_slot', slot_name)


func _make_sure_slot_dir_exists() -> void:
	if not DirAccess.dir_exists_absolute(SAVE_SLOTS_DIR):
		DirAccess.make_dir_recursive_absolute(SAVE_SLOTS_DIR)
	if not FileAccess.file_exists(SAVE_SLOTS_DIR.path_join('global_info.txt')):
		FileAccess.open(SAVE_SLOTS_DIR.path_join('global_info.txt'), FileAccess.WRITE)


####################################################################################################
##					SLOT INFO
####################################################################################################

func store_slot_info(slot_name:String, info: Dictionary) -> void:
	if slot_name.is_empty(): slot_name = get_default_slot()
	save_file(slot_name, 'info.txt', info)


func get_slot_info(slot_name:String = '') -> Dictionary:
	if slot_name.is_empty(): slot_name = get_default_slot()
	return load_file(slot_name, 'info.txt', {})


####################################################################################################
##					SLOT IMAGE
####################################################################################################

## Can be called manually to create a thumbnail. Then call save() with THUMBNAIL_MODE.STORE_ONLY
func take_thumbnail() -> void:
	latest_thumbnail = get_viewport().get_texture().get_image()


## No need to call from outside. Used to store the latest thumbnail to the given slot.
func save_slot_thumbnail(slot_name:String) -> void:
	if latest_thumbnail:
		latest_thumbnail.save_png(get_slot_path(slot_name).path_join('thumbnail.png'))


## Returns an ImageTexture containing the thumbnail of that slot.
func get_slot_thumbnail(slot_name:String) -> ImageTexture:
	if slot_name.is_empty(): slot_name = get_default_slot()
	
	var path := get_slot_path(slot_name).path_join('thumbnail.png')
	if FileAccess.file_exists(path):
		return ImageTexture.create_from_image(Image.load_from_file(path))
	return null


####################################################################################################
##					AUTOSAVE
####################################################################################################
## Reference to the autosave timer.
var autosave_timer := Timer.new()


func _ready() -> void:
	autosave_timer.one_shot = true
	DialogicUtil.update_timer_process_callback(autosave_timer)
	autosave_timer.name = "AutosaveTimer"
	autosave_timer.timeout.connect(_on_autosave_timer_timeout)
	add_child(autosave_timer)
	dialogic.event_handled.connect(_on_dialogic_event_handled)
	dialogic.timeline_started.connect(autosave_start_end)
	dialogic.timeline_ended.connect(autosave_start_end)
	_on_autosave_timer_timeout()


func _on_autosave_timer_timeout() -> void:
	if ProjectSettings.get_setting('dialogic/save/autosave_mode', 0) == 1:
		save('', true)
	autosave_timer.start(ProjectSettings.get_setting('dialogic/save/autosave_delay', 60))


func _on_dialogic_event_handled(event: DialogicEvent) -> void:
	if event is DialogicJumpEvent:
		if ProjectSettings.get_setting('dialogic/save/autosave_mode', 0) == 1:
			save('', true)
	if event is DialogicTextEvent:
		if ProjectSettings.get_setting('dialogic/save/autosave_mode', 0) == 1:
			save('', true)


func autosave_start_end() -> void:
	if ProjectSettings.get_setting('dialogic/save/autosave_mode', 0) == 1:
		save('', true)
