@tool
class_name QodotMap
extends QodotNode3D

## Builds Godot scenes from .map files
##
## A QodotMap node lets you define the source file for a map, as well as specify
## the definitions for entities, textures, and materials that appear in the map.
## To use this node, select an instance of the node in the Godot editor and
## select "Quick Build", "Full Build", or "Unwrap UV2" from the toolbar.
## Alternatively, call [method manual_build] from code.
##
## @tutorial: https://qodotplugin.github.io/docs/beginner's-guide-to-qodot/

## Force reinitialization of Qodot on map build
const DEBUG := false
## How long to wait between child/owner batches
const YIELD_DURATION := 0.0

## Emitted when the build process successfully completes
signal build_complete()
## Emitted when the build process finishes a step. [code]progress[/code] is from 0.0-1.0
signal build_progress(step, progress)
## Emitted when the build process fails
signal build_failed()

## Emitted when UV2 unwrapping is completed
signal unwrap_uv2_complete()

@export_category("Map")
## Trenchbroom Map file to build a scene from
@export_global_file("*.map") var map_file := ""
## Ratio between Trenchbroom units in the .map file and Godot units.
## An inverse scale factor of 16 would cause 16 Trenchbroom units to correspond to 1 Godot unit. See [url=https://qodotplugin.github.io/docs/geometry.html#scale]Scale[/url] in the Qodot documentation.
@export var inverse_scale_factor := 16.0
@export_category("Entities")
## [QodotFGDFile] for the map.
## This resource will translate between Trenchbroom classnames and Godot scripts/scenes. See [url=https://qodotplugin.github.io/docs/entities/]Entities[/url] in the Qodot manual.
@export var entity_fgd: QodotFGDFile = load("res://addons/qodot/game_definitions/fgd/qodot_fgd.tres")
@export_category("Textures")
## Base directory for textures. When building materials, Qodot will search this directory for texture files matching the textures assigned to Trenchbroom faces.
@export_dir var base_texture_dir := "res://textures"
## File extensions to search for texture data.
@export var texture_file_extensions := PackedStringArray(["png", "jpg", "jpeg", "bmp"])
## Optional. List of worldspawn layers.
## A worldspawn layer converts any brush of a certain texture to a certain kind of node. See example 1-2.
@export var worldspawn_layers: Array[QodotWorldspawnLayer]
## Optional. Path for the clip texture, relative to [member base_texture_dir].
## Brushes textured with the clip texture will be turned into invisible but solid volumes.
@export var brush_clip_texture := "special/clip"
## Optional. Path for the skip texture, relative to [member base_texture_dir].
## Faces textured with the skip texture will not be rendered.
@export var face_skip_texture := "special/skip"
## Optional. WAD files to pull textures from.
## Quake engine games are distributed with .WAD files, which are packed texture libraries. Qodot can import these files as [QuakeWadFile]s.
@export var texture_wads: Array[QuakeWadFile]
@export_category("Materials")
## File extensions to search for Material definitions
@export var material_file_extension := "tres"
## If true, all materials will be unshaded, i.e. will ignore light. Also known as "fullbright".
@export var unshaded := false
## Material used as template when generating missing materials.
@export var default_material : Material = StandardMaterial3D.new()
## Default albedo texture (used when [member default_material] is a [ShaderMaterial])
@export var default_material_albedo_uniform := ""
@export_category("UV Unwrap")
## Texel size for UV2 unwrapping.
## A texel size of 1 will lead to a 1:1 correspondence between texture texels and lightmap texels. Larger values will produce less detailed lightmaps. To conserve memory and filesize, use the largest value that still looks good.
@export var uv_unwrap_texel_size := 1.0
@export_category("Build")
## If true, print profiling data before and after each build step
@export var print_profiling_data := false
## If true, Qodot will build a hierarchy from Trenchbroom groups, each group being a node. Otherwise, Qodot nodes will ignore Trenchbroom groups and have a flat structure.
@export var use_trenchbroom_group_hierarchy := false
## If true, stop the whole editor until build is complete
@export var block_until_complete := false
## How many nodes to set the owner of, or add children of, at once. Higher values may lead to quicker build times, but a less responsive editor.
@export var set_owner_batch_size := 1000

# Build context variables
var qodot = null

var profile_timestamps := {}

var add_child_array := []
var set_owner_array := []

var should_add_children := true
var should_set_owners := true

var texture_list := []
var texture_loader = null
var texture_dict := {}
var texture_size_dict := {}
var material_dict := {}
var entity_definitions := {}
var entity_dicts := []
var worldspawn_layer_dicts := []
var entity_mesh_dict := {}
var worldspawn_layer_mesh_dict := {}
var entity_nodes := []
var worldspawn_layer_nodes := []
var entity_mesh_instances := {}
var entity_occluder_instances := {}
var worldspawn_layer_mesh_instances := {}
var entity_collision_shapes := []
var worldspawn_layer_collision_shapes := []

# Overrides
func _ready() -> void:
	if not DEBUG:
		return
	
	if not Engine.is_editor_hint():
		if verify_parameters():
			build_map()

# Utility
## Verify that Qodot is functioning and that [member map_file] exists. If so, build the map. If not, signal [signal build_failed]
func verify_and_build():
	if verify_parameters():
		build_map()
	else:
		emit_signal("build_failed")

## Build the map.
func manual_build():
	should_add_children = false
	should_set_owners = false
	verify_and_build()

## Return true if parameters are valid; Qodot should be functioning and [member map_file] should exist.
func verify_parameters():
	if not qodot or DEBUG:
		qodot = load("res://addons/qodot/src/core/qodot.gd").new()
	
	if not qodot:
		push_error("Error: Failed to load qodot.")
		return false
	
	if map_file == "":
		push_error("Error: Map file not set")
		return false
	
	if not FileAccess.file_exists(map_file):
		push_error("Error: No such file %s" % map_file)
		return false
	
	return true

