class_name AdtTerrainQuery
extends TerrainQuery

## TerrainQuery adapter for existing native ADT dictionaries.
## Parsed tiles, including unavailable results, are cached for this query's
## lifetime. The adapter is main-thread owned and creates no scene resources.

const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")

const CHUNK_SIZE_UNITS := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
const OUTER_GRID_UNIT_SIZE := CHUNK_SIZE_UNITS / 8.0

var _extracted_directory: String
var _map_directory_name: String
var _adt_tile_loader_override: Callable
var _adt_tile_cache: Dictionary = {}
var _failure_code_by_tile_key: Dictionary = {}


## Creates an ADT query for one map. `adt_tile_loader_override` is an optional
## test/data-source seam receiving the absolute ADT path and returning a Dictionary.
func _init(
		extracted_directory: String,
		map_directory_name: String,
		adt_tile_loader_override: Callable = Callable()
) -> void:
	_extracted_directory = extracted_directory
	_map_directory_name = map_directory_name
	_adt_tile_loader_override = adt_tile_loader_override


## Samples the existing 9x9 outer-height grid with bilinear interpolation.
func sample_ground_height(godot_world_position: GodotWorldPosition) -> TerrainGroundSample:
	var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(godot_world_position)
	var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(godot_world_position)
	var chunk_coordinate = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_chunk(
		tile_coordinate,
		tile_local_position
	)
	var tile_key := _tile_key(tile_coordinate.tile_x, tile_coordinate.tile_y)
	var adt_tile_data := _load_adt_tile(tile_coordinate.tile_x, tile_coordinate.tile_y)
	if adt_tile_data.is_empty():
		return TerrainGroundSample.unavailable(
			_failure_code_by_tile_key.get(tile_key, &"adt_tile_unavailable")
		)

	var tile_origin := _find_tile_origin(adt_tile_data)
	var chunk := _find_chunk(
		adt_tile_data,
		clampi(chunk_coordinate.chunk_x, 0, 15),
		clampi(chunk_coordinate.chunk_y, 0, 15),
		tile_origin
	)
	if chunk.is_empty():
		return TerrainGroundSample.unavailable(&"adt_chunk_missing")

	var heights: PackedFloat32Array = chunk.get("heights", PackedFloat32Array())
	if heights.size() < 145:
		return TerrainGroundSample.unavailable(&"adt_chunk_heights_invalid")

	var chunk_origin: Vector3 = chunk.get("origin", Vector3.ZERO)
	var chunk_local_x := godot_world_position.x_units - chunk_origin.x
	var chunk_local_z := godot_world_position.z_units - chunk_origin.z
	var grid_x := clampf(chunk_local_x / OUTER_GRID_UNIT_SIZE, 0.0, 8.0)
	var grid_z := clampf(chunk_local_z / OUTER_GRID_UNIT_SIZE, 0.0, 8.0)
	var x0 := clampi(int(floor(grid_x)), 0, 8)
	var z0 := clampi(int(floor(grid_z)), 0, 8)
	var x1 := mini(x0 + 1, 8)
	var z1 := mini(z0 + 1, 8)
	var fraction_x := grid_x - float(x0)
	var fraction_z := grid_z - float(z0)

	var height_x0 := lerpf(
		heights[_outer_height_index(z0, x0)],
		heights[_outer_height_index(z0, x1)],
		fraction_x
	)
	var height_x1 := lerpf(
		heights[_outer_height_index(z1, x0)],
		heights[_outer_height_index(z1, x1)],
		fraction_x
	)
	return TerrainGroundSample.available(
		chunk_origin.y + lerpf(height_x0, height_x1, fraction_z)
	)


func _load_adt_tile(tile_x: int, tile_y: int) -> Dictionary:
	var tile_key := _tile_key(tile_x, tile_y)
	if _adt_tile_cache.has(tile_key):
		return _adt_tile_cache[tile_key]

	var resource_path := _extracted_directory.path_join(
		"World/Maps/%s/%s_%d_%d.adt" % [_map_directory_name, _map_directory_name, tile_x, tile_y]
	)
	var absolute_path := ProjectSettings.globalize_path(resource_path)
	var adt_tile_data: Dictionary = {}
	var failure_code: StringName = &""
	if _adt_tile_loader_override.is_valid():
		var override_result = _adt_tile_loader_override.call(absolute_path)
		if override_result is Dictionary:
			adt_tile_data = override_result
		else:
			failure_code = &"adt_loader_result_invalid"
	elif not ClassDB.class_exists("ADTLoader"):
		failure_code = &"adt_loader_unavailable"
	elif not FileAccess.file_exists(absolute_path):
		failure_code = &"adt_source_missing"
	else:
		var loader = ClassDB.instantiate("ADTLoader")
		if loader != null:
			var load_result = loader.call("load_adt", absolute_path)
			if load_result is Dictionary:
				adt_tile_data = load_result
		if adt_tile_data.is_empty():
			failure_code = &"adt_load_failed"

	if adt_tile_data.is_empty() and failure_code.is_empty():
		failure_code = &"adt_tile_unavailable"
	_adt_tile_cache[tile_key] = adt_tile_data
	_failure_code_by_tile_key[tile_key] = failure_code
	return adt_tile_data


func _find_chunk(
		adt_tile_data: Dictionary,
		chunk_x: int,
		chunk_y: int,
		tile_origin: Vector3
) -> Dictionary:
	for chunk in adt_tile_data.get("chunks", []) as Array:
		if chunk.is_empty():
			continue
		var chunk_origin: Vector3 = chunk.get("origin", Vector3.ZERO)
		var local_origin := chunk_origin - tile_origin
		var candidate_chunk_x := clampi(int(round(local_origin.x / CHUNK_SIZE_UNITS)), 0, 15)
		var candidate_chunk_y := clampi(int(round(local_origin.z / CHUNK_SIZE_UNITS)), 0, 15)
		if candidate_chunk_x == chunk_x and candidate_chunk_y == chunk_y:
			return chunk
	return {}


func _find_tile_origin(adt_tile_data: Dictionary) -> Vector3:
	var has_origin := false
	var minimum_x := 0.0
	var minimum_z := 0.0
	for chunk in adt_tile_data.get("chunks", []) as Array:
		if chunk.is_empty():
			continue
		var chunk_origin: Vector3 = chunk.get("origin", Vector3.ZERO)
		if not has_origin:
			minimum_x = chunk_origin.x
			minimum_z = chunk_origin.z
			has_origin = true
		else:
			minimum_x = minf(minimum_x, chunk_origin.x)
			minimum_z = minf(minimum_z, chunk_origin.z)
	return Vector3(minimum_x, 0.0, minimum_z) if has_origin else Vector3.ZERO


func _tile_key(tile_x: int, tile_y: int) -> String:
	return "%d_%d" % [tile_x, tile_y]


func _outer_height_index(row: int, column: int) -> int:
	return row * 17 + column
