@tool
extends Node3D

# Child node of the terrain, used to render numerous small objects on the ground
# such as grass or rocks. They do so by using a texture covering the terrain
# (a "detail map"), which is found in the terrain data itself.
# A terrain can have multiple detail maps, and you can choose which one will be
# used with `layer_index`.
# Details use instanced rendering within their own chunk grid, scattered around
# the player. Importantly, the position and rotation of this node don't matter,
# and they also do NOT scale with map scale. Indeed, scaling the heightmap
# doesn't mean we want to scale grass blades (which is not a use case I know of).

const HTerrainData = preload("./hterrain_data.gd")
const HT_DirectMultiMeshInstance = preload("./util/direct_multimesh_instance.gd")
const HT_DirectMeshInstance = preload("./util/direct_mesh_instance.gd")
const HT_Util = preload("./util/util.gd")
const HT_Logger = preload("./util/logger.gd")
# TODO Can't preload because it causes the plugin to fail loading if assets aren't imported
const DEFAULT_MESH_PATH = "res://addons/zylann.hterrain/models/grass_quad.obj"

# Cannot use `const` because `HTerrain` depends on the current script
var HTerrain = load("res://addons/zylann.hterrain/hterrain.gd")

const CHUNK_SIZE = 32
const DEFAULT_SHADER_PATH = "res://addons/zylann.hterrain/shaders/detail.gdshader"
const DEBUG = false

# These parameters are considered built-in,
# they are managed internally so they are not directly exposed
const _API_SHADER_PARAMS = {
	"u_terrain_heightmap": true,
	"u_terrain_detailmap": true,
	"u_terrain_normalmap": true,
	"u_terrain_globalmap": true,
	"u_terrain_inverse_transform": true,
	"u_terrain_normal_basis": true,
	"u_albedo_alpha": true,
	"u_view_distance": true,
	"u_ambient_wind": true
}

# TODO Should be renamed `map_index`
# Which detail map this layer will use
@export var layer_index := 0:
	get:
		return layer_index
	set(v):
		if layer_index == v:
			return
		layer_index = v
		if is_inside_tree():
			_update_material()
			HT_Util.update_configuration_warning(self, false)


# Texture to render on the detail meshes.
@export var texture : Texture:
	get:
		return texture
	set(tex):
		texture = tex
		_material.set_shader_parameter("u_albedo_alpha", tex)


# How far detail meshes can be seen.
# TODO Improve speed of _get_chunk_aabb() so we can increase the limit
# See https://github.com/Zylann/godot_heightmap_plugin/issues/155
@export_range(1.0, 500.0) var view_distance := 100.0:
	get:
		return view_distance
	set(v):
		if view_distance == v:
			return
		view_distance = maxf(v, 1.0)
		if is_inside_tree():
			_update_material()


# Custom shader to replace the default one.
@export var custom_shader : Shader:
	get:
		return custom_shader

	set(shader):
		if custom_shader == shader:
			return

		if custom_shader != null:
			if Engine.is_editor_hint():
				custom_shader.changed.disconnect(_on_custom_shader_changed)

		custom_shader = shader
		
		if custom_shader == null:
			_material.shader = load(DEFAULT_SHADER_PATH)
		else:
			_material.shader = custom_shader

			if Engine.is_editor_hint():
				# Ability to fork default shader
				if shader.code == "":
					shader.code = _default_shader.code
				
				custom_shader.changed.connect(_on_custom_shader_changed)
		
				notify_property_list_changed()


# Density modifier, to make more or less detail meshes appear overall.
@export_range(0, 10) var density := 4.0:
	get:
		return density
	set(v):
		v = clampf(v, 0, 10)
		if v == density:
			return
		density = v
		_multimesh_need_regen = true


# Sets the seed of the random number generator that populates the multimesh
@export var fixed_seed := 0:
	get:
		return fixed_seed
	set(v):
		if v == fixed_seed:
			return
		fixed_seed = v
		_multimesh_need_regen = true


# If false, the multimesh seed will be picked from current time.
# Otherwise, `fixed_seed` will be used.
# Defaults to `false` because that was the behavior on old versions.
@export var fixed_seed_enabled := false:
	get:
		return fixed_seed_enabled
	set(v):
		if v == fixed_seed_enabled:
			return
		fixed_seed_enabled = v
		_multimesh_need_regen = true


