@tool
@icon("res://addons/escoria-core/design/esc_terrain.svg")
## A walkable Terrain for Escoria rooms.
extends Node2D
class_name ESCTerrain


## Logger class reference
const EscLogger = preload("res://addons/escoria-core/tools/logging/esc_logger.gd")


## Visualize scales or lightmap for debugging purposes if the editor
enum DebugMode {
	NONE,
	SCALES,
	LIGHTMAP,
}



@export_group("Scales")
## Scaling texture. If empty, items scale will always be 1.0.
## This is a greyscale image defining how close to the camera the character will
## appear at each pixel position. White is closer, black is further.
@export var scales: Texture2D: set = _set_scales

## Minimum scaling. Corresponds to the scale used for characters when they are
## located at the blackest pixel of scaling texture, at which they will appear
## smallest.
## Unused if scaling texture is not set.
@export var scale_min: float = 0.3

## Maximum scaling. Corresponds the scale used for characters when they are
## located at the whitest pixel of scaling texture, at which they will appear
## biggest.
## Unused if scaling texture is not set.
@export var scale_max: float = 1.0

@export_group("","")
@export_group("Lightmap")

## Lightmap texture.
@export var lightmap: Texture2D: set = _set_lightmap

## Additional modulator to the lightmap texture.
@export var lightmap_modulate: Color = Color(1, 1, 1, 1)

@export_group("","")
@export_group("Bitmaps scale")


## Scaling factor for the scale and light maps.
@export var bitmaps_scale: Vector2 = Vector2(1,1): set = _set_bm_scale

@export_group("","")
@export_group("Movement speed")

## Multiplier applied to the player speed on this terrain.
@export var player_speed_multiplier: float = 1.0

## Multiplier that determines how much faster the player will walk when fast
## mode is on (double clicked)
@export var player_doubleclick_speed_multiplier: float = 1.5

@export_group("","")
@export_group("Debug display")


## Debug display mode.[br]
## None: no debug display[br]
## Scales: displays the scales map[br]
## Lightmap: displays the lightmap
@export var editor_debug_mode = DebugMode.NONE: # (int, "None", "Scales", "Lightmap")
		set = _set_editor_debug_mode

@export_group("","")

## The currently active navigation polygon
var current_active_navigation_instance: NavigationRegion2D = null

## Currently visualized texture for debug mode
var _texture = null

## The image from the lightmap texture
var _lightmap_data

## Prohibits multiple calls to update_texture
var _texture_in_update = false

## Logger instance
@onready var logger = EscLogger.ESCLoggerFile.new()

## Ready method. Sets a reference to the active navigation polygon, register to Escoria and update the texture[br]
## [br]
## #### Parameters[br]
## [br]
## None.
## [br]
## #### Returns[br]
## [br]
## Returns nothing.
func _ready():
	connect("child_entered_tree", Callable(self, "_check_multiple_enabled_navpolys"))
	connect("child_exiting_tree", Callable(self, "_check_multiple_enabled_navpolys").bind(true))

	_check_multiple_enabled_navpolys()
	if !Engine.is_editor_hint():
		escoria.room_terrain = self
	_update_texture()


## All NavigationPolygonInstances defined as children of ESCTerrain in an Array.[br]
## [br]
## #### Parameters[br]
## [br]
## None.
## [br]
## #### Returns[br]
## [br]
## Returns all NavigationPolygonInstances defined as children of ESCTerrain in an Array. (`Array`)
func get_children_navpolys() -> Array:
	var navpolys: Array = []
	for n in get_children():
		if n is NavigationRegion2D:
			navpolys.push_back(n)
	return navpolys


## Checks whether multiple navigation polygons are enabled.
## Shows a warning in the terminal if this happens.[br]
# TODO: change this "simple" console log for an editor warning
# by overriding Node._get_configuration_warning() after we get rid of
# deprecated Navigation2D.
## Validates that only one navigation polygon is enabled at a time, logging a warning if multiple are active.[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |node|`Node`|if this method is triggered by child_entered_tree or child_exited_tree signals, parameter is the added node.|no|[br]
## |is_exiting|`bool`|`true` when the node is leaving the scene tree and should be ignored.|no|[br]
## [br]
## #### Returns[br]
## [br]
## Returns nothing.
func _check_multiple_enabled_navpolys(node: Node = null, is_exiting: bool = false) -> void:
	var navigation_enabled_found = false
	if (node != null
			and not is_exiting
			and node is NavigationRegion2D
			and node.enabled):
		navigation_enabled_found = true

	for n in get_children():
		if is_exiting and n == node:
			continue
		if n is NavigationRegion2D and n.enabled:
			if navigation_enabled_found:
				if Engine.is_editor_hint():
					logger.warn(
						self,
						"Multiple NavigationPolygonInstances enabled " + \
						"at the same time."
					)
				else:
					logger.error(
						self,
						"Multiple NavigationPolygonInstances enabled " + \
						"at the same time."
					)
				return
			else:
				navigation_enabled_found = true
				current_active_navigation_instance = n


## Return the Color of the lightmap pixel for the specified position.[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |pos|`Vector2`|Position to calculate lightmap for.|yes|[br]
## [br]
## #### Returns[br]
## [br]
## Returns a `Color` value. (`Color`)
func get_light(pos: Vector2) -> Color:
	if lightmap == null or lightmap.get_image().is_empty():
		return Color(1, 1, 1, 1)
	return _get_color(lightmap.get_image(), pos) * lightmap_modulate


