class_name CoordinateMapper
extends RefCounted

## Pure, stateless conversions between OpenWC coordinate value objects.
## This is the only domain contract allowed to change coordinate axes. It does
## not depend on Node, Resource, scene state, map assets or renderer globals.

const ADT_TILES_PER_MAP_AXIS: int = 64
const ADT_CHUNKS_PER_TILE_AXIS: int = 16
const ADT_TILE_SIZE_YARDS: float = 1600.0 / 3.0
const ADT_CHUNK_SIZE_YARDS: float = ADT_TILE_SIZE_YARDS / float(ADT_CHUNKS_PER_TILE_AXIS)
const WOW_WORLD_HALF_EXTENT_YARDS: float = ADT_TILE_SIZE_YARDS * float(ADT_TILES_PER_MAP_AXIS) * 0.5


## Converts server/wire X/Y/Z to canonical WoW X/Y/Z without an axis swap.
static func server_to_canonical(server_position: ServerWorldPosition) -> CanonicalWowWorldPosition:
	return CanonicalWowWorldPosition.new(
		server_position.x_yards,
		server_position.y_yards,
		server_position.z_yards
	)


## Converts canonical WoW X/Y/Z to server/wire X/Y/Z without an axis swap.
static func canonical_to_server(canonical_position: CanonicalWowWorldPosition) -> ServerWorldPosition:
	return ServerWorldPosition.new(
		canonical_position.x_yards,
		canonical_position.y_yards,
		canonical_position.z_yards
	)


## Converts canonical WoW world axes to the Godot Y-up compatibility basis.
static func canonical_to_godot(canonical_position: CanonicalWowWorldPosition) -> GodotWorldPosition:
	return GodotWorldPosition.new(
		WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.y_yards,
		canonical_position.z_yards,
		WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.x_yards
	)


## Converts the Godot Y-up compatibility basis to canonical WoW world axes.
static func godot_to_canonical(godot_position: GodotWorldPosition) -> CanonicalWowWorldPosition:
	return CanonicalWowWorldPosition.new(
		WOW_WORLD_HALF_EXTENT_YARDS - godot_position.z_units,
		WOW_WORLD_HALF_EXTENT_YARDS - godot_position.x_units,
		godot_position.y_units
	)


## Returns ADT filename indices directly from a Godot renderer position.
static func godot_to_adt_tile(godot_position: GodotWorldPosition) -> AdtTileCoordinate:
	return AdtTileCoordinate.new(
		int(floor(godot_position.x_units / ADT_TILE_SIZE_YARDS)),
		int(floor(godot_position.z_units / ADT_TILE_SIZE_YARDS))
	)


## Returns east/height/south offsets inside the owning ADT tile.
static func godot_to_adt_tile_local(godot_position: GodotWorldPosition) -> AdtTileLocalPosition:
	var tile_coordinate := godot_to_adt_tile(godot_position)
	return AdtTileLocalPosition.new(
		godot_position.x_units - float(tile_coordinate.tile_x) * ADT_TILE_SIZE_YARDS,
		godot_position.y_units,
		godot_position.z_units - float(tile_coordinate.tile_y) * ADT_TILE_SIZE_YARDS
	)


## Converts an ADT tile/local pair directly to the Godot renderer basis.
static func adt_tile_local_to_godot(
		tile_coordinate: AdtTileCoordinate,
		local_position: AdtTileLocalPosition
) -> GodotWorldPosition:
	return GodotWorldPosition.new(
		float(tile_coordinate.tile_x) * ADT_TILE_SIZE_YARDS + local_position.east_yards,
		local_position.height_yards,
		float(tile_coordinate.tile_y) * ADT_TILE_SIZE_YARDS + local_position.south_yards
	)


## Converts canonical WoW coordinates to MDDF/MODF X/height/Z file fields.
static func canonical_to_adt_placement(canonical_position: CanonicalWowWorldPosition) -> AdtPlacementPosition:
	return AdtPlacementPosition.new(
		WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.x_yards,
		canonical_position.z_yards,
		WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.y_yards
	)


