@tool
extends EditorPlugin

# === Constants ===
enum BuildMode {
	DISABLE,
	SELECT,
	ADD
}


const GRID_SCALE_1 = 0.01
const GRID_SCALE_2 = 0.1
const GRID_SCALE_3 = 0.25
const GRID_SCALE_4 = 0.5
const GRID_SCALE_5 = 0.75
const GRID_SCALE_6 = 1
const GRID_SCALE_7 = 2
const GRID_SCALE_8 = 5
const GRID_SCALE_9 = 10
const BASE_PREVIEW_THICKNESS = 0.05

# === Editor properties ===
var current_mode: BuildMode = BuildMode.SELECT
var toolbar: PanelContainer
var editor_viewport = EditorInterface.get_editor_viewport_3d()
var camera = editor_viewport.get_camera_3d()

# === Grid and CSG properties ===
var csg_root: CSGCombiner3D
var selected_grid: CubeGrid3D
var csg_mesh: MeshInstance3D = null

# === Rectangle drawing properties ===
var is_drawing: bool = false
var draw_normal: Vector3 = Vector3.UP
var draw_start: Vector3 = Vector3()
var draw_end: Vector3 = Vector3()
var draw_preview: MeshInstance3D = null
var draw_plane: Plane
var base_rect_points: Array = []

# === Extrusion properties ===
var is_extruding: bool = false
var has_started_extrusion: bool = false
var extrude_distance: float = 0.0
var initial_extrude_point: Vector3
var extrude_line_normal: Vector3

# === Highlight properties ===
var hover_preview: MeshInstance3D = null
var hover_point: Vector3 = Vector3.ZERO

# === Edge Movement properties ===
var edge_preview: MeshInstance3D = null
var current_edge: Array = []
var is_dragging_edge: bool = false
var dragged_mesh: CSGMesh3D = null
var drag_start_position: Vector3
var drag_plane: Plane
var drag_start_offset: Vector3
var is_mouse_in_viewport: bool = false

var undo_redo
var drag_start_vertices: PackedVector3Array
var drag_start_indices: PackedInt32Array

func _update_mesh_arrays(mesh_node: CSGMesh3D, vertices: PackedVector3Array, indices: PackedInt32Array) -> void:
	var arr_mesh = mesh_node.mesh as ArrayMesh
	if arr_mesh:
		var arrays = []
		arrays.resize(Mesh.ARRAY_MAX)
		arrays[Mesh.ARRAY_VERTEX] = vertices
		arrays[Mesh.ARRAY_INDEX] = indices
		arr_mesh.clear_surfaces()
		arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)

# === Lifecycle Methods ===
func _enter_tree() -> void:
	EditorInterface.get_selection().selection_changed.connect(_on_selection_changed)
	editor_viewport = EditorInterface.get_editor_viewport_3d()
	undo_redo = get_undo_redo()
	toolbar = preload("res://addons/boxconstructor/scripts/toolbar.gd").new(self) # Create the toolbar
	var viewport_base = editor_viewport.get_parent().get_parent()
	viewport_base.add_child(toolbar)
	toolbar.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP, 0, 10)

	toolbar.hide() # Hide toolbar by default
	_connect_toolbar_signals() # Connect signals


func _exit_tree() -> void:
	if toolbar:
		toolbar.queue_free() # Remove the toolbar

	if EditorInterface.get_selection().selection_changed.is_connected(_on_selection_changed):
		EditorInterface.get_selection().selection_changed.disconnect(_on_selection_changed) # Disconnect signals


