extends RefCounted
class_name WowWMOMaterial

const WMOBLEND_OPAQUE := 0
const WMOBLEND_ALPHA_KEY := 1
const WMOBLEND_ALPHA := 2
const SHADER_VERSION := 10

static var _shader_cache: Dictionary = {}


static func clear_cache() -> void:
	_shader_cache.clear()


static func _ensure_wow_shader_globals() -> void:
	pass


static func build(
		texture: Texture2D,
		texture2: Texture2D,
		material_flags: int,
		shader_id: int,
		blend_mode: int,
		texture_path: String = "",
		texture2_path: String = "",
		diffuse_color: Color = Color.WHITE,
		emissive_color: Color = Color.BLACK,
		secondary_color: Color = Color.WHITE,
		texture3: Texture2D = null,
		texture3_path: String = "") -> ShaderMaterial:

	var is_window_material := _is_window_texture(texture_path)
	var is_glass_blend_material := is_window_material and blend_mode == WMOBLEND_ALPHA
	var mode := _blend_shader_mode(blend_mode, is_glass_blend_material)
	var effective_diffuse := diffuse_color
	if maxf(maxf(effective_diffuse.r, effective_diffuse.g), effective_diffuse.b) <= 0.001:
		effective_diffuse = Color.WHITE

	_ensure_wow_shader_globals()
	var mat := ShaderMaterial.new()
	mat.resource_name = texture_path.get_file().get_basename()
	var pixel_shader := _wmo_pixel_shader_id(shader_id)
	mat.shader = _get_shader(mode, texture2 != null, texture3 != null, is_window_material)
	mat.render_priority = 0 if blend_mode <= WMOBLEND_ALPHA_KEY else 1
	mat.set_shader_parameter("albedo_tex", texture)
	mat.set_shader_parameter("detail_tex", texture2)
	mat.set_shader_parameter("extra_tex", texture3)
	mat.set_shader_parameter("use_texture", texture != null)
	mat.set_shader_parameter("use_detail_texture", texture2 != null)
	mat.set_shader_parameter("use_extra_texture", texture3 != null)
	mat.set_shader_parameter("pixel_shader", pixel_shader)
	mat.set_shader_parameter("alpha_ref", _alpha_ref(blend_mode, pixel_shader))
	mat.set_shader_parameter("alpha_output_strength", _alpha_output_strength(blend_mode, pixel_shader))
	mat.set_shader_parameter("vertex_color_strength", 0.72)
	mat.set_shader_parameter("ambient_floor", 0.22)
	mat.set_shader_parameter("unlit_material", 1.0 if (material_flags & 0x01) != 0 else 0.0)
	mat.set_shader_parameter("diffuse_color", effective_diffuse)
	mat.set_shader_parameter("emissive_color", emissive_color)
	mat.set_shader_parameter("material_emissive_strength", _material_emissive_strength(material_flags, emissive_color, texture_path))
	mat.set_shader_parameter("secondary_color", _effective_secondary_color(secondary_color))
	mat.set_shader_parameter("detail_strength", 1.0 if texture2 != null else 0.0)
	mat.set_shader_parameter("window_material", 1.0 if is_glass_blend_material else 0.0)
	mat.set_meta("texture0_path", texture_path)
	mat.set_meta("texture1_path", texture2_path)
	mat.set_meta("texture2_path", texture3_path)
	mat.set_meta("wow_flags", material_flags)
	mat.set_meta("wow_shader", shader_id)
	mat.set_meta("wow_wmo_pixel_shader", pixel_shader)
	mat.set_meta("wow_blend_mode", blend_mode)
	mat.set_meta("wow_wmo_window_material", is_window_material)
	mat.set_meta("wow_wmo_glass_blend_material", is_glass_blend_material)
	return mat


