class_name StreamingTargetPlanner
extends RefCounted

## Scene-free deterministic calculator for runtime/editor ADT target sets.
## It creates no tasks or renderer resources and never mutates its inputs.

const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const TARGET_PLAN_SCRIPT := preload("res://src/render/streaming/streaming_target_plan.gd")


## Builds runtime wanted and retained sets, including the existing edge/corner
## prefetch centers. `available_tile_paths_by_key` is read only for membership.
func plan_runtime(
		available_tile_paths_by_key: Dictionary,
		focus_position: GodotWorldPosition,
		policy
):
	var focus_tile = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(focus_position)
	var prefetch_centers := _prefetch_centers(focus_position, focus_tile, policy)
	var wanted_tile_keys := _available_keys_within_radius(
		available_tile_paths_by_key,
		prefetch_centers,
		policy.load_tile_radius()
	)
	var retained_tile_keys := _available_keys_within_radius(
		available_tile_paths_by_key,
		prefetch_centers,
		policy.retained_tile_radius()
	)
	return TARGET_PLAN_SCRIPT.new(focus_tile, wanted_tile_keys, retained_tile_keys)


## Builds the editor square preview set. Editor wanted and retained sets are
## identical and a negative radius is clamped to the focus tile.
func plan_editor(
		available_tile_paths_by_key: Dictionary,
		focus_position: GodotWorldPosition,
		editor_preview_tile_radius: int
):
	var focus_tile = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(focus_position)
	var centers: Array[AdtTileCoordinate] = [focus_tile]
	var wanted_tile_keys := _available_keys_within_radius(
		available_tile_paths_by_key,
		centers,
		maxi(0, editor_preview_tile_radius)
	)
	return TARGET_PLAN_SCRIPT.new(focus_tile, wanted_tile_keys, wanted_tile_keys)


func _prefetch_centers(
		focus_position: GodotWorldPosition,
		focus_tile: AdtTileCoordinate,
		policy
) -> Array[AdtTileCoordinate]:
	var centers: Array[AdtTileCoordinate] = [focus_tile]
	var threshold: float = policy.clamped_boundary_prefetch_threshold()
	if threshold <= 0.0:
		return centers

	var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(focus_position)
	var local_x_fraction: float = tile_local_position.east_yards / COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
	var local_z_fraction: float = tile_local_position.south_yards / COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
	var tile_x_offsets: Array[int] = [0]
	var tile_y_offsets: Array[int] = [0]

	if local_x_fraction <= threshold:
		tile_x_offsets.append(-1)
	elif local_x_fraction >= 1.0 - threshold:
		tile_x_offsets.append(1)

	if local_z_fraction <= threshold:
		tile_y_offsets.append(-1)
	elif local_z_fraction >= 1.0 - threshold:
		tile_y_offsets.append(1)

	for tile_x_offset in tile_x_offsets:
		for tile_y_offset in tile_y_offsets:
			if tile_x_offset == 0 and tile_y_offset == 0:
				continue
			centers.append(ADT_TILE_COORDINATE_SCRIPT.new(
				focus_tile.tile_x + tile_x_offset,
				focus_tile.tile_y + tile_y_offset
			))
	return centers


func _available_keys_within_radius(
		available_tile_paths_by_key: Dictionary,
		centers: Array[AdtTileCoordinate],
		tile_radius: int
) -> Dictionary:
	var available_tile_keys: Dictionary = {}
	for center in centers:
		for tile_y in range(center.tile_y - tile_radius, center.tile_y + tile_radius + 1):
			for tile_x in range(center.tile_x - tile_radius, center.tile_x + tile_radius + 1):
				var tile_key := _tile_key(tile_x, tile_y)
				if available_tile_paths_by_key.has(tile_key):
					available_tile_keys[tile_key] = true
	return available_tile_keys


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