extends Control

const GEOSET_CONTROLLER_SCRIPT := preload("res://src/scenes/character/character_geoset_controller.gd")
const TEXTURE_COMPOSITOR_SCRIPT := preload("res://src/scenes/character/character_texture_compositor.gd")
const OUTFIT_RESOLVER_SCRIPT := preload("res://src/scenes/character/wow_character_outfit_resolver.gd")
const CHARACTER_ROOT := "res://src/resources/characters"
const EXTRACTED_DIR := "res://data/extracted"
const CHARACTER_YAW_OFFSET_DEGREES := 90.0

const CLASS_IDS := {
	"Warrior": 1,
	"Paladin": 2,
	"Hunter": 3,
	"Rogue": 4,
	"Priest": 5,
	"Death Knight": 6,
	"Shaman": 7,
	"Mage": 8,
	"Warlock": 9,
	"Druid": 11,
}

const GEOSET_CONTROLS := [
	{"cat": "hair", "label": "Hair"},
	{"cat": "facial1", "label": "Facial 1"},
	{"cat": "facial2", "label": "Facial 2"},
	{"cat": "facial3", "label": "Facial 3"},
	{"cat": "ears", "label": "Ears"},
	{"cat": "eye_effects", "label": "Eyes"},
	{"cat": "wristbands", "label": "Wrist"},
]

const TOGGLE_CONTROLS := [
	{"property": "show_gloves", "label": "Gloves"},
	{"property": "show_boots", "label": "Boots"},
	{"property": "show_shirt", "label": "Shirt"},
	{"property": "show_kneepads", "label": "Knees"},
	{"property": "show_chest", "label": "Chest"},
	{"property": "show_pants", "label": "Pants"},
	{"property": "show_tabard", "label": "Tabard"},
	{"property": "show_legs", "label": "Legs"},
	{"property": "show_cape", "label": "Cape"},
	{"property": "show_belt", "label": "Belt"},
	{"property": "show_feet", "label": "Feet"},
]

var _models: Dictionary = {}
var _race_names: Array[String] = []
var _selected_race := ""
var _selected_gender := ""
var _selected_model: Dictionary = {}

var _viewport: SubViewport
var _preview_anchor: Node3D
var _camera: Camera3D
var _model_root: Node3D
var _controller: Node
var _compositor: Node
var _outfit_resolver: RefCounted
var _current_outfit: Dictionary = {}

var _race_option: OptionButton
var _gender_option: OptionButton
var _model_option: OptionButton
var _class_option: OptionButton
var _skin_spin: SpinBox
var _face_spin: SpinBox
var _hair_color_spin: SpinBox
var _flags_spin: SpinBox
var _rotation_slider: HSlider
var _status_label: Label
var _control_container: VBoxContainer
var _geoset_options: Dictionary = {}
var _toggle_checks: Dictionary = {}


func _ready() -> void:
	_outfit_resolver = OUTFIT_RESOLVER_SCRIPT.new()
	if not _outfit_resolver.call("load_dbcs", EXTRACTED_DIR):
		push_warning("CharacterCreator: CharStartOutfit/ItemDisplayInfo DBCs not loaded")
	_scan_models()
	_build_ui()
	_populate_races()
	if not _race_names.is_empty():
		_select_race(_race_names[0])


func _process(delta: float) -> void:
	if _preview_anchor:
		_preview_anchor.rotation.y = deg_to_rad(float(_rotation_slider.value))


func _scan_models() -> void:
	_models.clear()
	_race_names.clear()
	var root := DirAccess.open(CHARACTER_ROOT)
	if root == null:
		push_warning("CharacterCreator: missing character root: %s" % CHARACTER_ROOT)
		return

	root.list_dir_begin()
	var race_dir := root.get_next()
	while not race_dir.is_empty():
		if root.current_is_dir() and not race_dir.begins_with("."):
			_scan_race(race_dir)
		race_dir = root.get_next()
	root.list_dir_end()
	_race_names.sort_custom(func(a: String, b: String) -> bool: return _display_name(a) < _display_name(b))


func _scan_race(race: String) -> void:
	var race_path := CHARACTER_ROOT.path_join(race)
	var race_access := DirAccess.open(race_path)
	if race_access == null:
		return

	var race_data: Dictionary = {}
	race_access.list_dir_begin()
	var gender_dir := race_access.get_next()
	while not gender_dir.is_empty():
		if race_access.current_is_dir() and not gender_dir.begins_with("."):
			var models := _scan_gender_models(race, gender_dir)
			if not models.is_empty():
				race_data[gender_dir] = models
		gender_dir = race_access.get_next()
	race_access.list_dir_end()

	if not race_data.is_empty():
		_models[race] = race_data
		_race_names.append(race)


