## QueryBuilder[br]
## A utility class for constructing and executing queries to retrieve entities based on their components.
##
## The QueryBuilder supports filtering entities that have all, any, or exclude specific components,
## as well as filtering by enabled/disabled status using high-performance group indexing.
## [codeblock]
##     var enabled_entities = ECS.world.query
##                    	.with_all([Transform, Velocity])
##                    	.with_any([Health])
##                    	.with_none([Inactive])
##                    	.enabled(true)
##                    	.execute()
##
##     var disabled_entities = ECS.world.query.enabled(false).execute()
##     var all_entities = ECS.world.query.enabled(null).execute()
##[/codeblock]
## This will efficiently query entities using indexed group lookups rather than
## filtering the entire entity list.
class_name QueryBuilder
extends RefCounted

# The world instance to query against.
var _world: World
# Components that an entity must have all of.
var _all_components: Array = []
# Components that an entity must have at least one of.
var _any_components: Array = []
# Components that an entity must not have.
var _exclude_components: Array = []
# Relationships that entities must have
var _relationships: Array = [] # (Retained for entity-level filtering only; NOT part of cache key)
var _exclude_relationships: Array = []
# Components queries that an entity must match
var _all_components_queries: Array = []
# Components queries that an entity must match for any components
var _any_components_queries: Array = []
# Groups that an entity must be in
var _groups: Array = []
# Groups that an entity must not be in
var _exclude_groups: Array = []
# Enabled/disabled filter: true = enabled only, false = disabled only, null = all
var _enabled_filter = null
# Components to iterate in archetype mode (ordered array of component types)
var _iterate_components: Array = []

# Add fields for query result caching
var _cache_valid: bool = false
var _cached_result: Array = []

# OPTIMIZATION: Cache the query hash key to avoid recalculating FNV-1a hash every frame
var _cache_key: int = -1
var _cache_key_valid: bool = false


## Initializes the QueryBuilder with the specified [param world]
func _init(world: World = null):
	_world = world as World


## Allow setting the world after creation for editor time creation
func set_world(world: World):
	_world = world


## Clears the query criteria, resetting all filters. Mostly used in testing
## [param returns] -  The current instance of the QueryBuilder for chaining.
func clear():
	_all_components = []
	_any_components = []
	_exclude_components = []
	_relationships = []
	_exclude_relationships = []
	_all_components_queries = []
	_any_components_queries = []
	_groups = []
	_exclude_groups = []
	_enabled_filter = null
	_iterate_components = []
	_cache_valid = false
	_cache_key_valid = false
	return self


## Finds entities with all of the provided components.[br]
## [param components] An [Array] of [Component] classes.[br]
## [param returns]: [QueryBuilder] instance for chaining.
func with_all(components: Array = []) -> QueryBuilder:
	var processed = ComponentQueryMatcher.process_component_list(components)
	_all_components = processed.components
	_all_components_queries = processed.queries
	_cache_valid = false
	_cache_key_valid = false
	return self


## Entities must have at least one of the provided components.[br]
## [param components] An [Array] of [Component] classes.[br]
## [param reutrns] [QueryBuilder] instance for chaining.
func with_any(components: Array = []) -> QueryBuilder:
	var processed = ComponentQueryMatcher.process_component_list(components)
	_any_components = processed.components
	_any_components_queries = processed.queries
	_cache_valid = false
	_cache_key_valid = false
	return self


## Entities must not have any of the provided components.[br]
## Params: [param components] An [Array] of [Component] classes.[br]
## [param reutrns] [QueryBuilder] instance for chaining.
func with_none(components: Array = []) -> QueryBuilder:
	# Don't process queries for with_none, just take the components directly
	_exclude_components = components.map(
		func(comp): return comp if not comp is Dictionary else comp.keys()[0]
	)
	_cache_valid = false
	_cache_key_valid = false
	return self


## Finds entities with specific relationships using weak matching by default (component type and queries).
## [br][b]Weak Matching (default):[/b] Components match by type and component queries are evaluated.
## [br]For strong matching (exact component data), use [method Entity.has_relationship] with [code]weak=false[/code].
func with_relationship(relationships: Array = []) -> QueryBuilder:
	_relationships = relationships
	_cache_valid = false
	# Cache key unaffected by relationships (structural only)
	return self


