# MIT License
#
# Copyright (c) 2023 Mark McKay
# https://github.com/blackears/cyclopsLevelBuilder
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

@tool
extends EditorPlugin
class_name CyclopsLevelBuilder

signal active_node_changed
signal selection_changed
signal snapping_tool_changed

const AUTOLOAD_NAME = "CyclopsAutoload"
const CYCLOPS_HUD_NAME = "CyclopsGlobalHud"

var config:CyclopsConfig = preload("res://addons/cyclops_level_builder/data/configuration.tres")

var logger:CyclopsLogger = CyclopsLogger.new()

var material_dock:MaterialPaletteViewport
var convex_face_editor_dock:ConvexFaceEdtiorViewport
var tool_properties_dock:ToolPropertiesDock
var snapping_properties_dock:SnappingPropertiesDock
var cyclops_console_dock:CyclopsConsole
var main_toolbar:MainToolbar
var editor_toolbar:EditorToolbar
var upgrade_cyclops_blocks_toolbar:UpgradeCyclopsBlocksToolbar
var activated:bool = false

var always_on:bool = false:
	get:
		return always_on
	set(value):
		always_on = value
		#print("always_on %s" % always_on)
		update_activation()

var block_create_distance:float = 10
var tool:CyclopsTool = null
var snapping_system:CyclopsSnappingSystem = null
var lock_uvs:bool = false
var tool_overlay_extrude:float = .01

var tool_uv_transform:Transform2D
var tool_material_path:String

var handle_screen_radius:float = 6

var drag_start_radius:float = 6

enum Mode { OBJECT, EDIT }
var mode:Mode = Mode.OBJECT
enum EditMode { VERTEX, EDGE, FACE }
var edit_mode:CyclopsLevelBuilder.EditMode = CyclopsLevelBuilder.EditMode.VERTEX

var display_mode:DisplayMode.Type = DisplayMode.Type.MATERIAL

var cached_viewport_camera:Camera3D

var editor_cache:Dictionary
var editor_cache_file:String = "user://cyclops_editor_cache.json"

func get_snapping_manager()->SnappingManager:
	var mgr:SnappingManager = SnappingManager.new()
	mgr.snap_enabled = CyclopsAutoload.settings.get_property(CyclopsGlobalScene.SNAPPING_ENABLED)
	mgr.snap_tool = snapping_system
	
	return mgr

func _get_plugin_name()->String:
	return "CyclopsLevelBuilder"

func _get_plugin_icon()->Texture2D:
	return preload("res://addons/cyclops_level_builder/art/cyclops.svg")

