## System[br]
##
## The base class for all systems within the ECS framework.[br]
##
## Systems contain the core logic and behavior, processing [Entity]s that have specific [Component]s.[br]
## Each system overrides the [method System.query] and returns a query using [code]q[/code] or [code]ECS.world.query[/code][br]
## to define the required [Component]s for it to process [Entity]s and implements the [method System.process] method.[br][br]
## [b]Example (Simple):[/b]
##[codeblock]
##     class_name MovementSystem
##     extends System
##
##     func query():
##         return q.with_all([Transform, Velocity])
##
##     func process(entities: Array[Entity], components: Array, delta: float) -> void:
##         # Per-entity processing (simple but slower)
##         for entity in entities:
##             var transform = entity.get_component(Transform)
##             var velocity = entity.get_component(Velocity)
##             transform.position += velocity.direction * velocity.speed * delta
##[/codeblock]
## [b]Example (Optimized with iterate()):[/b]
##[codeblock]
##     func query():
##         return q.with_all([Transform, Velocity]).iterate([Transform, Velocity])
##
##     func process(entities: Array[Entity], components: Array, delta: float) -> void:
##         # Batch processing with component arrays (faster)
##         var transforms = components[0]
##         var velocities = components[1]
##         for i in entities.size():
##             transforms[i].position += velocities[i].velocity * delta
##[/codeblock]
@icon("res://addons/gecs/assets/system.svg")
class_name System
extends Node

#region Enums
## These control when the system should run in relation to other systems.
enum Runs {
	## This system should run before all the systems defined in the array ex: [TransformSystem] means it will run before the [TransformSystem] system runs
	Before,
	## This system should run after all the systems defined in the array ex: [TransformSystem] means it will run after the [TransformSystem] system runs
	After,
}

#endregion Enums

#region Exported Variables
## What group this system belongs to. Systems can be organized and run by group
@export var group: String = ""
## Determines whether the system should run even when there are no [Entity]s to process.
@export var process_empty := false
## Is this system active. (Will be skipped if false)
@export var active := true

@export_group("Parallel Processing")
## Enable parallel processing for this system's entities (No access to scene tree in process method)
@export var parallel_processing := false
## Minimum entities required to use parallel processing (performance threshold)
@export var parallel_threshold := 50

#endregion Exported Variables

#region Public Variables
## Is this system paused. (Will be skipped if true)
var paused := false

## Logger for system debugging and tracing
var systemLogger = GECSLogger.new().domain("System")
## Data for debugger and profiling - you can add ANY arbitrary data here when ECS.debug is enabled
## All keys and values will automatically appear in the GECS debugger tab
## Example:
##   if ECS.debug:
##       lastRunData["my_counter"] = 123
##       lastRunData["player_stats"] = {"health": 100, "mana": 50}
##       lastRunData["events"] = ["event1", "event2"]
var lastRunData := {}

## Reference to the world this system belongs to (set by World.add_system)
var _world: World = null
## Convenience property for accessing query builder (returns _world.query or ECS.world.query)
var q: QueryBuilder:
	get:
		return _world.query if _world else ECS.world.query
## Cached query to avoid recreating it every frame (lazily initialized)
var _query_cache: QueryBuilder = null
## Cached component paths for iterate() fast path (6.0.0 style)
var _component_paths: Array[String] = []
## Cached subsystems array (6.0.0 style)
var _subsystems_cache: Array = []

#endregion Public Variables


#region Public Methods
## Override this method to define the [System]s that this system depends on.[br]
## If not overridden the system will run based on the order of the systems in the [World][br]
## and the order of the systems in the [World] will be based on the order they were added to the [World].[br]
func deps() -> Dictionary[int, Array]:
	return {
		Runs.After: [],
		Runs.Before: [],
	}


## Override this method and return a [QueryBuilder] to define the required [Component]s for the system.[br]
## If not overridden, the system will run on every update with no entities.[br][br]
## You can use [code]q[/code] or [code]ECS.world.query[/code] - both are equivalent.
func query() -> QueryBuilder:
	process_empty = true
	return _world.query if _world else ECS.world.query