## Reset member variables that affect the current build
func reset_build_context():
	add_child_array = []
	set_owner_array = []
	
	texture_list = []
	texture_loader = null
	texture_dict = {}
	texture_size_dict = {}
	material_dict = {}
	entity_definitions = {}
	entity_dicts = []
	worldspawn_layer_dicts = []
	entity_mesh_dict = {}
	worldspawn_layer_mesh_dict = {}
	entity_nodes = []
	worldspawn_layer_nodes = []
	entity_mesh_instances = {}
	entity_occluder_instances = {}
	worldspawn_layer_mesh_instances = {}
	entity_collision_shapes = []
	worldspawn_layer_collision_shapes = []
	
	build_step_index = 0
	build_step_count = 0
	
	if qodot:
		qodot = load("res://addons/qodot/src/core/qodot.gd").new()
		
## Record the start time of a build step for profiling
func start_profile(item_name: String) -> void:
	if print_profiling_data:
		print(item_name)
		profile_timestamps[item_name] = Time.get_unix_time_from_system()

## Finish profiling for a build step; print associated timing data
func stop_profile(item_name: String) -> void:
	if print_profiling_data:
		if item_name in profile_timestamps:
			var delta: float = Time.get_unix_time_from_system() - profile_timestamps[item_name]
			print("Done in %s sec.\n" % snapped(delta, 0.01))
			profile_timestamps.erase(item_name)

## Run a build step. [code]step_name[/code] is the method corresponding to the step, [code]params[/code] are parameters to pass to the step, and [code]func_name[/code] does nothing.
func run_build_step(step_name: String, params: Array = [], func_name: String = ""):
	start_profile(step_name)
	if func_name == "":
		func_name = step_name
	var result = callv(step_name, params)
	stop_profile(step_name)
	return result

## Add [code]node[/code] as a child of parent, or as a child of [code]below[/code] if non-null. Also queue for ownership assignment.
func add_child_editor(parent, node, below = null) -> void:
	var prev_parent = node.get_parent()
	if prev_parent:
		prev_parent.remove_child(node)
	
	if below:
		below.add_sibling(node)
	else:
		parent.add_child(node)
	
	set_owner_array.append(node)

## Set the owner of [code]node[/code] to the current scene.
func set_owner_editor(node):
	var tree := get_tree()
	
	if not tree:
		return
	
	var edited_scene_root := tree.get_edited_scene_root()
	
	if not edited_scene_root:
		return
	
	node.set_owner(edited_scene_root)

var build_step_index := 0
var build_step_count := 0
var build_steps := []
var post_attach_steps := []

## Register a build step.
## [code]build_step[/code] is a string that corresponds to a method on this class, [code]arguments[/code] a list of arguments to pass to this method, and [code]target[/code] is a property on this class to save the return value of the build step in. If [code]post_attach[/code] is true, the step will be run after the scene hierarchy is completed.
func register_build_step(build_step: String, arguments := [], target := "", post_attach := false) -> void:
	(post_attach_steps if post_attach else build_steps).append([build_step, arguments, target])
	build_step_count += 1

## Run all build steps. Emits [signal build_progress] after each step.
## If [code]post_attach[/code] is true, run post-attach steps instead and signal [signal build_complete] when finished.
func run_build_steps(post_attach := false) -> void:
	var target_array = post_attach_steps if post_attach else build_steps
	
	while target_array.size() > 0:
		var build_step = target_array.pop_front()
		emit_signal("build_progress", build_step[0], float(build_step_index + 1) / float(build_step_count))
		
		var scene_tree := get_tree()
		if scene_tree and not block_until_complete:
			await get_tree().create_timer(YIELD_DURATION).timeout
		
		var result = run_build_step(build_step[0], build_step[1])
		var target = build_step[2]
		if target != "":
			set(target, result)
			
		build_step_index += 1
		
		if scene_tree and not block_until_complete:
			await get_tree().create_timer(YIELD_DURATION).timeout

	if post_attach:
		_build_complete()
	else:
		start_profile('add_children')
		add_children()

## Register all steps for the build. See [method register_build_step] and [method run_build_steps]
func register_build_steps() -> void:
	register_build_step('remove_children')
	register_build_step('load_map')
	register_build_step('fetch_texture_list', [], 'texture_list')
	register_build_step('init_texture_loader', [], 'texture_loader')
	register_build_step('load_textures', [], 'texture_dict')
	register_build_step('build_texture_size_dict', [], 'texture_size_dict')
	register_build_step('build_materials', [], 'material_dict')
	register_build_step('fetch_entity_definitions', [], 'entity_definitions')
	register_build_step('set_qodot_entity_definitions', [])
	register_build_step('set_qodot_worldspawn_layers', [])
	register_build_step('generate_geometry', [])
	register_build_step('fetch_entity_dicts', [], 'entity_dicts')
	register_build_step('fetch_worldspawn_layer_dicts', [], 'worldspawn_layer_dicts')
	register_build_step('build_entity_nodes', [], 'entity_nodes')
	register_build_step('build_worldspawn_layer_nodes', [], 'worldspawn_layer_nodes')
	register_build_step('resolve_group_hierarchy', [])
	register_build_step('build_entity_mesh_dict', [], 'entity_mesh_dict')
	register_build_step('build_worldspawn_layer_mesh_dict', [], 'worldspawn_layer_mesh_dict')
	register_build_step('build_entity_mesh_instances', [], 'entity_mesh_instances')
	register_build_step('build_entity_occluder_instances', [], 'entity_occluder_instances')
	register_build_step('build_worldspawn_layer_mesh_instances', [], 'worldspawn_layer_mesh_instances')
	register_build_step('build_entity_collision_shape_nodes', [], 'entity_collision_shapes')
	register_build_step('build_worldspawn_layer_collision_shape_nodes', [], 'worldspawn_layer_collision_shapes')

## Register all post-attach steps for the build. See [method register_build_step] and [method run_build_steps]
func register_post_attach_steps() -> void:
	register_build_step('build_entity_collision_shapes', [], "", true)
	register_build_step('build_worldspawn_layer_collision_shapes', [], "", true)
	register_build_step('apply_entity_meshes', [], "", true)
	register_build_step('apply_entity_occluders', [], "", true)
	register_build_step('apply_worldspawn_layer_meshes', [], "", true)
	register_build_step('apply_properties', [], "", true)
	register_build_step('connect_signals', [], "", true)
	register_build_step('remove_transient_nodes', [], "", true)

