# grass.gd
# This file is part of: SimpleGrassTextured
# Copyright (c) 2023 IcterusGames
#
# 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 MultiMeshInstance3D

## Define your custom mesh per grass, you can open a .obj, .mesh, etc, or can 
## copy paste your own mesh from any mesh component. Set as null for default 
## SimpleGrassTextured mesh.
@export var mesh : Mesh = null : set = _on_set_mesh
## Color albedo for mesh material
@export_color_no_alpha var albedo := Color.WHITE : set = _on_set_albedo
## Texture albedo for mesh, you can apply normal, metallic and roughness 
## textures on the "Material parameters" section
@export var texture_albedo : Texture = load("res://addons/simplegrasstextured/textures/grassbushcc008.png") : set = _on_set_texture_albedo
@export_group("Material parameters")
## Lets you setup a multi texture image by frames
@export var texture_frames : Vector2i = Vector2i(1, 1) : set = _on_set_texture_frames;
## Defines the texture image alpha threshold
@export_range(0.0, 1.0) var alpha_scissor_threshold := 0.5 : set = _on_set_alpha_scissor_threshold
## Ilumination mode[br]
## [b]Lambert[/b][br]Recomended for complex meshes such as flowers[br][br]
## [b]Normal grass[/b][br]The lighting will be calculated by the inclination of 
## the grass, recommended for very simple meshes[br][br]
## [b]Unshaded[/b][br]No lighting will affect the grass
@export_enum("Lambert", "Normal grass", "Unshaded") var light_mode := 1 : set = _on_set_light_mode
@export_enum("Nearest", "Linear", "Nearest mipmap", "Linear mipmap") var texture_filter := 3 : set = _on_set_texture_filter
@export_subgroup("Normal")
@export var texture_normal : Texture = null : set = _on_set_texture_normal
@export_range(-16.0, 16.0) var normal_scale := 1.0 : set = _on_set_normal_scale
@export_subgroup("Metallic")
@export var texture_metallic : Texture = null : set = _on_set_texture_metallic
@export_enum("Red","Green","Blue","Alpha","Gray") var metallic_texture_channel : int = 0 : set = _on_set_metallic_texture_channel
@export_range(0.0, 1.0) var metallic := 0.0 : set = _on_set_metallic
@export_range(0.0, 1.0) var specular := 0.5 : set = _on_set_specular
@export_subgroup("Roughness")
@export var texture_roughness : Texture = null : set = _on_set_texture_roughness
@export_enum("Red","Green","Blue","Alpha","Gray") var roughness_texture_channel : int = 0 : set = _on_set_roughness_texture_channel
@export_range(0.0, 1.0) var roughness := 1.0 : set = _on_set_roughness
@export_group("")
## Scale height factor of all the grass
@export var scale_h := 1.0 : set = _on_set_scale_h
## Scale width factor of all the grass
@export var scale_w := 1.0 : set = _on_set_scale_w
## Scale variable factor, scale of random grasses will be affected by this 
## factor
@export var scale_var := -0.25 : set = _on_set_scale_var
## Defines the strength of this grass, with large values ​​the grass will not be 
## moved by the wind, for example a bamboo can be 0.9 (it will almost not be 
## affected by the wind), and a tall grass can be 0.2 (very affected by the 
## wind)
@export_range(0.0, 1.0) var grass_strength := 0.55 : set = _on_set_grass_strength
## If true, this grass will be in "interactive mode", that means if an object 
## is near the grass, the grass will react and move.[br][br]
## [b]To setup the "interactive mode":[/b][br]
## 1. You must enable by code on the begin of your scene by call 
## [code]SimpleGrass.set_interactive(true)[/code][br]
## 2. Setup your objects to be visible on the Visual Layer 17[br]
## 3. Update the SimpleGrassTexture camera position by calling 
## [code]SimpleGrass.set_player_position()[/code] regulary (on your _process 
## function by example)[br][br]
## [b]You can see how to enable "interactive mode" on:[/b][br]
## [url]https://github.com/IcterusGames/SimpleGrassTextured?tab=readme-ov-file#how-to-enable-interactive-mode[/url]
@export var interactive : bool = true : set = _on_set_interactive
@export_group("Advanced")
## Allows you to define how much the grass will react to objects on axis X and Z
@export var interactive_level_xz : float = 3.0 : set = _on_set_interactive_level_xz
## Allows you to define how much the grass will react to objects on axis Y
@export var interactive_level_y : float = 0.3 : set = _on_set_interactive_level_y
## Locks the scale node of SimpleGrassTextured to 1
@export var disable_node_scale := true : set = _on_set_disable_node_scale
## Disable the ability to rotate the SimpleGrassTextured node
@export var disable_node_rotation := true : set = _on_set_disable_node_rotation
@export_group("Optimization")
@export var optimization_by_distance := false : set = _on_set_optimization_by_distance
@export var optimization_level := 7.0 : set = _on_set_optimization_level
@export var optimization_dist_min := 10.0 : set = _on_set_optimization_dist_min
@export var optimization_dist_max := 50.0 : set = _on_set_optimization_dist_max
@export_group("Height Map Data")
## This is the baked height map of this grass, this will speed up the load of 
## the scene. To setup this variable use the menu 
## SimpleGrassTextured->"Bake height map" on the Editor 3D
@export var baked_height_map : Image = null
@export_group("Draw Collision Mask")
## This is the collision mask for drawing, this allows you to define what your 
## terrain collision mask is, that way it will be easier to draw your grass.
@export_flags_3d_physics var collision_mask :int = pow(2, 32) - 1