## Entities must not have any of the provided relationships using weak matching by default (component type and queries).
## [br][b]Weak Matching (default):[/b] Components match by type and component queries are evaluated.
## [br]For strong matching (exact component data), use [method Entity.has_relationship] with [code]weak=false[/code].
func without_relationship(relationships: Array = []) -> QueryBuilder:
	_exclude_relationships = relationships
	_cache_valid = false
	return self


## Query for entities that are targets of specific relationships
func with_reverse_relationship(relationships: Array = []) -> QueryBuilder:
	for rel in relationships:
		if rel.relation != null:
			var rev_key = "reverse_" + rel.relation.get_script().resource_path
			if _world.reverse_relationship_index.has(rev_key):
				return self.with_all(_world.reverse_relationship_index[rev_key])
	_cache_valid = false
	return self


## Finds entities with specific groups.
func with_group(groups: Array[String] = []) -> QueryBuilder:
	_groups.append_array(groups)
	_cache_valid = false
	_cache_key_valid = false
	return self


## Entities must not have any of the provided groups.
func without_group(groups: Array[String] = []) -> QueryBuilder:
	_exclude_groups.append_array(groups)
	_cache_valid = false
	_cache_key_valid = false
	return self


## Filter to only enabled entities using internal arrays for optimal performance.[br]
## [param returns] [QueryBuilder] instance for chaining.
func enabled() -> QueryBuilder:
	_enabled_filter = true
	_cache_valid = false
	_cache_key_valid = false
	return self


## Filter to only disabled entities using internal arrays for optimal performance.[br]
## [param returns] [QueryBuilder] instance for chaining.
func disabled() -> QueryBuilder:
	_enabled_filter = false
	_cache_valid = false
	_cache_key_valid = false
	return self


## Specifies the component order for batch processing iteration.[br]
## This determines the order of component arrays passed to System.process_batch()[br]
## [param components] An array of component types in the desired iteration order[br]
## [param returns] [QueryBuilder] instance for chaining.[br][br]
## [b]Example:[/b]
## [codeblock]
## func query() -> QueryBuilder:
##     return q.with_all([C_Velocity, C_Timer]).enabled().iterate([C_Velocity, C_Timer])
##
## func process_batch(entities: Array[Entity], components: Array, delta: float) -> void:
##     var velocities = components[0] # C_Velocity (first in iterate)
##     var timers = components[1] # C_Timer (second in iterate)
## [/codeblock]
func iterate(components: Array) -> QueryBuilder:
	_iterate_components = components
	return self


func execute_one() -> Entity:
	# Execute the query and return the first matching entity
	var result = execute()
	if result.size() > 0:
		return result[0]
	return null


## Executes the constructed query and retrieves matching entities.[br]
## [param returns] -  An [Array] of [Entity] that match the query criteria.
func execute() -> Array:
	# For relationship or group filters we need fresh filtering every call (no stale cached filtered result)
	var uses_relationship_filters := (not _relationships.is_empty() or not _exclude_relationships.is_empty())
	var uses_group_filters := (not _groups.is_empty() or not _exclude_groups.is_empty())

	var structural_result: Array
	if _cache_valid and not uses_relationship_filters and not uses_group_filters:
		# Safe to reuse full cached result only for purely structural component queries
		structural_result = _cached_result
	else:
		# Recompute base structural/group result (without relationship filtering caching)
		structural_result = _internal_execute()
		# Only cache if no dynamic relationship/group filters are present
		if not uses_relationship_filters and not uses_group_filters:
			_cached_result = structural_result
			_cache_valid = true
		else:
			_cache_valid = false # force recompute next call

	var result = structural_result
	# Apply component property queries (post structural)
	if not _all_components_queries.is_empty() and _has_actual_queries(_all_components_queries):
		result = _filter_entities_by_queries(result, _all_components, _all_components_queries, true)
	if not _any_components_queries.is_empty() and _has_actual_queries(_any_components_queries):
		result = _filter_entities_by_queries(result, _any_components, _any_components_queries, false)

	return result