# Actions
## Build the map
func build_map() -> void:
	reset_build_context()
	
	print('Building %s\n' % map_file)
	start_profile('build_map')
	
	register_build_steps()
	register_post_attach_steps()
	
	run_build_steps()

## Recursively unwrap UV2s for [code]node[/code] and its children, in preparation for baked lighting.
func unwrap_uv2(node: Node = null) -> void:
	var target_node = null
	
	if node:
		target_node = node
	else:
		target_node = self
		print("Unwrapping mesh UV2s")
	
	if target_node is MeshInstance3D:
		var mesh = target_node.get_mesh()
		if mesh is ArrayMesh:
			mesh.lightmap_unwrap(Transform3D.IDENTITY, uv_unwrap_texel_size / inverse_scale_factor)
	
	for child in target_node.get_children():
		unwrap_uv2(child)
	
	if not node:
		print("Unwrap complete")
		emit_signal("unwrap_uv2_complete")

# Build Steps
## Recursively remove and delete all children of this node
func remove_children() -> void:
	for child in get_children():
		remove_child(child)
		child.queue_free()

## Parse and load [member map_file]
func load_map() -> void:
	var file: String = map_file
	qodot.load_map(file)

## Get textures found in [member map_file]
func fetch_texture_list() -> Array:
	return qodot.get_texture_list() as Array

## Initialize texture loader, allowing textures in [member base_texture_dir] and [member texture_wads] to be turned into materials
func init_texture_loader() -> QodotTextureLoader:
	var tex_ldr := QodotTextureLoader.new(
		base_texture_dir,
		texture_file_extensions,
		texture_wads
	)
	tex_ldr.unshaded = unshaded
	return tex_ldr

## Build a dictionary from Trenchbroom texture names to their corresponding Texture2D resources in Godot
func load_textures() -> Dictionary:
	return texture_loader.load_textures(texture_list) as Dictionary

## Build a dictionary from Trenchbroom texture names to Godot materials
func build_materials() -> Dictionary:
	return texture_loader.create_materials(texture_list, material_file_extension, default_material, default_material_albedo_uniform)

## Collect entity definitions from [member entity_fgd], as a dictionary from Trenchbroom classnames to entity definitions
func fetch_entity_definitions() -> Dictionary:
	return entity_fgd.get_entity_definitions()

## Hand the Qodot C# core the entity definitions
func set_qodot_entity_definitions() -> void:
	qodot.set_entity_definitions(build_libmap_entity_definitions(entity_definitions))

## Hand the Qodot C# core the worldspawn layer definitions. See [member worldspawn_layers]
func set_qodot_worldspawn_layers() -> void:
	qodot.set_worldspawn_layers(build_libmap_worldspawn_layers(worldspawn_layers))

## Generate geometry from map file
func generate_geometry() -> void:
	qodot.generate_geometry(texture_size_dict);

## Get a list of dictionaries representing each entity from the Qodot C# core
func fetch_entity_dicts() -> Array:
	return qodot.get_entity_dicts()

## Get a list of dictionaries representing each worldspawn layer from the Qodot C# core
func fetch_worldspawn_layer_dicts() -> Array:
	var layer_dicts = qodot.get_worldspawn_layer_dicts()
	return layer_dicts if layer_dicts else []

## Build a dictionary from Trenchbroom textures to the sizes of their corresponding Godot textures
func build_texture_size_dict() -> Dictionary:
	var texture_size_dict := {}
	
	for tex_key in texture_dict:
		var texture := texture_dict[tex_key] as Texture2D
		if texture:
			texture_size_dict[tex_key] = texture.get_size()
		else:
			texture_size_dict[tex_key] = Vector2.ONE
	
	return texture_size_dict

## Marshall Qodot FGD definitions for transfer to libmap
func build_libmap_entity_definitions(entity_definitions: Dictionary) -> Dictionary:
	var libmap_entity_definitions = {}
	for classname in entity_definitions:
		libmap_entity_definitions[classname] = {}
		if entity_definitions[classname] is QodotFGDSolidClass:
			libmap_entity_definitions[classname]['spawn_type'] = entity_definitions[classname].spawn_type
	return libmap_entity_definitions

## Marshall worldspawn layer definitions for transfer to libmap
func build_libmap_worldspawn_layers(worldspawn_layers: Array) -> Array:
	var libmap_worldspawn_layers := []
	for worldspawn_layer in worldspawn_layers:
		libmap_worldspawn_layers.append({
			'name': worldspawn_layer.name,
			'texture': worldspawn_layer.texture,
			'node_class': worldspawn_layer.node_class,
			'build_visuals': worldspawn_layer.build_visuals,
			'collision_shape_type': worldspawn_layer.collision_shape_type,
			'script_class': worldspawn_layer.script_class
		})
	return libmap_worldspawn_layers