var sgt_radius := 2.0
var sgt_density := 25
var sgt_scale := 1.0
var sgt_rotation := 0.0
var sgt_rotation_rand := 1.0
var sgt_dist_min := 0.25
var sgt_follow_normal := false
var sgt_slope := Vector2(0, 45)
var sgt_tool_shape := {}

var temp_dist_min := 0.0

# Deprecated vars:
var player_pos := Vector3(1000000, 1000000, 1000000) : set = _on_set_player_pos
var player_radius := 0.5 : set = _on_set_player_radius
var wind_dir := Vector3.RIGHT : set = _on_set_wind_dir
var wind_strength := 0.15 : set = _on_set_wind_strength
var wind_turbulence := 1.0 : set = _on_set_wind_turbulence
var wind_pattern : Texture = null : set = _on_set_wind_pattern

var _default_mesh : Mesh = load("res://addons/simplegrasstextured/default_mesh.tres").duplicate()
var _buffer_add : Array[Transform3D] = []
var _material := load("res://addons/simplegrasstextured/materials/grass.material").duplicate() as ShaderMaterial
var _force_update_multimesh := false
var _properties = []
var _node_height_map = null
var _singleton = null

var _wrng_deprec_playerpos = true
var _wrng_deprec_playerrad = true
var _wrng_deprec_windir = true
var _wrng_deprec_windstrng = true
var _wrng_deprec_windturb = true
var _wrng_deprec_windpatt = true


func _init():
	if Engine.is_editor_hint():
		if collision_mask == pow(2, 32) - 1:
			collision_mask = ProjectSettings.get_setting("SimpleGrassTextured/General/default_terrain_physics_layer", pow(2, 32) -1)
		for var_i in get_property_list():
			if not var_i.name.begins_with("sgt_"):
				continue
			_properties.append({
				"name": var_i.name,
				"type": var_i.type,
				"usage": PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_SCRIPT_VARIABLE,
			})


func _ready():
	if Engine.is_editor_hint():
		set_process(true)
	else:
		set_process(false)
	_singleton = get_node("/root/SimpleGrass")
	if not has_meta(&"SimpleGrassTextured"):
		set_meta(&"SimpleGrassTextured", "2.0.5")
	else:
		if get_meta(&"SimpleGrassTextured") == "1.0.2":
			# New default mesh update tangents
			set_meta(&"SimpleGrassTextured", "2.0.3")
			_force_update_multimesh = true
			if multimesh != null:
				if mesh != null:
					multimesh.mesh = mesh
				else:
					multimesh.mesh = _default_mesh
		if get_meta(&"SimpleGrassTextured") == "2.0.3":
			set_meta(&"SimpleGrassTextured", "2.0.5")
			disable_node_scale = false
			disable_node_rotation = false
	if multimesh == null:
		multimesh = MultiMesh.new()
		multimesh.transform_format = MultiMesh.TRANSFORM_3D
	if multimesh.mesh == null:
		if mesh != null:
			multimesh.mesh = mesh
		else:
			multimesh.mesh = _default_mesh
	_update_material_shader()
	for isur in range(multimesh.mesh.get_surface_count()):
		if multimesh.mesh.surface_get_material(isur) != null:
			_material = multimesh.mesh.surface_get_material(isur)
			if _material.get_reference_count() > 2:
				_material = _material.duplicate()
			break
	for isur in range(multimesh.mesh.get_surface_count()):
		if multimesh.mesh.surface_get_material(isur) == null:
			multimesh.mesh.surface_set_material(isur, _material)
	set_disable_scale(disable_node_scale)
	if disable_node_rotation:
		set_notify_transform(true)
	update_all_material()


func _notification(what: int) -> void:
	if what == NOTIFICATION_TRANSFORM_CHANGED:
		if disable_node_rotation and global_rotation != Vector3.ZERO:
			var prev_scale := scale
			global_rotation = Vector3.ZERO
			scale = prev_scale


