const FWD = { MetroidvaniaSystem.R: Vector2i.RIGHT, MetroidvaniaSystem.D: Vector2i.DOWN, MetroidvaniaSystem.L: Vector2i.LEFT, MetroidvaniaSystem.U: Vector2i.UP }

class CellData:
	enum { DATA_EXITS, DATA_COLORS, DATA_SYMBOL, DATA_MAP, OVERRIDE_COORDS, OVERRIDE_CUSTOM }
	
	var color: Color = Color.TRANSPARENT
	var borders: Array[int] = [-1, -1, -1, -1]
	var border_colors: Array[Color] = [Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT]
	var symbol := -1
	var assigned_scene: String
	var override_map: String
	
	var loading
	
	func _init(line: String) -> void:
		if line.is_empty():
			return
		loading = [line, -1]
		
		var chunk := load_next_chunk()
		for i in 4:
			borders[i] = chunk.get_slice(",", i).to_int()
		
		chunk = load_next_chunk()
		if not chunk.is_empty():
			var color_slice := chunk.get_slice(",", 0)
			if not color_slice.is_empty():
				color = Color(color_slice)
			
			for i in 4:
				color_slice = chunk.get_slice(",", i + 1)
				if not color_slice.is_empty():
					border_colors[i] = Color(color_slice)
		
		chunk = load_next_chunk()
		if not chunk.is_empty():
			symbol = chunk.to_int()
		
		assigned_scene = load_next_chunk()
		loading = null
	
	func get_string() -> String:
		var data: PackedStringArray
		data.append("%s,%s,%s,%s" % borders)
		
		var colors: Array[Color]
		colors.assign([color] + Array(border_colors))
		if colors.any(func(col: Color): return col.a > 0):
			data.append("%s,%s,%s,%s,%s" % colors.map(func(col: Color): return col.to_html(false) if col.a > 0 else ""))
		else:
			data.append("")
		
		if symbol > -1:
			data.append(str(symbol))
		else:
			data.append("")
		data.append(assigned_scene.trim_prefix(MetSys.settings.map_root_folder + "/"))
		return "|".join(data)
	
	func load_next_chunk() -> String:
		loading[1] += 1
		return loading[0].get_slice("|", loading[1])
	
	func get_color() -> Color:
		var c: Color
		var override := get_override()
		if override and override.color.a > 0:
			c = override.color
		else:
			c = color
		
		if c.a > 0:
			return c
		return MetSys.settings.theme.default_center_color
	
	func get_border(idx: int) -> int:
		var override := get_override()
		if override and override.borders[idx] != -2:
			return override.borders[idx]
		return borders[idx]
	
	func get_border_color(idx: int) -> Color:
		var c: Color
		var override := get_override()
		if override and override.border_colors[idx].a > 0:
			c = override.border_colors[idx]
		else:
			c = border_colors[idx]
		
		if c.a > 0:
			return c
		return MetSys.settings.theme.default_border_color
	
	func get_symbol() -> int:
		var override := get_override()
		if override and override.symbol != -2:
			return override.symbol
		return symbol
	
	func get_assigned_scene() -> String:
		var override := get_override()
		if override and override.assigned_scene != "/":
			return override.assigned_scene
		if not override_map.is_empty():
			return override_map
		return assigned_scene
	
	func get_override() -> CellOverride:
		if not MetSys.save_data:
			return null
		return MetSys.save_data.cell_overrides.get(self)
	
	func get_coords() -> Vector3i:
		return MetSys.map_data.cells.find_key(self)

