# Implementing Gizmo
const ProtoGizmoWrapper = preload("res://addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd")
const ProtoGizmoUtils = preload("res://addons/proto_shape/proto_gizmo/proto_gizmo_utils.gd")
const ProtoRamp = preload("res://addons/proto_shape/proto_ramp/proto_ramp.gd")
const ProtoGizmoPlugin = preload("res://addons/proto_shape/proto_gizmo/proto_gizmo.gd")
var width_gizmo_id: int
var depth_gizmo_id: int
var height_gizmo_id: int
var fill_gizmo_id1: int
var fill_gizmo_id2: int
var gizmo_utils := ProtoGizmoUtils.new()
var ramp: ProtoRamp = null

var undo_redo: EditorUndoRedoManager
var is_editing := false

func attach_ramp(node: ProtoRamp) -> void:
	ramp = node
	if ramp.get_parent() is ProtoGizmoWrapper:
		var parent: ProtoGizmoWrapper = ramp.get_parent()
		parent.redraw_gizmos_for_child_signal.connect(redraw_gizmos)
		parent.set_handle_for_child_signal.connect(set_handle)
		parent.commit_handle.connect(commit_handle)

func remove_ramp() -> void:
	if ramp.get_parent() is ProtoGizmoWrapper:
		var parent: ProtoGizmoWrapper = ramp.get_parent()
		parent.redraw_gizmos_for_child_signal.disconnect(redraw_gizmos)
		parent.set_handle_for_child_signal.disconnect(set_handle)
		parent.commit_handle.disconnect(commit_handle)
	# Disconnecting any leftover connections
	if plugin != null:
		if current_fine_snap_callable != null:
			if plugin.fine_snapping_changed.is_connected(current_fine_snap_callable):
				plugin.fine_snapping_changed.disconnect(current_fine_snap_callable)
		if current_snap_callable != null:
			if plugin.snapping_changed.is_connected(current_snap_callable):
				plugin.snapping_changed.disconnect(current_snap_callable)
		plugin = null
	ramp = null

# Snapping to grid
var snapping_enabled: bool = false
var snap_unit: float = 1.0
var fine_snapping_enabled: bool = false
var fine_snap_unit: float = 0.1

# Captured arguments required for signal connections
#  ramp: ProtoRamp - on closing a scene, the ramp is freed, so we need to check for null
#    Unfortunately, disconnecting does not work (bug?), the lambda is still called afterwards
#  plugin: ProtoGizmoPlugin - captured plugin argument will not be null (even after setting the field to null in remove_ramp)
func node_snap_listener(ramp: ProtoRamp, plugin: ProtoGizmoPlugin) -> Callable:
	return func (enabled: bool) -> void:
		snapping_enabled = enabled
		if ramp != null:
			ramp.update_gizmos()
		else:
			plugin.snapping_changed.disconnect(current_snap_callable)

func node_fine_snap_listener(ramp: ProtoRamp, plugin: ProtoGizmoPlugin) -> Callable:
	return func (enabled: bool) -> void:
		fine_snapping_enabled = enabled
		if ramp != null:
			ramp.update_gizmos()
		else:
			plugin.fine_snapping_changed.disconnect(current_fine_snap_callable)

var current_snap_callable: Callable

var current_fine_snap_callable: Callable

var plugin: ProtoGizmoPlugin

func init_gizmo(plugin: ProtoGizmoPlugin) -> void:
	# Generate a "random" id for each gizmo
	width_gizmo_id = Time.get_ticks_usec()
	depth_gizmo_id = width_gizmo_id + 1
	height_gizmo_id = width_gizmo_id + 2
	fill_gizmo_id1 = width_gizmo_id + 3
	fill_gizmo_id2 = width_gizmo_id + 4
	undo_redo = plugin.undo_redo
	plugin = plugin
	current_snap_callable = node_snap_listener(ramp, plugin)
	current_fine_snap_callable = node_fine_snap_listener(ramp, plugin)
	plugin.fine_snapping_changed.connect(current_fine_snap_callable)
	plugin.snapping_changed.connect(current_snap_callable)
	snapping_enabled = plugin.snapping
	fine_snapping_enabled = plugin.fine_snapping

# Debug purposes
var screen_pos: Vector2
var debug_gizmo_handler_id: int
var camera_position: Vector3