func update_all_material():
	_on_set_albedo(albedo)
	_on_set_texture_albedo(texture_albedo)
	_on_set_alpha_scissor_threshold(alpha_scissor_threshold)
	_on_set_light_mode(light_mode)
	_on_set_texture_normal(texture_normal)
	_on_set_normal_scale(normal_scale)
	_on_set_texture_metallic(texture_metallic)
	_on_set_metallic_texture_channel(metallic_texture_channel)
	_on_set_metallic(metallic)
	_on_set_specular(specular)
	_on_set_texture_roughness(texture_roughness)
	_on_set_roughness_texture_channel(roughness_texture_channel)
	_on_set_roughness(roughness)
	_on_set_scale_h(scale_h)
	_on_set_scale_w(scale_w)
	_on_set_scale_var(scale_var)
	_on_set_grass_strength(grass_strength)
	_on_set_interactive(interactive)
	_on_set_interactive_level_xz(interactive_level_xz)
	_on_set_interactive_level_y(interactive_level_y)
	_on_set_optimization_by_distance(optimization_by_distance)
	_on_set_optimization_level(optimization_level)
	_on_set_optimization_dist_min(optimization_dist_min)
	_on_set_optimization_dist_max(optimization_dist_max)


func _enter_tree():
	if interactive:
		_update_height_map.call_deferred()


func _exit_tree():
	if _node_height_map:
		_node_height_map.queue_free()
		_node_height_map = null


func _process(_delta : float):
	if _buffer_add.size() != 0 or _force_update_multimesh:
		_force_update_multimesh = false
		_update_multimesh()


func _get_property_list() -> Array:
	if _properties == null:
		return []
	return _properties


func eval_grass_transform(pos : Vector3, normal : Vector3, scale : Vector3, rotated : float) -> Transform3D:
	var trans := Transform3D()
	if abs(normal.z) == 1:
		trans.basis.x = Vector3(1,0,0)
		trans.basis.y = Vector3(0,0,normal.z)
		trans.basis.z = Vector3(0,normal.z,0)
		trans.basis = trans.basis.orthonormalized()
	else:
		trans.basis.y = normal
		trans.basis.x = normal.cross(trans.basis.z)
		trans.basis.z = trans.basis.x.cross(normal)
		trans.basis = trans.basis.orthonormalized()
	trans = trans.rotated_local(Vector3.UP, rotated)
	trans = trans.scaled(scale)
	trans = trans.translated(pos)
	return trans


func add_grass(pos : Vector3, normal : Vector3, scale : Vector3, rotated : float):
	var trans := eval_grass_transform(pos, normal, scale, rotated)
	if sgt_dist_min > 0:
		for trans_prev in _buffer_add:
			if trans.origin.distance_to(trans_prev.origin) <= sgt_dist_min:
				return
	_buffer_add.append(trans)


func add_grass_batch(transforms : Array):
	var distmin = temp_dist_min
	if temp_dist_min == 0:
		distmin = sgt_dist_min
	if _buffer_add.size() > 0 and distmin > 0:
		for trans_prev in _buffer_add:
			for trans in transforms:
				if trans.origin.distance_to(trans_prev.origin) <= distmin:
					transforms.erase(trans)
					break
	_buffer_add.append_array(transforms)


func erase(pos: Vector3, radius: float) -> void:
	if not multimesh.get_aabb().intersects(AABB(pos - Vector3(radius, radius, radius), Vector3(radius, radius, radius) * 2)):
		return
	_apply_erase_tool(func(array: Array[Transform3D]) -> int:
			var num_to_erase := 0
			for i in range(multimesh.instance_count):
				var trans := multimesh.get_instance_transform(i)
				if trans.origin.distance_to(pos) > radius:
					array.append(trans)
				else:
					num_to_erase += 1
			return num_to_erase
	)


func erase_cylinder(pos: Vector3, rx: float, height: float, rz: float, shape_transform: Transform3D) -> void:
	var aabb := AABB(Vector3(-rx, -height / 2, -rz), Vector3(rx, height / 2, rz) * 2)
	aabb = shape_transform * aabb
	if not (global_transform * multimesh.get_aabb()).intersects(aabb):
		return
	_apply_erase_tool(func(array: Array[Transform3D]) -> int:
			var num_to_erase := 0
			for i in range(multimesh.instance_count):
				var trans := multimesh.get_instance_transform(i)
				var point := global_transform * trans.origin * shape_transform
				var r = (point.x * point.x) / (rx * rx) + (point.z * point.z) / (rz * rz)
				if point.y < -height or point.y > height or r >= 1:
					array.append(trans)
				else:
					num_to_erase += 1
			return num_to_erase
	)