# This section handles all of the inputs
func _input(event: InputEvent) -> void:
	if not selected_grid or not selected_grid.is_inside_tree():
		return

	# Pressing the X key will move the grid to mouse position
	if event is InputEventKey and event.pressed and event.keycode == KEY_X:
		if not camera or not selected_grid:
			return
		# Cast a ray and get the hit position
		var from = camera.project_ray_origin(editor_viewport.get_mouse_position())
		var to = from + camera.project_ray_normal(editor_viewport.get_mouse_position()) * 5000
		var ray_query = PhysicsRayQueryParameters3D.new()
		ray_query.from = from
		ray_query.to = to
		var hit = EditorInterface.get_edited_scene_root().get_world_3d().direct_space_state.intersect_ray(ray_query)
		if hit:
			var snapped_pos = _snap_to_grid(hit.position)
			# Move the Grid to the hit position
			_align_grid_to_surface(hit.normal, snapped_pos)

	# Pressing Z resets the grid to the 0,0,0 position
	if event is InputEventKey  and event.pressed and event.keycode == KEY_Z:
		_reset_grid_transform()
		
	# Edge movement logic
	if current_mode == BuildMode.SELECT:
		if event is InputEventMouseButton:
			if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
				if not is_dragging_edge:
					if edge_preview and edge_preview.visible:
						for child in csg_root.get_children():
							if child is CSGBox3D or child is CSGMesh3D:
								# Get all of the edges of CSGBox3D or CSGMesh3D
								var edges = _get_edges(child)
								for edge in edges:

									# Check if the currently hovered edge is the same as the one we are dragging
									if edge[0].is_equal_approx(current_edge[0]) and edge[1].is_equal_approx(current_edge[1]):
										var from = camera.project_ray_origin(event.position)
										var dir = camera.project_ray_normal(event.position)

										# Create a plane for the edge to drag along
										var edge_dir = (edge[1] - edge[0]).normalized()
										drag_plane = Plane(edge_dir, edge[0].dot(edge_dir))
										
										# Get the intersection point of the ray and the plane
										var intersection = drag_plane.intersects_ray(from, dir)
										if intersection:
											# We turn the CSGBox3D into a custom mesh that allows use to move the vertecies
											if child is CSGBox3D:
												dragged_mesh = _convert_box_to_CSGMesh(child)

											else:
												dragged_mesh = child

											var arr_mesh = dragged_mesh.mesh as ArrayMesh
											if arr_mesh:
												var arrays = arr_mesh.surface_get_arrays(0)
												drag_start_vertices = arrays[Mesh.ARRAY_VERTEX].duplicate()
												drag_start_indices = arrays[Mesh.ARRAY_INDEX].duplicate()
											is_dragging_edge = true # Set dragging to true
											current_edge = edge		# Set the current edge to the one we are dragging
											drag_start_offset = _snap_to_grid(intersection) # Starting position of edge drag
										break
				else:
					if is_dragging_edge and dragged_mesh:
						var arr_mesh = dragged_mesh.mesh as ArrayMesh
						if arr_mesh:
							var final_arrays = arr_mesh.surface_get_arrays(0)
							undo_redo.create_action("Move Edge")
							
							# Store the current state
							var current_mesh = arr_mesh.duplicate(true)
							
							# Create original mesh
							var original_mesh = ArrayMesh.new()
							var arrays = []
							arrays.resize(Mesh.ARRAY_MAX)
							arrays[Mesh.ARRAY_VERTEX] = drag_start_vertices
							arrays[Mesh.ARRAY_INDEX] = drag_start_indices
							original_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
							
							undo_redo.add_do_property(dragged_mesh, "mesh", current_mesh)
							undo_redo.add_undo_property(dragged_mesh, "mesh", original_mesh)
							undo_redo.commit_action()
						
						is_dragging_edge = false
						dragged_mesh = null
						edge_preview.hide()
					
		if event is InputEventMouseMotion:
			if is_dragging_edge and dragged_mesh:
				edge_preview.hide()
				var from = camera.project_ray_origin(event.position)
				var dir = camera.project_ray_normal(event.position)
				
				# Project mouse position onto drag plane
				var intersection = drag_plane.intersects_ray(from, dir)
				if intersection:
					# Snapped position on the grid
					var snapped_pos = _snap_to_grid(intersection)
					# Offset from the start position
					var offset = snapped_pos - drag_start_offset
					
					var arr_mesh = dragged_mesh.mesh as ArrayMesh
					if arr_mesh:
						var arrays = arr_mesh.surface_get_arrays(0)
						var vertices = arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array

						# Get the edge points in local space
						var local_edge = [
							dragged_mesh.to_local(current_edge[0]),
							dragged_mesh.to_local(current_edge[1])
						]
						# Calculate the new offset
						var local_offset = dragged_mesh.global_transform.basis.inverse() * offset
						var new_vertices = PackedVector3Array()
						new_vertices.resize(vertices.size())
						
						# Move vertices that match the edge points
						for i in range(vertices.size()):
							var vertex = vertices[i]
							if vertex.is_equal_approx(local_edge[0]) or vertex.is_equal_approx(local_edge[1]):
								new_vertices[i] = vertex + local_offset
							else:
								new_vertices[i] = vertex
						
						# Update the mesh
						var new_arrays = []
						new_arrays.resize(Mesh.ARRAY_MAX)
						new_arrays[Mesh.ARRAY_VERTEX] = new_vertices
						new_arrays[Mesh.ARRAY_INDEX] = arrays[Mesh.ARRAY_INDEX]
						
						arr_mesh.clear_surfaces()
						arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, new_arrays)
					
						current_edge = [
							dragged_mesh.global_transform * (local_edge[0] + local_offset),
							dragged_mesh.global_transform * (local_edge[1] + local_offset)
						]
						drag_start_offset = snapped_pos
			
			# Highlight the closest edge
			elif not is_dragging_edge:
				if not camera or not csg_root:
					if edge_preview:
						edge_preview.hide()
					return
				
				# Get the closest node 
				var mouse_pos = editor_viewport.get_mouse_position()
				var closest_node = null
				var closest_distance = INF
				
				for child in csg_root.get_children():
					if not (child is CSGBox3D or child is CSGMesh3D):
						continue
					
					var node_center = child.global_position
					var screen_pos = camera.unproject_position(node_center) # Takes the position in 3D converts it to 2D
					var distance = screen_pos.distance_to(mouse_pos)
					
					if distance < closest_distance:
						closest_node = child
						closest_distance = distance
				
				if closest_distance:
					var closest_edge = _find_closest_edge(closest_node, mouse_pos)
					current_edge = closest_edge
					_create_edge_preview(closest_edge)
				else:
					current_edge = []
					if edge_preview:
						edge_preview.hide()
				
	# Handle Rectangle Drawing and Extrusion Logic
	if current_mode == BuildMode.ADD:
		if event is InputEventMouseButton:

			# If clicked inside toolbar ignore
			if toolbar and toolbar.get_global_rect().has_point(event.position):
				return

			if event.button_index == MOUSE_BUTTON_LEFT:
				if event.pressed:
					if not camera: return
					
					# Extruson end
					if is_extruding and has_started_extrusion:
						# Create the box when clicking during extrusion
						_create_CSGBox3D()
						is_drawing = false
						is_extruding = false 
						has_started_extrusion = false
						if draw_preview:
							draw_preview.queue_free()
							draw_preview = null
						return
					
					# Start dragging the base rectangle
					var ray_query = PhysicsRayQueryParameters3D.new()
					ray_query.from = camera.project_ray_origin(editor_viewport.get_mouse_position())
					ray_query.to = ray_query.from + camera.project_ray_normal(editor_viewport.get_mouse_position()) * 1000
					
					var hit = EditorInterface.get_editor_viewport_3d(0).find_world_3d().direct_space_state.intersect_ray(ray_query)
					if hit:
						is_drawing = true
						draw_normal = hit.normal
						draw_start = _snap_to_grid(hit.position)
						draw_end = draw_start
						draw_plane = Plane(draw_normal, hit.position.dot(draw_normal))
						create_rectangle_preview()
						
				else:
					# End dragging and start extrusion if we were drawing
					if is_drawing and not is_extruding:
						is_extruding = true
						has_started_extrusion = false
						extrude_distance = 0.0
						
						var from = camera.project_ray_origin(editor_viewport.get_mouse_position())
						var dir = camera.project_ray_normal(editor_viewport.get_mouse_position())
						var intersection = draw_plane.intersects_ray(from, dir)
						
						if intersection:
							initial_extrude_point = _snap_to_grid(intersection)
							extrude_line_normal = draw_normal
							_update_rectangle_preview()

		elif event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
			is_drawing = false
			is_extruding = false
			has_started_extrusion = false
			if draw_preview:
				draw_preview.queue_free()
				draw_preview = null
				
			get_viewport().set_input_as_handled()

		# Update section		
		elif event is InputEventMouseMotion:
			if current_mode == BuildMode.ADD:
				if not is_drawing:
					if not camera: return
					
					var ray_query = PhysicsRayQueryParameters3D.new()
					ray_query.from = camera.project_ray_origin(editor_viewport.get_mouse_position())
					ray_query.to = ray_query.from + camera.project_ray_normal(editor_viewport.get_mouse_position()) * 1000
					ray_query.collide_with_bodies = true
					
					var hit = EditorInterface.get_editor_viewport_3d(0).find_world_3d().direct_space_state.intersect_ray(ray_query)
					if hit:
						hover_point = _snap_to_grid(hit.position)
						if not hover_preview:
							_create_hover_preview()
						_update_hover_preview()
					elif hover_preview:
						hover_preview.queue_free()

			if is_drawing and not is_extruding:
				if not camera: return
				
				var from = camera.project_ray_origin(editor_viewport.get_mouse_position())
				var dir = camera.project_ray_normal(editor_viewport.get_mouse_position())
				
				var intersection = draw_plane.intersects_ray(from, dir)
				if intersection:
					draw_end = _snap_to_grid(intersection)
					_calculate_base_rect_points()
					_update_rectangle_preview()
			
			elif is_extruding:
				if not camera: return
				
				var from = camera.project_ray_origin(editor_viewport.get_mouse_position())
				var dir = camera.project_ray_normal(editor_viewport.get_mouse_position())
				
				if not has_started_extrusion:
					if event.relative.length() > 0.01:
						has_started_extrusion = true
				
				if has_started_extrusion:
					var grid_unit = selected_grid.grid_scale
					var e_line1 = initial_extrude_point - extrude_line_normal * 5000
					var e_line2 = initial_extrude_point + extrude_line_normal * 5000
					var m_line1 = from 
					var m_line2 = from + dir * 5000
					var closest_point = Geometry3D.get_closest_points_between_segments(e_line1, e_line2, m_line1, m_line2)
					var mouse_on_exturde_line = closest_point[0]
					var distance_vec =  mouse_on_exturde_line - initial_extrude_point
					var distance = distance_vec.length() * distance_vec.normalized().dot(extrude_line_normal)
				
					var new_distance = round(distance / grid_unit) * grid_unit
					#print(new_distance)
					if not is_equal_approx(extrude_distance, new_distance):
						extrude_distance = new_distance
						_update_rectangle_preview()