# Mesh used for every detail instance (for example, every grass patch).
# If not assigned, an internal quad mesh will be used.
# I would have called it `mesh` but that's too broad and conflicts with local vars ._.
@export var instance_mesh : Mesh:
	get:
		return instance_mesh
	set(p_mesh):
		if p_mesh == instance_mesh:
			return
		instance_mesh = p_mesh
		_multimesh.mesh = _get_used_mesh()


# Exposes rendering layers, similar to `VisualInstance.layers`
# (IMO this annotation is not specific enough, something might be off...)
@export_flags_3d_render var render_layers := 1:
	get:
		return render_layers
	set(mask):
		render_layers = mask
		for k in _chunks:
			var chunk : Chunk = _chunks[k]
			chunk.mmi.set_layer_mask(mask)


# Exposes shadow casting setting.
# Possible values are the same as the enum `GeometryInstance.SHADOW_CASTING_SETTING_*`.
# TODO Casting to `int` should not be necessary! Had to do it otherwise GDScript complains...
@export_enum("Off", "On", "DoubleSided", "ShadowsOnly") \
	var cast_shadow := int(GeometryInstance3D.SHADOW_CASTING_SETTING_ON):
	get:
		return cast_shadow
	set(option):
		if option == cast_shadow:
			return
		cast_shadow = option
		for k in _chunks:
			var chunk : Chunk = _chunks[k]
			chunk.mmi.set_cast_shadow(option)


class Chunk:
	var mmi: HT_DirectMultiMeshInstance
	var pending_update: bool = false
	var pending_aabb_update: bool = false


var _material: ShaderMaterial = null
var _default_shader: Shader = null

# Vector2i => Chunk
var _chunks := {}

var _multimesh: MultiMesh
var _multimesh_need_regen = true
var _multimesh_instance_pool : Array[HT_DirectMultiMeshInstance] = []
var _ambient_wind_time := 0.0
#var _auto_pick_index_on_enter_tree := Engine.is_editor_hint()
var _debug_wirecube_mesh: Mesh = null
var _debug_cubes := []
var _logger := HT_Logger.get_for(self)
var _prev_frame_cmin := Vector3i()
var _prev_frame_cmax := Vector3i()
var _pending_chunk_updates : Array[Vector2i] = []
var _force_chunk_updates_next_frame : bool = false


func _init():
	_default_shader = load(DEFAULT_SHADER_PATH)
	_material = ShaderMaterial.new()
	_material.shader = _default_shader
	
	_multimesh = MultiMesh.new()
	_multimesh.transform_format = MultiMesh.TRANSFORM_3D
	# TODO Godot 3 had the option to specify color format, but Godot 4 no longer does...
	# I only need 8-bit, but Godot 4 uses 32-bit components colors...
	#_multimesh.color_format = MultiMesh.COLOR_8BIT
	_multimesh.use_colors = true


func _enter_tree():
	var terrain = _get_terrain()
	if terrain != null:
		terrain.transform_changed.connect(_on_terrain_transform_changed)

		#if _auto_pick_index_on_enter_tree:
		#	_auto_pick_index_on_enter_tree = false
		#	_auto_pick_index()
		
		# Register this detail layer in the terrain node (this does not create a new layer)
		terrain._internal_add_detail_layer(self)

	_update_material()


func _exit_tree():
	var terrain = _get_terrain()
	if terrain != null:
		terrain.transform_changed.disconnect(_on_terrain_transform_changed)
		# Unregister from terrain
		terrain._internal_remove_detail_layer(self)
	_update_material()
	for k in _chunks.keys():
		_recycle_chunk(k)
	_chunks.clear()


#func _auto_pick_index():
#	# Automatically pick an unused layer
#
#	var terrain = _get_terrain()
#	if terrain == null:
#		return
#
#	var terrain_data = terrain.get_data()
#	if terrain_data == null or terrain_data.is_locked():
#		return
#
#	var auto_index := layer_index
#	var others = terrain.get_detail_layers()
#
#	if len(others) > 0:
#		var used_layers := []
#		for other in others:
#			used_layers.append(other.layer_index)
#		used_layers.sort()
#
#		auto_index = used_layers[-1]
#		for i in range(1, len(used_layers)):
#			if used_layers[i - 1] - used_layers[i] > 1:
#				# Found a hole, take it instead
#				auto_index = used_layers[i] - 1
#				break
#
#	print("Auto picked ", auto_index, " ")
#	layer_index = auto_index