## A runtime override for the cell's properties that allows to modify it's default appearance designed in the editor.
##
## CellOverride can be obtained from [method MetroidvaniaSystem.get_cell_override] or using the MapBuilder. It comes with a set of methods that allows customizing the cells. [signal MetroidvaniaSystem.map_updated] signal is automatically emitted after modifications, unless the cell comes from MapBuilder.
class CellOverride extends CellData:
	var original_room: CellData
	var custom_cell_coords := Vector3i.MAX
	var commit_queued: bool
	
	func _init(from: CellData) -> void:
		original_room = from
		borders = [-2, -2, -2, -2]
		symbol = -2
		assigned_scene = "/"
	
	static func load_from_line(line: String) -> CellOverride:
		var cell: CellData
		var coords_string := line.get_slice("|", CellData.OVERRIDE_COORDS)
		var coords := Vector3i(coords_string.get_slice(",", 0).to_int(), coords_string.get_slice(",", 1).to_int(), coords_string.get_slice(",", 2).to_int())
		
		var is_custom := line.get_slice("|", CellData.OVERRIDE_CUSTOM) == "true"
		if is_custom:
			cell = MetSys.map_data.create_cell_at(coords)
		else:
			cell = MetSys.map_data.get_cell_at(coords)
		
		var override := CellOverride.new(cell)
		if is_custom:
			override.custom_cell_coords = coords
		
		var fake_cell := CellData.new(line)
		override.borders = fake_cell.borders
		override.border_colors = fake_cell.border_colors
		override.color = fake_cell.color
		override.symbol = fake_cell.symbol
		override.set_assigned_scene(fake_cell.assigned_scene)
		
		return override
	
	## Sets a border of the cell. [param idx] is the direction index of the cell. Use [constant MetroidvaniaSystem.R], [constant MetroidvaniaSystem.D], [constant MetroidvaniaSystem.L], [constant MetroidvaniaSystem.U] constants here. The [param value] must be within the [member MapTheme.borders] array bounds + 2 (0 and 1 are the default wall and passage). Use the default to reset to the border assigned in the editor. Value of [code]-1[/code] will remove the border, but it's not recommended.
	func set_border(idx: int, value := -2):
		assert(idx >= 0 and idx < 4)
		borders[idx] = value
		_queue_commit()
	
	## Sets the color of a cell's border. [param idx] is the direction index of the cell (see [method set_border]). If the default [param value] is used (or any color with [code]0[/code] alpha), the border will be reset to the default color.
	func set_border_color(idx: int, value := Color.TRANSPARENT):
		assert(idx >= 0 and idx < 4)
		border_colors[idx] = value
		_queue_commit()
	
	## Sets the color of the cell's center texture.
	func set_color(value := Color.TRANSPARENT):
		color = value
		_queue_commit()
	
	## Sets the cell's assigned symbol. Value of [code]-1[/code] will remove the symbol. The markers assigned from [method MetroidvaniaSystem.set_custom_marker] still have a priority.
	func set_symbol(value := -2):
		assert(value >= -2 and value < MetSys.settings.theme.symbols.size())
		symbol = value
		_queue_commit()
	
	## Changes the scene assigned to the cell. Unlike other methods, this has effect on the whole room that contains this cell. Using the default value will reset it to the scene assigned in the editor.
	func set_assigned_scene(value := "/"):
		if value == "/":
			_cleanup_assigned_scene()
		else:
			if custom_cell_coords != Vector3i.MAX:
				if not value in MetSys.map_data.assigned_scenes:
					MetSys.map_data.assigned_scenes[value] = []
				MetSys.map_data.assigned_scenes[value].append(custom_cell_coords)
			else:
				MetSys.map_data.scene_overrides[value] = original_room.assigned_scene
				
				for coords in MetSys.map_data.get_whole_room(original_room.get_coords()):
					var cell: CellData = MetSys.map_data.cells[coords]
					if not cell.override_map.is_empty():
						push_warning("Assigned map already overriden at: %s" % coords)
					cell.override_map = value
		
		assigned_scene = value
		MetSys.room_assign_updated.emit()
		_queue_commit()
	
	## Applies the values from this CellOverride to all cells that belong to the specified [param group_id]. You need to apply your customization [i]before[/i] calling this method.
	func apply_to_group(group_id: int):
		assert(group_id in MetSys.map_data.cell_groups)
		
		for coords in MetSys.map_data.cell_groups[group_id]:
			var override: CellOverride = MetSys.get_cell_override(coords)
			if override == self:
				continue
			
			override.borders = borders.duplicate()
			override.color = color
			override.border_colors = border_colors.duplicate()
			override.symbol = symbol
		
		_queue_commit()
	
	## Destroys the cell. This method can only be used on the overrides from the MapBuilder.
	func destroy() -> void:
		if custom_cell_coords == Vector3i.MAX:
			push_error("Only custom cell can be destroyed.")
			return
		
		var cell_coords := custom_cell_coords
		custom_cell_coords = Vector3i.MAX
		
		MetSys.remove_cell_override(cell_coords)
		MetSys.map_data.erase_cell(cell_coords)
		MetSys.map_data.cell_overrides.erase(cell_coords)
		MetSys.map_data.custom_cells.erase(self)
		
		if assigned_scene != "/":
			MetSys.map_data.assigned_scenes[assigned_scene].erase(cell_coords)
		
		if MetSys.save_data:
			MetSys.save_data.discovered_cells.erase(cell_coords)
	
	func _cleanup_assigned_scene() -> void:
		if assigned_scene == "/":
			return
		
		MetSys.map_data.scene_overrides.erase(assigned_scene)
		for coords in MetSys.map_data.get_whole_room(original_room.get_coords()):
			MetSys.map_data.cells[coords].override_map = ""
	
	func _get_override_string(coords: Vector3i) -> String:
		return str(get_string(), "|", coords.x, ",", coords.y, ",", coords.z, "|", custom_cell_coords != Vector3i.MAX)
	
	func _queue_commit():
		if commit_queued or custom_cell_coords != Vector3i.MAX:
			return
		commit_queued = true
		_commit.call_deferred()
	
	func _commit() -> void:
		commit_queued = false
		MetSys.map_updated.emit()