# === Grid Methods ===
# Changes the grid size based on the input from the toolbar
func _on_grid_size_changed(size: float) -> void:
	if selected_grid and selected_grid.grid_material:
		selected_grid.grid_scale = size
		selected_grid.grid_material.set_shader_parameter("grid_scale", size)

		# Destroy the hover preview so it gets updated
		if hover_preview:
			hover_preview.queue_free()
			hover_preview = null


# Snaps the position to the grid size
func _snap_to_grid(pos: Vector3) -> Vector3:
	if not selected_grid:
		return pos

	# Get the grid size of the selected grid
	var grid_unit = selected_grid.grid_scale
	
	# Divide the coordinates of the given position by the grid scale, to get how far it is from the origin
	# and round it to the nearest integer
	return Vector3(
		round(pos.x / grid_unit) * grid_unit,
		round(pos.y / grid_unit) * grid_unit,
		round(pos.z / grid_unit) * grid_unit
	)


func _align_grid_to_surface(normal: Vector3, hit_position: Vector3) -> void:
	if not selected_grid:
		return

	var mesh = selected_grid.get_node_or_null("CubeGridMesh3D")
	var collision = selected_grid.get_node_or_null("CubeGridCollisionShape3D")

	var mesh_size = mesh.scale
	var collision_size = collision.scale

	if not mesh or not collision:
		return

	# Normalize the normal vector
	normal = normal.normalized()

	var up = normal
	var right = up.cross(Vector3.FORWARD).normalized()
	if right.length() < 0.1:  
		right = up.cross(Vector3.RIGHT).normalized()
	var forward = right.cross(up)

	# Create the Basis and Transform3D
	var basis = Basis(right, up, forward)
	var transform = Transform3D(basis, hit_position + up * 0.01)

	# Apply the transform
	mesh.transform = transform
	collision.transform = transform

	# Restore correct scale
	mesh.scale = mesh_size
	collision.scale = collision_size

	# Update the material
	selected_grid._update_material()


