## Archetype
##
## Represents a unique combination of component types in the ECS framework.
## Entities with the exact same set of components share an archetype.
##
## Archetypes enable high-performance queries by grouping entities with identical
## component structures together in flat arrays, providing excellent cache locality
## and eliminating the need for set intersections during queries.
##
## [b]Key Concepts:[/b]
## - [b]Signature:[/b] Hash of all component types (determines archetype identity)
## - [b]Entities:[/b] Flat array of entities with this exact component combination
## - [b]Edges:[/b] Fast lookup for when components are added/removed (future optimization)
##
## [b]Example:[/b]
## [codeblock]
## # Archetype for entities with Position + Velocity
## var archetype = Archetype.new(12345, ["Position", "Velocity"])
## archetype.add_entity(player)
## archetype.add_entity(enemy)
## # Now both entities are stored contiguously for fast iteration
## [/codeblock]
##
## [b]Performance:[/b]
## - Add entity: O(1) amortized (array append)
## - Remove entity: O(1) (swap-remove with index tracking)
## - Query match: O(1) (check if archetype signature matches query)
## - Iterate entities: O(n) with excellent cache locality
class_name Archetype
extends RefCounted

## Unique hash identifying this component combination
## Generated by QueryCacheKey.build() from sorted component types
var signature: int = 0

## Sorted array of component resource paths (e.g., ["res://c_position.gd", "res://c_velocity.gd"])
## Used for debugging and archetype matching logic
var component_types: Array = []

## Flat array of entities with this exact component combination
## Provides excellent cache locality when iterating in systems
var entities: Array[Entity] = []

## Fast lookup: Entity -> index in entities array
## Enables O(1) entity removal using swap-remove technique
var entity_to_index: Dictionary = {} # Entity -> int

## OPTIMIZATION: Bitset for enabled/disabled state instead of archetype splitting
## Uses PackedInt64Array where each bit represents whether entity at that index is enabled
## Reduces archetype count by 2x and enables O(1) enabled/disabled filtering
var enabled_bitset: PackedInt64Array = []

## OPTIMIZATION: Structure of Arrays (SoA) column storage for cache-friendly iteration
## Maps component_path -> Array of component instances
## Enables Flecs-style direct array iteration without dictionary lookups
## Example: columns["res://c_velocity.gd"] = [vel1, vel2, vel3, ...]
var columns: Dictionary = {} # String (component_path) -> Array of components

## Archetype edges for fast component add/remove (future optimization)
## Maps: component_path -> Archetype (the archetype you get by adding/removing that component)
var add_edges: Dictionary = {} # String -> Archetype
var remove_edges: Dictionary = {} # String -> Archetype


## Initialize archetype with signature and component types
func _init(p_signature: int, p_component_types: Array):
	signature = p_signature
	component_types = p_component_types.duplicate()
	component_types.sort() # Ensure sorted for consistent matching

	# Initialize column arrays for each component type
	for comp_type in component_types:
		columns[comp_type] = []


## Add an entity to this archetype
## Uses O(1) append and tracks index for fast removal
## OPTIMIZATION: Also populates column arrays for cache-friendly iteration
func add_entity(entity: Entity) -> void:
	var index = entities.size()
	entities.append(entity)
	entity_to_index[entity] = index

	# OPTIMIZATION: Update enabled bitset
	_ensure_bitset_capacity(index + 1)
	_set_enabled_bit(index, entity.enabled)

	# OPTIMIZATION: Populate column arrays from entity.components
	for comp_path in component_types:
		if entity.components.has(comp_path):
			(columns[comp_path]
				.append(entity.components[comp_path]))
		else:
			# Entity doesn't have this component yet (might be mid-initialization)
			# Push null placeholder, will be fixed when component is added
			columns[comp_path].append(null)


## Remove an entity from this archetype using swap-remove
## O(1) operation: swaps with last entity and pops
## OPTIMIZATION: Also maintains column arrays in sync
func remove_entity(entity: Entity) -> bool:
	if not entity_to_index.has(entity):
		return false

	var index = entity_to_index[entity]
	var last_index = entities.size() - 1

	# Swap with last element in entities array
	if index != last_index:
		var last_entity = entities[last_index]
		entities[index] = last_entity
		entity_to_index[last_entity] = index

		# OPTIMIZATION: Swap in column arrays too (maintain same ordering)
		for comp_path in component_types:
			columns[comp_path][index] = columns[comp_path][last_index]

		# OPTIMIZATION: Swap enabled bit
		var last_enabled = _get_enabled_bit(last_index)
		_set_enabled_bit(index, last_enabled)

	# Remove last element from entities
	entities.pop_back()
	entity_to_index.erase(entity)

	# OPTIMIZATION: Remove last element from all columns
	for comp_path in component_types:
		columns[comp_path].pop_back()

	# OPTIMIZATION: Update bitset size (no need to clear the bit, just reduce logical size)
	# The bit will be overwritten when a new entity is added

	return true