func erase_box(pos: Vector3, size: Vector3, shape_transform: Transform3D) -> void:
	var aabb := AABB(-size / 2, size)
	if not (global_transform * multimesh.get_aabb()).intersects(shape_transform * aabb):
		return
	_apply_erase_tool(func(array: Array[Transform3D]) -> int:
			var num_to_erase := 0
			for i in range(multimesh.instance_count):
				var trans := multimesh.get_instance_transform(i)
				if not aabb.has_point(global_transform * trans.origin * shape_transform):
					array.append(trans)
				else:
					num_to_erase += 1
			return num_to_erase
	)


func snap_to_terrain() -> void:
	await get_tree().physics_frame
	for i in multimesh.instance_count:
		var trans := multimesh.get_instance_transform(i)
		var origin_ws := to_global(trans.origin)
		var rc = _raycast(self, origin_ws + Vector3.UP * 100, Vector3.DOWN, 1000, collision_mask)
		if not rc:
			continue
		trans.origin = to_local(rc["position"])
		var normal : Vector3 = rc["normal"] if sgt_follow_normal else Vector3.UP
		if abs(normal.z) == 1:
			trans.basis.x = Vector3(1, 0, 0)
			trans.basis.y = Vector3(0, 0, normal.z)
			trans.basis.z = Vector3(0, normal.z, 0)
			trans.basis = trans.basis.orthonormalized()
		else:
			trans.basis.y = normal
			trans.basis.x = normal.cross(trans.basis.z)
			trans.basis.z = trans.basis.x.cross(normal)
		multimesh.set_instance_transform(i, trans)
	recalculate_custom_aabb()


static func _raycast(from: Node3D, pos: Vector3, dir: Vector3, dist: float, mask: int) -> Dictionary:
	var space_state = from.get_world_3d().direct_space_state
	var query = PhysicsRayQueryParameters3D.create(pos, pos + dir * dist, mask, [from])
	return space_state.intersect_ray(query)


func _apply_erase_tool(func_tool: Callable):
	var multi_new := MultiMesh.new()
	var array : Array[Transform3D] = []
	multi_new.transform_format = MultiMesh.TRANSFORM_3D
	if mesh != null:
		multi_new.mesh = mesh
	else:
		multi_new.mesh = _default_mesh
	if multimesh == null:
		multimesh = MultiMesh.new()
		multimesh.mesh = mesh if mesh != null else _default_mesh
	if func_tool.call(array) == 0:
		return
	multi_new.instance_count = array.size()
	for i in range(array.size()):
		multi_new.set_instance_transform(i, array[i])
	if Engine.is_editor_hint() and multimesh.resource_path.length():
		var path := multimesh.resource_path
		multimesh = multi_new
		multimesh.take_over_path(path)
	else:
		multimesh = multi_new
	if _material != null:
		for isur in range(multimesh.mesh.get_surface_count()):
			if multimesh.mesh.surface_get_material(isur) == null:
				multimesh.mesh.surface_set_material(isur, _material)
	if Engine.is_editor_hint():
		baked_height_map = null
		custom_aabb.position = Vector3.ZERO
		custom_aabb.end = Vector3.ZERO


func auto_center_position():
	if multimesh == null:
		multimesh = MultiMesh.new()
		multimesh.mesh = mesh if mesh != null else _default_mesh
	var aabb : AABB = multimesh.get_aabb()
	var center : Vector3 = global_position + aabb.position + (aabb.size / 2)
	var align : Vector3 = global_position - center
	if center == global_position:
		return
	global_position = center
	var multi_new := MultiMesh.new()
	multi_new.transform_format = MultiMesh.TRANSFORM_3D
	if mesh != null:
		multi_new.mesh = mesh
	else:
		multi_new.mesh = _default_mesh
	multi_new.instance_count = multimesh.instance_count
	for i in range(multimesh.instance_count):
		var trans := multimesh.get_instance_transform(i)
		trans.origin += align
		multi_new.set_instance_transform(i, trans)
	if Engine.is_editor_hint() and multimesh.resource_path.length():
		var path := multimesh.resource_path
		multimesh = multi_new
		multimesh.take_over_path(path)
	else:
		multimesh = multi_new
	if _material != null:
		for isur in range(multimesh.mesh.get_surface_count()):
			if multimesh.mesh.surface_get_material(isur) == null:
				multimesh.mesh.surface_set_material(isur, _material)
	if Engine.is_editor_hint():
		if baked_height_map != null:
			baked_height_map = null
			bake_height_map()
		custom_aabb.position = Vector3.ZERO
		custom_aabb.end = Vector3.ZERO
	else:
		baked_height_map = null


func recalculate_custom_aabb():
	if multimesh == null:
		multimesh = MultiMesh.new()
		multimesh.mesh = mesh if mesh != null else _default_mesh
	var start := Vector3.ONE * 0x7FFFFFFF
	var end := start * -1
	var mesh_end := multimesh.mesh.get_aabb().end * Vector3(scale_w, scale_h, scale_w)
	for i in range(multimesh.instance_count):
		var trans := multimesh.get_instance_transform(i)
		var point : Vector3 = trans * mesh_end
		if point.x < start.x: start.x = point.x
		if point.y < start.y: start.y = point.y
		if point.z < start.z: start.z = point.z
		point = trans * mesh_end
		if point.x > end.x: end.x = point.x
		if point.y > end.y: end.y = point.y
		if point.z > end.z: end.z = point.z
	custom_aabb.position = start
	custom_aabb.end = end