## As gizmos can only be used in the Editor, we can cast the [gizmo] to [EditorNode3DGizmo] and [plugin] to [EditorNode3DGizmoPlugin].
func redraw_gizmos(gizmo: EditorNode3DGizmo, plugin: ProtoGizmoPlugin) -> void:
	if gizmo.get_node_3d() != ramp:
		return

	if width_gizmo_id == 0 or depth_gizmo_id == 0 or height_gizmo_id == 0 or fill_gizmo_id1 == 0 or fill_gizmo_id2 == 0:
		init_gizmo(plugin)

	gizmo.clear()
	var true_depth: float = ramp.get_true_depth()
	var true_height: float = ramp.get_true_height()
	var anchor_offset: Vector3 = ramp.get_anchor_offset(ramp.anchor)
	var fill: float = ramp.get_fill()
	var depth_gizmo_position := Vector3(0, true_height / 2, true_depth) + anchor_offset
	var width_gizmo_position := Vector3(ramp.width / 2, true_height / 2, true_depth / 2) + anchor_offset
	var height_gizmo_position := Vector3(0, true_height, true_depth / 2) + anchor_offset

	# Calculate perpendicular line points for hypotenuse and the offset
	var fill_gizmo_hypotenuse_projection := _get_fill_max_offset() * (1 - fill)
	var fill_gizmo_position1 := Vector3(-ramp.width / 2, fill_gizmo_hypotenuse_projection.y, true_depth - fill_gizmo_hypotenuse_projection.z) + anchor_offset
	var fill_gizmo_position2 := Vector3(ramp.width / 2, fill_gizmo_hypotenuse_projection.y, true_depth - fill_gizmo_hypotenuse_projection.z) + anchor_offset

	# When on the left, width gizmo is on the right
	# When in the back (top, base), depth gizmo is on the front
	# When on the top, height gizmo is on the bottom
	# Don't offset fill gizmo positions
	match ramp.anchor:
		ProtoRamp.Anchor.BOTTOM_LEFT:
			width_gizmo_position.x = -ramp.width
		ProtoRamp.Anchor.TOP_LEFT:
			width_gizmo_position.x = -ramp.width
			depth_gizmo_position.z = -true_depth
			height_gizmo_position.y = -true_height
		ProtoRamp.Anchor.BASE_LEFT:
			width_gizmo_position.x = -ramp.width
			depth_gizmo_position.z = -true_depth
		ProtoRamp.Anchor.BASE_CENTER:
			depth_gizmo_position.z = -true_depth
		ProtoRamp.Anchor.BASE_RIGHT:
			depth_gizmo_position.z = -true_depth
		ProtoRamp.Anchor.TOP_RIGHT:
			depth_gizmo_position.z = -true_depth
			height_gizmo_position.y = -true_height
		ProtoRamp.Anchor.TOP_CENTER:
			depth_gizmo_position.z = -true_depth
			height_gizmo_position.y = -true_height

	var handles = PackedVector3Array()
	handles.push_back(depth_gizmo_position)
	handles.push_back(width_gizmo_position)
	handles.push_back(height_gizmo_position)
	handles.push_back(fill_gizmo_position1)
	handles.push_back(fill_gizmo_position2)

	gizmo.add_handles(handles, plugin.get_material("proto_handler", gizmo), [depth_gizmo_id, width_gizmo_id, height_gizmo_id, fill_gizmo_id1, fill_gizmo_id2])

	if ramp.shape_polygon != null and ramp.shape_polygon.get_meshes().size() > 1:
		var offset := ramp.get_anchor_offset(ramp.anchor)
		var polygon_offset := offset - Vector3(ramp.width / 2, 0, 0)
		var mesh: Mesh = ramp.shape_polygon.get_meshes()[1]
		var mdt := MeshDataTool.new()
		mdt.create_from_surface(mesh, 0)
		for i in range(mdt.get_vertex_count()):
			var vertex := mdt.get_vertex(i)
			vertex = vertex.rotated(Vector3.UP, -PI / 2.0)
			vertex += polygon_offset
			mdt.set_vertex(i, vertex)

		var newMesh: Mesh = ArrayMesh.new()
		newMesh.clear_surfaces()
		mdt.commit_to_surface(newMesh)
		mdt.clear()
		gizmo.add_collision_triangles(newMesh.generate_triangle_mesh())
		gizmo.add_mesh(newMesh.create_outline(0.001), plugin.get_material("selected", gizmo))

	# Adding debug lines for gizmo if we have cursor screen position set
	if screen_pos:
		var grid_size_modifier = 1.0
		# Grid size is always the max of the two other dimensions
		match debug_gizmo_handler_id:
			depth_gizmo_id:
				# Setting depth
				grid_size_modifier = max(ramp.get_true_height(), ramp.get_width())
				var local_offset_axis = Vector3(0, 0, 1)
				gizmo_utils.debug_draw_handle_grid(camera_position, screen_pos, depth_gizmo_position, local_offset_axis, ramp, gizmo, plugin, grid_size_modifier)
			width_gizmo_id:
				# Setting width
				grid_size_modifier = max(ramp.get_true_height(), ramp.get_true_depth())
				var local_offset_axis = Vector3(1, 0, 0)
				gizmo_utils.debug_draw_handle_grid(camera_position, screen_pos, width_gizmo_position, local_offset_axis, ramp, gizmo, plugin, grid_size_modifier)
			height_gizmo_id:
				# Setting height
				grid_size_modifier = max(ramp.get_width(), ramp.get_true_depth())
				var local_offset_axis = Vector3(0, 1, 0)
				gizmo_utils.debug_draw_handle_grid(camera_position, screen_pos, height_gizmo_position, local_offset_axis, ramp, gizmo, plugin, grid_size_modifier)
			fill_gizmo_id1:
				# Setting fill 1
				grid_size_modifier = max(ramp.get_true_height(), ramp.get_true_depth())
				var local_plane_normal = Vector3(1, 0, 0)
				var local_offset_axis = _get_fill_max_offset().normalized()
				local_offset_axis.z = -local_offset_axis.z
				gizmo_utils.debug_draw_handle_grid_on_plane(fill_gizmo_position1, local_offset_axis, local_plane_normal, ramp, gizmo, plugin, grid_size_modifier)
			fill_gizmo_id2:
				# Setting fill 2
				grid_size_modifier = max(ramp.get_true_height(), ramp.get_true_depth())
				var local_plane_normal = Vector3(1, 0, 0)
				var local_offset_axis = _get_fill_max_offset().normalized()
				local_offset_axis.z = -local_offset_axis.z
				gizmo_utils.debug_draw_handle_grid_on_plane(fill_gizmo_position2, local_offset_axis, local_plane_normal, ramp, gizmo, plugin, grid_size_modifier)

