extends RefCounted

const SLOT_UPPER_ARM := 0
const SLOT_LOWER_ARM := 1
const SLOT_HAND := 2
const SLOT_UPPER_TORSO := 3
const SLOT_LOWER_TORSO := 4
const SLOT_UPPER_LEG := 5
const SLOT_LOWER_LEG := 6
const SLOT_FOOT := 7
const SLOT_COUNT := 8

const COMPONENT_FOLDERS := [
	"ArmUpperTexture",
	"ArmLowerTexture",
	"HandTexture",
	"TorsoUpperTexture",
	"TorsoLowerTexture",
	"LegUpperTexture",
	"LegLowerTexture",
	"FootTexture",
]

const RACE_IDS := {
	"human": 1,
	"orc": 2,
	"dwarf": 3,
	"nightelf": 4,
	"scourge": 5,
	"undead": 5,
	"tauren": 6,
	"gnome": 7,
	"troll": 8,
	"bloodelf": 10,
	"draenei": 11,
}

var extracted_dir := "res://data/extracted"
var _char_start := {}
var _item_display := {}
var _item_display_by_id := {}
var _texture_cache := {}


func load_dbcs(p_extracted_dir: String) -> bool:
	extracted_dir = p_extracted_dir.trim_suffix("/")
	var base := extracted_dir.path_join("DBFilesClient")
	_char_start = _load_wdbc(base.path_join("CharStartOutfit.dbc"))
	_item_display = _load_wdbc(base.path_join("ItemDisplayInfo.dbc"))
	_index_item_display()
	return not _char_start.is_empty() and not _item_display.is_empty()


func resolve_start_outfit(race_name: String, gender_name: String, class_id: int) -> Dictionary:
	if _char_start.is_empty() or _item_display.is_empty():
		return {}
	var race_id := race_id_for_name(race_name)
	var gender_id := gender_id_for_name(gender_name)
	if race_id <= 0 or gender_id < 0:
		return {}

	var wanted_key := race_id | ((class_id & 0xff) << 8) | ((gender_id & 0xff) << 16)
	for i in int(_char_start["records"]):
		if int(_dbc_u32(_char_start, i, 1)) != wanted_key:
			continue
		return _resolve_start_record(i, gender_id)
	for i in int(_char_start["records"]):
		var race_class_gender := int(_dbc_u32(_char_start, i, 1))
		if (race_class_gender & 0xff) == race_id and ((race_class_gender >> 16) & 0xff) == gender_id:
			return _resolve_start_record(i, gender_id)
	return {}


func _resolve_start_record(record: int, gender_id: int) -> Dictionary:
	var display_start := _char_start_display_field()
	var display_count := mini(24, int(_char_start["fields"]) - display_start)
	var display_ids := PackedInt32Array()
	for n in display_count:
		var display_id := int(_dbc_u32(_char_start, record, display_start + n))
		if display_id > 0 and display_id != 0xffffffff:
			display_ids.append(display_id)
	var outfit := resolve_display_ids(display_ids, gender_id)
	outfit["source_record"] = record
	outfit["source_class"] = (int(_dbc_u32(_char_start, record, 1)) >> 8) & 0xff
	return outfit


func resolve_display_ids(display_ids: PackedInt32Array, gender_id: int) -> Dictionary:
	var textures := PackedStringArray()
	textures.resize(SLOT_COUNT)
	var geosets := PackedInt32Array()
	geosets.resize(3)
	var flags := 0
	var used_display_ids := PackedInt32Array()

	var texture_base := _item_display_texture_base()
	var geoset_base := _item_display_geoset_base()
	var flags_field := _item_display_flags_field()
	for display_id in display_ids:
		var record := int(_item_display_by_id.get(display_id, -1))
		if record < 0:
			continue
		used_display_ids.append(display_id)
		for i in 3:
			var geoset_group := int(_dbc_u32(_item_display, record, geoset_base + i))
			if geoset_group != 0:
				geosets[i] = geoset_group
		flags |= int(_dbc_u32(_item_display, record, flags_field))
		for slot in SLOT_COUNT:
			var stem := _dbc_string(_item_display, record, texture_base + slot)
			if stem.is_empty():
				continue
			var rel_path := component_texture_path(stem, slot, gender_id)
			if not rel_path.is_empty():
				textures[slot] = rel_path

	return {
		"display_ids": used_display_ids,
		"textures": textures,
		"geosets": geosets,
		"flags": flags,
	}