func _reset_grid_transform() -> void:
	if not selected_grid:
		return
		
	var mesh = selected_grid.get_node_or_null("CubeGridMesh3D")
	var collision = selected_grid.get_node_or_null("CubeGridCollisionShape3D")
	
	if not mesh or not collision:
		return

	# Get the current mesh and collision scale	
	var mesh_scale = mesh.scale
	var collision_scale = collision.scale
	
	# Reset the transform
	mesh.transform = Transform3D()
	collision.transform = Transform3D()
	
	# Restore the scale
	mesh.scale = mesh_scale
	collision.scale = collision_scale
	
	# Update the shader
	selected_grid._update_material()


# === Drawing Methods ===
# Creates a MeshInstance3D shpere at the current hovered location when in ADD mode
func _create_hover_preview() -> void:
	# Clear the existing preview
	if hover_preview:
		hover_preview.queue_free()

	# Create new hover preview 
	hover_preview = MeshInstance3D.new()
	var sphere = SphereMesh.new()
	var scale = selected_grid.grid_scale * BASE_PREVIEW_THICKNESS
	sphere.radius = scale
	sphere.height = scale * 2
	hover_preview.mesh = sphere

	# Create the material for the hover preview
	var material = StandardMaterial3D.new()
	material.albedo_color = Color.RED
	material.no_depth_test = true # Always visible Renders ontop of other objects
	hover_preview.material_override = material

	# Add it to the scene
	if csg_root:
		csg_root.add_child(hover_preview)
		hover_preview.position = hover_point
	# Do not add owner 
		
	
# Changes the position of the hover preview
func _update_hover_preview() -> void:
	if not hover_preview:
		return
	hover_preview.global_position = hover_point


func _calculate_base_rect_points() -> void:
	if not selected_grid:
		return
		
	var grid_unit = selected_grid.grid_scale

	# Calculate the base rectangle points
	var min_x = floor(min(draw_start.x, draw_end.x) / grid_unit) * grid_unit
	var max_x = ceil(max(draw_start.x, draw_end.x) / grid_unit) * grid_unit
	var min_y = floor(min(draw_start.y, draw_end.y) / grid_unit) * grid_unit
	var max_y = ceil(max(draw_start.y, draw_end.y) / grid_unit) * grid_unit
	var min_z = floor(min(draw_start.z, draw_end.z) / grid_unit) * grid_unit
	var max_z = ceil(max(draw_start.z, draw_end.z) / grid_unit) * grid_unit
	
	if draw_normal.abs().is_equal_approx(Vector3.UP) or draw_normal.abs().is_equal_approx(Vector3.DOWN):
		base_rect_points = [
			Vector3(min_x, draw_start.y, min_z),
			Vector3(max_x, draw_start.y, min_z),
			Vector3(max_x, draw_start.y, max_z),
			Vector3(min_x, draw_start.y, max_z)
		]
	elif draw_normal.abs().is_equal_approx(Vector3.RIGHT) or draw_normal.abs().is_equal_approx(Vector3.LEFT):
		base_rect_points = [
			Vector3(draw_start.x, min_y, min_z),
			Vector3(draw_start.x, max_y, min_z),
			Vector3(draw_start.x, max_y, max_z),
			Vector3(draw_start.x, min_y, max_z)
		]
	else:
		base_rect_points = [
			Vector3(min_x, min_y, draw_start.z),
			Vector3(max_x, min_y, draw_start.z),
			Vector3(max_x, max_y, draw_start.z),
			Vector3(min_x, max_y, draw_start.z)
		]