func _scan_gender_models(race: String, gender: String) -> Array:
	var result: Array = []
	var gender_path := CHARACTER_ROOT.path_join(race).path_join(gender)
	var access := DirAccess.open(gender_path)
	if access == null:
		return result

	access.list_dir_begin()
	var file := access.get_next()
	while not file.is_empty():
		if not access.current_is_dir() and file.get_extension().to_lower() == "glb":
			var glb_path := gender_path.path_join(file)
			result.append({
				"race": race,
				"gender": gender,
				"name": file.get_basename(),
				"path": glb_path,
				"textures_dir": gender_path.path_join(file.get_basename() + "_textures"),
			})
		file = access.get_next()
	access.list_dir_end()
	result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return String(a["name"]) < String(b["name"]))
	return result


func _build_ui() -> void:
	set_anchors_preset(Control.PRESET_FULL_RECT)

	var root := HSplitContainer.new()
	root.name = "Layout"
	root.split_offset = 780
	root.set_anchors_preset(Control.PRESET_FULL_RECT)
	add_child(root)

	var viewport_container := SubViewportContainer.new()
	viewport_container.name = "Preview"
	viewport_container.stretch = true
	viewport_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	viewport_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
	root.add_child(viewport_container)

	_viewport = SubViewport.new()
	_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
	_viewport.size = Vector2i(900, 720)
	viewport_container.add_child(_viewport)
	_build_preview_world()

	var panel := PanelContainer.new()
	panel.name = "Controls"
	panel.custom_minimum_size = Vector2(360, 0)
	root.add_child(panel)

	var scroll := ScrollContainer.new()
	scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
	panel.add_child(scroll)

	_control_container = VBoxContainer.new()
	_control_container.add_theme_constant_override("separation", 8)
	scroll.add_child(_control_container)

	var title := Label.new()
	title.text = "Character Creator"
	title.add_theme_font_size_override("font_size", 22)
	_control_container.add_child(title)

	_race_option = _add_option("Race", _on_race_selected)
	_gender_option = _add_option("Gender", _on_gender_selected)
	_model_option = _add_option("Model", _on_model_selected)
	_class_option = _add_option("Class", _on_class_selected)
	for class_label in CLASS_IDS.keys():
		_class_option.add_item(class_label)
		_class_option.set_item_metadata(_class_option.item_count - 1, int(CLASS_IDS[class_label]))

	_skin_spin = _add_spin("Skin", 0, 0, 1, _on_skin_changed)
	_face_spin = _add_spin("Face", 0, 0, 1, _on_face_changed)
	_hair_color_spin = _add_spin("Hair Color", 0, 15, 1, _on_hair_color_changed)
	_flags_spin = _add_spin("Flags", 0, 31, 1, _on_flags_changed)

	for spec in GEOSET_CONTROLS:
		var option := _add_option(String(spec["label"]), _on_geoset_selected.bind(String(spec["cat"])))
		_geoset_options[String(spec["cat"])] = option

	var equipment_label := Label.new()
	equipment_label.text = "Equipment Geosets"
	equipment_label.add_theme_font_size_override("font_size", 16)
	_control_container.add_child(equipment_label)

	var toggle_grid := GridContainer.new()
	toggle_grid.columns = 2
	_control_container.add_child(toggle_grid)
	for spec in TOGGLE_CONTROLS:
		var check := CheckBox.new()
		check.text = String(spec["label"])
		check.toggled.connect(_on_toggle_changed.bind(String(spec["property"])))
		toggle_grid.add_child(check)
		_toggle_checks[String(spec["property"])] = check

	_rotation_slider = _add_slider("Rotation", -180.0, 180.0, 0.0)

	var reload := Button.new()
	reload.text = "Rescan Models"
	reload.pressed.connect(_on_rescan_pressed)
	_control_container.add_child(reload)

	var starter := Button.new()
	starter.text = "Apply Starter Outfit"
	starter.pressed.connect(_apply_starter_outfit)
	_control_container.add_child(starter)

	_status_label = Label.new()
	_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	_status_label.text = ""
	_control_container.add_child(_status_label)