func component_texture_path(stem: String, slot: int, gender_id: int) -> String:
	if stem.is_empty() or slot < 0 or slot >= SLOT_COUNT:
		return ""
	var suffix := "F" if gender_id == 1 else "M"
	var folder := String(COMPONENT_FOLDERS[slot])
	var candidates := [
		"ITEM/TEXTURECOMPONENTS/%s/%s_%s.blp" % [folder, stem, suffix],
		"Item/TextureComponents/%s/%s_%s.blp" % [folder, stem, suffix],
		"ITEM/TEXTURECOMPONENTS/%s/%s_U.blp" % [folder, stem],
		"Item/TextureComponents/%s/%s_U.blp" % [folder, stem],
	]
	for rel_path in candidates:
		if _texture_exists(rel_path):
			return rel_path
	return candidates[0]


func race_id_for_name(race_name: String) -> int:
	var normalized := race_name.replace("_", "").replace(" ", "").to_lower()
	return int(RACE_IDS.get(normalized, 0))


func gender_id_for_name(gender_name: String) -> int:
	var normalized := gender_name.to_lower()
	if normalized == "male" or normalized == "m":
		return 0
	if normalized == "female" or normalized == "f":
		return 1
	return -1


func infer_race_gender_from_model_path(model_path: String) -> Dictionary:
	var parts := model_path.replace("\\", "/").split("/", false)
	for i in range(parts.size() - 2):
		if String(parts[i]).to_lower() == "characters":
			return {"race": String(parts[i + 1]), "gender": String(parts[i + 2])}
	return {}


func _index_item_display() -> void:
	_item_display_by_id.clear()
	if _item_display.is_empty():
		return
	for i in int(_item_display["records"]):
		var display_id := int(_dbc_u32(_item_display, i, 0))
		if display_id > 0:
			_item_display_by_id[display_id] = i


func _char_start_display_field() -> int:
	if int(_char_start.get("fields", 0)) >= 77:
		return 26
	return 14


func _item_display_texture_base() -> int:
	return 15 if int(_item_display.get("fields", 0)) >= 25 else 14


func _item_display_geoset_base() -> int:
	return 7 if int(_item_display.get("fields", 0)) >= 25 else 6


func _item_display_flags_field() -> int:
	return 10 if int(_item_display.get("fields", 0)) >= 25 else 9


func _texture_exists(rel_path: String) -> bool:
	if _texture_cache.has(rel_path):
		return bool(_texture_cache[rel_path])
	var exists := FileAccess.file_exists(ProjectSettings.globalize_path(extracted_dir.path_join(rel_path)))
	_texture_cache[rel_path] = exists
	return exists


func _load_wdbc(path: String) -> Dictionary:
	var abs_path := ProjectSettings.globalize_path(path)
	if not FileAccess.file_exists(abs_path):
		return {}
	var file := FileAccess.open(abs_path, FileAccess.READ)
	if not file:
		return {}
	var bytes := file.get_buffer(file.get_length())
	if bytes.size() < 20 or bytes[0] != 0x57 or bytes[1] != 0x44 or bytes[2] != 0x42 or bytes[3] != 0x43:
		return {}
	var records := int(bytes.decode_u32(4))
	var fields := int(bytes.decode_u32(8))
	var record_size := int(bytes.decode_u32(12))
	var string_size := int(bytes.decode_u32(16))
	var required := 20 + records * record_size + string_size
	if records < 0 or fields <= 0 or record_size <= 0 or required > bytes.size():
		return {}
	return {
		"bytes": bytes,
		"records": records,
		"fields": fields,
		"record_size": record_size,
		"records_offset": 20,
		"strings_offset": 20 + records * record_size,
		"string_size": string_size,
	}


func _dbc_u32(dbc: Dictionary, record: int, field: int) -> int:
	if record < 0 or record >= int(dbc["records"]) or field < 0:
		return 0
	var record_size := int(dbc["record_size"])
	var field_offset := field * 4
	if field_offset + 4 > record_size:
		return 0
	var bytes: PackedByteArray = dbc["bytes"]
	return int(bytes.decode_u32(int(dbc["records_offset"]) + record * record_size + field_offset))


func _dbc_string(dbc: Dictionary, record: int, field: int) -> String:
	var offset := _dbc_u32(dbc, record, field)
	var string_size := int(dbc.get("string_size", 0))
	if offset <= 0 or offset >= string_size:
		return ""
	var bytes: PackedByteArray = dbc["bytes"]
	var pos := int(dbc["strings_offset"]) + offset
	var end := pos
	var max_end := int(dbc["strings_offset"]) + string_size
	while end < max_end and bytes[end] != 0:
		end += 1
	if end <= pos:
		return ""
	return bytes.slice(pos, end).get_string_from_utf8()