func _internal_execute() -> Array:
	# If we have groups or exclude groups, gather entities from those groups
	if not _groups.is_empty() or not _exclude_groups.is_empty():
		var entities_in_group = []

		# Use Godot's optimized get_nodes_in_group() instead of filtering
		if not _groups.is_empty():
			# For multiple groups, use set operations for efficiency
			var group_set: Set

			for i in range(_groups.size()):
				var group_name = _groups[i]
				var nodes_in_group = _world.get_tree().get_nodes_in_group(group_name)

				# Filter to only Entity nodes
				var entities_in_this_group = nodes_in_group.filter(func(n): return n is Entity)

				if i == 0:
					# First group - start with these entities
					group_set = Set.new(entities_in_this_group)
				else:
					# Subsequent groups - intersect (entity must be in ALL groups)
					group_set = group_set.intersect(Set.new(entities_in_this_group))

			entities_in_group = group_set.to_array() if group_set else []
		else:
			# If no required groups but we have exclude_groups, start with ALL entities from component query
			# This handles the case of "without_group" queries
			entities_in_group = (
				_world._query(_all_components, _any_components, _exclude_components, _enabled_filter, get_cache_key()) as Array[Entity]
			)

		# Filter out entities in excluded groups
		if not _exclude_groups.is_empty():
			var exclude_set = Set.new()
			for group_name in _exclude_groups:
				var nodes_in_group = _world.get_tree().get_nodes_in_group(group_name)
				var entities_in_excluded = nodes_in_group.filter(func(n): return n is Entity)
				exclude_set = exclude_set.union(Set.new(entities_in_excluded))

			# Remove excluded entities
			var result_set = Set.new(entities_in_group)
			entities_in_group = result_set.difference(exclude_set).to_array()

		# match the entities in the group with the query
		return matches(entities_in_group)

	# Otherwise, query the world with enabled filter for optimal performance
	# OPTIMIZATION: Pass pre-calculated cache key to avoid rehashing
	var result = (
		_world._query(_all_components, _any_components, _exclude_components, _enabled_filter, get_cache_key()) as Array[Entity]
	)

	# Handle relationship filtering
	if not _relationships.is_empty() or not _exclude_relationships.is_empty():
		var filtered_entities: Array = []
		for entity in result:
			var matches = true
			# Required relationships
			for relationship in _relationships:
				if not entity.has_relationship(relationship):
					matches = false
					break
			# Excluded relationships
			if matches:
				for ex_relationship in _exclude_relationships:
					if entity.has_relationship(ex_relationship):
						matches = false
						break
			if matches:
				filtered_entities.append(entity)
		result = filtered_entities

	# Return the structural query result (caching handled in execute())
	# Note: enabled/disabled filtering is now handled in World._query for optimal performance
	return result


## Check if any query in the array has actual property filters (not just empty {})
func _has_actual_queries(queries: Array) -> bool:
	for query in queries:
		if not query.is_empty():
			return true
	return false


## Filter entities based on component queries
func _filter_entities_by_queries(
	entities: Array, components: Array, queries: Array, require_all: bool
) -> Array:
	var filtered = []
	for entity in entities:
		if entity == null:
			continue
		if require_all:
			# Must match all queries
			var matches = true
			for i in range(components.size()):
				var component = entity.get_component(components[i])
				var query = queries[i]
				if not ComponentQueryMatcher.matches_query(component, query):
					matches = false
					break
			if matches:
				filtered.append(entity)
		else:
			# Must match any query
			for i in range(components.size()):
				var component = entity.get_component(components[i])
				var query = queries[i]
				if component and ComponentQueryMatcher.matches_query(component, query):
					filtered.append(entity)
					break
	return filtered


## Check if entity matches any of the queries
func _entity_matches_any_query(entity: Entity, components: Array, queries: Array) -> bool:
	for i in range(components.size()):
		var component = entity.get_component(components[i])
		if component and ComponentQueryMatcher.matches_query(component, queries[i]):
			return true
	return false


## Filters a provided list of entities using the current query criteria.[br]
## Unlike execute(), this doesn't query the world but instead filters the provided entities.[br][br]
## [param entities] Array of entities to filter[br]
## [param returns] Array of entities that match the query criteria[br]
func matches(entities: Array) -> Array:
	# if the query is empty all entities match
	if is_empty():
		return entities
	var result = []

	for entity in entities:
		# If it's null skip it
		if entity == null:
			continue
		assert(entity is Entity, "Must be an entity")
		var matches = true

		# Check all required components
		for component in _all_components:
			if not entity.has_component(component):
				matches = false
				break

		# If still matching and we have any_components, check those
		if matches and not _any_components.is_empty():
			matches = false
			for component in _any_components:
				if entity.has_component(component):
					matches = true
					break

		# Check excluded components
		if matches:
			for component in _exclude_components:
				if entity.has_component(component):
					matches = false
					break

		# Check required relationships
		if matches and not _relationships.is_empty():
			for relationship in _relationships:
				if not entity.has_relationship(relationship):
					matches = false
					break

		# Check excluded relationships
		if matches and not _exclude_relationships.is_empty():
			for relationship in _exclude_relationships:
				if entity.has_relationship(relationship):
					matches = false
					break

		if matches:
			result.append(entity)

	return result