func _build_preview_world() -> void:
	var world := Node3D.new()
	world.name = "PreviewWorld"
	_viewport.add_child(world)

	var env := WorldEnvironment.new()
	var environment := Environment.new()
	environment.background_mode = Environment.BG_COLOR
	environment.background_color = Color(0.04, 0.045, 0.055)
	environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
	environment.ambient_light_color = Color(0.55, 0.58, 0.62)
	environment.ambient_light_energy = 1.0
	env.environment = environment
	world.add_child(env)

	var key_light := DirectionalLight3D.new()
	key_light.rotation_degrees = Vector3(-35, -35, 0)
	key_light.light_energy = 2.4
	world.add_child(key_light)

	var fill_light := OmniLight3D.new()
	fill_light.position = Vector3(-2.5, 1.8, 2.2)
	fill_light.light_energy = 1.2
	fill_light.omni_range = 5.0
	world.add_child(fill_light)

	_preview_anchor = Node3D.new()
	_preview_anchor.name = "CharacterAnchor"
	world.add_child(_preview_anchor)

	_camera = Camera3D.new()
	_camera.name = "Camera"
	_camera.position = Vector3(0, 1.25, 3.2)
	_camera.rotation_degrees = Vector3(-7, 0, 0)
	_camera.fov = 38.0
	_camera.current = true
	world.add_child(_camera)


func _add_option(label_text: String, callback: Callable) -> OptionButton:
	var label := Label.new()
	label.text = label_text
	_control_container.add_child(label)
	var option := OptionButton.new()
	option.item_selected.connect(callback)
	_control_container.add_child(option)
	return option


func _add_spin(label_text: String, min_value: float, max_value: float, step: float, callback: Callable) -> SpinBox:
	var label := Label.new()
	label.text = label_text
	_control_container.add_child(label)
	var spin := SpinBox.new()
	spin.min_value = min_value
	spin.max_value = max_value
	spin.step = step
	spin.value_changed.connect(callback)
	_control_container.add_child(spin)
	return spin


func _add_slider(label_text: String, min_value: float, max_value: float, value: float) -> HSlider:
	var label := Label.new()
	label.text = label_text
	_control_container.add_child(label)
	var slider := HSlider.new()
	slider.min_value = min_value
	slider.max_value = max_value
	slider.step = 1.0
	slider.value = value
	_control_container.add_child(slider)
	return slider


func _populate_races() -> void:
	_race_option.clear()
	for race in _race_names:
		_race_option.add_item(_display_name(race))
		_race_option.set_item_metadata(_race_option.item_count - 1, race)


func _select_race(race: String) -> void:
	_selected_race = race
	_gender_option.clear()
	var genders: Array[String] = []
	for gender in (_models.get(race, {}) as Dictionary).keys():
		genders.append(String(gender))
	genders.sort()
	for gender in genders:
		_gender_option.add_item(_display_name(gender))
		_gender_option.set_item_metadata(_gender_option.item_count - 1, gender)
	if not genders.is_empty():
		_select_gender(genders[0])


func _select_gender(gender: String) -> void:
	_selected_gender = gender
	_model_option.clear()
	var models: Array = (_models.get(_selected_race, {}) as Dictionary).get(gender, [])
	for model in models:
		_model_option.add_item(String(model["name"]))
		_model_option.set_item_metadata(_model_option.item_count - 1, model)
	if not models.is_empty():
		_select_model(models[0])


func _select_model(model: Dictionary) -> void:
	_selected_model = model
	_load_model(model)


func _load_model(model: Dictionary) -> void:
	if _model_root and is_instance_valid(_model_root):
		_model_root.queue_free()
	_model_root = null
	_controller = null
	_compositor = null

	var scene: PackedScene = load(String(model["path"]))
	if scene == null:
		_status_label.text = "Failed to load model: %s" % String(model["path"])
		return

	_model_root = Node3D.new()
	_model_root.name = "%s_%s" % [String(model["race"]), String(model["gender"])]
	_model_root.set_script(GEOSET_CONTROLLER_SCRIPT)
	_model_root.rotation_degrees.y = CHARACTER_YAW_OFFSET_DEGREES

	var instance := scene.instantiate()
	instance.name = String(model["name"])
	_model_root.add_child(instance)

	var compositor := Node.new()
	compositor.name = "CharacterTextureCompositor"
	compositor.set_script(TEXTURE_COMPOSITOR_SCRIPT)
	compositor.set("textures_dir", String(model["textures_dir"]))
	compositor.set("extracted_dir", EXTRACTED_DIR)
	_model_root.add_child(compositor)

	_preview_anchor.add_child(_model_root)
	_controller = _model_root
	_compositor = compositor
	_fit_camera_to_model()
	await get_tree().process_frame
	_refresh_dynamic_controls()
	_apply_all_controls()
	_apply_starter_outfit()
	_update_status()