func create_rectangle_preview() -> void:
	# Clear the previous preview
	if draw_preview:
		draw_preview.queue_free()
	
	# Create a new preview 
	draw_preview = MeshInstance3D.new()
	var immediate_mesh = ImmediateMesh.new()
	draw_preview.mesh = immediate_mesh
	
	var material = StandardMaterial3D.new()
	material.albedo_color = Color.RED
	material.cull_mode = BaseMaterial3D.CULL_DISABLED
	material.no_depth_test = true
	draw_preview.material_override = material
	
	if csg_root:
		csg_root.add_child(draw_preview)
		draw_preview.owner = EditorInterface.get_edited_scene_root()


func _update_rectangle_preview() -> void:
	if not draw_preview: return
	var immediate_mesh = draw_preview.mesh as ImmediateMesh
	immediate_mesh.clear_surfaces()
	
	var base_thickness = BASE_PREVIEW_THICKNESS * selected_grid.grid_scale
	var thickness = base_thickness
	var grid_unit = selected_grid.grid_scale
	var material = draw_preview.material_override as StandardMaterial3D

	if is_extruding:
		material.albedo_color = Color.GREEN if extrude_distance >= 0 else Color.RED
	var preview_offset = draw_normal * (grid_unit * 0.000001)
	immediate_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)
	
	var preview_points = []
	for point in base_rect_points:
		preview_points.append(point + preview_offset)

	# Rectangle base lines
	for i in range(preview_points.size()):
		add_thick_line(
			immediate_mesh,
			preview_points[i],
			preview_points[(i + 1) % preview_points.size()],
			thickness
		)
	# Extrusion lines
	if is_extruding:
		add_thick_line(immediate_mesh, 
			initial_extrude_point + preview_offset,
			initial_extrude_point + draw_normal * (extrude_distance + preview_offset.length()),
			thickness * 0.5)
		
		if has_started_extrusion:
			var extrude_offset = draw_normal * extrude_distance
			
			for i in range(preview_points.size()):
				var extruded_point = preview_points[i] + extrude_offset
				add_thick_line(immediate_mesh, 
					preview_points[i], 
					extruded_point, 
					thickness)
				add_thick_line(
					immediate_mesh,
					extruded_point,
					preview_points[(i + 1) % preview_points.size()] + extrude_offset,
					thickness
				)
	
	immediate_mesh.surface_end()


func add_thick_line(immediate_mesh: ImmediateMesh, start: Vector3, end: Vector3, thickness: float) -> void:
	var direction = (end - start).normalized()

	# Find a perpendicular vector to the line
	var perpendicular = Vector3.UP.cross(direction).normalized()
	if perpendicular.length() < 0.1:
		perpendicular = Vector3.RIGHT.cross(direction).normalized()

	# Calculate the four corners of the line
	var offset = perpendicular * thickness
	var v1 = start + offset
	var v2 = start - offset
	var v3 = end + offset
	var v4 = end - offset

	# Add the two faces to the line
	create_rectangle(immediate_mesh, v1, v2, v3, v4)


# Creates a rectangle using the given vertecies out of two triangles
func create_rectangle(immediate_mesh: ImmediateMesh, v1: Vector3, v2: Vector3, v3: Vector3, v4: Vector3) -> void:
	# Add two triangles to form a rectangle
	immediate_mesh.surface_add_vertex(v1)
	immediate_mesh.surface_add_vertex(v2)
	immediate_mesh.surface_add_vertex(v3)

	immediate_mesh.surface_add_vertex(v2)
	immediate_mesh.surface_add_vertex(v4)
	immediate_mesh.surface_add_vertex(v3)


# === CSG Management Methods ===
func _create_CSGBox3D() -> void:
	var new_box = CSGBox3D.new()
	new_box.use_collision = true
	new_box.set_meta("_edit_lock_", true)
	new_box.set_meta("_edit_group_", true)
	

	var min_point = base_rect_points[0]
	var max_point = base_rect_points[0]
	
	# Minimum and maximum points of the base rectangle
	for point in base_rect_points:
		min_point = Vector3(
			min(min_point.x, point.x),
			min(min_point.y, point.y),
			min(min_point.z, point.z)
		)
		max_point = Vector3(
			max(max_point.x, point.x),
			max(max_point.y, point.y),
			max(max_point.z, point.z)
		)

	# Initial size and center of the box
	var size = (max_point - min_point)
	var center = (max_point + min_point) * 0.5
	
	# Adjust size and center based on the extrusion
	if draw_normal.abs().is_equal_approx(Vector3.UP) or draw_normal.abs().is_equal_approx(Vector3.DOWN):
		size.y = abs(extrude_distance)
		center += draw_normal * (extrude_distance * 0.5)
	elif draw_normal.abs().is_equal_approx(Vector3.RIGHT) or draw_normal.abs().is_equal_approx(Vector3.LEFT):
		size.x = abs(extrude_distance)
		center += draw_normal * (extrude_distance * 0.5)
	else:
		size.z = abs(extrude_distance)
		center += draw_normal * (extrude_distance * 0.5)

	if size.x < 0.0001 or size.y < 0.0001 or size.z < 0.0001:
		return

	new_box.size = size
	new_box.position = center

	# Depending on the extrusion distance set the operation
	if extrude_distance < 0:
		new_box.operation = CSGShape3D.OPERATION_SUBTRACTION

	undo_redo.create_action("Create CSGBox3D")
	undo_redo.add_do_method(csg_root, "add_child", new_box)
	undo_redo.add_do_method(new_box, "set_owner", EditorInterface.get_edited_scene_root())
	undo_redo.add_undo_method(csg_root, "remove_child", new_box)
	undo_redo.commit_action()
	_update_toolbar_states()
	