func _update_multimesh():
	if multimesh == null:
		multimesh = MultiMesh.new()
		multimesh.mesh = mesh if mesh != null else _default_mesh
	var multi_new := MultiMesh.new()
	var count_prev := multimesh.instance_count
	multi_new.transform_format = MultiMesh.TRANSFORM_3D
	if mesh != null:
		multi_new.mesh = mesh
	else:
		multi_new.mesh = _default_mesh
	if count_prev > 0 and _buffer_add.size() > 0 and (sgt_dist_min > 0 or temp_dist_min > 0):
		var pos_min := Vector3(10000000, 10000000, 10000000)
		var pos_max := pos_min * -1
		for trans in _buffer_add:
			if pos_min.x > trans.origin.x: pos_min.x = trans.origin.x
			if pos_min.y > trans.origin.y: pos_min.y = trans.origin.y
			if pos_min.z > trans.origin.z: pos_min.z = trans.origin.z
			if pos_max.x < trans.origin.x: pos_max.x = trans.origin.x
			if pos_max.y < trans.origin.y: pos_max.y = trans.origin.y
			if pos_max.z < trans.origin.z: pos_max.z = trans.origin.z
		pos_min -= Vector3.ONE
		pos_max += Vector3.ONE
		var dist_min := temp_dist_min
		if dist_min == 0:
			dist_min = sgt_dist_min
		for i in range(multimesh.instance_count):
			var trans := multimesh.get_instance_transform(i)
			if trans.origin.x < pos_min.x or trans.origin.x > pos_max.x: continue
			if trans.origin.y < pos_min.y or trans.origin.y > pos_max.y: continue
			if trans.origin.z < pos_min.z or trans.origin.z > pos_max.z: continue
			for trans_add in _buffer_add:
				if trans_add.origin.distance_to(trans.origin) > dist_min:
					continue
				_buffer_add.erase(trans_add)
				break
		if _buffer_add.size() == 0:
			return
	multi_new.instance_count = count_prev + _buffer_add.size()
	for i in range(multimesh.instance_count):
		multi_new.set_instance_transform(i, multimesh.get_instance_transform(i))
	for i in range(_buffer_add.size()):
		multi_new.set_instance_transform(i + count_prev, _buffer_add[i])
	if Engine.is_editor_hint() and multimesh.resource_path.length():
		var path := multimesh.resource_path
		multimesh = multi_new
		multimesh.take_over_path(path)
	else:
		multimesh = multi_new
	if _material != null:
		for isur in range(multimesh.mesh.get_surface_count()):
			if multimesh.mesh.surface_get_material(isur) == null:
				multimesh.mesh.surface_set_material(isur, _material)
	_buffer_add.clear()
	temp_dist_min = 0
	if Engine.is_editor_hint():
		baked_height_map = null
		custom_aabb.position = Vector3.ZERO
		custom_aabb.end = Vector3.ZERO


func _create_height_map_image(local : bool) -> Image:
	if multimesh == null:
		multimesh = MultiMesh.new()
		multimesh.mesh = mesh if mesh != null else _default_mesh
	var aabb : AABB = multimesh.get_aabb()
	var img_size := Vector2i(
			clamp(snappedi(aabb.size.x * 4, 32), 32, 128),
			clamp(snappedi(aabb.size.z * 4, 32), 32, 128)
	)
	var img := Image.create(img_size.x, img_size.y, false, Image.FORMAT_RGBA8)
	var relx := float(img_size.x) / aabb.size.x
	var relz := float(img_size.y) / aabb.size.z
	
	img.fill(Color(1.0, 1.0, 1.0, 0.0))
	for i in range(multimesh.instance_count):
		var trans : Transform3D = multimesh.get_instance_transform(i)
		trans.origin -= aabb.position
		var x := clampi(int(trans.origin.x * relx), 0, img_size.x)
		var y := clampi(int(trans.origin.z * relz), 0, img_size.y)
		var posy := trans.origin.y + aabb.position.y + 16200.0
		if local:
			posy += global_position.y
		var r := (floorf(posy / 180.0) + 75.0) / 255.0
		var g := (floorf(posy - ((roundf(r * 255.0) - 75.0) * 180.0)) + 75.0) / 255.0
		var b := fmod(absf(posy), 1.0)
		var color := Color(r, g, b, 1.0)
		img.set_pixel(x, y, color)
		for n in range(1, 3):
			if x - n >= 0 and img.get_pixel(x - n, y).a == 0:
				img.set_pixel(x - n, y, color)
				if y - n >= 0 and img.get_pixel(x - n, y - n).a == 0:
					img.set_pixel(x - n, y - n, color)
				if y + n < img_size.y and img.get_pixel(x - n, y + n).a == 0:
					img.set_pixel(x - n, y + n, color)
			if x + n < img_size.x and img.get_pixel(x + n, y).a == 0:
				img.set_pixel(x + n, y, color)
				if y - n >= 0 and img.get_pixel(x + n, y - n).a == 0:
					img.set_pixel(x + n, y - n, color)
				if y + n < img_size.y and img.get_pixel(x + n, y + n).a == 0:
					img.set_pixel(x + n, y + n, color)
			if y - n >= 0 and img.get_pixel(x, y - n).a == 0:
				img.set_pixel(x, y - n, color)
			if y + n < img_size.y and img.get_pixel(x, y + n).a == 0:
				img.set_pixel(x, y + n, color)
	return img