func _fit_camera_to_model() -> void:
	if _model_root == null:
		return
	var aabb := _calculate_aabb(_model_root)
	if aabb.size == Vector3.ZERO:
		_camera.position = Vector3(0, 1.25, 3.2)
		return
	var center := aabb.get_center()
	_model_root.position = Vector3(-center.x, -aabb.position.y, -center.z)
	var height := maxf(aabb.size.y, 1.0)
	_camera.position = Vector3(0, height * 0.55, height * 1.85)
	_camera.look_at(Vector3(0, height * 0.52, 0), Vector3.UP)


func _calculate_aabb(root: Node) -> AABB:
	var result := AABB()
	var has_aabb := false
	for mesh_inst in _iter_mesh_instances(root):
		if mesh_inst.mesh == null:
			continue
		var local_aabb := mesh_inst.mesh.get_aabb()
		var global_aabb := mesh_inst.global_transform * local_aabb
		if not has_aabb:
			result = global_aabb
			has_aabb = true
		else:
			result = result.merge(global_aabb)
	return result if has_aabb else AABB()


func _iter_mesh_instances(root: Node) -> Array[MeshInstance3D]:
	var result: Array[MeshInstance3D] = []
	_collect_mesh_instances(root, result)
	return result


func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
	if node is MeshInstance3D:
		result.append(node)
	for child in node.get_children():
		_collect_mesh_instances(child, result)


func _refresh_dynamic_controls() -> void:
	if _controller == null:
		return

	_skin_spin.max_value = maxi(0, int(_controller.call("get_skin_color_count")) - 1)
	_face_spin.max_value = maxi(0, int(_controller.call("get_face_style_count")) - 1)
	_skin_spin.value = clampf(_skin_spin.value, _skin_spin.min_value, _skin_spin.max_value)
	_face_spin.value = clampf(_face_spin.value, _face_spin.min_value, _face_spin.max_value)

	for spec in GEOSET_CONTROLS:
		var cat := String(spec["cat"])
		var option: OptionButton = _geoset_options[cat]
		option.clear()
		var ids: Array = _controller.call("get_ids", cat)
		for id in ids:
			option.add_item(str(id))
			option.set_item_metadata(option.item_count - 1, int(id))
		option.disabled = ids.is_empty()
		if not ids.is_empty():
			option.select(0)


func _apply_all_controls() -> void:
	_on_skin_changed(_skin_spin.value)
	_on_face_changed(_face_spin.value)
	_on_hair_color_changed(_hair_color_spin.value)
	_on_flags_changed(_flags_spin.value)
	for cat in _geoset_options.keys():
		var option: OptionButton = _geoset_options[cat]
		if option.item_count > 0:
			_apply_geoset(cat, int(option.get_item_metadata(option.selected)))
	for property in _toggle_checks.keys():
		var check: CheckBox = _toggle_checks[property]
		_set_controller_property(property, check.button_pressed)


func _on_race_selected(index: int) -> void:
	_select_race(String(_race_option.get_item_metadata(index)))


func _on_gender_selected(index: int) -> void:
	_select_gender(String(_gender_option.get_item_metadata(index)))


func _on_model_selected(index: int) -> void:
	_select_model(_model_option.get_item_metadata(index))


func _on_class_selected(_index: int) -> void:
	_apply_starter_outfit()
	_update_status()


func _on_skin_changed(value: float) -> void:
	_set_controller_property("skin_color", int(value))
	_update_status()


func _on_face_changed(value: float) -> void:
	_set_controller_property("face_style", int(value))
	_update_status()


func _on_hair_color_changed(_value: float) -> void:
	_update_status()


func _on_flags_changed(_value: float) -> void:
	_update_status()


func _on_geoset_selected(index: int, cat: String) -> void:
	var option: OptionButton = _geoset_options[cat]
	if index < 0 or index >= option.item_count:
		return
	_apply_geoset(cat, int(option.get_item_metadata(index)))
	_update_status()


func _on_toggle_changed(pressed: bool, property: String) -> void:
	_set_controller_property(property, pressed)


func _on_rescan_pressed() -> void:
	_scan_models()
	_populate_races()
	if _models.has(_selected_race):
		_select_race(_selected_race)
	elif not _race_names.is_empty():
		_select_race(_race_names[0])