var cells: Dictionary#[Vector3i, CellData]
var custom_cells: Dictionary#[Vector3i, CellData]
var assigned_scenes: Dictionary#[String, Array[Vector3i]]
var cell_groups: Dictionary#[int, Array[Vector3i]]
var custom_elements: Dictionary#[Vector3i, Struct]

var cell_overrides: Dictionary#[Vector3i, CellOverride]
var scene_overrides: Dictionary#[String, String]

func load_data():
	var file := FileAccess.open(MetSys.settings.map_setting_folder.path_join("MapData.txt"), FileAccess.READ)
	if not file:
		push_warning("Map data file does not exist.")
		return
	
	var data := file.get_as_text().split("\n")
	var i: int
	
	var current_section := 0 # groups, custom_elements, cells
	while i < data.size():
		var line := data[i]
		if line.begins_with("["):
			current_section = 2
			line = line.trim_prefix("[").trim_suffix("]")
			
			var coords: Vector3i
			coords.x = line.get_slice(",", 0).to_int()
			coords.y = line.get_slice(",", 1).to_int()
			coords.z = line.get_slice(",", 2).to_int()
			
			i += 1
			line = data[i]
			
			var cell_data := CellData.new(line)
			if not cell_data.assigned_scene.is_empty():
				assigned_scenes[cell_data.assigned_scene] = [coords]
			
			cells[coords] = cell_data
		elif current_section == 1 or current_section == 0 and line.contains("/"):
			current_section = 1
			var element_data := line.split("/")
			var element: Dictionary
			element.name = element_data[1]
			element.size = Vector2i(element_data[2].get_slice("x", 0).to_int(), element_data[2].get_slice("x", 1).to_int())
			if element_data.size() == 4:
				element.data = element_data[3]
			else:
				element.data = ""
			
			var coords: Vector3i
			coords.x = element_data[0].get_slice(",", 0).to_int()
			coords.y = element_data[0].get_slice(",", 1).to_int()
			coords.z = element_data[0].get_slice(",", 2).to_int()
			custom_elements[coords] = element
		elif current_section == 0:
			var group_data := line.split(":")
			var group_id := group_data[0].to_int()
			var rooms_in_group: Array
			for j in range(1, group_data.size()):
				var coords: Vector3i
				coords.x = group_data[j].get_slice(",", 0).to_int()
				coords.y = group_data[j].get_slice(",", 1).to_int()
				coords.z = group_data[j].get_slice(",", 2).to_int()
				rooms_in_group.append(coords)
			
			cell_groups[group_id] = rooms_in_group
		
		i += 1
	
	for map in assigned_scenes.keys():
		var assigned_cells: Array[Vector3i]
		assigned_cells.assign(assigned_scenes[map])
		assigned_scenes[map] = get_whole_room(assigned_cells[0])