## Build nodes from the entities in [member entity_dicts]
func build_entity_nodes() -> Array:
	var entity_nodes := []

	for entity_idx in range(0, entity_dicts.size()):
		var entity_dict := entity_dicts[entity_idx] as Dictionary
		var properties := entity_dict['properties'] as Dictionary
		
		var node = QodotEntity.new()
		var node_name = "entity_%s" % entity_idx
		
		var should_add_child = should_add_children
		
		if 'classname' in properties:
			var classname = properties['classname']
			node_name += "_" + classname
			if classname in entity_definitions:
				var entity_definition := entity_definitions[classname] as QodotFGDClass
				if entity_definition is QodotFGDSolidClass:
					if entity_definition.spawn_type == QodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
						entity_nodes.append(null)
						continue
					elif use_trenchbroom_group_hierarchy and entity_definition.spawn_type == QodotFGDSolidClass.SpawnType.GROUP:
						should_add_child = false
					if entity_definition.node_class != "":
						node.queue_free()
						node = ClassDB.instantiate(entity_definition.node_class)
				elif entity_definition is QodotFGDPointClass:
					if entity_definition.scene_file:
						var flag = PackedScene.GEN_EDIT_STATE_DISABLED
						if Engine.is_editor_hint():
							flag = PackedScene.GEN_EDIT_STATE_INSTANCE
						node.queue_free()
						node = entity_definition.scene_file.instantiate(flag)
					elif entity_definition.node_class != "":
						node.queue_free()
						node = ClassDB.instantiate(entity_definition.node_class)
					if 'rotation_degrees' in node and entity_definition.apply_rotation_on_map_build:
						var angles := Vector3.ZERO
						if 'angles' in properties or 'mangle' in properties:
							var key := 'angles' if 'angles' in properties else 'mangle'
							var angles_raw = properties[key]
							if not angles_raw is Vector3:
								angles_raw = angles_raw.split_floats(' ')
							if angles_raw.size() > 2:
								angles = Vector3(-angles_raw[0], angles_raw[1], -angles_raw[2])
								if key == 'mangle':
									if entity_definition.classname.begins_with('light'):
										angles = Vector3(angles_raw[1], angles_raw[0], -angles_raw[2])
									elif entity_definition.classname == 'info_intermission':
										angles = Vector3(angles_raw[0], angles_raw[1], -angles_raw[2])
							else:
								push_error("Invalid vector format for \'" + key + "\' in entity \'" + classname + "\'")
						elif 'angle' in properties:
							var angle = properties['angle']
							if not angle is float:
								angle = float(angle)
							angles.y += angle
						angles.y += 180
						node.rotation_degrees = angles
				if entity_definition.script_class:
					node.set_script(entity_definition.script_class)
		
		node.name = node_name
		
		if 'origin' in properties:
			var origin_vec = Vector3.ZERO
			var origin_comps = properties['origin'].split_floats(' ')
			if origin_comps.size() > 2:
				origin_vec = Vector3(origin_comps[1], origin_comps[2], origin_comps[0])
			else:
				push_error("Invalid vector format for \'origin\' in " + node.name)
			if "position" in node:
				if node.position is Vector3:
					node.position = origin_vec / inverse_scale_factor
				elif node.position is Vector2:
					node.position = Vector2(origin_vec.z, -origin_vec.y)
		else:
			if entity_idx != 0 and "position" in node:
				if node.position is Vector3:
					node.position = entity_dict['center'] / inverse_scale_factor
		
		entity_nodes.append(node)
		
		if should_add_child:
			queue_add_child(self, node)
	
	return entity_nodes

## Build nodes from the worldspawn layers in [member worldspawn_layers]
func build_worldspawn_layer_nodes() -> Array:
	var worldspawn_layer_nodes := []
	
	for worldspawn_layer in worldspawn_layers:
		var node = ClassDB.instantiate(worldspawn_layer.node_class)
		node.name = "entity_0_" + worldspawn_layer.name
		if worldspawn_layer.script_class:
			node.set_script(worldspawn_layer.script_class)
		
		worldspawn_layer_nodes.append(node)
		queue_add_child(self, node, entity_nodes[0])
	
	return worldspawn_layer_nodes

## Resolve entity group hierarchy, turning Trenchbroom groups into nodes and queueing their contents to be added to said nodes as children
func resolve_group_hierarchy() -> void:
	if not use_trenchbroom_group_hierarchy:
		return
	
	var group_entities := {}
	var owner_entities := {}
	
	# Gather group entities and their owning children
	for node_idx in range(0, entity_nodes.size()):
		var node = entity_nodes[node_idx]
		var properties = entity_dicts[node_idx]['properties']
		
		if not properties: continue
		
		if not '_tb_id' in properties and not '_tb_group' in properties:
			continue
		
		if not 'classname' in properties: continue
		var classname = properties['classname']
		
		if not classname in entity_definitions: continue
		var entity_definition = entity_definitions[classname]
		# TODO: Add clause on this line for point entities, which do not have a spawn type. Add as child of the current group owner.
		if entity_definition.spawn_type == QodotFGDSolidClass.SpawnType.GROUP:
			group_entities[node_idx] = node
		else:
			owner_entities[node_idx] = node
	
	var group_to_entity_map := {}
	
	for node_idx in owner_entities:
		var node = owner_entities[node_idx]
		var properties = entity_dicts[node_idx]['properties']
		var tb_group = properties['_tb_group']
		
		var parent_idx = null
		var parent = null
		var parent_properties = null
		for group_idx in group_entities:
			var group_entity = group_entities[group_idx]
			var group_properties = entity_dicts[group_idx]['properties']
			if group_properties['_tb_id'] == tb_group:
				parent_idx = group_idx
				parent = group_entity
				parent_properties = group_properties
				break
		
		if parent:
			group_to_entity_map[parent_idx] = node_idx
	
	var group_to_group_map := {}
	
	for node_idx in group_entities:
		var node = group_entities[node_idx]
		var properties = entity_dicts[node_idx]['properties']
		
		if not '_tb_group' in properties:
			continue
		
		var tb_group = properties['_tb_group']
		
		var parent_idx = null
		var parent = null
		var parent_properties = null
		for group_idx in group_entities:
			var group_entity = group_entities[group_idx]
			var group_properties = entity_dicts[group_idx]['properties']
			if group_properties['_tb_id'] == tb_group:
				parent_idx = group_idx
				parent = group_entity
				parent_properties = group_properties
				break
		
		if parent:
			group_to_group_map[parent_idx] = node_idx
	
	for parent_idx in group_to_group_map:
		var child_idx = group_to_group_map[parent_idx]
		
		var parent_entity_idx = group_to_entity_map[parent_idx]
		var child_entity_idx = group_to_entity_map[child_idx]
		
		var parent = entity_nodes[parent_entity_idx]
		var child = entity_nodes[child_entity_idx]
		
		queue_add_child(parent, child, null, true)
	
	for child_idx in group_to_entity_map:
		var parent_idx = group_to_entity_map[child_idx]
		
		var parent = entity_nodes[parent_idx]
		var child = entity_nodes[child_idx]
		
		queue_add_child(parent, child, null, true)

## Return the node associated with a Trenchbroom index. Unused.
func get_node_by_tb_id(target_id: String, entity_nodes: Dictionary):
	for node_idx in entity_nodes:
		var node = entity_nodes[node_idx]
		
		if not node:
			continue
		
		if not 'properties' in node:
			continue
		
		var properties = node['properties']
		
		if not '_tb_id' in properties:
			continue
		
		var parent_id = properties['_tb_id']
		if parent_id == target_id:
			return node
		
	return null