func _get_property_list() -> Array:
	# Dynamic properties coming from the shader
	var props := []
	if _material != null:
		var shader_params = _material.shader.get_shader_uniform_list(true)
		for p in shader_params:
			if _API_SHADER_PARAMS.has(p.name):
				continue
			var cp := {}
			for k in p:
				cp[k] = p[k]
			# See HTerrain._get_property_list for more information about this
			if cp.usage == PROPERTY_USAGE_GROUP:
				cp.hint_string = "shader_params/"
			else:
				cp.name = str("shader_params/", p.name)
			props.append(cp)
	return props


func _get(key: StringName):
	var key_str := String(key)
	if key_str.begins_with("shader_params/"):
		var param_name = key_str.substr(len("shader_params/"))
		return get_shader_param(param_name)


func _set(key: StringName, v):
	var key_str := String(key)
	if key_str.begins_with("shader_params/"):
		var param_name = key_str.substr(len("shader_params/"))
		set_shader_param(param_name, v)


func get_shader_param(param_name: String):
	return HT_Util.get_shader_material_parameter(_material, param_name)


func set_shader_param(param_name: String, v):
	_material.set_shader_parameter(param_name, v)


func _get_terrain():
	if is_inside_tree():
		return get_parent()
	return null


# Compat
func set_texture(tex: Texture):
	texture = tex


# Compat
func get_texture() -> Texture:
	return texture


# Compat
func set_layer_index(v: int):
	layer_index = v


# Compat
func get_layer_index() -> int:
	return layer_index


# Compat
func set_view_distance(v: float):
	return view_distance


# Compat
func get_view_distance() -> float:
	return view_distance


# Compat
func set_custom_shader(shader: Shader):
	custom_shader = shader


# Compat
func get_custom_shader() -> Shader:
	return custom_shader


# Compat
func set_instance_mesh(p_mesh: Mesh):
	instance_mesh = p_mesh


# Compat
func get_instance_mesh() -> Mesh:
	return instance_mesh


# Compat
func set_render_layer_mask(mask: int):
	render_layers = mask


# Compat
func get_render_layer_mask() -> int:
	return render_layers


func _get_used_mesh() -> Mesh:
	if instance_mesh == null:
		var mesh := load(DEFAULT_MESH_PATH) as Mesh
		if mesh == null:
			_logger.error(str("Failed to load default mesh: ", DEFAULT_MESH_PATH))
		return mesh
	return instance_mesh


# Compat
func set_density(v: float):
	density = v


# Compat
func get_density() -> float:
	return density


# Updates texture references and values that come from the terrain itself.
# This is typically used when maps are being swapped around in terrain data,
# so we can restore texture references that may break.
func update_material():
	_update_material()
	# Formerly update_ambient_wind, reset


func _notification(what: int):
	match what:
		NOTIFICATION_ENTER_WORLD:
			_set_world(get_world_3d())

		NOTIFICATION_EXIT_WORLD:
			_set_world(null)

		NOTIFICATION_VISIBILITY_CHANGED:
			_set_visible(is_visible_in_tree())
			
		NOTIFICATION_PREDELETE:
			# Force DirectMeshInstances to be destroyed before the material.
			# Otherwise it causes RenderingServer errors...
			_chunks.clear()
			_multimesh_instance_pool.clear()


func _set_visible(v: bool):
	for k in _chunks:
		var chunk : Chunk = _chunks[k]
		chunk.mmi.set_visible(v)


func _set_world(w: World3D):
	for k in _chunks:
		var chunk : Chunk = _chunks[k]
		chunk.mmi.set_world(w)


func _on_terrain_transform_changed(gt: Transform3D):
	_update_material()

	var terrain = _get_terrain()
	if terrain == null:
		_logger.error("Detail layer is not child of a terrain!")
		return
	
	var terrain_transform : Transform3D = terrain.get_internal_transform_unscaled()

	# Update AABBs and transforms, because scale might have changed
	for k in _chunks:
		var chunk : Chunk = _chunks[k]
		var mmi := chunk.mmi
		var aabb := _get_chunk_aabb(terrain, Vector3(k.x * CHUNK_SIZE, 0, k.y * CHUNK_SIZE))
		# Nullify XZ translation because that's done by transform already
		aabb.position.x = 0
		aabb.position.z = 0
		mmi.set_aabb(aabb)
		mmi.set_transform(_get_chunk_transform(terrain_transform, k.x, k.y))