var start_offset := 0.0
var end_offset := 0.0

func set_handle(
	gizmo: EditorNode3DGizmo,
	plugin: ProtoGizmoPlugin,
	handle_id: int,
	secondary: bool,
	camera: Camera3D,
	screen_pos: Vector2) -> void:
	# Set debug parameters for redraw
	var child := gizmo.get_node_3d()
	if child != ramp:
		return

	self.screen_pos = screen_pos
	self.camera_position = camera.position

	match handle_id:
		depth_gizmo_id:
			end_offset = _get_depth_handle_offset(camera, screen_pos)
			if snapping_enabled and not fine_snapping_enabled:
				end_offset = gizmo_utils.snap_to_grid(end_offset, snap_unit)
			elif fine_snapping_enabled:
				end_offset = gizmo_utils.snap_to_grid(end_offset, fine_snap_unit)
			ramp.depth = _get_ramp_depth(end_offset)
		width_gizmo_id:
			end_offset = _get_width_handle_offset(camera, screen_pos)
			if snapping_enabled and not fine_snapping_enabled:
				end_offset = gizmo_utils.snap_to_grid(end_offset, snap_unit)
			elif fine_snapping_enabled:
				end_offset = gizmo_utils.snap_to_grid(end_offset, fine_snap_unit)
			ramp.width = _get_ramp_width(end_offset)
		height_gizmo_id:
			end_offset = _get_height_handle_offset(camera, screen_pos)
			if snapping_enabled and not fine_snapping_enabled:
				end_offset = gizmo_utils.snap_to_grid(end_offset, snap_unit)
			elif fine_snapping_enabled:
				end_offset = gizmo_utils.snap_to_grid(end_offset, fine_snap_unit)
			ramp.height = _get_ramp_height(end_offset)
		fill_gizmo_id1:
			end_offset = _get_fill_handle_offset(camera, screen_pos, Vector3(-ramp.width / 2, 0, 0))
			if snapping_enabled and not fine_snapping_enabled:
				end_offset = gizmo_utils.snap_to_grid(end_offset, snap_unit)
			elif fine_snapping_enabled:
				end_offset = gizmo_utils.snap_to_grid(end_offset, fine_snap_unit)
			ramp.fill = end_offset
		fill_gizmo_id2:
			end_offset = _get_fill_handle_offset(camera, screen_pos, Vector3(ramp.width / 2, 0, 0))
			if snapping_enabled and not fine_snapping_enabled:
				end_offset = gizmo_utils.snap_to_grid(end_offset, snap_unit)
			elif fine_snapping_enabled:
				end_offset = gizmo_utils.snap_to_grid(end_offset, fine_snap_unit)
			ramp.fill = end_offset

	if !is_editing:
		match handle_id:
			depth_gizmo_id:
				debug_gizmo_handler_id = depth_gizmo_id
				start_offset = _get_depth_handle_offset(camera, screen_pos)
			width_gizmo_id:
				debug_gizmo_handler_id = width_gizmo_id
				start_offset = _get_width_handle_offset(camera, screen_pos)
			height_gizmo_id:
				debug_gizmo_handler_id = height_gizmo_id
				start_offset = _get_height_handle_offset(camera, screen_pos)
			fill_gizmo_id1:
				debug_gizmo_handler_id = fill_gizmo_id1
				start_offset = _get_fill_handle_offset(camera, screen_pos, Vector3(-ramp.width / 2, 0, 0))
			fill_gizmo_id2:
				debug_gizmo_handler_id = fill_gizmo_id2
				start_offset = _get_fill_handle_offset(camera, screen_pos, Vector3(ramp.width / 2, 0, 0))
		is_editing = true

	ramp.update_gizmos()