## Converts MDDF/MODF X/height/Z file fields to canonical WoW coordinates.
static func adt_placement_to_canonical(adt_position: AdtPlacementPosition) -> CanonicalWowWorldPosition:
	return CanonicalWowWorldPosition.new(
		WOW_WORLD_HALF_EXTENT_YARDS - adt_position.x_offset_yards,
		WOW_WORLD_HALF_EXTENT_YARDS - adt_position.z_offset_yards,
		adt_position.height_yards
	)


## Returns unclamped ADT filename indices for a canonical position.
## Exact south/east boundaries belong to the next tile by floor semantics.
static func canonical_to_adt_tile(canonical_position: CanonicalWowWorldPosition) -> AdtTileCoordinate:
	return AdtTileCoordinate.new(
		int(floor(float(ADT_TILES_PER_MAP_AXIS) * 0.5 - canonical_position.y_yards / ADT_TILE_SIZE_YARDS)),
		int(floor(float(ADT_TILES_PER_MAP_AXIS) * 0.5 - canonical_position.x_yards / ADT_TILE_SIZE_YARDS))
	)


## Converts a canonical position to east/south offsets inside its ADT tile.
static func canonical_to_adt_tile_local(canonical_position: CanonicalWowWorldPosition) -> AdtTileLocalPosition:
	var tile_coordinate := canonical_to_adt_tile(canonical_position)
	var tile_west_edge_y_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_x)) * ADT_TILE_SIZE_YARDS
	var tile_north_edge_x_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_y)) * ADT_TILE_SIZE_YARDS
	return AdtTileLocalPosition.new(
		tile_west_edge_y_yards - canonical_position.y_yards,
		canonical_position.z_yards,
		tile_north_edge_x_yards - canonical_position.x_yards
	)


## Reconstructs canonical coordinates from an ADT tile and tile-local offsets.
static func adt_tile_local_to_canonical(
		tile_coordinate: AdtTileCoordinate,
		local_position: AdtTileLocalPosition
) -> CanonicalWowWorldPosition:
	var tile_west_edge_y_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_x)) * ADT_TILE_SIZE_YARDS
	var tile_north_edge_x_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_y)) * ADT_TILE_SIZE_YARDS
	return CanonicalWowWorldPosition.new(
		tile_north_edge_x_yards - local_position.south_yards,
		tile_west_edge_y_yards - local_position.east_yards,
		local_position.height_yards
	)


## Returns unclamped MCNK indices for a tile-local position.
static func adt_tile_local_to_chunk(
		tile_coordinate: AdtTileCoordinate,
		local_position: AdtTileLocalPosition
) -> AdtChunkCoordinate:
	return AdtChunkCoordinate.new(
		tile_coordinate,
		int(floor(local_position.east_yards / ADT_CHUNK_SIZE_YARDS)),
		int(floor(local_position.south_yards / ADT_CHUNK_SIZE_YARDS))
	)


## Converts server movement yaw to canonical yaw and normalizes it to [-PI, PI).
static func server_to_canonical_yaw(server_yaw: ServerWorldYaw) -> CanonicalWowWorldYaw:
	return CanonicalWowWorldYaw.new(_normalize_yaw(server_yaw.radians))


## Converts canonical yaw to server movement yaw and normalizes it to [-PI, PI).
static func canonical_to_server_yaw(canonical_yaw: CanonicalWowWorldYaw) -> ServerWorldYaw:
	return ServerWorldYaw.new(_normalize_yaw(canonical_yaw.radians))


## Converts canonical world yaw to Godot Y-axis yaw in the mapped world basis.
static func canonical_to_godot_yaw(canonical_yaw: CanonicalWowWorldYaw) -> GodotWorldYaw:
	return GodotWorldYaw.new(_normalize_yaw(canonical_yaw.radians))


## Converts Godot Y-axis world yaw to canonical yaw in the mapped world basis.
static func godot_to_canonical_yaw(godot_yaw: GodotWorldYaw) -> CanonicalWowWorldYaw:
	return CanonicalWowWorldYaw.new(_normalize_yaw(godot_yaw.radians))


static func _normalize_yaw(radians: float) -> float:
	return fposmod(radians + PI, TAU) - PI