func process(delta: float, viewer_pos: Vector3):
	var terrain = _get_terrain()
	if terrain == null:
		_logger.error("DetailLayer processing while terrain is null!")
		return

	if _multimesh_need_regen:
		print("Regen multimesh")
		_regen_multimesh()
		_multimesh_need_regen = false
		# Crash workaround for Godot 3.1
		# See https://github.com/godotengine/godot/issues/32500
		for k in _chunks:
			var chunk : Chunk = _chunks[k]
			chunk.mmi.set_multimesh(_multimesh)

	# Detail layers are unaffected by ground map_scale
	var terrain_transform_without_map_scale : Transform3D = \
		terrain.get_internal_transform_unscaled()
	var local_viewer_pos : Vector3 = \
		terrain_transform_without_map_scale.affine_inverse() * viewer_pos

	var viewer_cpos := Vector3i(local_viewer_pos / CHUNK_SIZE)

	var cr := int(view_distance) / CHUNK_SIZE + 1

	var cmin := viewer_cpos - Vector3i(cr, cr, cr)
	var cmax := viewer_cpos + Vector3i(cr, cr, cr)

	var terrain_data : HTerrainData = terrain.get_data()
	var map_res := terrain_data.get_resolution()
	var map_scale : Vector3 = terrain.map_scale

	var terrain_size_v2i := Vector2i(map_res * map_scale.x, map_res * map_scale.z)
	var terrain_num_chunks_v2i := terrain_size_v2i / CHUNK_SIZE
	
	cmin.x = clampi(cmin.x, 0, terrain_num_chunks_v2i.x)
	cmin.z = clampi(cmin.z, 0, terrain_num_chunks_v2i.y)
	
	cmax.x = clampi(cmax.x, 0, terrain_num_chunks_v2i.x)
	cmax.z = clampi(cmax.z, 0, terrain_num_chunks_v2i.y)
	
	# This algorithm isn't the most efficient ever.
	# Maybe we could switch to a clipbox algorithm eventually, and updating only when the viewer
	# changes chunk position?

	if DEBUG and visible:
		_debug_cubes.clear()
		#for cz in range(cmin_z, cmax_z):
			#for cx in range(cmin_x, cmax_x):
				#_add_debug_cube(terrain, _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE),
					#terrain_transform_without_map_scale)
		for k in _chunks:
			var aabb := _get_chunk_aabb(terrain, Vector3(k.x, 0, k.y) * CHUNK_SIZE)
			_add_debug_cube(terrain, aabb, terrain_transform_without_map_scale)
		
		_add_debug_cube(terrain, 
			AABB(local_viewer_pos - Vector3(1,1,1), Vector3(2,2,2)), 
			terrain_transform_without_map_scale)
	
	var time_enter := 0
	var time_exit := 0
	
	# Only update when the camera moves across chunks
	if cmin != _prev_frame_cmin or cmax != _prev_frame_cmax or _force_chunk_updates_next_frame:
		_force_chunk_updates_next_frame = false
		
		for cz in range(cmin.z, cmax.z):
			for cx in range(cmin.x, cmax.x):
		
				var cpos2d := Vector2i(cx, cz)
				if _chunks.has(cpos2d):
					continue
		
				var aabb := _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE)
				var d := _get_approx_distance_to_chunk_aabb(aabb, local_viewer_pos)
		
				if d < view_distance:
					_load_chunk(terrain_transform_without_map_scale, cx, cz, aabb)
		
		for k in _chunks:
			var chunk : Chunk = _chunks[k]
			# TODO Maybe we shouldn't include chunks that were just loaded above?
			if not chunk.pending_update:
				chunk.pending_update = true
				_pending_chunk_updates.append(k)
		
	_process_pending_updates(terrain, local_viewer_pos)
	
	# Update time manually, so we can accelerate the animation when strength is increased,
	# without causing phase jumps (which would be the case if we just scaled TIME)
	var ambient_wind_frequency = 1.0 + 3.0 * terrain.ambient_wind
	_ambient_wind_time += delta * ambient_wind_frequency
	var awp = _get_ambient_wind_params()
	_material.set_shader_parameter("u_ambient_wind", awp)
	
	_prev_frame_cmin = cmin
	_prev_frame_cmax = cmax