## Build [CollisionShape3D] nodes for brush entities
func build_entity_collision_shape_nodes() -> Array:
	var entity_collision_shapes_arr := []
	
	for entity_idx in range(0, entity_nodes.size()):
		var entity_collision_shapes := []
		
		var entity_dict = entity_dicts[entity_idx]
		var properties = entity_dict['properties']
		
		var node := entity_nodes[entity_idx] as Node
		var concave = false
		
		if 'classname' in properties:
			var classname = properties['classname']
			if classname in entity_definitions:
				var entity_definition := entity_definitions[classname] as QodotFGDSolidClass
				if entity_definition:
					if entity_definition.collision_shape_type == QodotFGDSolidClass.CollisionShapeType.NONE:
						entity_collision_shapes_arr.append(null)
						continue
					elif entity_definition.collision_shape_type == QodotFGDSolidClass.CollisionShapeType.CONCAVE:
						concave = true
					
					if entity_definition.spawn_type == QodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
						# TODO: Find the worldspawn object instead of assuming index 0
						node = entity_nodes[0] as Node
					
					if node and node is CollisionObject3D:
						(node as CollisionObject3D).collision_layer = entity_definition.collision_layer
						(node as CollisionObject3D).collision_mask = entity_definition.collision_mask
						(node as CollisionObject3D).collision_priority = entity_definition.collision_priority
		
		# don't create collision shapes that wont be attached to a CollisionObject3D as they are a waste
		if not node or (not node is CollisionObject3D):
			entity_collision_shapes_arr.append(null)
			continue
		
		if concave:
			var collision_shape := CollisionShape3D.new()
			collision_shape.name = "entity_%s_collision_shape" % entity_idx
			entity_collision_shapes.append(collision_shape)
			queue_add_child(node, collision_shape)
		else:
			for brush_idx in entity_dict['brush_indices']:
				var collision_shape := CollisionShape3D.new()
				collision_shape.name = "entity_%s_brush_%s_collision_shape" % [entity_idx, brush_idx]
				entity_collision_shapes.append(collision_shape)
				queue_add_child(node, collision_shape)
		entity_collision_shapes_arr.append(entity_collision_shapes)
	
	return entity_collision_shapes_arr

## Build CollisionShape3D nodes for worldspawn layers
func build_worldspawn_layer_collision_shape_nodes() -> Array:
	var worldspawn_layer_collision_shapes := []
	
	for layer_idx in range(0, worldspawn_layers.size()):
		if layer_idx >= worldspawn_layer_dicts.size():
			continue
		
		var layer = worldspawn_layers[layer_idx]
		
		var layer_dict = worldspawn_layer_dicts[layer_idx]
		var node := worldspawn_layer_nodes[layer_idx] as Node
		var concave = false
		
		var shapes := []
		
		if layer.collision_shape_type == QodotFGDSolidClass.CollisionShapeType.NONE:
			worldspawn_layer_collision_shapes.append(shapes)
			continue
		elif layer.collision_shape_type == QodotFGDSolidClass.CollisionShapeType.CONCAVE:
			concave = true
		
		if not node:
			worldspawn_layer_collision_shapes.append(shapes)
			continue
		
		if concave:
			var collision_shape := CollisionShape3D.new()
			collision_shape.name = "entity_0_%s_collision_shape" % layer.name
			shapes.append(collision_shape)
			queue_add_child(node, collision_shape)
		else:
			for brush_idx in layer_dict['brush_indices']:
				var collision_shape := CollisionShape3D.new()
				collision_shape.name = "entity_0_%s_brush_%s_collision_shape" % [layer.name, brush_idx]
				shapes.append(collision_shape)
				queue_add_child(node, collision_shape)
		
		worldspawn_layer_collision_shapes.append(shapes)
	
	return worldspawn_layer_collision_shapes

## Build the concrete [Shape3D] resources for each brush
func build_entity_collision_shapes() -> void:
	for entity_idx in range(0, entity_dicts.size()):
		var entity_dict := entity_dicts[entity_idx] as Dictionary
		var properties = entity_dict['properties']
		var entity_position: Vector3 = Vector3.ZERO
		if entity_nodes[entity_idx] != null and entity_nodes[entity_idx].get("position"):
			if entity_nodes[entity_idx].position is Vector3:
				entity_position = entity_nodes[entity_idx].position
		var entity_collision_shape = entity_collision_shapes[entity_idx]
		
		if entity_collision_shape == null:
			continue
		
		var concave: bool = false
		var shape_margin: float = 0.04
		
		if 'classname' in properties:
			var classname = properties['classname']
			if classname in entity_definitions:
				var entity_definition = entity_definitions[classname] as QodotFGDSolidClass
				if entity_definition:
					match(entity_definition.collision_shape_type):
						QodotFGDSolidClass.CollisionShapeType.NONE:
							continue
						QodotFGDSolidClass.CollisionShapeType.CONVEX:
							concave = false
						QodotFGDSolidClass.CollisionShapeType.CONCAVE:
							concave = true
					shape_margin = entity_definition.collision_shape_margin
		
		if entity_collision_shapes[entity_idx] == null:
			continue
		
		if concave:
			qodot.gather_entity_concave_collision_surfaces(entity_idx, face_skip_texture)
		else:
			qodot.gather_entity_convex_collision_surfaces(entity_idx)
		
		var entity_surfaces := qodot.fetch_surfaces(inverse_scale_factor) as Array
		
		var entity_verts := PackedVector3Array()
		
		for surface_idx in range(0, entity_surfaces.size()):
			var surface_verts = entity_surfaces[surface_idx]
			
			if surface_verts == null:
				continue
			
			if concave:
				var vertices := surface_verts[Mesh.ARRAY_VERTEX] as PackedVector3Array
				var indices := surface_verts[Mesh.ARRAY_INDEX] as PackedInt32Array
				for vert_idx in indices:
					entity_verts.append(vertices[vert_idx])
			else:
				var shape_points = PackedVector3Array()
				for vertex in surface_verts[Mesh.ARRAY_VERTEX]:
					if not vertex in shape_points:
						shape_points.append(vertex)
				
				var shape = ConvexPolygonShape3D.new()
				shape.set_points(shape_points)
				shape.margin = shape_margin
				
				var collision_shape = entity_collision_shape[surface_idx]
				collision_shape.set_shape(shape)
				
		if concave:
			if entity_verts.size() == 0:
				continue
			
			var shape = ConcavePolygonShape3D.new()
			shape.set_faces(entity_verts)
			shape.margin = shape_margin
			
			var collision_shape = entity_collision_shapes[entity_idx][0]
			collision_shape.set_shape(shape)