func _on_merge_mesh() -> void:
	if not csg_root or csg_root.get_child_count() == 0:
		return
	# Dont allow to merge an already merged mesh
	if csg_root.has_node("CSGMesh"):
		push_warning("Already merged!")
		return
	
	if edge_preview:
		edge_preview.queue_free()
		edge_preview = null

	current_edge = []
	is_dragging_edge = false

	var nodes_to_keep = []
	# Go over all of the children of the csg root and check if they are CSGBox3D or CSGMesh3D
	for node in csg_root.get_children():
		if node is MeshInstance3D:
			continue
		
		# For subtraction operations, check if it actually cuts something
		if node.operation == CSGShape3D.OPERATION_SUBTRACTION:
			var cuts_something = false
			for other_node in csg_root.get_children():
				if other_node.operation == CSGShape3D.OPERATION_UNION:
					if node is CSGBox3D and other_node is CSGBox3D:
						var node_bounds = AABB(
							node.position - (node.size * 0.5),
							node.size
						)
						var other_bounds = AABB(
							other_node.position - (other_node.size * 0.5),
							other_node.size
						)
						# Check if the bounding boxes intersect if does keep it
						if node_bounds.intersects(other_bounds):
							cuts_something = true
							break
					else:
						cuts_something = false
						break
			# Only keep if it actually cuts something
			if cuts_something:
				nodes_to_keep.append(node)
		else:
			nodes_to_keep.append(node)

	var nodes_data = []
	for node in nodes_to_keep:
		nodes_data.append(_store_mesh_data(node))
	
	var meshes = csg_root.get_meshes()
	if meshes.size() > 1:
		if not csg_mesh:
			csg_mesh = MeshInstance3D.new()
			csg_mesh.name = "CSGMesh"
			csg_root.add_child(csg_mesh)
			csg_mesh.owner = EditorInterface.get_edited_scene_root()

		csg_mesh.mesh = meshes[1]
		csg_mesh.set_meta("csg_data", {
			"nodes": nodes_data
		})
		
		for child in csg_root.get_children():
			if child != csg_mesh:
				child.queue_free()
				
		_update_toolbar_states()
		_change_mode(BuildMode.DISABLE)


func _on_edit_mesh() -> void:
	if not csg_root:
		push_warning("No Mesh root found!")
		return
		
	if not csg_root.has_node("CSGMesh"):
		push_warning("No CSGMesh to edit!")
		return

	csg_mesh = csg_root.get_node("CSGMesh")
	var data = csg_mesh.get_meta("csg_data")
	if not data:
		push_warning("No CSG data found in mesh!")
		return
	# Deconstruct the mesh into CSGBox3D or CSGMesh3D
	_convert_to_boxes()

# Stores the information about the CSGBox3D or CSGMesh3D
func _store_mesh_data(node: Node) -> Dictionary:
	# Create a dictionary to store information
	var data = {
		"position": node.position,
		"operation": node.operation,
		"use_collision": node.use_collision,
		"type": "box" if node is CSGBox3D else "mesh"
	}
	# Store the size of the CSGBox3D or the vertices and indices of the CSGMesh3D
	if node is CSGBox3D:
		data["size"] = node.size
	# Store vertices and indices of the CSGMesh3D
	elif node is CSGMesh3D:
		var mesh = node.mesh as ArrayMesh
		if mesh:
			data["vertices"] = mesh.surface_get_arrays(0)[Mesh.ARRAY_VERTEX]
			data["indices"] = mesh.surface_get_arrays(0)[Mesh.ARRAY_INDEX]
	
	return data


# Recreates the CSGBox3D or CSGMesh3D from the stored metadata
func _convert_to_boxes() -> void:

	csg_mesh = csg_root.get_node("CSGMesh")
	if not csg_mesh:
		push_warning("No CSGMesh node found!")
		return
	var data = csg_mesh.get_meta("csg_data")
	if not data:
		push_warning("No CSG data found in mesh!")
		return
	
	# Go through all of the nodes and recreate them
	for node_info in data["nodes"]:
		var new_node
		
		# Based on the type, recreate CSGBox3D or CSGMesh3D
		if node_info["type"] == "box":
			new_node = CSGBox3D.new()		  # Create a new CSGBox3D
			new_node.size = node_info["size"] # Set the size of the box by getting the size from the metadata
		else:
			new_node = CSGMesh3D.new() 		  # Create a new CSGMesh3D
			var arr_mesh = ArrayMesh.new()	  # Create a new ArrayMesh
			var arrays = []
			arrays.resize(Mesh.ARRAY_MAX)	 
			
			arrays[Mesh.ARRAY_VERTEX] = node_info["vertices"]
			arrays[Mesh.ARRAY_INDEX] = node_info["indices"]
			arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) # Add the surface to the ArrayMesh
			new_node.mesh = arr_mesh

		new_node.position = node_info["position"] # Set the position
		new_node.operation = node_info["operation"]
		new_node.use_collision = node_info["use_collision"]
		
		new_node.set_meta("_edit_lock_", true)
		new_node.set_meta("_edit_group_", true)
		
		csg_root.add_child(new_node)
		new_node.owner = EditorInterface.get_edited_scene_root()

	# Remove the CSGMesh
	csg_mesh.queue_free()
	csg_mesh = null

	toolbar.update_button_states(false)
	_change_mode(BuildMode.DISABLE)


