extends Node2D


## Grid with cells in it
class_name Grid

## Out of bounds return value
const OUT_OF_BOUNDS: int = -1

## Amount of rows (max Y value)
@export var rows: int = 1
## Amount of columns (max X value)
@export var columns: int = 1

## Node that marks right down position of grid
@export var right_border_marker: Node2D

## Computed size of one cell
@onready var cell_size = (right_border_marker.global_position - global_position) / Vector2(columns,rows)
## Computed rect of grid
@onready var rect: Rect2 = Rect2(global_position,(right_border_marker.global_position - global_position))

## Array that holds units
var grid: Array[Unit] = []

func _ready() -> void:
	grid.resize(rows*columns)

## Returns true if position is in bounds else returns false.
func is_position_valid(pos: Vector2) -> bool:
	return rect.has_point(pos)

## Turns position into internal array index. Returns -1 if position is incorrect
func to_index(pos: Vector2) -> int:
	if is_position_valid(pos) == false: return OUT_OF_BOUNDS
	var snapped_position: Vector2 = (pos - global_position)/cell_size
	return int(snapped_position.x) + columns * int(snapped_position.y)

## Turns index into global world position
func from_index(index: int, centered: bool = false) -> Vector2:
	if index < 0 or index >= columns * rows:
		return Vector2.ZERO
	return Vector2(index % columns, index / columns) * cell_size + (cell_size/2.0 if centered else Vector2.ZERO)

## Snaps position through grid
func snap_position(pos: Vector2, centered: bool = false) -> Vector2:
	return global_position + (pos - global_position).snapped(cell_size) + (cell_size / 2) if centered else Vector2.ZERO

## Returns position as grid snapped local coordinates
func to_snapped(pos: Vector2) -> Vector2:
	if is_position_valid(pos) == false: return Vector2.ZERO
	var snapped_position: Vector2 = (pos - global_position)/cell_size
	return Vector2(floor(snapped_position.x),floor(snapped_position.y))

## Tries to set unit at position and returns false if unit already exists or out of bounds
func try_set_unit(pos: Vector2, unit: Unit) -> bool:
	var index: int = to_index(pos)
	if index == OUT_OF_BOUNDS:
		return false
	
	if grid[index] != null:
		return false
	
	grid[index] = unit
	return true

## Returns true if cell is occupied or out of bounds and false if cell is empty.
func is_occupied(pos: Vector2) -> bool:
	var index: int = to_index(pos)
	if index == OUT_OF_BOUNDS:
		return true
	
	return grid[index] != null

## Returns units with given rule predicate.[br]
## rule - predicate with signature [code](x,y,source_x,source_y)[/code] -> bool 
## that needs to return true if cell needs to be checked.[br]
## unit - source unit.
func get_units(rule: Callable,unit: Unit) -> Array[Unit]:
	var unit_pos = to_snapped(unit.global_position)
	var result: Array[Unit] = []
	for i in range(columns * rows):
		if rule.call(i%columns,i/columns,int(unit_pos.x),int(unit_pos.y)) and grid[i] != null:
			result.append(grid[i])
	
	return result

## WIP[br]
## Returns units with given rule predicate. Checks from source unit with rays and stops only when ray failed.[br]
## rule - predicate with signature [code](x,y,source_x,source_y)[/code] -> bool 
## that needs to return true if cell needs to be checked.[br]
## unit - source unit.[br]
## rays - amount of rays to check from unit.
func get_units_stoppable(rule: Callable, unit: Unit, rays: int = 8) -> Array[Unit]:
	return []

## Returns empty cells with given rule predicate.[br]
## rule - predicate with signature [code](x,y,source_x,source_y)[/code] -> bool 
## that needs to return true if cell needs to be checked.[br]
## unit - source unit.
func get_empty(rule: Callable, unit: Unit) -> Array[int]:
	var unit_pos = to_snapped(unit.global_position)
	var result: Array[int] = []
	for i: int in range(columns * rows):
		if rule.call(i%columns,i/columns,int(unit_pos.x),int(unit_pos.y)) and grid[i] == null:
			result.append(i)
	
	return result

## WIP[br]
## Returns empty with given rule predicate. Checks from source unit with rays and stops only when ray failed.[br]
## rule - predicate with signature [code](x,y,source_x,source_y)[/code] -> bool 
## that needs to return true if cell needs to be checked.[br]
## unit - source unit.[br]
## rays - amount of rays to check from unit.
func get_empty_stoppable(rule: Callable, unit: Unit, rays: int = 8) -> Array[int]:
	return []

## Casts a ray along [code]direction[/code] from [code]from[/code] to find unit. If distance is given, restricts lookup to [code]distance[/code] cells.
func raycast_unit(from: Vector2,direction: Vector2,distance: float = -1) -> Unit:
	if not direction.is_normalized():
		direction = direction.normalized()
	var result: Unit = null
	
	# Simple algorithm
	var step: float = max(abs(direction.x),abs(direction.y))
	var step_v: Vector2 = Vector2(direction.x/step,direction.y/step)
	
	var check_position: Vector2 = from + step_v * cell_size
	while is_position_valid(check_position) and distance != 0:
		var index: int = to_index(check_position)
		if index == -1:
			break
		
		if grid[index] != null:
			result = grid[index]
			break
		
		check_position += step_v * cell_size
	
	return result

## Casts a ray along [code]direction[/code] from [code]from[/code] to find all units along the line. If distance is given, restricts lookup to [code]distance[/code] cells.
func raycast_units(from: Vector2,direction: Vector2,distance: float = -1) -> Array[Unit]:
	if not direction.is_normalized():
		direction = direction.normalized()
	var result: Array[Unit] = []
	
	# Simple algorithm
	var step: float = max(abs(direction.x),abs(direction.y))
	var step_v: Vector2 = Vector2(direction.x/step,direction.y/step)
	
	var check_position: Vector2 = from + step_v * cell_size
	while is_position_valid(check_position) and distance != 0:
		var index: int = to_index(check_position)
		if index == -1:
			break
		
		if grid[index] != null:
			result.append(grid[index])
		
		check_position += step_v * cell_size
	
	return result

## Casts a ray along [code]direction[/code] from [code]from[/code] to find all empty cells. If distance is given, restricts lookup to [code]distance[/code] cells.
func raycast_empty(from: Vector2,direction: Vector2,distance: float = -1) -> Array[int]:
	if not direction.is_normalized():
		direction = direction.normalized()
	var result: Array[int] = []
	
	# Simple algorithm
	var step: float = max(abs(direction.x),abs(direction.y))
	var step_v: Vector2 = Vector2(direction.x/step,direction.y/step)
	
	var check_position: Vector2 = from + step_v * cell_size
	while is_position_valid(check_position) and distance != 0:
		var index: int = to_index(check_position)
		if index == -1:
			break
		
		if grid[index] == null:
			result.append(index)
		
		check_position += step_v * cell_size
	
	return result