static func _wmo_pixel_shader_id(shader_id: int) -> int:
	match shader_id:
		0:
			return 0 # Diffuse
		1:
			return 1 # Specular
		2:
			return 2 # Metal
		3:
			return 3 # Env
		4:
			return 4 # Opaque
		5:
			return 5 # EnvMetal
		6:
			return 6 # TwoLayerDiffuse
		7:
			return 7 # TwoLayerEnvMetal
		8:
			return 8 # TwoLayerTerrain
		9:
			return 9 # DiffuseEmissive
		11:
			return 10 # MaskedEnvMetal
		12:
			return 11 # EnvMetalEmissive
		13:
			return 12 # TwoLayerDiffuseOpaque
		15:
			return 13 # TwoLayerDiffuseEmissive
		16:
			return 0 # DiffuseTerrain
		17:
			return 14 # AdditiveMaskedEnvMetal
		18:
			return 15 # TwoLayerDiffuseMod2x
		19:
			return 16 # TwoLayerDiffuseMod2xNA
		20:
			return 17 # TwoLayerDiffuseAlpha
		21:
			return 18 # Lod
		22:
			return 19 # Parallax
		23:
			return 20 # DF shader
		_:
			return 0


static func _shader_alpha_is_opacity(pixel_shader: int) -> bool:
	match pixel_shader:
		0, 1, 2, 4, 18, 19:
			return true
		_:
			return false


static func _alpha_ref(blend_mode: int, pixel_shader: int) -> float:
	if blend_mode == WMOBLEND_ALPHA_KEY:
		return 0.501960814
	return 0.0


static func _alpha_output_strength(blend_mode: int, pixel_shader: int) -> float:
	if blend_mode == WMOBLEND_ALPHA and _shader_alpha_is_opacity(pixel_shader):
		return 1.0
	return 0.0


static func _effective_secondary_color(color: Color) -> Color:
	if maxf(maxf(color.r, color.g), color.b) <= 0.001:
		return Color.WHITE
	return color


static func _material_emissive_strength(material_flags: int, color: Color, texture_path: String) -> float:
	var color_strength := maxf(maxf(color.r, color.g), color.b) * color.a
	if color_strength <= 0.001:
		return 0.0
	var strength := 0.12
	if (material_flags & 0x10) != 0:
		strength = 0.26
	if _is_window_texture(texture_path):
		strength = minf(strength, 0.18)
	return strength


static func _is_window_texture(texture_path: String) -> bool:
	var lower_path := texture_path.to_lower().replace("\\", "/")
	var file_name := lower_path.get_file()
	return lower_path.contains("/windows/") \
		or lower_path.contains("/glass/") \
		or file_name.contains("window") \
		or file_name.contains("glass") \
		or file_name.contains("_wnd_")


static func _blend_shader_mode(blend_mode: int, is_glass_blend_material: bool) -> String:
	if is_glass_blend_material:
		return "glass"
	match blend_mode:
		WMOBLEND_ALPHA_KEY:
			return "cutout"
		WMOBLEND_ALPHA:
			return "alpha"
		_:
			return "opaque"


static func _get_shader(mode: String, has_detail: bool, has_extra: bool, stable_albedo_sampler: bool = false) -> Shader:
	var key := "v=%d|%s|detail=%s|extra=%s|stable_albedo=%s" % [SHADER_VERSION, mode, str(has_detail), str(has_extra), str(stable_albedo_sampler)]
	if _shader_cache.has(key):
		return _shader_cache[key]

	var shader := Shader.new()
	shader.code = _shader_code(mode, has_detail, has_extra, stable_albedo_sampler)
	_shader_cache[key] = shader
	return shader