func on_heightmap_region_changed(rect: Rect2i) -> void:
	# Whether chunks should be loaded or not and their AABB may change,
	# even if the camera didn't move
	_force_chunk_updates_next_frame = true
	
	# The AABB of existing chunks may change
	var cmin : Vector2i = rect.position / CHUNK_SIZE
	var cmax : Vector2i = ceildiv_vec2i_int(rect.end, CHUNK_SIZE)
	for cz in range(cmin.y, cmax.y):
		for cx in range(cmin.x, cmax.x):
			var cpos := Vector2i(cx, cz)
			var chunk : Chunk = _chunks.get(cpos)
			if chunk != null and not chunk.pending_update:
				chunk.pending_update = true
				chunk.pending_aabb_update = true
				_pending_chunk_updates.append(cpos)


func on_heightmap_resolution_changed() -> void:
	_reset_chunks()


func _reset_chunks() -> void:
	for k in _chunks.keys():
		_recycle_chunk(k)
	_prev_frame_cmin = Vector3i()
	_prev_frame_cmax = Vector3i()
	_pending_chunk_updates.clear()


static func ceildiv(x: int, d: int) -> int:
	assert(d > 0);
	if x < 0:
		return (x - d + 1) / d
	return x / d


static func ceildiv_vec2i_int(v: Vector2i, d: int) -> Vector2i:
	return Vector2i(ceildiv(v.x, d), ceildiv(v.y, d))


func _process_pending_updates(terrain, local_viewer_pos: Vector3):
	# Defer this over multiple frames
	var budget_us := 1000
	var time_before := Time.get_ticks_usec()
	
	while _pending_chunk_updates.size() > 0:
		var cpos : Vector2i = _pending_chunk_updates.pop_back()
		var chunk : Chunk = _chunks[cpos]
		chunk.pending_update = false
		
		var aabb := _get_chunk_aabb(terrain, Vector3(cpos.x, 0, cpos.y) * CHUNK_SIZE)
		var d := _get_approx_distance_to_chunk_aabb(aabb, local_viewer_pos)
		if d > view_distance:
			_recycle_chunk(cpos)
		elif chunk.pending_aabb_update:
			chunk.pending_aabb_update = false
			chunk.mmi.set_aabb(aabb)
		
		if Time.get_ticks_usec() - time_before > budget_us:
			break


# It would be nice if Godot had "AABB.distance_squared_to(vec3)"...
# Using an approximation here cuz GDScript is slow.
static func _get_approx_distance_to_chunk_aabb(aabb: AABB, pos: Vector3) -> float:
	# Distance to center is faster but has issues with very large map scale.
	# Detail chunks are based on cached vertical bounds.
	# Usually it works fine on moderate scales, but on huge map scales,
	# vertical bound chunks cover such large areas that detail chunks often
	# stay exceedingly tall, in turn causing their centers to be randomly
	# far from the ground, and then getting unloaded because assumed too far away
	#return aabb.get_center().distance_to(pos)
	
	if aabb.has_point(pos):
		return 0.0
	var delta := (pos - aabb.get_center()).abs() - aabb.size * 0.5
	return maxf(delta.x, maxf(delta.y, delta.z))


# Gets local-space AABB of a detail chunk.
# This only apply map_scale in Y, because details are not affected by X and Z map scale.
func _get_chunk_aabb(terrain, lpos: Vector3) -> AABB:
	var terrain_scale = terrain.map_scale
	var terrain_data = terrain.get_data()
	var origin_cells_x := int(lpos.x / terrain_scale.x)
	var origin_cells_z := int(lpos.z / terrain_scale.z)
	# We must at least sample 1 cell, in cases where map_scale is greater than the size of our 
	# chunks. This is quite an extreme situation though
	var size_cells_x := int(ceilf(CHUNK_SIZE / terrain_scale.x))
	var size_cells_z := int(ceilf(CHUNK_SIZE / terrain_scale.z))
	
	var aabb = terrain_data.get_region_aabb(
		origin_cells_x, origin_cells_z, size_cells_x, size_cells_z)
	
	aabb.position = Vector3(lpos.x, lpos.y + aabb.position.y * terrain_scale.y, lpos.z)
	aabb.size = Vector3(CHUNK_SIZE, aabb.size.y * terrain_scale.y, CHUNK_SIZE)
	return aabb


