class_name MapLoader
extends Node
## Handles map loading.
@export_group("Player Nodes")
@export var player : Player
@export var input_recorder : InputRecorder

@export_group("Scene Pairs")
@export var _scene_pairs : Array[ScenePair] = []

@export_group("Offset Margins")
## Margin between present room and past room.
@export var _margin_to_past : float = 500
## Margin between two scene pairs.
@export var _margin_to_next : float = 500

## Scene pairs in memory, identified by an int [param id].
var _stored_scene_pairs : Dictionary[int, StoredScenePair]

## Stored offsets per scene pair, identified by an int [param id].[br][br]
## The [param x]-value determines offset from most recent scene pair loaded for the first time to current scene pair.[br]
## The [param y]-value determines offset from present to past room.
var _scene_offsets : Dictionary[int, Vector2]

## Offset along x-axis for next scene pair loaded for the first time.[br][br]
## Whenever a new scene pair is loaded for the first time, its position is set to the right of
## all previously loaded scene pairs, creating a row of rooms.
## To make sure no two scene pairs overlap, the current total offset needs to be remembered.
var _next_scene_pair_offset : float = 0

## Stores whether the global position of the scene is already set.
var _is_global_position_set : Array[bool] = []

## Emitted when current map room has handled player positioning.[br][br]
## This is basically just a propagation of the current map room's handled_player_positioning signal.
signal handled_player_positioning

func _ready() -> void:
	_is_global_position_set.resize(_scene_pairs.size())
	SignalBus.entered_door.connect(_enter_door)
	_switch_and_move()

#region Room Changing
func _enter_door(door_id : int):
	var next_room_id = _get_next_scene_pair_id(door_id)
	var target_door_id = _get_target_door_id(door_id)
	_switch_and_move(next_room_id, target_door_id)
#endregion

#region Time Shift
## Returns the offset between the current present and past room.[br][br]
## The returned value is positive when target [param time] is [code]PAST[/code]
## and negative when target [param time] is [code]PRESENT[/code].
func get_timeshift_offset(time : Utils.TIME = Globals.current_time) -> float:
	if not _scene_offsets.has(Globals.current_room_id):
		_calculate_offsets()
	return _scene_offsets[Globals.current_room_id].y * (1 if time == Utils.TIME.PAST else -1)
#endregion

#region Map Loading
func _switch_and_move(id : int = Globals.current_room_id, target_door_id : int = -1):
	_switch_to_scene_pair(id)
	_handle_player_position(target_door_id)
	SignalBus.load_room()

func _handle_player_position(target_door_id : int = -1):
	var target_room : MapRoom = _get_current_scene_pair().past if Globals.current_time == Utils.TIME.PAST else _get_current_scene_pair().present
	target_room.handle(player, input_recorder, target_door_id)
	if not target_room.is_player_positioning_handled:
		await target_room.handled_player_positioning
	handled_player_positioning.emit()

func _switch_to_scene_pair(id : int = Globals.current_room_id):
	if not _is_id_valid(id):
		push_error("Invalid id in _switch_to_scene_pair(): ", id)
		assert(false)
	
	_remove_previous_scene_pair()
	
	_load_scene_pair(id)
	
	var scene_pair = _stored_scene_pairs[id]
	add_child(scene_pair.present)
	add_child(scene_pair.past)
	
	_calculate_offsets(id)
	if not _is_global_position_set[id]:
		scene_pair.present.global_position = Vector2(_scene_offsets[id].x, 0)
		scene_pair.past.global_position = _scene_offsets[id]
		_is_global_position_set[id] = true
	Globals.current_room_id = id
	
	# remove distant rooms
	_unload_distant_rooms.call_deferred(id)
	
	# load adjacent rooms into memory
	for adjacent_id : int in _get_adjacent_room_ids(id):
		_load_scene_pair.call_deferred(adjacent_id)

## Loads scene pair by [param id] into memory.
func _load_scene_pair(id : int = Globals.current_room_id):
	if not _is_id_valid(id):
		push_error("Invalid id in _load_scene_pair(): ", id)
		assert(false)
	
	# cancel loading if scene pair is already in memory
	if _stored_scene_pairs.has(id):
		return
	
	var new_scene_pair = StoredScenePair.new()
	var tmp_present = load(_scene_pairs[id].present).instantiate()
	var tmp_past = load(_scene_pairs[id].past).instantiate()
	if not tmp_present is MapRoom or not tmp_past is MapRoom:
		push_error("Loaded scenes are not map rooms.")
		assert(false)
	new_scene_pair.present = tmp_present
	new_scene_pair.past = tmp_past
	_stored_scene_pairs[id] = new_scene_pair