func _enter_tree():
	if FileAccess.file_exists(editor_cache_file):
		#print(">> _enter_tree")
		var text:String = FileAccess.get_file_as_string(editor_cache_file)
		#print("load text:", text)
		editor_cache = JSON.parse_string(text)
		
	add_custom_type("CyclopsScene", "Node3D", preload("nodes/cyclops_scene.gd"), preload("nodes/cyclops_blocks_icon.png"))
	
	add_custom_type("CyclopsBlock", "Node3D", preload("nodes/cyclops_block.gd"), preload("nodes/cyclops_blocks_icon.png"))
	add_custom_type("CyclopsBlocks", "Node3D", preload("nodes/cyclops_blocks.gd"), preload("nodes/cyclops_blocks_icon.png"))
	add_custom_type("CyclopsConvexBlock", "Node", preload("nodes/cyclops_convex_block.gd"), preload("nodes/cyclops_blocks_icon.png"))
	add_custom_type("CyclopsConvexBlockBody", "Node", preload("nodes/cyclops_convex_block_body.gd"), preload("nodes/cyclops_blocks_icon.png"))

	add_autoload_singleton(AUTOLOAD_NAME, "res://addons/cyclops_level_builder/cyclops_global_scene.tscn")
	#add_autoload_singleton(CYCLOPS_HUD_NAME, "res://addons/cyclops_level_builder/cyclops_global_hud.tscn")
	
	material_dock = preload("res://addons/cyclops_level_builder/docks/material_palette/material_palette_viewport.tscn").instantiate()
	material_dock.builder = self
	
	convex_face_editor_dock = preload("res://addons/cyclops_level_builder/docks/convex_face_editor/convex_face_editor_viewport.tscn").instantiate()
	convex_face_editor_dock.builder = self
	
	tool_properties_dock = preload("res://addons/cyclops_level_builder/docks/tool_properties/tool_properties_dock.tscn").instantiate()
	tool_properties_dock.builder = self
	
	snapping_properties_dock = preload("res://addons/cyclops_level_builder/docks/snapping_properties/snapping_properties_dock.tscn").instantiate()
	snapping_properties_dock.builder = self
	
	cyclops_console_dock = preload("res://addons/cyclops_level_builder/docks/cyclops_console/cyclops_console.tscn").instantiate()
	cyclops_console_dock.editor_plugin = self
	
	main_toolbar = preload("menu/main_toolbar.tscn").instantiate()
	main_toolbar.editor_plugin = self
	
	editor_toolbar = preload("menu/editor_toolbar.tscn").instantiate()
	editor_toolbar.editor_plugin = self

	upgrade_cyclops_blocks_toolbar = preload("res://addons/cyclops_level_builder/menu/upgrade_cyclops_blocks_toolbar.tscn").instantiate()
	upgrade_cyclops_blocks_toolbar.editor_plugin = self

	add_control_to_bottom_panel(cyclops_console_dock, "Cyclops")
	
	add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, main_toolbar)
	
	var editor:EditorInterface = get_editor_interface()
	var selection:EditorSelection = editor.get_selection()
	selection.selection_changed.connect(on_selection_changed)
	
	update_activation()


	#Wait until everything is loaded	
	await get_tree().process_frame
	
	var global_scene:CyclopsGlobalScene = get_node("/root/CyclopsAutoload")
	global_scene.builder = self
	
	switch_to_snapping_system(SnappingSystemGrid.new())
	switch_to_tool(ToolBlock.new())


func _exit_tree():
	var file:FileAccess = FileAccess.open(editor_cache_file, FileAccess.WRITE)
	#var text:String = JSON.stringify(editor_cache, "  ")
	#print("saving cache:", text)
	file.store_string(JSON.stringify(editor_cache, "    "))
	file.close()
		
	# Clean-up of the plugin goes here.
	remove_autoload_singleton(AUTOLOAD_NAME)
	#remove_autoload_singleton(CYCLOPS_HUD_NAME)
	
	remove_custom_type("CyclopsScene")
	
	remove_custom_type("CyclopsBlock")
	remove_custom_type("CyclopsBlocks")
	remove_custom_type("CyclopsConvexBlock")
	remove_custom_type("CyclopsConvexBlockBody")
	
	remove_control_from_bottom_panel(cyclops_console_dock)
	remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, main_toolbar)
	
	if activated:
		remove_control_from_docks(material_dock)
		remove_control_from_docks(convex_face_editor_dock)
		remove_control_from_docks(tool_properties_dock)
		remove_control_from_docks(snapping_properties_dock)
		remove_control_from_docks(cyclops_console_dock)
		remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, editor_toolbar)

	if upgrade_cyclops_blocks_toolbar.activated:
		remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, upgrade_cyclops_blocks_toolbar)

	material_dock.queue_free()
	convex_face_editor_dock.queue_free()
	tool_properties_dock.queue_free()
	snapping_properties_dock.queue_free()
	cyclops_console_dock.queue_free()
	main_toolbar.queue_free()
	editor_toolbar.queue_free()
	upgrade_cyclops_blocks_toolbar.queue_free()

func log(message:String, level:CyclopsLogger.LogLevel = CyclopsLogger.LogLevel.ERROR):
	logger.log(message, level)

func get_blocks()->Array[CyclopsBlock]:
	return get_blocks_recursive(get_editor_interface().get_edited_scene_root())

func get_blocks_recursive(node:Node)->Array[CyclopsBlock]:
	var result:Array[CyclopsBlock]
	
	if node is CyclopsBlock:
		result.append(node)
	for child in node.get_children():
		result.append_array(get_blocks_recursive(child))
	return result