## Build the concrete [Shape3D] resources for each worldspawn layer
func build_worldspawn_layer_collision_shapes() -> void:
	for layer_idx in range(0, worldspawn_layers.size()):
		if layer_idx >= worldspawn_layer_dicts.size():
			continue
		
		var layer = worldspawn_layers[layer_idx]
		var concave = false
		
		match(layer.collision_shape_type):
			QodotFGDSolidClass.CollisionShapeType.NONE:
				continue
			QodotFGDSolidClass.CollisionShapeType.CONVEX:
				concave = false
			QodotFGDSolidClass.CollisionShapeType.CONCAVE:
				concave = true
		
		var layer_dict = worldspawn_layer_dicts[layer_idx]
		
		if not worldspawn_layer_collision_shapes[layer_idx]:
			continue
		
		qodot.gather_worldspawn_layer_collision_surfaces(0)
		
		var layer_surfaces := qodot.FetchSurfaces(inverse_scale_factor) as Array
		
		var verts := PackedVector3Array()
		
		for i in range(0, layer_dict.brush_indices.size()):
			var surface_idx = layer_dict.brush_indices[i]
			var surface_verts = layer_surfaces[surface_idx]
			
			if not surface_verts:
				continue
			
			if concave:
				var vertices := surface_verts[0] as PackedVector3Array
				var indices := surface_verts[8] as PackedInt32Array
				for vert_idx in indices:
					verts.append(vertices[vert_idx])
			else:
				var shape_points = PackedVector3Array()
				for vertex in surface_verts[0]:
					if not vertex in shape_points:
						shape_points.append(vertex)
				
				var shape = ConvexPolygonShape3D.new()
				shape.set_points(shape_points)
				
				var collision_shape = worldspawn_layer_collision_shapes[layer_idx][i]
				collision_shape.set_shape(shape)
		
		if concave:
			if verts.size() == 0:
				continue
			
			var shape = ConcavePolygonShape3D.new()
			shape.set_faces(verts)
			
			var collision_shape = worldspawn_layer_collision_shapes[layer_idx][0]
			collision_shape.set_shape(shape)

## Build Dictionary from entity indices to [ArrayMesh] instances
func build_entity_mesh_dict() -> Dictionary:
	var meshes := {}
	
	var texture_surf_map: Dictionary
	for texture in texture_dict:
		texture_surf_map[texture] = Array()
	
	var gather_task = func(i):
		var texture = texture_dict.keys()[i]
		texture_surf_map[texture] = qodot.gather_texture_surfaces_mt(texture, brush_clip_texture, face_skip_texture, inverse_scale_factor)
	
	var task_id:= WorkerThreadPool.add_group_task(gather_task, texture_dict.keys().size(), 4, true)
	WorkerThreadPool.wait_for_group_task_completion(task_id)
	
	for texture in texture_dict:
		var texture_surfaces := texture_surf_map[texture] as Array
		
		for entity_idx in range(0, texture_surfaces.size()):
			var entity_dict := entity_dicts[entity_idx] as Dictionary
			var properties = entity_dict['properties']
			
			var entity_surface = texture_surfaces[entity_idx]
			
			if 'classname' in properties:
				var classname = properties['classname']
				if classname in entity_definitions:
					var entity_definition = entity_definitions[classname] as QodotFGDSolidClass
					if entity_definition:
						if entity_definition.spawn_type == QodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
							entity_surface = null
							
						if not entity_definition.build_visuals and not entity_definition.build_occlusion:
							entity_surface = null
						
			if entity_surface == null:
				continue
			
			if not entity_idx in meshes:
				meshes[entity_idx] = ArrayMesh.new()
			
			var mesh: ArrayMesh = meshes[entity_idx]
			mesh.add_surface_from_arrays(ArrayMesh.PRIMITIVE_TRIANGLES, entity_surface)
			mesh.surface_set_name(mesh.get_surface_count() - 1, texture)
			mesh.surface_set_material(mesh.get_surface_count() - 1, material_dict[texture])
	
	return meshes

## Build Dictionary from worldspawn layers (via textures) to [ArrayMesh] instances
func build_worldspawn_layer_mesh_dict() -> Dictionary:
	var meshes := {}
	
	for layer in worldspawn_layer_dicts:
		var texture = layer.texture
		qodot.gather_worldspawn_layer_surfaces(texture, brush_clip_texture, face_skip_texture)
		var texture_surfaces := qodot.fetch_surfaces(inverse_scale_factor) as Array
		
		var mesh: Mesh = null
		if not texture in meshes:
			meshes[texture] = ArrayMesh.new()
		
		mesh = meshes[texture]
		mesh.add_surface_from_arrays(ArrayMesh.PRIMITIVE_TRIANGLES, texture_surfaces[0])
		mesh.surface_set_name(mesh.get_surface_count() - 1, texture)
		mesh.surface_set_material(mesh.get_surface_count() - 1, material_dict[texture])
	
	return meshes

## Build [MeshInstance3D]s from brush entities and add them to the add child queue
func build_entity_mesh_instances() -> Dictionary:
	var entity_mesh_instances := {}
	for entity_idx in entity_mesh_dict:
		var use_in_baked_light = false
		var shadow_casting_setting := GeometryInstance3D.SHADOW_CASTING_SETTING_DOUBLE_SIDED
		var render_layers: int = 1
		
		var entity_dict := entity_dicts[entity_idx] as Dictionary
		var properties = entity_dict['properties']
		var classname = properties['classname']
		if classname in entity_definitions:
			var entity_definition = entity_definitions[classname] as QodotFGDSolidClass
			if entity_definition:
				if not entity_definition.build_visuals:
					continue
				
				if entity_definition.use_in_baked_light:
					use_in_baked_light = true
				elif '_shadow' in properties:
					if properties['_shadow'] == "1":
						use_in_baked_light = true
				shadow_casting_setting = entity_definition.shadow_casting_setting
				render_layers = entity_definition.render_layers
		
		if not entity_mesh_dict[entity_idx]:
			continue
		
		var mesh_instance := MeshInstance3D.new()
		mesh_instance.name = 'entity_%s_mesh_instance' % entity_idx
		mesh_instance.gi_mode = MeshInstance3D.GI_MODE_STATIC if use_in_baked_light else GeometryInstance3D.GI_MODE_DISABLED
		mesh_instance.cast_shadow = shadow_casting_setting
		mesh_instance.layers = render_layers
		
		queue_add_child(entity_nodes[entity_idx], mesh_instance)
		
		entity_mesh_instances[entity_idx] = mesh_instance
	
	return entity_mesh_instances