## Calculate the scale inside the scale range for a given scale factor.[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |factor|`float`|The factor for the scaling according to the scale map|yes|[br]
## [br]
## #### Returns[br]
## [br]
## Returns a `Vector2` value. (`Vector2`)
func get_scale_range(factor: float) -> Vector2:
	factor = scale_min + (scale_max - scale_min) * factor
	return Vector2(factor, factor)


## Get the terrain scale factor for a given position[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |pos|`Vector2`|The position to calculate for|yes|[br]
## [br]
## #### Returns[br]
## [br]
## Returns a `float` value. (`float`)
func get_terrain(pos: Vector2) -> float:
	if scales == null || scales.get_image().is_empty():
		return 1.0
	return _get_color(scales.get_image(), pos).v


## Small helper to get the color of an image at a position[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |image|`Image`|image to search the position in|yes|[br]
## |pos|`Vector2`|The position to calculate for|yes|[br]
## [br]
## #### Returns[br]
## [br]
## Returns a `Color` value. (`Color`)
func _get_color(image: Image, pos: Vector2) -> Color:
	return image.get_pixel(pos.x, pos.y)


## Set the bitmap scaling[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |p_scale|`Vector2`|Scale to set|yes|[br]
## [br]
## #### Returns[br]
## [br]
## Returns nothing.
func _set_bm_scale(p_scale: Vector2):
	bitmaps_scale = p_scale
	_update_texture()


## Set the lightmap texture[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |p_lightmap|`Texture2D`|Lightmap texture to set|yes|[br]
## [br]
## #### Returns[br]
## [br]
## Returns nothing.
func _set_lightmap(p_lightmap: Texture2D):
	var need_init = (lightmap != p_lightmap) or (lightmap and not _lightmap_data)

	lightmap = p_lightmap

	# It's bad enough a new copy is created when reading a pixel, we don't
	# also need to get the data for every read to make yet another copy
	if need_init:
		if _lightmap_data == null:
			_lightmap_data = lightmap.get_image().get_data()

	_update_texture()


## Set the scales texture[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |p_scales|`Texture2D`|Scale texture to set|yes|[br]
## [br]
## #### Returns[br]
## [br]
## Returns nothing.
func _set_scales(p_scales: Texture2D):
	scales = p_scales
	_update_texture()


## Set the debug mode[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |p_mode|`int`|Debug mode to set|yes|[br]
## [br]
## #### Returns[br]
## [br]
## Returns nothing.
func _set_editor_debug_mode(p_mode: int):
	editor_debug_mode = p_mode
	_update_texture()


## Update the debug texture, if it is dirty.[br]
## [br]
## #### Parameters[br]
## [br]
## None.
## [br]
## #### Returns[br]
## [br]
## Returns nothing.
func _update_texture():
	if _texture_in_update:
		return
	_texture_in_update = true
	call_deferred("_do_update_texture")


## Update the texture and optionally set the debug texture[br]
## [br]
## #### Parameters[br]
## [br]
## None.
## [br]
## #### Returns[br]
## [br]
## Returns nothing.
func _do_update_texture():
	_texture_in_update = false
	if !is_inside_tree() or !Engine.is_editor_hint():
		return

	if editor_debug_mode == DebugMode.NONE:
		queue_redraw()
		return

	_texture = ImageTexture.new()
	if editor_debug_mode == DebugMode.SCALES:
		if scales != null:
			_texture = scales
	elif editor_debug_mode == DebugMode.LIGHTMAP:
		if lightmap != null:
			_texture = lightmap

	queue_redraw()


## Draw debugging visualizations[br]
## [br]
## #### Parameters[br]
## [br]
## None.
## [br]
## #### Returns[br]
## [br]
## Returns nothing.
func _draw():
	if _texture == null or \
			not Engine.is_editor_hint() or \
			editor_debug_mode == DebugMode.NONE:
		if current_active_navigation_instance:
			current_active_navigation_instance.visible = true
		return

	var scale_vect = bitmaps_scale

	if current_active_navigation_instance:
		current_active_navigation_instance.visible = false

	var src = Rect2(0, 0, _texture.get_width(), _texture.get_height())
	var dst = Rect2(
		0,
		0,
		_texture.get_width() * scale_vect.x,
		_texture.get_height() * scale_vect.y
	)

	draw_texture_rect_region(_texture, dst, src)

## Calculate a path from a given point to another. The path consists in a list of points.[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |from|`Vector2`|Origin point|yes|[br]
## |to|`Vector2`|Destination point|yes|[br]
## |optimize|`bool`|(default is true) enabled optimisation|no|[br]
## |layers|`int`|bitmask of all region navigation layers that are allowed to be in the path.|no|[br]
## [br]
## #### Returns[br]
## [br]
## Returns a `PackedVector2Array` value. (`PackedVector2Array`)
func get_simple_path(
		from: Vector2,
		to: Vector2,
		optimize: bool = true,
		layers: int = 1) -> PackedVector2Array:
	return NavigationServer2D.map_get_path(
		get_world_2d().navigation_map,
		from,
		to,
		optimize,
		layers)