func combine(other: QueryBuilder) -> QueryBuilder:
	_all_components += other._all_components
	_all_components_queries += other._all_components_queries
	_any_components += other._any_components
	_any_components_queries += other._any_components_queries
	_exclude_components += other._exclude_components
	_relationships += other._relationships
	_exclude_relationships += other._exclude_relationships
	_groups += other._groups
	_exclude_groups += other._exclude_groups
	_cache_valid = false
	return self


func as_array() -> Array:
	return [
		_all_components,
		_any_components,
		_exclude_components,
		_relationships,
		_exclude_relationships
	]


func is_empty() -> bool:
	return (
		_all_components.is_empty()
		and _any_components.is_empty()
		and _exclude_components.is_empty()
		and _relationships.is_empty()
		and _exclude_relationships.is_empty()
	)


func _to_string() -> String:
	var parts = []

	if not _all_components.is_empty():
		parts.append("with_all(" + _format_components(_all_components) + ")")

	if not _any_components.is_empty():
		parts.append("with_any(" + _format_components(_any_components) + ")")

	if not _exclude_components.is_empty():
		parts.append("with_none(" + _format_components(_exclude_components) + ")")

	if not _relationships.is_empty():
		parts.append("with_relationship(" + _format_relationships(_relationships) + ")")

	if not _exclude_relationships.is_empty():
		parts.append("without_relationship(" + _format_relationships(_exclude_relationships) + ")")

	if not _groups.is_empty():
		parts.append("with_group(" + str(_groups) + ")")

	if not _exclude_groups.is_empty():
		parts.append("without_group(" + str(_exclude_groups) + ")")

	if _enabled_filter != null:
		if _enabled_filter:
			parts.append("enabled()")
		else:
			parts.append("disabled()")

	if not _all_components_queries.is_empty():
		parts.append("component_queries(" + _format_component_queries(_all_components_queries) + ")")

	if not _any_components_queries.is_empty():
		parts.append("any_component_queries(" + _format_component_queries(_any_components_queries) + ")")

	if parts.is_empty():
		return "ECS.world.query"

	return "ECS.world.query." + ".".join(parts)


func _format_components(components: Array) -> String:
	var names = []
	for component in components:
		if component is Script:
			names.append(component.get_global_name())
		else:
			names.append(str(component))
	return "[" + ", ".join(names) + "]"


func _format_relationships(relationships: Array) -> String:
	var names = []
	for relationship in relationships:
		if relationship.has_method("to_string"):
			names.append(relationship.to_string())
		else:
			names.append(str(relationship))
	return "[" + ", ".join(names) + "]"


func _format_component_queries(queries: Array) -> String:
	var formatted = []
	for query in queries:
		if query.has_method("to_string"):
			formatted.append(query.to_string())
		else:
			formatted.append(str(query))
	return "[" + ", ".join(formatted) + "]"


func compile(query: String) -> QueryBuilder:
	return QueryBuilder.new(_world)


func invalidate_cache():
	_cache_valid = false
	_cache_key_valid = false


## Called when a relationship is added or removed (only for queries using relationships)
## Relationship changes do NOT affect structural cache key; queries only re-filter at execute time
func _on_relationship_changed(_entity: Entity, _relationship: Relationship):
	_cache_valid = false # only result cache


## Get the cached query hash key, calculating it only once
## OPTIMIZATION: Avoids recalculating FNV-1a hash every frame in hot path queries
func get_cache_key() -> int:
	# Structural cache key excludes relationships/groups (matches 6.0.0 behavior)
	if not _cache_key_valid:
		if _world:
			_cache_key = QueryCacheKey.build(_all_components, _any_components, _exclude_components)
			_cache_key_valid = true
		else:
			return -1
	return _cache_key


## Get matching archetypes directly for column-based iteration
## OPTIMIZATION: Skip entity flattening, return archetypes directly for cache-friendly processing
## [br][br]
## [b]Example:[/b]
## [codeblock]
## func process_all(entities: Array, delta: float):
##     for archetype in query().archetypes():
##         var transforms = archetype.get_column(transform_path)
##         for i in range(transforms.size()):
##             # Process transform directly from packed array
## [/codeblock]
func archetypes() -> Array[Archetype]:
	return _world.get_matching_archetypes(self )