func build_entity_occluder_instances() -> Dictionary:
	var entity_occluder_instances := {}
	for entity_idx in entity_mesh_dict:
		var entity_dict := entity_dicts[entity_idx] as Dictionary
		var properties = entity_dict['properties']
		var classname = properties['classname']
		if classname in entity_definitions:
			var entity_definition = entity_definitions[classname] as QodotFGDSolidClass
			if entity_definition:
				if entity_definition.build_occlusion:
					if not entity_mesh_dict[entity_idx]:
						continue
					
					var occluder_instance := OccluderInstance3D.new()
					occluder_instance.name = 'entity_%s_occluder_instance' % entity_idx
					
					queue_add_child(entity_nodes[entity_idx], occluder_instance)
					entity_occluder_instances[entity_idx] = occluder_instance
	
	return entity_occluder_instances

## Build Dictionary from worldspawn layers (via textures) to [MeshInstance3D]s
func build_worldspawn_layer_mesh_instances() -> Dictionary:
	var worldspawn_layer_mesh_instances := {}
	var idx = 0
	for i in range(0, worldspawn_layers.size()):
		var worldspawn_layer = worldspawn_layers[i]
		var texture_name = worldspawn_layer.texture
		
		if not texture_name in worldspawn_layer_mesh_dict:
			continue
		
		var mesh := worldspawn_layer_mesh_dict[texture_name] as Mesh
		
		if not mesh:
			continue
		
		var mesh_instance := MeshInstance3D.new()
		mesh_instance.name = 'entity_0_%s_mesh_instance' % worldspawn_layer.name
		mesh_instance.gi_mode = MeshInstance3D.GI_MODE_STATIC
		
		queue_add_child(worldspawn_layer_nodes[idx], mesh_instance)
		idx += 1
		
		worldspawn_layer_mesh_instances[texture_name] = mesh_instance
	
	return worldspawn_layer_mesh_instances

## Assign [ArrayMesh]es to their [MeshInstance3D] counterparts
func apply_entity_meshes() -> void:
	for entity_idx in entity_mesh_instances:
		var mesh := entity_mesh_dict[entity_idx] as Mesh
		var mesh_instance := entity_mesh_instances[entity_idx] as MeshInstance3D
		if not mesh or not mesh_instance:
			continue
		
		mesh_instance.set_mesh(mesh)
		queue_add_child(entity_nodes[entity_idx], mesh_instance)

func apply_entity_occluders() -> void:
	for entity_idx in entity_mesh_dict:
		var mesh := entity_mesh_dict[entity_idx] as Mesh
		var occluder_instance : OccluderInstance3D
		
		if entity_idx in entity_occluder_instances:
			occluder_instance = entity_occluder_instances[entity_idx]
		
		if not mesh or not occluder_instance:
			continue
		
		var verts: PackedVector3Array
		var indices: PackedInt32Array
		for surf_idx in range(mesh.get_surface_count()):
			var vert_count := verts.size()
			var surf_array := mesh.surface_get_arrays(surf_idx)
			verts.append_array(surf_array[Mesh.ARRAY_VERTEX])
			indices.resize(indices.size() + surf_array[Mesh.ARRAY_INDEX].size())
			for new_index in surf_array[Mesh.ARRAY_INDEX]:
				indices.append(new_index + vert_count)
		
		var occluder := ArrayOccluder3D.new()
		occluder.set_arrays(verts, indices)
		
		occluder_instance.occluder = occluder
		
## Assign [ArrayMesh]es to their [MeshInstance3D] counterparts for worldspawn layers
func apply_worldspawn_layer_meshes() -> void:
	for texture_name in worldspawn_layer_mesh_dict:
		var mesh = worldspawn_layer_mesh_dict[texture_name]
		var mesh_instance = worldspawn_layer_mesh_instances[texture_name]
		
		if not mesh or not mesh_instance:
			continue
		
		mesh_instance.set_mesh(mesh)

## Add a child and its new parent to the add child queue. If [code]below[/code] is a node, add it as a child to that instead. If [code]relative[/code] is true, set the location of node relative to parent.
func queue_add_child(parent, node, below = null, relative = false) -> void:
	add_child_array.append({"parent": parent, "node": node, "below": below, "relative": relative})

## Assign children to parents based on the contents of the add child queue (see [method queue_add_child])
func add_children() -> void:
	while true:
		for i in range(0, set_owner_batch_size):
			var data = add_child_array.pop_front()
			if data:
				add_child_editor(data['parent'], data['node'], data['below'])
				if data['relative']:
					data['node'].global_transform.origin -= data['parent'].global_transform.origin
			else:
				add_children_complete()
				return
		
		var scene_tree := get_tree()
		if scene_tree and not block_until_complete:
			await get_tree().create_timer(YIELD_DURATION).timeout

## Set owners and start post-attach build steps
func add_children_complete():
	stop_profile('add_children')
	
	if should_set_owners:
		start_profile('set_owners')
		set_owners()
	else:
		run_build_steps(true)

## Set owner of nodes generated by Qodot to scene root based on [member set_owner_array]
func set_owners():
	while true:
		for i in range(0, set_owner_batch_size):
			var node = set_owner_array.pop_front()
			if node:
				set_owner_editor(node)
			else:
				set_owners_complete()
				return
				
		var scene_tree := get_tree()
		if scene_tree and not block_until_complete:
			await get_tree().create_timer(YIELD_DURATION).timeout