func _local_height_map_to_global(img : Image) -> Image:
	var result : Image = Image.create(img.get_width(), img.get_height(), false, Image.FORMAT_RGBA8)
	result.fill(Color(0, 0, 0, 0))
	for y in img.get_height():
		for x in img.get_width():
			var color : Color = img.get_pixel(x, y)
			if color.a == 0:
				continue
			var posy : float = (((roundf(color.r * 255.0) - 75.0) * 180.0) + (roundf(color.g * 255.0) - 75.0) + color.b)
			posy += global_position.y
			color.r = (floorf(posy / 180.0) + 75.0) / 255.0
			color.g = (floorf(posy - ((roundf(color.r * 255.0) - 75.0) * 180.0)) + 75.0) / 255.0
			color.b = fmod(absf(posy), 1.0)
			result.set_pixel(x, y, color)
	return result


func bake_height_map():
	if not Engine.is_editor_hint():
		return null
	if multimesh == null:
		multimesh = MultiMesh.new()
		multimesh.mesh = mesh if mesh != null else _default_mesh
	await get_tree().process_frame
	var _dummy = multimesh.buffer.size()
	await get_tree().process_frame
	var img : Image = _create_height_map_image(false)
	baked_height_map = img


func clear_all():
	if multimesh == null:
		multimesh = MultiMesh.new()
	if Engine.is_editor_hint() and multimesh.resource_path.length():
		var path := multimesh.resource_path
		multimesh = MultiMesh.new()
		multimesh.take_over_path(path)
	else:
		multimesh = MultiMesh.new()
	multimesh.mesh = mesh if mesh != null else _default_mesh
	multimesh.transform_format = MultiMesh.TRANSFORM_3D
	if Engine.is_editor_hint():
		if baked_height_map != null:
			baked_height_map = null
			bake_height_map()
		custom_aabb.position = Vector3.ZERO
		custom_aabb.end = Vector3.ZERO


func _update_height_map():
	if Engine.is_editor_hint():
		return
	if multimesh == null:
		multimesh = MultiMesh.new()
		multimesh.mesh = mesh if mesh != null else _default_mesh
	
	var img : Image = null
	
	if baked_height_map == null:
		await get_tree().process_frame
		var _dummy = multimesh.buffer.size()
		await get_tree().process_frame
		img = _create_height_map_image(true)
	else:
		img = _local_height_map_to_global(baked_height_map)
	
	var aabb : AABB = multimesh.get_aabb()
	var texture := ImageTexture.create_from_image(img)
	if _node_height_map != null:
		_node_height_map.queue_free()
	_node_height_map = MeshInstance3D.new()
	_node_height_map.mesh = PlaneMesh.new()
	_node_height_map.mesh.size = Vector2(aabb.size.x, aabb.size.z)
	var mat = load("res://addons/simplegrasstextured/materials/position.material").duplicate(true)
	mat.set_shader_parameter("texture_albedo", texture)
	_node_height_map.material_override = mat
	_singleton._height_view.add_child(_node_height_map)
	var align := Vector3(
		(aabb.position.x + (aabb.size.x / 2.0)),
		0,
		(aabb.position.z + (aabb.size.z / 2.0))
	)
	_node_height_map.global_position = global_position + align
	_node_height_map.visible = visible