func _get_depth_handle_offset(
	camera: Camera3D,
	screen_pos: Vector2) -> float:
	var local_offset_axis = Vector3(0, 0, 1)
	var gizmo_position = Vector3(0, ramp.get_true_height() / 2, ramp.get_true_depth()) + ramp.get_anchor_offset(ramp.anchor)
	var handle_offset = gizmo_utils.get_handle_offset(camera, screen_pos, gizmo_position, local_offset_axis, ramp)
	return handle_offset.z

func _get_width_handle_offset(
	camera: Camera3D,
	screen_pos: Vector2) -> float:
	var local_offset_axis = Vector3(1, 0, 0)
	var gizmo_position = Vector3(ramp.width / 2, ramp.get_true_height() / 2, ramp.get_true_depth() / 2) + ramp.get_anchor_offset(ramp.anchor)
	var handle_offset = gizmo_utils.get_handle_offset(camera, screen_pos, gizmo_position, local_offset_axis, ramp)
	return handle_offset.x

func _get_height_handle_offset(
	camera: Camera3D,
	screen_pos: Vector2) -> float:
	var local_offset_axis = Vector3(0, 1, 0)
	var gizmo_position = Vector3(0, ramp.get_true_height(), ramp.get_true_depth() / 2) + ramp.get_anchor_offset(ramp.anchor)
	var handle_offset = gizmo_utils.get_handle_offset(camera, screen_pos, gizmo_position, local_offset_axis, ramp)
	return handle_offset.y

func _get_fill_handle_offset(
	camera: Camera3D,
	screen_pos: Vector2,
	gizmo_position_offset: Vector3) -> float:
	var fill_gizmo_axis := _get_fill_max_offset()
	fill_gizmo_axis.z = ramp.get_true_depth() - fill_gizmo_axis.z
	var fill_gizmo_offset := fill_gizmo_axis * (1 - ramp.fill)
	var gizmo_position := Vector3(0, fill_gizmo_offset.y, fill_gizmo_offset.z) + ramp.get_anchor_offset(ramp.anchor) + gizmo_position_offset
	var local_plane_normal := Vector3(1, 0, 0)
	var handle_offset = gizmo_utils.get_handle_offset_by_plane(camera, screen_pos, gizmo_position, local_plane_normal, ramp)
	var gizmo_base_position := Vector3(0, 0, ramp.get_true_depth())
	handle_offset -= gizmo_base_position
	var gizmo_max_position := fill_gizmo_axis - gizmo_base_position
	gizmo_max_position.x = 0
	handle_offset -= ramp.get_anchor_offset(ramp.anchor)
	handle_offset.x = 0
	if (handle_offset.dot(gizmo_max_position) < 0):
		return 1
	return min(1.0, max(0.0, 1 - handle_offset.project(gizmo_max_position).length() / gizmo_max_position.length()))