func _apply_starter_outfit() -> void:
	_current_outfit.clear()
	if _outfit_resolver == null or _controller == null or _compositor == null:
		return
	var class_id := int(_class_option.get_item_metadata(maxi(0, _class_option.selected)))
	var outfit: Dictionary = _outfit_resolver.call(
		"resolve_start_outfit",
		_selected_race,
		_selected_gender,
		class_id)
	if outfit.is_empty():
		_update_status()
		return
	_current_outfit = outfit
	if _compositor.has_method("set_equipment_components"):
		_compositor.call("set_equipment_components", outfit.get("textures", PackedStringArray()))
	if _controller.has_method("apply_outfit_defaults"):
		_controller.call("apply_outfit_defaults", outfit)
	_sync_outfit_controls()
	_update_status()


func _sync_outfit_controls() -> void:
	if _controller == null:
		return
	for property in _toggle_checks.keys():
		var check: CheckBox = _toggle_checks[property]
		check.set_pressed_no_signal(bool(_controller.get(property)))
	var wrist: int = int(_controller.get("wristband_style"))
	if _geoset_options.has("wristbands"):
		_select_option_by_metadata(_geoset_options["wristbands"], wrist)


func _apply_geoset(cat: String, id: int) -> void:
	match cat:
		"hair":
			_set_controller_property("hair_style", id)
		"facial1":
			_set_controller_property("facial1", id)
		"facial2":
			_set_controller_property("facial2", id)
		"facial3":
			_set_controller_property("facial3", id)
		"ears":
			_set_controller_property("ears", id)
		"eye_effects":
			_set_controller_property("eye_effects", id)
		"wristbands":
			_set_controller_property("wristband_style", id)


func _set_controller_property(property: String, value: Variant) -> void:
	if _controller == null:
		return
	_controller.set(property, value)


func _update_status() -> void:
	if _status_label == null:
		return
	var packed := _pack_appearance()
	var equipment := _pack_equipment()
	var outfit_display_count := 0
	var outfit_texture_count := 0
	if not _current_outfit.is_empty():
		outfit_display_count = (_current_outfit.get("display_ids", PackedInt32Array()) as PackedInt32Array).size()
		for rel_path in (_current_outfit.get("textures", PackedStringArray()) as PackedStringArray):
			if not String(rel_path).is_empty():
				outfit_texture_count += 1
	_status_label.text = "Appearance 0x%08X\nEquipment  0x%08X\nStarter outfit: %d displays, %d textures\n%s / %s / %s" % [
		packed,
		equipment,
		outfit_display_count,
		outfit_texture_count,
		_display_name(_selected_race),
		_display_name(_selected_gender),
		String(_selected_model.get("name", "")),
	]


func _pack_appearance() -> int:
	var skin := int(_skin_spin.value) & 0x1f
	var face := int(_face_spin.value) & 0x1f
	var hair := _selected_local_geoset_id("hair", 1) & 0x1f
	var hair_color := int(_hair_color_spin.value) & 0x0f
	var facial := _selected_local_geoset_id("facial1", 101) & 0x0f
	var class_id := int(_class_option.get_item_metadata(maxi(0, _class_option.selected))) & 0x0f
	var flags := int(_flags_spin.value) & 0x1f
	return skin | (face << 5) | (hair << 10) | (hair_color << 15) | (facial << 19) | (class_id << 23) | (flags << 27)


func _pack_equipment() -> int:
	var upper := 1 if bool((_toggle_checks.get("show_chest") as CheckBox).button_pressed) else 0
	var lower := 1 if bool((_toggle_checks.get("show_pants") as CheckBox).button_pressed) else 0
	var hands := 1 if bool((_toggle_checks.get("show_gloves") as CheckBox).button_pressed) else 0
	var feet := 1 if bool((_toggle_checks.get("show_boots") as CheckBox).button_pressed) else 0
	return upper | (lower << 8) | (hands << 16) | (feet << 24)


func _selected_local_geoset_id(cat: String, base_id: int) -> int:
	if not _geoset_options.has(cat):
		return 0
	var option: OptionButton = _geoset_options[cat]
	if option.item_count <= 0 or option.selected < 0:
		return 0
	return maxi(0, int(option.get_item_metadata(option.selected)) - base_id)


func _select_option_by_metadata(option: OptionButton, metadata: int) -> void:
	for i in option.item_count:
		if int(option.get_item_metadata(i)) == metadata:
			option.select(i)
			return


func _display_name(value: String) -> String:
	var s := value.replace("_", " ").to_lower()
	var parts := s.split(" ", false)
	for i in parts.size():
		parts[i] = String(parts[i]).capitalize()
	return " ".join(parts)
