@tool
extends EditorPlugin
#class_name Terrain3DEditorPlugin Cannot be named until Godot #75388


# Includes
const UI: Script = preload("res://addons/terrain_3d/src/ui.gd")
const RegionGizmo: Script = preload("res://addons/terrain_3d/src/region_gizmo.gd")
const ASSET_DOCK: String = "res://addons/terrain_3d/src/asset_dock.tscn"
const PS_DOCK_POSITION: String = "terrain3d/config/dock_position"
const PS_DOCK_PINNED: String = "terrain3d/config/dock_pinned"

var terrain: Terrain3D
var _last_terrain: Terrain3D
var nav_region: NavigationRegion3D

var editor: Terrain3DEditor
var ui: Node # Terrain3DUI see Godot #75388
var asset_dock: PanelContainer
var region_gizmo: RegionGizmo
var visible: bool
var current_region_position: Vector2
var mouse_global_position: Vector3 = Vector3.ZERO

# Track negative input (CTRL)
var _negative_input: bool = false
# Track state prior to pressing CTRL: -1 not tracked, 0 false, 1 true
var _prev_enable_state: int = -1


func _enter_tree() -> void:
	editor = Terrain3DEditor.new()
	ui = UI.new()
	ui.plugin = self
	add_child(ui)

	region_gizmo = RegionGizmo.new()

	scene_changed.connect(_on_scene_changed)

	asset_dock = load(ASSET_DOCK).instantiate()
	asset_dock.initialize(self)

	
func _exit_tree() -> void:
	asset_dock.remove_dock(true)
	asset_dock.queue_free()
	ui.queue_free()
	editor.free()

	scene_changed.disconnect(_on_scene_changed)


func _handles(p_object: Object) -> bool:
	if p_object is Terrain3D:
		return true
	
	# Terrain3DObjects requires access to EditorUndoRedoManager. The only way to make sure it
	# always has it, is to pass it in here. _edit is NOT called if the node is cut and pasted.
	if p_object is Terrain3DObjects:
		p_object.editor_setup(self)
	elif p_object is Node3D and p_object.get_parent() is Terrain3DObjects:
		p_object.get_parent().editor_setup(self)
	
	if is_instance_valid(_last_terrain) and _last_terrain.is_inside_tree() and p_object is NavigationRegion3D:
		return true
	
	return false


func _edit(p_object: Object) -> void:
	if !p_object:
		_clear()

	if p_object is Terrain3D:
		if p_object == terrain:
			return
		terrain = p_object
		_last_terrain = terrain
		editor.set_terrain(terrain)
		region_gizmo.set_node_3d(terrain)
		terrain.add_gizmo(region_gizmo)
		terrain.set_plugin(self)
		
		# Connect to new Assets resource
		if not terrain.assets_changed.is_connected(asset_dock.update_assets):
			terrain.assets_changed.connect(asset_dock.update_assets)
		asset_dock.update_assets()
		# Connect to new Storage resource
		if not terrain.storage_changed.is_connected(_load_storage):
			terrain.storage_changed.connect(_load_storage)
		_load_storage()
	else:
		_clear()

	if is_instance_valid(_last_terrain) and _last_terrain.is_inside_tree():
		if p_object is NavigationRegion3D:
			nav_region = p_object
		else:
			nav_region = null


func _make_visible(p_visible: bool, p_redraw: bool = false) -> void:
	visible = p_visible
	ui.set_visible(visible)
	update_region_grid()
	asset_dock.update_dock(visible)


func _clear() -> void:
	if is_terrain_valid():
		terrain.storage_changed.disconnect(_load_storage)
		
		terrain.clear_gizmos()
		terrain = null
		editor.set_terrain(null)
		
		ui.clear_picking()
		
	region_gizmo.clear()