func _update_material_shader() -> bool:
	var shader_name := "grass"
	if light_mode == 2:
		shader_name += "_unshaded"
	# "Nearest" = 0, "Linear" = 1, "Nearest mipmap" = 2, "Linear mipmap" = 3
	if texture_filter == 0:
		shader_name += "_nearest"
	elif texture_filter == 1:
		shader_name += "_linear"
	elif texture_filter == 2:
		shader_name += "_nearest_mipmap"
	elif texture_filter == 3:
		shader_name += "" # Linear mipmap is the default filter
	if _material.get_shader().resource_path != "res://addons/simplegrasstextured/shaders/" + shader_name + ".gdshader":
		_material.shader = load("res://addons/simplegrasstextured/shaders/" + shader_name + ".gdshader")
		if _material.get_shader() == null:
			_material.shader = load("res://addons/simplegrasstextured/shaders/grass.gdshader")
			_material.shader.take_over_path("res://addons/simplegrasstextured/shaders/" + shader_name + ".gdshader")
		return true
	return false


func _on_set_mesh(value : Mesh):
	mesh = value
	if _material != null:
		if mesh != null:
			_material.set_shader_parameter("grass_size_y", mesh.get_aabb().size.y)
		else:
			_material.set_shader_parameter("grass_size_y", 1.0)
	if Engine.is_editor_hint() and is_inside_tree():
		_update_multimesh()


func _on_set_albedo(value : Color):
	albedo = value;
	if _material != null:
		_material.set_shader_parameter("albedo", albedo)


func _on_set_texture_albedo(value : Texture):
	texture_albedo = value
	if _material != null:
		_material.set_shader_parameter("texture_albedo", texture_albedo)


func _on_set_texture_frames(value : Vector2i):
	texture_frames = value
	if texture_frames.x <= 0:
		texture_frames.x = 1
	if texture_frames.y <= 0:
		texture_frames.y = 1
	if _material != null:
		_material.set_shader_parameter("texture_frames", Vector2(texture_frames.x, texture_frames.y))


func _on_set_light_mode(value : int):
	light_mode = value
	if _material == null:
		return
	if _update_material_shader():
		if multimesh == null:
			multimesh = MultiMesh.new()
			multimesh.mesh = mesh if mesh != null else _default_mesh
		if multimesh.mesh != null:
			for isur in range(multimesh.mesh.get_surface_count()):
				multimesh.mesh.surface_set_material(isur, _material)
		update_all_material()
	_material.set_shader_parameter("light_mode", light_mode)


func _on_set_texture_filter(value : int) -> void:
	texture_filter = value
	if _material == null:
		return
	if _update_material_shader():
		if multimesh == null:
			multimesh = MultiMesh.new()
			multimesh.mesh = mesh if mesh != null else _default_mesh
		if multimesh.mesh != null:
			for isur in range(multimesh.mesh.get_surface_count()):
				multimesh.mesh.surface_set_material(isur, _material)
		update_all_material()


func _on_set_texture_normal(value : Texture):
	texture_normal = value
	if _material != null:
		_material.set_shader_parameter("texture_normal", texture_normal)


func _on_set_normal_scale(value : float):
	normal_scale = value
	if _material != null:
		_material.set_shader_parameter("normal_scale", normal_scale)


func _on_set_texture_metallic(value : Texture):
	texture_metallic = value
	if _material != null:
		_material.set_shader_parameter("texture_metallic", texture_metallic)


func _on_set_metallic_texture_channel(value : int):
	metallic_texture_channel = value
	if _material != null:
		var channel : Vector4
		if value == 0:
			channel = Vector4(1,0,0,0)
		elif value == 1:
			channel = Vector4(0,1,0,0)
		elif value == 2:
			channel = Vector4(0,0,1,0)
		elif value == 3:
			channel = Vector4(0,0,0,1)
		elif value == 4:
			channel = Vector4(1,1,1,1)
		_material.set_shader_parameter("metallic_texture_channel", channel)


func _on_set_metallic(value : float):
	metallic = value
	if _material != null:
		_material.set_shader_parameter("metallic", metallic)


func _on_set_specular(value : float):
	specular = value
	if _material != null:
		_material.set_shader_parameter("specular", specular)


func  _on_set_texture_roughness(value : Texture):
	texture_roughness = value
	if _material != null:
		_material.set_shader_parameter("texture_roughness", texture_roughness)


func  _on_set_roughness_texture_channel(value : int):
	roughness_texture_channel = value
	if _material != null:
		var channel : Vector4
		if value == 0:
			channel = Vector4(1,0,0,0)
		elif value == 1:
			channel = Vector4(0,1,0,0)
		elif value == 2:
			channel = Vector4(0,0,1,0)
		elif value == 3:
			channel = Vector4(0,0,0,1)
		elif value == 4:
			channel = Vector4(1,1,1,1)
		_material.set_shader_parameter("roughness_texture_channel", channel)


func  _on_set_roughness(value : float):
	roughness = value
	if _material != null:
		_material.set_shader_parameter("roughness", roughness)


func _on_set_alpha_scissor_threshold(value : float):
	alpha_scissor_threshold = value
	if _material != null:
		_material.set_shader_parameter("alpha_scissor_threshold", alpha_scissor_threshold)