# === UI Management Methods ===
func _connect_toolbar_signals() -> void:
	toolbar.select_button_pressed.connect(func(): _change_mode(BuildMode.SELECT))
	toolbar.add_button_pressed.connect(func(): _change_mode(BuildMode.ADD))
	toolbar.disable_button_pressed.connect(func(): _change_mode(BuildMode.DISABLE))
	toolbar.grid_size_changed.connect(_on_grid_size_changed)
	toolbar.reset_grid_pressed.connect(_reset_grid_transform)
	toolbar.merge_mesh.connect(_on_merge_mesh)
	toolbar.edit_mesh.connect(_on_edit_mesh)


func _update_toolbar_states() -> void:
	if not csg_root:
		return
		
	var has_csg_mesh = csg_root.has_node("CSGMesh")
	var has_csg_boxes = false
	
	for child in csg_root.get_children():
		if child is CSGBox3D or child is CSGMesh3D:
			has_csg_boxes = true
			break
			
	if has_csg_mesh:
		toolbar.update_button_states(true)
	else:
		toolbar.update_button_states(false)
	
	toolbar.set_merge_button_enabled(has_csg_boxes)
	toolbar.set_select_button_enabled(has_csg_boxes)
	toolbar.set_edit_button_enabled(has_csg_mesh)


func _on_selection_changed() -> void:
	var selected = EditorInterface.get_selection().get_selected_nodes()
	if selected.size() == 1 and selected[0] is CubeGrid3D:
		selected_grid = selected[0]
		csg_root = selected_grid.get_node("CSGCombiner3D")
		toolbar.show()
		toolbar.connect_to_grid(selected_grid)
		_update_toolbar_states()

		var root := EditorInterface.get_base_control()
		var toolbar = root.find_children("", "Node3DEditor", true, false)[0].get_child(0).find_children("", "HBoxContainer", true, false)[0]
		var btn = toolbar.get_child(2)
		btn.pressed.emit()
		if hover_preview:
			hover_preview.queue_free()
			hover_preview = null
	else:
		_change_mode(BuildMode.DISABLE)
		if hover_preview:
			hover_preview.queue_free()
			hover_preview = null
		selected_grid = null
		csg_root = null
		toolbar.hide()


func _change_mode(new_mode: BuildMode) -> void:
	if new_mode == BuildMode.ADD and csg_root and csg_root.has_node("CSGMesh"):
		push_warning("Can't switch to ADD mode while CSGMesh exists. Use Edit to modify.")
		toolbar.set_active_mode(current_mode)
		return

	current_mode = new_mode
	toolbar.set_active_mode(current_mode)

	if edge_preview:
		edge_preview.queue_free()
		edge_preview = null
	current_edge = []
	is_dragging_edge = false

	if csg_root:
		csg_root.set_meta("_edit_lock_", current_mode != BuildMode.SELECT)


# === Edge Movement Methods ===
func _get_edges(node: Node) -> Array:
	var edges = []
	
	if node is CSGBox3D:
		var aabb = AABB(
			node.global_position - (node.size * 0.5),
			node.size
		)
		# Corners of the CSGBox3D
		var corners = [
			Vector3(aabb.position.x, aabb.position.y, aabb.position.z),
			Vector3(aabb.end.x, aabb.position.y, aabb.position.z),
			Vector3(aabb.end.x, aabb.end.y, aabb.position.z),
			Vector3(aabb.position.x, aabb.end.y, aabb.position.z),
			Vector3(aabb.position.x, aabb.position.y, aabb.end.z),
			Vector3(aabb.end.x, aabb.position.y, aabb.end.z),
			Vector3(aabb.end.x, aabb.end.y, aabb.end.z),
			Vector3(aabb.position.x, aabb.end.y, aabb.end.z)
		]
		# Edges of the CSGBox3D
		var edge_indices = [
			[0, 1], [1, 2], [2, 3], [3, 0],
			[4, 5], [5, 6], [6, 7], [7, 4],
			[0, 4], [1, 5], [2, 6], [3, 7]
		]
		# Create edges by taking pairs of corners
		for pair in edge_indices:
			edges.append([corners[pair[0]], corners[pair[1]]])
	# CSGMesh3D get edges by getting the verticies and indicies		
	elif node is CSGMesh3D:
		var arr_mesh = node.mesh as ArrayMesh
		if not arr_mesh:
			return edges
		
		# Arraymesh
		var arrays = arr_mesh.surface_get_arrays(0)
		# Get the vertices and indices
		var vertices = arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
		var indices = arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
		# Create a set to store edges
		var edge_set = {}
		
		# Go through the indicies and create an edgemap
		for i in range(0, indices.size(), 3):
			var tri_indices = [
				indices[i],
				indices[i + 1],
				indices[i + 2]
			]
			
			for j in range(3):
				var idx1 = tri_indices[j]
				var idx2 = tri_indices[(j + 1) % 3] # Get the next 2 indicies
				
				var edge_key = str(min(idx1, idx2)) + "_" + str(max(idx1, idx2))
				if not edge_set.has(edge_key):
					edge_set[edge_key] = true
					var vert1 = node.global_transform * vertices[idx1]
					var vert2 = node.global_transform * vertices[idx2]
					edges.append([vert1, vert2])
	
	return edges