func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> int:
	if not is_terrain_valid():
		return AFTER_GUI_INPUT_PASS
	
	## Track negative input (CTRL)
	if p_event is InputEventKey and not p_event.echo and p_event.keycode == KEY_CTRL:
		if p_event.is_pressed():
			_negative_input = true
			_prev_enable_state = int(ui.toolbar_settings.get_setting("enable"))
			ui.toolbar_settings.set_setting("enable", false)
		else:
			_negative_input = false
			ui.toolbar_settings.set_setting("enable", bool(_prev_enable_state))
			_prev_enable_state = -1
	
	## Handle mouse movement
	if p_event is InputEventMouseMotion:
		if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
			return AFTER_GUI_INPUT_PASS

		if _prev_enable_state >= 0 and not Input.is_key_pressed(KEY_CTRL):
			_negative_input = false
			ui.toolbar_settings.set_setting("enable", bool(_prev_enable_state))
			_prev_enable_state = -1

		## Setup for active camera & viewport
		
		# Snap terrain to current camera 
		terrain.set_camera(p_viewport_camera)

		# Detect if viewport is set to half_resolution
		# Structure is: Node3DEditorViewportContainer/Node3DEditorViewport(4)/SubViewportContainer/SubViewport/Camera3D
		var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent()
		var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true

		## Get mouse location on terrain
		
		# Project 2D mouse position to 3D position and direction
		var mouse_pos: Vector2 = p_event.position if full_resolution else p_event.position/2
		var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos)
		var camera_dir: Vector3 = p_viewport_camera.project_ray_normal(mouse_pos)

		# If region tool, grab mouse position without considering height
		if editor.get_tool() == Terrain3DEditor.REGION:
			var t = -Vector3(0, 1, 0).dot(camera_pos) / Vector3(0, 1, 0).dot(camera_dir)
			mouse_global_position = (camera_pos + t * camera_dir)
		else:			
			# Else look for intersection with terrain
			var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir)
			if intersection_point.z > 3.4e38 or is_nan(intersection_point.z): # max double or nan
				return AFTER_GUI_INPUT_STOP
			mouse_global_position = intersection_point
		
		## Update decal
		ui.decal.global_position = mouse_global_position
		ui.decal.albedo_mix = 1.0
		if ui.decal_timer.is_stopped():
			ui.update_decal()
		else:
			ui.decal_timer.start()

		## Update region highlight
		var region_size = terrain.get_storage().get_region_size()
		var region_position: Vector2 = ( Vector2(mouse_global_position.x, mouse_global_position.z) \
			/ (region_size * terrain.get_mesh_vertex_spacing()) ).floor()
		if current_region_position != region_position:
			current_region_position = region_position
			update_region_grid()
			
		if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and editor.is_operating():
			editor.operate(mouse_global_position, p_viewport_camera.rotation.y)
			return AFTER_GUI_INPUT_STOP

	elif p_event is InputEventMouseButton:
		ui.update_decal()
			
		if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
			if p_event.is_pressed():
				if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
					return AFTER_GUI_INPUT_STOP
					
				# If picking
				if ui.is_picking():
					ui.pick(mouse_global_position)
					if not ui.operation_builder or not ui.operation_builder.is_ready():
						return AFTER_GUI_INPUT_STOP
				
				# If adjusting regions
				if editor.get_tool() == Terrain3DEditor.REGION:
					# Skip regions that already exist or don't
					var has_region: bool = terrain.get_storage().has_region(mouse_global_position)
					var op: int = editor.get_operation()
					if	( has_region and op == Terrain3DEditor.ADD) or \
						( not has_region and op == Terrain3DEditor.SUBTRACT ):
						return AFTER_GUI_INPUT_STOP
				
				# If an automatic operation is ready to go (e.g. gradient)
				if ui.operation_builder and ui.operation_builder.is_ready():
					ui.operation_builder.apply_operation(editor, mouse_global_position, p_viewport_camera.rotation.y)
					return AFTER_GUI_INPUT_STOP
				
				# Mouse clicked, start editing
				editor.start_operation(mouse_global_position)
				editor.operate(mouse_global_position, p_viewport_camera.rotation.y)
				return AFTER_GUI_INPUT_STOP
			
			elif editor.is_operating():
				# Mouse released, save undo data
				editor.stop_operation()
				return AFTER_GUI_INPUT_STOP
	
	return AFTER_GUI_INPUT_PASS

	
func _load_storage() -> void:
	if terrain:
		update_region_grid()


func update_region_grid() -> void:
	if not region_gizmo:
		return

	region_gizmo.set_hidden(not visible)
	
	if is_terrain_valid():
		region_gizmo.show_rect = editor.get_tool() == Terrain3DEditor.REGION
		region_gizmo.use_secondary_color = editor.get_operation() == Terrain3DEditor.SUBTRACT
		region_gizmo.region_position = current_region_position
		region_gizmo.region_size = terrain.get_storage().get_region_size() * terrain.get_mesh_vertex_spacing()
		region_gizmo.grid = terrain.get_storage().get_region_offsets()
		
		terrain.update_gizmos()
		return
		
	region_gizmo.show_rect = false
	region_gizmo.region_size = 1024
	region_gizmo.grid = [Vector2i.ZERO]


func _on_scene_changed(scene_root: Node) -> void:
	if not scene_root:
		return
		
	for node in scene_root.find_children("", "Terrain3DObjects"):
		node.editor_setup(self)

	asset_dock.update_assets()
	await get_tree().create_timer(2).timeout
	asset_dock.update_thumbnails()

		
func is_terrain_valid(p_terrain: Terrain3D = null) -> bool:
	var t: Terrain3D
	if p_terrain:
		t = p_terrain
	else:
		t = terrain
	if is_instance_valid(t) and t.is_inside_tree() and t.get_storage():
		return true
	return false


func is_selected() -> bool:
	var selected: Array[Node] = get_editor_interface().get_selection().get_selected_nodes()
	for node in selected:
		if node.get_instance_id() == _last_terrain.get_instance_id():
			return true
			
	return false	


func select_terrain() -> void:
	if is_instance_valid(_last_terrain) and is_terrain_valid(_last_terrain) and not is_selected():
		var es: EditorSelection = get_editor_interface().get_selection()
		es.clear()
		es.add_node(_last_terrain)