func is_selected(node:Node)->bool:
	var selection:EditorSelection = get_editor_interface().get_selection()
	for n in selection.get_selected_nodes():
		if n == node:
			return true
	return false
	

func is_active_block(block:CyclopsBlock)->bool:
	var selection:EditorSelection = get_editor_interface().get_selection()
	var nodes:Array[Node] = selection.get_selected_nodes()
	
	return !nodes.is_empty() && nodes.back() == block
	
func get_active_block()->CyclopsBlock:
	var selection:EditorSelection = get_editor_interface().get_selection()
	var nodes:Array[Node] = selection.get_selected_nodes()
	
	var back:Node = nodes.back()
	if back is CyclopsBlock:
		return back
	return null
	

#Blocks listed in order of selection with last block being the most recent (ie, active) one
func get_selected_blocks()->Array[CyclopsBlock]:
	var result:Array[CyclopsBlock]

	var selection:EditorSelection = get_editor_interface().get_selection()
	for node in selection.get_selected_nodes():
		if node is CyclopsBlock:
			result.append(node)

	return result

func get_block_add_parent()->Node:
	var selection:EditorSelection = get_editor_interface().get_selection()
	var nodes:Array = selection.get_selected_nodes()
	if nodes.is_empty():
		return get_editor_interface().get_edited_scene_root()
	
	if nodes[0] is CyclopsBlock:
		#print("getting parent of ", nodes[0].name)
		return nodes[0].get_parent()
	return nodes[0]

func update_activation():
	var editor:EditorInterface = get_editor_interface()
	var selection:EditorSelection = editor.get_selection()
	var nodes:Array[Node] = selection.get_selected_nodes()
	
	#Node list ordered in order of selection with most recently sdelected at end
	var node:Node = null
	if !nodes.is_empty():
		node = nodes[0]
		
	if node is CyclopsBlock || always_on:
		#print("updarting activation")
		if !activated:
			add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, editor_toolbar)
			add_control_to_bottom_panel(material_dock, "Materials")
			add_control_to_dock(DOCK_SLOT_RIGHT_BL, convex_face_editor_dock)
			add_control_to_dock(DOCK_SLOT_RIGHT_BL, tool_properties_dock)
			add_control_to_dock(DOCK_SLOT_RIGHT_BL, snapping_properties_dock)
			activated = true
	else:
		if activated:
			remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, editor_toolbar)
			remove_control_from_bottom_panel(material_dock)
			remove_control_from_docks(convex_face_editor_dock)
			remove_control_from_docks(tool_properties_dock)
			remove_control_from_docks(snapping_properties_dock)
			activated = false
	
	if node is CyclopsBlocks:
		if !upgrade_cyclops_blocks_toolbar.activated:
			add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, upgrade_cyclops_blocks_toolbar)
			upgrade_cyclops_blocks_toolbar.activated = true
	else:
		if upgrade_cyclops_blocks_toolbar.activated:
			remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, upgrade_cyclops_blocks_toolbar)
			upgrade_cyclops_blocks_toolbar.activated = false

func on_selection_changed():
	update_activation()
	
	if cached_viewport_camera:
		tool._draw_tool(cached_viewport_camera)

func _handles(object:Object):
#	return object is CyclopsBlocks or object is CyclopsConvexBlock
	return object is CyclopsBlock or object is CyclopsBlocks or always_on

func _forward_3d_draw_over_viewport(viewport_control:Control):
	var global_scene:CyclopsGlobalScene = get_global_scene()
	global_scene.draw_over_viewport(viewport_control)
	#Draw on top of viweport here

func _forward_3d_gui_input(viewport_camera:Camera3D, event:InputEvent):
	#print("plugin: " + event.as_text())
	cached_viewport_camera = viewport_camera
	
	if tool:
		var result:bool = tool._gui_input(viewport_camera, event)
		tool._draw_tool(viewport_camera)
		return EditorPlugin.AFTER_GUI_INPUT_STOP if result else EditorPlugin.AFTER_GUI_INPUT_PASS
	
	return EditorPlugin.AFTER_GUI_INPUT_PASS