# Finds the closest edge to the mouse
func _find_closest_edge(node: Node, mouse_pos: Vector2) -> Array:
	if not camera:
		return []

	# Get all of the edges
	var edges = _get_edges(node)
	# Intialize closest edge
	var closest_edge = []
	# Set the distance to infinity
	var min_distance = INF
	
	# Cast ray from the camera to the mouse position
	var from = camera.project_ray_origin(mouse_pos)
	var dir = camera.project_ray_normal(mouse_pos)
	var m_line1 = from
	var m_line2 = from + dir * 5000 

	# Go over all of the edges
	for edge in edges:
		# Take the two first endpoints of the edge
		var e_line1 = edge[0]
		var e_line2 = edge[1]

		# Get the closest points between the edge and the ray
		var closest_points = Geometry3D.get_closest_points_between_segments(e_line1, e_line2, m_line1, m_line2)
		# Return the closest edge
		var point_on_edge = closest_points[0]
		var point_on_ray = closest_points[1]

		# Calculate the distance between the two points
		var distance_vec = point_on_ray - point_on_edge
		var distance = distance_vec.length()

		# If the distance is smaller than the current minimum, update the closest edge
		if distance < min_distance:
			min_distance = distance
			closest_edge = edge

	return closest_edge


# Method that draws a line on the currently hovered edge
func _create_edge_preview(edge: Array) -> void:

	if edge.is_empty():
		if edge_preview:
			edge_preview.hide()
		return

	# Create the edge preview material
	if not edge_preview:
		edge_preview = MeshInstance3D.new()
		var immediate_mesh = ImmediateMesh.new()
		edge_preview.mesh = immediate_mesh
		
		var material = StandardMaterial3D.new()
		material.albedo_color = Color.RED
		material.cull_mode = BaseMaterial3D.CULL_DISABLED
		material.no_depth_test = true
		edge_preview.material_override = material
		
		if csg_root:
			csg_root.add_child(edge_preview)
	
	edge_preview.show()
	var immediate_mesh = edge_preview.mesh as ImmediateMesh
	immediate_mesh.clear_surfaces()
	
	immediate_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)
	var thickness = selected_grid.grid_scale * BASE_PREVIEW_THICKNESS # Scale the thickness based on grid size
	add_thick_line(immediate_mesh, edge[0], edge[1], thickness) # Use the add_thick_line methdod to create the line
	immediate_mesh.surface_end()


func _convert_box_to_CSGMesh(box: CSGBox3D) -> CSGMesh3D:
	var csg_mesh = CSGMesh3D.new()
	var arr_mesh = ArrayMesh.new()
	var vertices = PackedVector3Array()
	var indices = PackedInt32Array()
	var half_size = box.size * 0.5
	
	var local_verts = [
		Vector3(-half_size.x, -half_size.y, -half_size.z),  # 0
		Vector3(half_size.x, -half_size.y, -half_size.z),   # 1
		Vector3(half_size.x, half_size.y, -half_size.z),    # 2
		Vector3(-half_size.x, half_size.y, -half_size.z),   # 3
		Vector3(-half_size.x, -half_size.y, half_size.z),   # 4
		Vector3(half_size.x, -half_size.y, half_size.z),    # 5
		Vector3(half_size.x, half_size.y, half_size.z),     # 6
		Vector3(-half_size.x, half_size.y, half_size.z)     # 7
	]
	
	vertices.append_array(local_verts)
	
	var faces = [
		[0, 1, 2, 2, 3, 0],  # Front
		[1, 5, 6, 6, 2, 1],  # Right
		[5, 4, 7, 7, 6, 5],  # Back
		[4, 0, 3, 3, 7, 4],  # Left
		[3, 2, 6, 6, 7, 3],  # Top
		[4, 5, 1, 1, 0, 4]   # Bottom
	]
	
	for face in faces:
		indices.append_array(face)
	
	var arrays = []
	arrays.resize(Mesh.ARRAY_MAX)
	arrays[Mesh.ARRAY_VERTEX] = vertices
	arrays[Mesh.ARRAY_INDEX] = indices
	
	arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
	
	csg_mesh.mesh = arr_mesh
	csg_mesh.transform = box.transform
	csg_mesh.operation = box.operation
	csg_mesh.use_collision = box.use_collision
	
	csg_root.add_child(csg_mesh)
	csg_mesh.owner = EditorInterface.get_edited_scene_root()
	
	box.queue_free()
	
	return csg_mesh