## Override this method to define any sub-systems that should be processed by this system.[br]
## Each subsystem is defined as [QueryBuilder, Callable][br]
## Return empty array if not using subsystems (base implementation)[br][br]
## You can use [code]q[/code] or [code]ECS.world.query[/code] in subsystems - both work.[br][br]
## [b]Example:[/b]
## [codeblock]
## func sub_systems() -> Array[Array]:
##     return [
##         [q.with_all([C_Velocity]).iterate([C_Velocity]), process_velocity],
##         [q.with_all([C_Health]), process_health]
##     ]
##
## func process_velocity(entities: Array[Entity], components: Array, delta: float):
##     var velocities = components[0]
##     for i in entities.size():
##         entities[i].position += velocities[i].velocity * delta
##
## func process_health(entities: Array[Entity], components: Array, delta: float):
##     for entity in entities:
##         var health = entity.get_component(C_Health)
##         health.regenerate(delta)
## [/codeblock]
func sub_systems() -> Array[Array]:
	return [] # Base returns empty - overridden systems return populated Array[Array]


## Runs once after the system has been added to the [World] to setup anything on the system one time[br]
func setup():
	pass # Override in subclasses if needed


## The main processing function for the system.[br]
## Override this method to define your system's behavior.[br]
## [param entities] Array of entities matching the system's query[br]
## [param components] Array of component arrays (in order from iterate()), or empty if no iterate() call[br]
## [param delta] The time elapsed since the last frame[br][br]
## [b]Simple approach:[/b] Loop through entities and use get_component()[br]
## [b]Fast approach:[/b] Use iterate() in query and access component arrays directly
func process(entities: Array[Entity], components: Array, delta: float) -> void:
	pass # Override in subclasses - base implementation does nothing

#endregion Public Methods

#region Private Methods


## INTERNAL: Called by World.add_system() to initialize the system
## DO NOT CALL OR OVERRIDE - this is framework code
func _internal_setup():
	# Call user setup
	setup()


## Process entities in parallel using WorkerThreadPool
## Splits entities into batches and processes them concurrently
func _process_parallel(entities: Array[Entity], components: Array, delta: float) -> void:
	if entities.is_empty():
		return

	# Use OS thread count as fallback since WorkerThreadPool.get_thread_count() doesn't exist
	var worker_count = OS.get_processor_count()
	var batch_size = max(1, entities.size() / worker_count)
	var tasks = []

	# Submit tasks for each batch
	for batch_start in range(0, entities.size(), batch_size):
		var batch_end = min(batch_start + batch_size, entities.size())

		# Slice entities and components for this batch
		var batch_entities = entities.slice(batch_start, batch_end)
		var batch_components = []
		for comp_array in components:
			batch_components.append(comp_array.slice(batch_start, batch_end))

		var task_id = WorkerThreadPool.add_task(_process_batch_callable.bind(batch_entities, batch_components, delta))
		tasks.append(task_id)

	# Wait for all tasks to complete
	for task_id in tasks:
		WorkerThreadPool.wait_for_task_completion(task_id)


## Process a batch of entities - called by worker threads
func _process_batch_callable(entities: Array[Entity], components: Array, delta: float) -> void:
	process(entities, components, delta)


## Called by World.process() each frame - main entry point for system execution
## [param delta] The time elapsed since the last frame
func _handle(delta: float) -> void:
	if not active or paused:
		return
	var start_time_usec := 0
	if ECS.debug:
		start_time_usec = Time.get_ticks_usec()
		lastRunData = {
			"system_name": get_script().resource_path.get_file().get_basename(),
			"frame_delta": delta,
		}
	var subs = sub_systems()
	if not subs.is_empty():
		_run_subsystems(delta)
	else:
		_run_process(delta)
	if ECS.debug:
		var end_time_usec = Time.get_ticks_usec()
		lastRunData["execution_time_ms"] = (end_time_usec - start_time_usec) / 1000.0