## Finish profiling for set_owners and start post-attach build steps
func set_owners_complete():
	stop_profile('set_owners')
	run_build_steps(true)

## Apply Trenchbroom properties to [QodotEntity] instances, transferring Trenchbroom dictionaries to [QodotEntity.properties]
func apply_properties() -> void:
	for entity_idx in range(0, entity_nodes.size()):
		var entity_node = entity_nodes[entity_idx]
		if not entity_node:
			continue
		
		var entity_dict := entity_dicts[entity_idx] as Dictionary
		var properties := entity_dict['properties'] as Dictionary
		
		if 'classname' in properties:
			var classname = properties['classname']
			if classname in entity_definitions:
				var entity_definition := entity_definitions[classname] as QodotFGDClass
				
				for property in properties:
					var prop_string = properties[property]
					if property in entity_definition.class_properties:
						var prop_default = entity_definition.class_properties[property]
						if prop_default is int:
							properties[property] = prop_string.to_int()
						elif prop_default is float:
							properties[property] = prop_string.to_float()
						elif prop_default is Vector3:
							var prop_comps = prop_string.split_floats(" ")
							if prop_comps.size() > 2:
								properties[property] = Vector3(prop_comps[0], prop_comps[1], prop_comps[2])
							else:
								push_error("Invalid vector format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string)
								properties[property] = prop_default
						elif prop_default is Color:
							var prop_color = prop_default
							var prop_comps = prop_string.split(" ")
							if prop_comps.size() > 2:
								if "." in prop_comps[0] or "." in prop_comps[1] or "." in prop_comps[2]:
									prop_color.r = prop_comps[0].to_float()
									prop_color.g = prop_comps[1].to_float()
									prop_color.b = prop_comps[2].to_float()
								else:
									prop_color.r8 = prop_comps[0].to_int()
									prop_color.g8 = prop_comps[1].to_int()
									prop_color.b8 = prop_comps[2].to_int()
							else:
								push_error("Invalid color format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string)
								
							properties[property] = prop_color
						elif prop_default is Dictionary:
							properties[property] = prop_string.to_int()
						elif prop_default is Array:
							properties[property] = prop_string.to_int()
				
				# Assign properties not defined with defaults from the entity definition
				for property in entity_definitions[classname].class_properties:
					if not property in properties:
						var prop_default = entity_definition.class_properties[property]
						# Flags
						if prop_default is Array:
							var prop_flags_sum := 0
							for prop_flag in prop_default:
								if prop_flag is Array and prop_flag.size() > 2:
									if prop_flag[2] and prop_flag[1] is int:
										prop_flags_sum += prop_flag[1]
							properties[property] = prop_flags_sum
						# Choices
						elif prop_default is Dictionary:
							var prop_desc = entity_definition.class_property_descriptions[property]
							if prop_desc is Array and prop_desc.size() > 1 and prop_desc[1] is int:
								properties[property] = prop_desc[1]
							else:
								properties[property] = 0
						# Everything else
						else:
							properties[property] = prop_default
						
		if 'properties' in entity_node:
			entity_node.properties = properties

## Wire signals based on Trenchbroom [code]target[/code] and [code]targetname[/code] properties
func connect_signals() -> void:
	for entity_idx in range(0, entity_nodes.size()):
		var entity_node = entity_nodes[entity_idx]
		if not entity_node:
			continue
		
		var entity_dict := entity_dicts[entity_idx] as Dictionary
		var entity_properties := entity_dict['properties'] as Dictionary
		
		if not 'target' in entity_properties:
			continue
		
		var target_nodes := get_nodes_by_targetname(entity_properties['target'])
		for target_node in target_nodes:
			connect_signal(entity_node, target_node)

## Connect a signal on [code]entity_node[/code] to [code]target_node[/code], possibly mediated by the contents of a [code]signal[/code] or [code]receiver[/code] entity
func connect_signal(entity_node: Node, target_node: Node) -> void:
	if target_node.properties['classname'] == 'signal':
		var signal_name = target_node.properties['signal_name']
		
		var receiver_nodes := get_nodes_by_targetname(target_node.properties['target'])
		for receiver_node in receiver_nodes:
			if receiver_node.properties['classname'] != 'receiver':
				continue
			
			var receiver_name = receiver_node.properties['receiver_name']
			
			var target_nodes := get_nodes_by_targetname(receiver_node.properties['target'])
			for node in target_nodes:
				entity_node.connect(signal_name,Callable(node,receiver_name),CONNECT_PERSIST)
	else:
		var signal_list = entity_node.get_signal_list()
		for signal_dict in signal_list:
			if signal_dict['name'] == 'trigger':
				entity_node.connect("trigger",Callable(target_node,"use"),CONNECT_PERSIST)
				break

## Remove nodes marked transient. See [member QodotFGDClass.transient_node]
func remove_transient_nodes() -> void:
	for entity_idx in range(0, entity_nodes.size()):
		var entity_node = entity_nodes[entity_idx]
		if not entity_node:
			continue
		
		var entity_dict := entity_dicts[entity_idx] as Dictionary
		var entity_properties := entity_dict['properties'] as Dictionary
		
		if not 'classname' in entity_properties:
			continue
		
		var classname = entity_properties['classname']
		
		if not classname in entity_definitions:
			continue
		
		var entity_definition = entity_definitions[classname]
		if entity_definition.transient_node:
			entity_node.get_parent().remove_child(entity_node)
			entity_node.queue_free()

## Find all nodes with matching targetname property
func get_nodes_by_targetname(targetname: String) -> Array:
	var nodes := []
	
	for node_idx in range(0, entity_nodes.size()):
		var node = entity_nodes[node_idx]
		if not node:
			continue
		
		var entity_dict := entity_dicts[node_idx] as Dictionary
		var entity_properties := entity_dict['properties'] as Dictionary
		
		if not 'targetname' in entity_properties:
			continue
		
		if entity_properties['targetname'] == targetname:
			nodes.append(node)
	
	return nodes

# Cleanup after build is finished (internal)
func _build_complete():
	reset_build_context()
	
	stop_profile('build_map')
	if not print_profiling_data:
		print('\n')
	print('Build complete\n')
	
	emit_signal("build_complete")