func _get_chunk_transform(terrain_transform: Transform3D, cx: int, cz: int) -> Transform3D:
	var lpos := Vector3(cx, 0, cz) * CHUNK_SIZE
	# `terrain_transform` should be the terrain's internal transform, without `map_scale`.
	var trans := Transform3D(
		terrain_transform.basis,
		terrain_transform.origin + terrain_transform.basis * lpos)
	return trans


func _load_chunk(terrain_transform_without_map_scale: Transform3D, cx: int, cz: int, aabb: AABB):
	aabb.position.x = 0
	aabb.position.z = 0

	var mmi : HT_DirectMultiMeshInstance = null
	if len(_multimesh_instance_pool) != 0:
		mmi = _multimesh_instance_pool[-1]
		_multimesh_instance_pool.pop_back()
	else:
		mmi = HT_DirectMultiMeshInstance.new()
		mmi.set_world(get_world_3d())
		mmi.set_multimesh(_multimesh)

	var trans := _get_chunk_transform(terrain_transform_without_map_scale, cx, cz)
	
	mmi.set_material_override(_material)
	mmi.set_transform(trans)
	mmi.set_aabb(aabb)
	mmi.set_layer_mask(render_layers)
	mmi.set_cast_shadow(cast_shadow)
	mmi.set_visible(visible)
	
	var chunk := Chunk.new()
	chunk.mmi = mmi
	_chunks[Vector2i(cx, cz)] = chunk


func _recycle_chunk(cpos2d: Vector2i):
	var chunk : Chunk = _chunks[cpos2d]
	_chunks.erase(cpos2d)
	chunk.mmi.set_visible(false)
	_multimesh_instance_pool.append(chunk.mmi)


func _get_ambient_wind_params() -> Vector2:
	var aw := 0.0
	var terrain = _get_terrain()
	if terrain != null:
		aw = terrain.ambient_wind
	# amplitude, time
	return Vector2(aw, _ambient_wind_time)


func _update_material():
	# Sets API shader properties. Custom properties are assumed to be set already
	_logger.debug("Updating detail layer material")

	var terrain_data : HTerrainData = null
	var terrain = _get_terrain()
	var it := Transform3D()
	var normal_basis := Basis()

	if terrain != null:
		var gt : Transform3D = terrain.get_internal_transform()
		it = gt.affine_inverse()
		terrain_data = terrain.get_data()
		# This is needed to properly transform normals if the terrain is scaled.
		# However we don't want to pick up rotation because it's already factored in the instance
		#normal_basis = gt.basis.inverse().transposed()
		normal_basis = Basis().scaled(terrain.map_scale).inverse().transposed()

	var mat := _material

	mat.set_shader_parameter("u_terrain_inverse_transform", it)
	mat.set_shader_parameter("u_terrain_normal_basis", normal_basis)
	mat.set_shader_parameter("u_albedo_alpha", texture)
	mat.set_shader_parameter("u_view_distance", view_distance)
	mat.set_shader_parameter("u_ambient_wind", _get_ambient_wind_params())

	var heightmap_texture : Texture = null
	var normalmap_texture : Texture = null
	var detailmap_texture : Texture = null
	var globalmap_texture : Texture = null

	if terrain_data != null:
		if terrain_data.is_locked():
			_logger.error("Terrain data locked, can't update detail layer now")
			return

		heightmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_HEIGHT)
		normalmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_NORMAL)

		if layer_index < terrain_data.get_map_count(HTerrainData.CHANNEL_DETAIL):
			detailmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_DETAIL, layer_index)

		if terrain_data.get_map_count(HTerrainData.CHANNEL_GLOBAL_ALBEDO) > 0:
			globalmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_GLOBAL_ALBEDO)
	else:
		_logger.error("Terrain data is null, can't update detail layer completely")

	mat.set_shader_parameter("u_terrain_heightmap", heightmap_texture)
	mat.set_shader_parameter("u_terrain_detailmap", detailmap_texture)
	mat.set_shader_parameter("u_terrain_normalmap", normalmap_texture)
	mat.set_shader_parameter("u_terrain_globalmap", globalmap_texture)