func save_data():
	var file := FileAccess.open(MetSys.settings.map_setting_folder.path_join("MapData.txt"), FileAccess.WRITE)
	if not file:
		push_error("Could not open file '%s' for writing." % MetSys.settings.map_setting_folder.path_join("MapData.txt"))
		return
	
	for group in cell_groups:
		if cell_groups[group].is_empty():
			continue
		
		var line: PackedStringArray
		line.append(str(group))
		for coords in cell_groups[group]:
			line.append("%s,%s,%s" % [coords.x, coords.y, coords.z])
		
		file.store_line(":".join(line))
	
	for coords in custom_elements:
		var line: PackedStringArray
		line.append("%s,%s,%s" % [coords.x, coords.y, coords.z])
		
		var element: Dictionary = custom_elements[coords]
		line.append(element.name)
		line.append("%sx%s" % [element.size.x, element.size.y])
		if not element.data.is_empty():
			line.append(element.data)
		
		file.store_line("/".join(line))
	
	for coords in cells:
		file.store_line("[%s,%s,%s]" % [coords.x, coords.y, coords.z])
		
		var cell_data := get_cell_at(coords)
		file.store_line(cell_data.get_string())

func get_cell_at(coords: Vector3i) -> CellData:
	return cells.get(coords)

func create_cell_at(coords: Vector3i) -> CellData:
	cells[coords] = CellData.new("")
	return cells[coords]

func create_custom_cell(coords: Vector3i) -> CellOverride:
	assert(not coords in cells, "A cell already exists at this position")
	var cell := create_cell_at(coords)
	custom_cells[coords] = cell
	
	var override: CellOverride = MetSys.save_data.add_cell_override(cell)
	override.custom_cell_coords = coords
	return override

func get_whole_room(at: Vector3i) -> Array[Vector3i]:
	var room: Array[Vector3i]
	
	var to_check: Array[Vector2i] = [Vector2i(at.x, at.y)]
	var checked: Array[Vector2i]
	
	while not to_check.is_empty():
		var p: Vector2i = to_check.pop_back()
		checked.append(p)
		
		var coords := Vector3i(p.x, p.y, at.z)
		if coords in cells:
			room.append(coords)
			for i in 4:
				if cells[coords].borders[i] == -1:
					var p2: Vector2i = p + FWD[i]
					if not p2 in to_check and not p2 in checked:
						to_check.append(p2)
	
	return room

func get_cells_assigned_to(map: String) -> Array[Vector3i]:
	if map in scene_overrides:
		map = scene_overrides[map]
	
	var ret: Array[Vector3i]
	ret.assign(assigned_scenes.get(map, []))
	return ret

func get_assigned_scene_at(coords: Vector3i) -> String:
	var cell := get_cell_at(coords)
	if cell:
		return cell.get_assigned_scene()
	else:
		return ""

func erase_cell(coords: Vector3i):
	var assigned_scene: String = cells[coords].assigned_scene
	assigned_scenes.erase(assigned_scene)
	
	cells.erase(coords)
	
	for group in cell_groups.values():
		group.erase(coords)