static func _shader_code(mode: String, has_detail: bool, has_extra: bool, stable_albedo_sampler: bool = false) -> String:
	var render_modes: Array[String] = ["blend_mix", "unshaded"]
	render_modes.append("cull_back" if mode == "glass" or stable_albedo_sampler else "cull_disabled")
	if mode == "opaque" or mode == "cutout":
		render_modes.append("depth_draw_opaque")
	else:
		render_modes.append("depth_draw_never")
	var albedo_sampler_hint := "filter_linear, repeat_disable" if stable_albedo_sampler else "filter_linear_mipmap_anisotropic, repeat_enable"

	return """
shader_type spatial;
render_mode %s;

uniform sampler2D albedo_tex : source_color, %s;
uniform sampler2D detail_tex : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
uniform sampler2D extra_tex : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
uniform bool use_texture = false;
uniform bool use_detail_texture = false;
uniform bool use_extra_texture = false;
uniform int pixel_shader = 0;
uniform float alpha_ref = 0.0;
uniform float alpha_output_strength = 0.0;
uniform float vertex_color_strength = 0.88;
uniform float ambient_floor = 0.20;
uniform float unlit_material = 0.0;
uniform vec4 diffuse_color : source_color = vec4(1.0);
uniform vec4 emissive_color : source_color = vec4(0.0);
uniform float material_emissive_strength = 0.0;
uniform vec4 secondary_color : source_color = vec4(1.0);
uniform float detail_strength = 0.0;
uniform float window_material = 0.0;

global uniform vec4 wow_ambient_color;
global uniform vec4 wow_light_color;
global uniform vec3 wow_light_dir;
global uniform vec4 wow_fog_color;
global uniform vec2 wow_fog_range;
global uniform float wow_fog_density;
global uniform float wow_sun_elevation;

varying vec3 world_normal;
varying vec3 world_pos;
varying vec3 view_pos;
varying vec3 view_normal;

void vertex() {
	world_normal = normalize(MODEL_NORMAL_MATRIX * NORMAL);
	world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
	view_pos = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
	view_normal = normalize((MODELVIEW_MATRIX * vec4(NORMAL, 0.0)).xyz);
}

vec3 apply_wow_fog(vec3 color) {
	float dist = length(view_pos);
	float range_fog = smoothstep(wow_fog_range.x, wow_fog_range.y, dist);
	float fog_amount = range_fog * clamp(wow_fog_density, 0.0, 0.55);
	return mix(color, wow_fog_color.rgb, fog_amount);
}

float color_luma(vec3 color) {
	return dot(color, vec3(0.2126, 0.7152, 0.0722));
}

vec3 textured_material_tint(vec3 color) {
	float luma = max(color_luma(color), 0.001);
	vec3 hue = clamp(color / luma, vec3(0.58), vec3(1.20));
	return mix(vec3(1.0), hue, 0.28);
}

vec3 untextured_material_color(vec3 color) {
	float luma = color_luma(color);
	vec3 muted = mix(vec3(luma), color, 0.42);
	return clamp(muted * 0.82, vec3(0.08), vec3(0.72));
}

void resolve_wmo_pixel(vec4 tex1, vec4 tex2, vec4 tex3, bool has_detail, bool has_extra, out vec3 diffuse, out vec3 emissive) {
	float layer_weight = clamp(secondary_color.a, 0.0, 1.0);
	diffuse = tex1.rgb;
	emissive = vec3(0.0);
	if (!has_detail) {
		return;
	}

	if (pixel_shader == 3) {
		emissive = tex2.rgb * tex1.a;
	} else if (pixel_shader == 5) {
		emissive = (tex1.rgb * tex1.a) * tex2.rgb;
	} else if (pixel_shader == 6) {
		vec3 layer2 = mix(tex1.rgb, tex2.rgb, tex2.a);
		diffuse = mix(layer2, tex1.rgb, layer_weight);
	} else if (pixel_shader == 8 || pixel_shader == 12) {
		diffuse = mix(tex2.rgb, tex1.rgb, layer_weight);
	} else if (pixel_shader == 9) {
		emissive = tex2.rgb * tex2.a * layer_weight;
	} else if (pixel_shader == 10 && has_extra) {
		float mix_factor = clamp(tex3.a * layer_weight, 0.0, 1.0);
		diffuse = mix(mix(tex1.rgb * tex2.rgb * 2.0, tex3.rgb, mix_factor), tex1.rgb, tex1.a);
	} else if (pixel_shader == 11 && has_extra) {
		emissive = ((tex1.rgb * tex1.a) * tex2.rgb) + ((tex3.rgb * tex3.a) * layer_weight);
	} else if (pixel_shader == 13) {
		vec3 layer = tex2.rgb * (1.0 - tex2.a);
		diffuse = mix(layer, tex1.rgb, layer_weight);
		emissive = (tex2.rgb * tex2.a) * (1.0 - layer_weight);
	} else if (pixel_shader == 14 && has_extra) {
		diffuse = mix(
			(tex1.rgb * tex2.rgb * 2.0) + (tex3.rgb * clamp(tex3.a * layer_weight, 0.0, 1.0)),
			tex1.rgb,
			tex1.a
		);
	} else if (pixel_shader == 15) {
		vec3 layer2 = mix(tex1.rgb, tex2.rgb, tex2.a);
		vec3 layer3 = mix(layer2, tex1.rgb, layer_weight);
		diffuse = has_extra ? layer3 * tex3.rgb * 2.0 : layer3;
	} else if (pixel_shader == 16) {
		vec3 mod_layer = tex1.rgb * tex2.rgb * 2.0;
		diffuse = mix(tex1.rgb, mod_layer, layer_weight);
	} else if (pixel_shader == 17) {
		vec3 layer2 = mix(tex1.rgb, tex2.rgb, tex2.a);
		vec3 layer3 = mix(layer2, tex1.rgb, has_extra ? tex3.a : layer_weight);
		diffuse = has_extra ? layer3 * tex3.rgb * 2.0 : layer3;
	}
}

void fragment() {
	vec4 tex1 = use_texture ? texture(albedo_tex, UV) : vec4(1.0);
	vec4 tex2 = use_detail_texture ? texture(detail_tex, UV) : vec4(1.0);
	vec4 tex3 = use_extra_texture ? texture(extra_tex, UV) : vec4(1.0);
	if (alpha_ref > 0.0 && tex1.a < alpha_ref) {
		discard;
	}

	vec3 mat_diffuse;
	vec3 shader_emissive;
	resolve_wmo_pixel(tex1, tex2, tex3, use_detail_texture, use_extra_texture, mat_diffuse, shader_emissive);

	vec3 baked = max(COLOR.rgb, vec3(ambient_floor));
	vec3 baked_lit = mix(vec3(1.0), baked, clamp(vertex_color_strength, 0.0, 1.0));
	vec3 n = normalize(world_normal);
	if (!FRONT_FACING) {
		n = -n;
	}
	float ndl = max(dot(n, normalize(wow_light_dir)), 0.0);
	vec3 zone_ambient = mix(vec3(0.78), max(wow_ambient_color.rgb, vec3(0.08)), 0.16);
	vec3 zone_light = mix(vec3(1.0), wow_light_color.rgb, 0.10);
	float day_strength = mix(0.48, 0.92, clamp(wow_sun_elevation, 0.0, 1.0));
	vec3 lit = baked_lit * (zone_ambient * 0.86 + zone_light * ndl * 0.24 * day_strength);
	if (unlit_material > 0.5) {
		lit = vec3(1.0);
	}
	lit = clamp(lit, vec3(0.46), vec3(1.05));
	vec3 material_color = untextured_material_color(diffuse_color.rgb);
	if (use_texture) {
		material_color = mat_diffuse * textured_material_tint(diffuse_color.rgb);
	}
	float emissive_mask = smoothstep(0.35, 0.92, color_luma(mat_diffuse));
	vec3 material_emissive = mat_diffuse * emissive_color.rgb * emissive_color.a * material_emissive_strength * emissive_mask;
	vec3 emissive = shader_emissive * 0.35 + material_emissive;
	vec3 color = clamp(material_color * lit + emissive, vec3(0.0), vec3(1.0));
	float window_facing = 1.0;
	if (window_material > 0.5) {
		vec3 vn = normalize(view_normal);
		if (!FRONT_FACING) {
			vn = -vn;
		}
		window_facing = abs(dot(vn, normalize(-view_pos)));
		float edge_reflect = pow(1.0 - window_facing, 3.0);
		vec3 glass_tint = mix(color, vec3(0.58, 0.70, 0.82), 0.24);
		color = clamp(mix(color, glass_tint + vec3(edge_reflect) * 0.12, 0.36), vec3(0.0), vec3(1.0));
	}
	ALBEDO = apply_wow_fog(color);
%s
}
""" % [
		", ".join(render_modes),
		albedo_sampler_hint,
		_alpha_output_code(mode),
	]


static func _alpha_output_code(mode: String) -> String:
	if mode == "opaque" or mode == "cutout":
		return ""
	if mode == "glass":
		return "\tALPHA = clamp(mix(0.40, 0.95, window_facing) * max(diffuse_color.a, 0.45), 0.35, 0.95);"
	return "\tALPHA = mix(diffuse_color.a, tex1.a * diffuse_color.a, clamp(alpha_output_strength, 0.0, 1.0));"