func _add_debug_cube(terrain: Node3D, aabb: AABB, terrain_transform_without_scale: Transform3D):
	var world : World3D = terrain.get_world_3d()

	if _debug_wirecube_mesh == null:
		_debug_wirecube_mesh = HT_Util.create_wirecube_mesh()
		var mat := StandardMaterial3D.new()
		mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
		_debug_wirecube_mesh.surface_set_material(0, mat)
	
	# Flat terrain makes cubes invisible
	aabb.size.y = maxf(aabb.size.y, 1.0)

	var debug_cube := HT_DirectMeshInstance.new()
	debug_cube.set_mesh(_debug_wirecube_mesh)
	debug_cube.set_world(world)
	#aabb.position.y += 0.2*randf()
	debug_cube.set_transform(terrain_transform_without_scale \
		* Transform3D(Basis().scaled(aabb.size), aabb.position))

	_debug_cubes.append(debug_cube)


func _regen_multimesh():
	# We modify the existing multimesh instead of replacing it.
	# DirectMultiMeshInstance does not keep a strong reference to them,
	# so replacing would break pooled instances.
	
	var rng := RandomNumberGenerator.new()
	if fixed_seed_enabled:
		rng.seed = fixed_seed
	else:
		rng.randomize()
	
	_generate_multimesh(CHUNK_SIZE, density, _get_used_mesh(), _multimesh, rng)


func is_layer_index_valid() -> bool:
	var terrain = _get_terrain()
	if terrain == null:
		return false
	var data = terrain.get_data()
	if data == null:
		return false
	return layer_index >= 0 and layer_index < data.get_map_count(HTerrainData.CHANNEL_DETAIL)


func _get_configuration_warnings() -> PackedStringArray:
	var warnings := PackedStringArray()

	var terrain = _get_terrain()
	if not (is_instance_of(terrain, HTerrain)):
		warnings.append("This node must be child of an HTerrain node")
		return warnings

	var data = terrain.get_data()
	if data == null:
		warnings.append("The terrain has no data")
		return warnings

	if data.get_map_count(HTerrainData.CHANNEL_DETAIL) == 0:
		warnings.append("The terrain does not have any detail map")
		return warnings

	if layer_index < 0 or layer_index >= data.get_map_count(HTerrainData.CHANNEL_DETAIL):
		warnings.append("Layer index is out of bounds")
		return warnings

	var tex = data.get_texture(HTerrainData.CHANNEL_DETAIL, layer_index)
	if tex == null:
		warnings.append("The terrain does not have a map assigned in slot {0}" \
			.format([layer_index]))

	return warnings


# Compat
func set_cast_shadow(option: int):
	cast_shadow = option


# Compat
func get_cast_shadow() -> int:
	return cast_shadow


func _on_custom_shader_changed():
	notify_property_list_changed()


static func _generate_multimesh(
		resolution: int, density: float, mesh: Mesh, multimesh: MultiMesh, 
		rng: RandomNumberGenerator):
	
	assert(multimesh != null)
	
	var position_randomness := 0.5
	var scale_randomness := 0.0
	#var color_randomness = 0.5

	var cell_count := resolution * resolution
	var idensity := int(density)
	var random_instance_count := int(cell_count * (density - floorf(density)))
	var total_instance_count := cell_count * idensity + random_instance_count
	
	multimesh.instance_count = total_instance_count
	multimesh.mesh = mesh
	
	# First pass ensures uniform spread
	var i := 0
	for z in resolution:
		for x in resolution:
			for j in idensity:
				var pos := Vector3(x, 0, z)
				pos.x += rng.randf_range(-position_randomness, position_randomness)
				pos.z += rng.randf_range(-position_randomness, position_randomness)
	
				multimesh.set_instance_color(i, Color(1, 1, 1))
				multimesh.set_instance_transform(i, \
					Transform3D(_get_random_instance_basis(scale_randomness, rng), pos))
				i += 1
	
	# Second pass adds the rest
	for j in random_instance_count:
		var pos = Vector3(rng.randf_range(0, resolution), 0, rng.randf_range(0, resolution))
		multimesh.set_instance_color(i, Color(1, 1, 1))
		multimesh.set_instance_transform(i, \
			Transform3D(_get_random_instance_basis(scale_randomness, rng), pos))
		i += 1


static func _get_random_instance_basis(scale_randomness: float, rng: RandomNumberGenerator) -> Basis:
	var sr := rng.randf_range(0, scale_randomness)
	var s := 1.0 + (sr * sr * sr * sr * sr) * 50.0

	var basis := Basis()
	basis = basis.scaled(Vector3(1, s, 1))
	basis = basis.rotated(Vector3(0, 1, 0), rng.randf_range(0, PI))
	
	return basis