func _get_ramp_width(offset: float) -> float:
	# If anchor is on the left, offset is negative
	# If anchor is not centered, offset is divided by 2
	match ramp.anchor:
		ProtoRamp.Anchor.BOTTOM_LEFT:
			offset = -offset / 2
		ProtoRamp.Anchor.TOP_LEFT:
			offset = -offset / 2
		ProtoRamp.Anchor.BASE_LEFT:
			offset = -offset / 2
		ProtoRamp.Anchor.BOTTOM_RIGHT:
			offset = offset / 2
		ProtoRamp.Anchor.TOP_RIGHT:
			offset = offset / 2
		ProtoRamp.Anchor.BASE_RIGHT:
			offset = offset / 2
	return offset * 2

func _get_ramp_depth(offset: float) -> float:
	if ramp.calculation == ProtoRamp.Calculation.STEP_DIMENSIONS and ramp.type == ProtoRamp.Type.STAIRCASE:
		offset = offset / ramp.steps
	# If anchor is on the back, offset is negative
	match ramp.anchor:
		ProtoRamp.Anchor.BASE_CENTER:
			offset = -offset
		ProtoRamp.Anchor.BASE_RIGHT:
			offset = -offset
		ProtoRamp.Anchor.BASE_LEFT:
			offset = -offset
		ProtoRamp.Anchor.TOP_CENTER:
			offset = -offset
		ProtoRamp.Anchor.TOP_RIGHT:
			offset = -offset
		ProtoRamp.Anchor.TOP_LEFT:
			offset = -offset
	return offset

func _get_ramp_height(offset: float) -> float:
	# If anchor is TOP, offset is negative
	if ramp.calculation == ProtoRamp.Calculation.STEP_DIMENSIONS and ramp.type == ProtoRamp.Type.STAIRCASE:
		offset = offset / ramp.steps
	match ramp.anchor:
		ProtoRamp.Anchor.TOP_LEFT:
			offset = -offset
		ProtoRamp.Anchor.TOP_CENTER:
			offset = -offset
		ProtoRamp.Anchor.TOP_RIGHT:
			offset = -offset
	return offset

func _get_fill_max_offset() -> Vector3:
	var A := Vector2(0, ramp.get_true_height())
	var B := Vector2(ramp.get_true_depth(), 0)
	var fill_gizmo_base_position := Vector2(0, 0)
	var dir := (B - A).normalized()
	var t := (fill_gizmo_base_position - A).dot(dir)
	var fill_gizmo_hypotenuse_projection := A + dir * t
	var projection_vector := fill_gizmo_hypotenuse_projection - fill_gizmo_base_position
	return Vector3(0, projection_vector.y, projection_vector.x)

func commit_handle(
	gizmo: EditorNode3DGizmo,
	handle_id: int,
	secondary: bool,
	restore: Variant,
	cancel: bool) -> void:
	if gizmo.get_node_3d() != ramp:
		return

	match handle_id:
		depth_gizmo_id:
			restore = _get_ramp_depth(start_offset)
			undo_redo.create_action("Edit ramp depth", 0, ramp, true)
			undo_redo.add_do_property(ramp, "depth", _get_ramp_depth(end_offset))
			undo_redo.add_undo_property(ramp, "depth", restore)
		width_gizmo_id:
			restore = _get_ramp_width(start_offset)
			undo_redo.create_action("Edit ramp width", 0, ramp, true)
			undo_redo.add_do_property(ramp, "width", _get_ramp_width(end_offset))
			undo_redo.add_undo_property(ramp, "width", restore)
		height_gizmo_id:
			restore = _get_ramp_height(start_offset)
			undo_redo.create_action("Edit ramp height", 0, ramp, true)
			undo_redo.add_do_property(ramp, "height", _get_ramp_height(end_offset))
			undo_redo.add_undo_property(ramp, "height", restore)
		fill_gizmo_id1, fill_gizmo_id2:
			restore = start_offset
			undo_redo.create_action("Edit ramp fill", 0, ramp, true)
			undo_redo.add_do_property(ramp, "fill", end_offset)
			undo_redo.add_undo_property(ramp, "fill", restore)

	undo_redo.commit_action()
	is_editing = false