func _on_set_scale_h(value : float):
	scale_h = value
	if _material != null:
		_material.set_shader_parameter("scale_h", scale_h)


func _on_set_scale_w(value : float):
	scale_w = value
	if _material != null:
		_material.set_shader_parameter("scale_w", scale_w)


func _on_set_scale_var(value : float):
	scale_var = value
	if _material != null:
		_material.set_shader_parameter("scale_var", scale_var)


func _on_set_grass_strength(value : float):
	grass_strength = value
	if _material != null:
		_material.set_shader_parameter("grass_strength", grass_strength)


func _on_set_interactive(value : bool):
	if interactive == value:
		return
	interactive = value
	if Engine.is_editor_hint():
		return
	if interactive:
		_update_height_map()
	else:
		if _node_height_map != null:
			_node_height_map.queue_free()
			_node_height_map = null
	if _material != null:
		_material.set_shader_parameter("interactive_mode", interactive)


func _on_set_interactive_level_xz(value : float):
	interactive_level_xz = value
	if _material != null:
		_material.set_shader_parameter("interactive_level_xz", interactive_level_xz)


func _on_set_interactive_level_y(value : float):
	interactive_level_y = value
	if _material != null:
		_material.set_shader_parameter("interactive_level_y", interactive_level_y)


func _on_set_disable_node_scale(value : bool) -> void:
	disable_node_scale = value
	if disable_node_scale:
		scale = Vector3.ONE
	set_disable_scale(disable_node_scale)


func _on_set_disable_node_rotation(value : bool) -> void:
	disable_node_rotation = value
	if disable_node_rotation:
		var prev_scale := scale
		global_rotation = Vector3.ZERO
		scale = prev_scale
		set_notify_transform(true)


func _on_set_optimization_by_distance(value : bool):
	optimization_by_distance = value
	if _material != null:
		_material.set_shader_parameter("optimization_by_distance", optimization_by_distance)


func _on_set_optimization_level(value : float):
	optimization_level = value
	if _material != null:
		_material.set_shader_parameter("optimization_level", optimization_level)


func _on_set_optimization_dist_min(value : float):
	optimization_dist_min = value
	if _material != null:
		_material.set_shader_parameter("optimization_dist_min", optimization_dist_min)


func _on_set_optimization_dist_max(value : float):
	optimization_dist_max = value
	if _material != null:
		_material.set_shader_parameter("optimization_dist_max", optimization_dist_max)


func _on_set_player_pos(value : Vector3):
	player_pos = Vector3(1000000, 1000000, 1000000)
	if value != Vector3(1000000, 1000000, 1000000):
		#_singleton.set_player_position(value)
		if _wrng_deprec_playerpos and (Engine.is_editor_hint() or OS.is_debug_build()):
			_wrng_deprec_playerpos = false
			push_warning("Simple Grass Textured: ("+name+") player_pos parameter is deprecated, use SimpleGrass.set_player_position")


func _on_set_player_radius(value : float):
	player_radius = 0.5
	if _wrng_deprec_playerrad and (Engine.is_editor_hint() or OS.is_debug_build()):
		_wrng_deprec_playerrad = false
		push_warning("Simple Grass Textured: ("+name+") player_radius parameter is deprecated")


func _on_set_wind_dir(value : Vector3):
	wind_dir = Vector3.RIGHT
	#_singleton.wind_direction = value
	if _wrng_deprec_windir and (Engine.is_editor_hint() or OS.is_debug_build()):
		_wrng_deprec_windir = false
		push_warning("Simple Grass Textured: ("+name+") wind_dir parameter is deprecated, use SimpleGrass.wind_direction")


func _on_set_wind_strength(value : float):
	wind_strength = 0.15
	#_singleton.wind_strength = value
	if _wrng_deprec_windstrng and (Engine.is_editor_hint() or OS.is_debug_build()):
		_wrng_deprec_windstrng = false
		push_warning("Simple Grass Textured: ("+name+") wind_strength parameter is deprecated, use SimpleGrass.wind_strength")


func _on_set_wind_turbulence(value : float):
	wind_turbulence = 1.0
	#_singleton.wind_turbulence = value
	if _wrng_deprec_windturb and (Engine.is_editor_hint() or OS.is_debug_build()):
		_wrng_deprec_windturb = false
		push_warning("Simple Grass Textured: ("+name+") wind_turbulence parameter is deprecated, use SimpleGrass.wind_turbulence")


func _on_set_wind_pattern(value : Texture):
	wind_pattern = null
	#RenderingServer.global_shader_parameter_set("sgt_wind_pattern", value)
	if value != null and _wrng_deprec_windpatt and (Engine.is_editor_hint() or OS.is_debug_build()):
		_wrng_deprec_windpatt = false
		push_warning("Simple Grass Textured: ("+name+") wind_pattern parameter is deprecated, use SimpleGrass.set_wind_pattern")