## UNIFIED execution function for both main systems and subsystems
## This ensures consistent behavior and entity processing logic
## Subsystems and main systems execute IDENTICALLY - no special behavior
## [param query_builder] The query to execute
## [param callable] The function to call with matched entities
## [param delta] Time delta
## [param subsystem_index] Index for debug tracking (-1 for main system)
func _run_subsystems(delta: float) -> void:
	if _subsystems_cache.is_empty():
		_subsystems_cache = sub_systems()
	var subsystem_index := 0
	for subsystem_tuple in _subsystems_cache:
		var subsystem_query := subsystem_tuple[0] as QueryBuilder
		var subsystem_callable := subsystem_tuple[1] as Callable
		var uses_non_structural := _query_has_non_structural_filters(subsystem_query)
		var iterate_comps = subsystem_query._iterate_components
		if uses_non_structural:
			# Gather ALL structural entities first then filter once (avoid per-archetype filtering churn)
			var all_entities: Array[Entity] = []
			for arch in subsystem_query.archetypes():
				if not arch.entities.is_empty():
					all_entities.append_array(arch.entities) # no snapshot to allow mid-frame changes visible to later subsystems
			var filtered = _filter_entities_global(subsystem_query, all_entities)
			if filtered.is_empty():
				if ECS.debug:
					lastRunData[subsystem_index] = {"subsystem_index": subsystem_index, "entity_count": 0, "fallback_execute": true}
				subsystem_index += 1
				continue
			var components := []
			if not iterate_comps.is_empty():
				for comp_type in iterate_comps:
					components.append(_build_component_column_from_entities(filtered, comp_type))
			subsystem_callable.call(filtered, components, delta)
			if ECS.debug:
				lastRunData[subsystem_index] = {"subsystem_index": subsystem_index, "entity_count": filtered.size(), "fallback_execute": true}
		else:
			# Structural fast path archetype iteration
			var total_entity_count := 0
			for archetype in subsystem_query.archetypes():
				if archetype.entities.is_empty():
					continue
				# Snapshot to avoid losing entities during add/remove component archetype moves mid-iteration
				var arch_entities = archetype.entities.duplicate()
				total_entity_count += arch_entities.size()
				var components = []
				if not iterate_comps.is_empty():
					for comp_type in iterate_comps:
						var comp_path = comp_type.resource_path if comp_type is Script else comp_type.get_script().resource_path
						components.append(archetype.get_column(comp_path))
				subsystem_callable.call(arch_entities, components, delta)
			if ECS.debug:
				lastRunData[subsystem_index] = {"subsystem_index": subsystem_index, "entity_count": total_entity_count, "fallback_execute": false}
		subsystem_index += 1


func _run_process(delta: float) -> void:
	if not _query_cache:
		_query_cache = query()
	if _component_paths.is_empty():
		var iterate_comps = _query_cache._iterate_components
		for comp_type in iterate_comps:
			var comp_path = comp_type.resource_path if comp_type is Script else comp_type.get_script().resource_path
			_component_paths.append(comp_path)
	var uses_non_structural := _query_has_non_structural_filters(_query_cache)
	var iterate_comps = _query_cache._iterate_components
	if uses_non_structural:
		# Gather all entities across structural archetypes and then filter once
		var all_entities: Array[Entity] = []
		for arch in _query_cache.archetypes():
			if not arch.entities.is_empty():
				all_entities.append_array(arch.entities)
		if all_entities.is_empty():
			if process_empty:
				process([], [], delta)
			return
		var filtered = _filter_entities_global(_query_cache, all_entities)
		if filtered.is_empty():
			if process_empty:
				process([], [], delta)
			return
		var components := []
		if not iterate_comps.is_empty():
			for comp_type in iterate_comps:
				components.append(_build_component_column_from_entities(filtered, comp_type))
		if parallel_processing and filtered.size() >= parallel_threshold:
			_process_parallel(filtered, components, delta)
		else:
			process(filtered, components, delta)
		if ECS.debug:
			lastRunData["entity_count"] = filtered.size()
			lastRunData["archetype_count"
				] = _query_cache.archetypes().size()
			lastRunData["fallback_execute"] = true
			lastRunData["parallel"] = parallel_processing and filtered.size() >= parallel_threshold
		return
	# Structural fast path
	var matching_archetypes = _query_cache.archetypes()
	var has_entities = false
	var total_entity_count := 0
	for arch in matching_archetypes:
		if not arch.entities.is_empty():
			has_entities = true
			total_entity_count += arch.entities.size()
	if ECS.debug:
		lastRunData["entity_count"] = total_entity_count
		lastRunData["archetype_count"] = matching_archetypes.size()
		lastRunData["fallback_execute"] = false
	if not has_entities and not process_empty:
		return
	if not has_entities and process_empty:
		process([], [], delta)
		return
	for arch in matching_archetypes:
		var arch_entities = arch.entities
		if arch_entities.is_empty():
			continue
		# Snapshot structural entities to avoid mutation skipping during component add/remove
		var snapshot_entities = arch_entities.duplicate()
		var components = []
		if not iterate_comps.is_empty():
			for comp_path in _component_paths:
				components.append(arch.get_column(comp_path))
		if parallel_processing and snapshot_entities.size() >= parallel_threshold:
			if ECS.debug:
				lastRunData["parallel"] = true
				lastRunData["threshold"] = parallel_threshold
			_process_parallel(snapshot_entities, components, delta)
		else:
			if ECS.debug:
				lastRunData["parallel"] = false
			process(snapshot_entities, components, delta)