## Check if this archetype has a specific entity
func has_entity(entity: Entity) -> bool:
	return entity_to_index.has(entity)


## Get entity count in this archetype
func size() -> int:
	return entities.size()


## Check if archetype is empty
func is_empty() -> bool:
	return entities.is_empty()


## Clear all entities from this archetype
func clear() -> void:
	entities.clear()
	entity_to_index.clear()

	# OPTIMIZATION: Clear column arrays
	for comp_path in component_types:
		columns[comp_path].clear()

	# OPTIMIZATION: Clear bitset
	enabled_bitset.clear()


## Check if this archetype matches a query with all/any/exclude components
## [param all_comp_types] Component paths that must all be present
## [param any_comp_types] Component paths where at least one must be present
## [param exclude_comp_types] Component paths that must not be present
func matches_query(all_comp_types: Array, any_comp_types: Array, exclude_comp_types: Array) -> bool:
	# Check all_components: must have ALL of these
	for comp_type in all_comp_types:
		if not component_types.has(comp_type):
			return false

	# Check any_components: must have AT LEAST ONE of these
	if not any_comp_types.is_empty():
		var has_any = false
		for comp_type in any_comp_types:
			if component_types.has(comp_type):
				has_any = true
				break
		if not has_any:
			return false

	# Check exclude_components: must have NONE of these
	for comp_type in exclude_comp_types:
		if component_types.has(comp_type):
			return false

	return true


## Get a debug-friendly string representation
func _to_string() -> String:
	var comp_names = []
	for comp_type in component_types:
		# Extract just the class name from the path
		var parts = comp_type.split("/")
		var filename = parts[parts.size() - 1].replace(".gd", "")
		comp_names.append(filename)

	return "Archetype[sig=%d, comps=%s, entities=%d]" % [
		signature,
		str(comp_names),
		entities.size()
	]


## Set up an edge to another archetype when a component is added
## Enables O(1) archetype transitions when components change
func set_add_edge(component_path: String, target_archetype: Archetype) -> void:
	add_edges[component_path] = target_archetype


## Set up an edge to another archetype when a component is removed
## Enables O(1) archetype transitions when components change
func set_remove_edge(component_path: String, target_archetype: Archetype) -> void:
	remove_edges[component_path] = target_archetype


## Get the target archetype when adding a component (if edge exists)
func get_add_edge(component_path: String) -> Archetype:
	return add_edges.get(component_path, null)


## Get the target archetype when removing a component (if edge exists)
func get_remove_edge(component_path: String) -> Archetype:
	return remove_edges.get(component_path, null)


## OPTIMIZATION: Get component column array for cache-friendly iteration
## Enables Flecs-style direct array access instead of dictionary lookups per entity
## [param component_path] The resource path of the component type (e.g., C_Velocity.resource_path)
## [returns] Array of component instances in entity index order, or empty array if not found
##
## Example:
## [codeblock]
## var velocities = archetype.get_column(C_Velocity.resource_path)
## for i in range(velocities.size()):
##     var velocity = velocities[i]
##     var entity = archetype.entities[i]
##     # Process with cache-friendly sequential access
## [/codeblock]
func get_column(component_path: String) -> Array:
	return columns.get(component_path, [])


## OPTIMIZATION: Get entities filtered by enabled state using bitset
## [param enabled_only] If true, return only enabled entities; if false, only disabled
## [returns] Array of entities matching the enabled state
func get_entities_by_enabled_state(enabled_only: bool) -> Array[Entity]:
	var result: Array[Entity] = []
	for i in range(entities.size()):
		if _get_enabled_bit(i) == enabled_only:
			result.append(entities[i])
	return result


## OPTIMIZATION: Update entity enabled state in bitset
## [param entity] The entity to update
## [param enabled] The new enabled state
func update_entity_enabled_state(entity: Entity, enabled: bool) -> void:
	if entity_to_index.has(entity):
		var index = entity_to_index[entity]
		_set_enabled_bit(index, enabled)


## OPTIMIZATION: Ensure bitset has enough capacity for the given number of entities
func _ensure_bitset_capacity(required_size: int) -> void:
	var required_int64s = (required_size + 63) / 64 # Round up to nearest 64-bit boundary
	while enabled_bitset.size() < required_int64s:
		enabled_bitset.append(0)


## OPTIMIZATION: Set enabled bit for entity at index
func _set_enabled_bit(index: int, enabled: bool) -> void:
	var int64_index = index / 64
	var bit_index = index % 64

	_ensure_bitset_capacity(index + 1)

	if enabled:
		enabled_bitset[int64_index] |= (1 << bit_index)
	else:
		enabled_bitset[int64_index] &= ~(1 << bit_index)


## OPTIMIZATION: Get enabled bit for entity at index
func _get_enabled_bit(index: int) -> bool:
	if index >= entities.size():
		return false

	var int64_index = index / 64
	var bit_index = index % 64

	if int64_index >= enabled_bitset.size():
		return false

	return (enabled_bitset[int64_index] & (1 << bit_index)) != 0