func _get_state()->Dictionary:
	var state:Dictionary = {}
	
	#print("ed cache ", str(editor_cache))
	#state["editor_cache"] = editor_cache.duplicate()
	
	material_dock.save_state(state)
	convex_face_editor_dock.save_state(state)
	tool_properties_dock.save_state(state)
	snapping_properties_dock.save_state(state)
	cyclops_console_dock.save_state(state)
	
	return state
	
func _set_state(state):
	#print("ed set_state ", str(state))
	
	#editor_cache = state.get("editor_cache", {}).duplicate()
	
	material_dock.load_state(state)
	convex_face_editor_dock.load_state(state)
	tool_properties_dock.load_state(state)
	snapping_properties_dock.load_state(state)
	cyclops_console_dock.load_state(state)


func get_tool_cache(tool_id:String):
	if !editor_cache.has("tool"):
		return {}
	
	if !editor_cache.tool.has(tool_id):
		return {}
	
	return editor_cache.tool[tool_id]

func set_tool_cache(tool_id:String, cache:Dictionary):
	if !editor_cache.has("tool"):
		editor_cache["tool"] = {}
	
	editor_cache.tool[tool_id] = cache

func get_snapping_cache(tool_id:String):
	if !editor_cache.has("snapping"):
		return {}
	
	if !editor_cache.snapping.has(tool_id):
		return {}
	
	return editor_cache.snapping[tool_id]

func set_snapping_cache(tool_id:String, cache:Dictionary):
	if !editor_cache.has("snapping"):
		editor_cache["snapping"] = {}
	
	editor_cache.snapping[tool_id] = cache

func switch_to_tool(_tool:CyclopsTool):
	#print(">> switch to tool")
	
	if tool:
		tool._deactivate()
	
	tool = _tool

	if tool:
		tool._activate(self)
		var control:Control = tool._get_tool_properties_editor()
		tool_properties_dock.set_editor(control)

func switch_to_snapping_system(_snapping_system:CyclopsSnappingSystem):
	if snapping_system:
		snapping_system._deactivate()
		
	snapping_system = _snapping_system
	
	if snapping_system:
		snapping_system._activate(self)
		var control:Control = snapping_system._get_properties_editor()
		snapping_properties_dock.set_editor(control)
	
	snapping_tool_changed.emit()

func get_global_scene()->CyclopsGlobalScene:
	var scene:CyclopsGlobalScene = get_node("/root/CyclopsAutoload")
	return scene



func intersect_ray_closest(origin:Vector3, dir:Vector3)->IntersectResults:
	var best_result:IntersectResults

	var blocks:Array[CyclopsBlock] = get_blocks()

	for block in blocks:
		if !block.is_visible_in_tree():
			continue
		
		var result:IntersectResults = block.intersect_ray_closest(origin, dir)
#			print("isect %s %s" % [node.name, result])
		if result:
			if !best_result or result.distance_squared < best_result.distance_squared:
#				print("setting best result %s" % node.name)
				best_result = result
#				print("best_result %s" % ray_best_result)
		
#	print("returning best result %s" % ray_best_result)
	return best_result

func intersect_ray_closest_selected_only(origin:Vector3, dir:Vector3)->IntersectResults:
	var best_result:IntersectResults

	var blocks:Array[CyclopsBlock] = get_selected_blocks()
	for block in blocks:
		var result:IntersectResults = block.intersect_ray_closest(origin, dir)
		if result:
			if !best_result or result.distance_squared < best_result.distance_squared:
				best_result = result			
	
	return best_result		
	

func intersect_frustum_all(frustum:Array[Plane])->Array[CyclopsBlock]:
	var result:Array[CyclopsBlock] = []
	
	var blocks:Array[CyclopsBlock] = get_blocks()
	for block in blocks:
		var xform:Transform3D = block.global_transform.affine_inverse()
		
		var frustum_local:Array[Plane]
		for p in frustum:
			frustum_local.append(xform * p)
		
		#print("intersect_frustum_all block %s" % block.get_path())
		var vol:ConvexVolume = block.control_mesh
#		if !vol:
#			print("nil vol %s" % block.get_path())
		if vol && vol.intersects_frustum(frustum_local):
			result.append(block)
	
	return result