## Calculates scene offsets for scene pair by [param id] and stores them in [member _scene_offsets].
func _calculate_offsets(id : int = Globals.current_room_id):
	if not _is_id_valid(id):
		push_error("Invalid id in _calculate_offsets(): ", id)
		assert(false)
	
	# cancel calculation if offset has already been calculated and stored
	if _scene_offsets.has(id):
		return
	
	# calculating offsets requires the scene pair to be loaded.
	if not _stored_scene_pairs.has(id):
		_load_scene_pair(id)
	
	var new_offset = Vector2.ZERO
	
	var present_room_size = _stored_scene_pairs[id].present.get_room_size()
	
	# calculate offset between present and past room
	new_offset.y = present_room_size.y + _margin_to_past
	
	# calculate offset between last first-time loaded scene pair and this scene pair
	new_offset.x = _next_scene_pair_offset
	
	_scene_offsets[id] = new_offset
	
	# calculate offset for next scene pair loaded for the first time
	_next_scene_pair_offset += max(present_room_size.x, _stored_scene_pairs[id].past.get_room_size().x) + _margin_to_next

## Removes all current map rooms from scene tree.
func _remove_previous_scene_pair():
	for scene : Node in get_children():
		if scene is MapRoom:
			remove_child(scene)
#endregion

#region Housekeeping
## Returns current StoredScenePair
func _get_current_scene_pair() -> StoredScenePair:
	if not _is_id_valid():
		push_error("Current ID is not valid!")
		assert(false)
	if not _stored_scene_pairs.has(Globals.current_room_id):
		push_warning("Current id is not in stored scenes")
		_switch_to_scene_pair()
	return _stored_scene_pairs[Globals.current_room_id]

## Returns the current room for [param time] (default: current time)
func _get_current_room_by_time(time : Utils.TIME = Globals.current_time) -> MapRoom:
	var scene_pair = _get_current_scene_pair()
	return scene_pair.past if time == Utils.TIME.PAST else scene_pair.present

## Makes sure that [param id] is within bounds of possible scene pairs.
func _is_id_valid(id : int = Globals.current_room_id) -> bool:
	return id >= 0 and id < _scene_pairs.size()

func _is_door_id_valid(scene_pair : ScenePair, door_id : int = 0) -> int:
	return not scene_pair.connections.is_empty() and door_id >= 0  and door_id < scene_pair.connections.size()

func _get_connection(door_id : int = 0, id : int = Globals.current_room_id) -> ScenePairConnection:
	if not _is_id_valid(id):
		push_error("Invalid ID in _get_id_of_next_room(): ", id)
		assert(false)
	
	var scene_pair = _scene_pairs[id]
	if not _is_door_id_valid(scene_pair, door_id):
		push_error("Invalid Door ID in _get_id_of_next_room(): ", door_id, " (amt of doors: ", scene_pair.connections.size(),")")
		assert(false)
	
	return scene_pair.connections[door_id]

func _get_next_scene_pair_id(door_id : int = 0, id : int = Globals.current_room_id) -> int:
	var connection : ScenePairConnection = _get_connection(door_id, id)
	return connection.target_scene_pair_id

func _get_target_door_id(door_id : int = 0, id : int = Globals.current_room_id) -> int:
	var connection : ScenePairConnection = _get_connection(door_id, id)
	return connection.target_scene_pair_door_id

func _get_adjacent_room_ids(id : int = Globals.current_room_id) -> Array[int]:
	if not _is_id_valid(id):
		push_error("Invalid ID in _get_adjacent_rooms(): ", id)
		assert(false)
	var result : Array[int] = []
	for connection : ScenePairConnection in _scene_pairs[id].connections:
		result.append(connection.target_scene_pair_id)
	return result

func _unload_distant_rooms(id : int = Globals.current_room_id):
	var required_rooms : Array[int] = _get_adjacent_room_ids(id)
	required_rooms.append(id)
	for i : int in range(_scene_pairs.size()):
		if i not in required_rooms and _stored_scene_pairs.has(i):
			_is_global_position_set[i] = false
			_stored_scene_pairs.erase(i)
#endregion