## Determine if a query includes non-structural filters requiring execute() fallback
func _query_has_non_structural_filters(qb: QueryBuilder) -> bool:
	if not qb._relationships.is_empty():
		return true
	if not qb._exclude_relationships.is_empty():
		return true
	if not qb._groups.is_empty():
		return true
	if not qb._exclude_groups.is_empty():
		return true
	# Component property queries (ensure actual queries, not placeholders)
	if not qb._all_components_queries.is_empty():
		for query in qb._all_components_queries:
			if not query.is_empty():
				return true
	if not qb._any_components_queries.is_empty():
		for query in qb._any_components_queries:
			if not query.is_empty():
				return true
	return false


## Build component arrays for iterate() when falling back to execute() result (no archetype columns)
func _build_component_column_from_entities(entities: Array[Entity], comp_type) -> Array:
	var out := []
	for e in entities:
		if e == null:
			out.append(null)
			continue
		var comp = e.get_component(comp_type)
		out.append(comp)
	return out


## Filter entities in an archetype for non-structural query criteria (relationships/groups/property queries)
## Filter a flat entity array for non-structural criteria
func _filter_entities_global(qb: QueryBuilder, entities: Array[Entity]) -> Array[Entity]:
	var result: Array[Entity] = []
	for e in entities:
		if e == null:
			continue
		var include := true
		for rel in qb._relationships:
			if not e.has_relationship(rel):
				include = false; break
		if include:
			for ex_rel in qb._exclude_relationships:
				if e.has_relationship(ex_rel):
					include = false; break
		if include and not qb._groups.is_empty():
			for g in qb._groups:
				if not e.is_in_group(g):
					include = false; break
		if include and not qb._exclude_groups.is_empty():
			for g in qb._exclude_groups:
				if e.is_in_group(g):
					include = false; break
		if include and not qb._all_components_queries.is_empty():
			for i in range(qb._all_components.size()):
				if i >= qb._all_components_queries.size():
					break
				var comp_type = qb._all_components[i]
				var query = qb._all_components_queries[i]
				if not query.is_empty():
					var comp = e.get_component(comp_type)
					if comp == null or not ComponentQueryMatcher.matches_query(comp, query):
						include = false; break
		if include and not qb._any_components_queries.is_empty():
			var any_match := qb._any_components_queries.is_empty()
			for i in range(qb._any_components.size()):
				if i >= qb._any_components_queries.size():
					break
				var comp_type = qb._any_components[i]
				var query = qb._any_components_queries[i]
				if not query.is_empty():
					var comp = e.get_component(comp_type)
					if comp and ComponentQueryMatcher.matches_query(comp, query):
						any_match = true; break
			if not any_match and not qb._any_components.is_empty():
				include = false
		if include:
			result.append(e)
	return result


## Debug helper - updates lastRunData (compiled out in production)
func _update_debug_data(callable: Callable = func(): return {}) -> bool:
	if ECS.debug:
		var data = callable.call()
		if data:
			lastRunData.assign(data)
	return true


## Debug helper - sets lastRunData (compiled out in production)
func _debug_data(_lrd: Dictionary, callable: Callable = func(): return {}) -> bool:
	if ECS.debug:
		lastRunData = _lrd
		lastRunData.assign(callable.call())
	return true

#endregion Private Methods
