class_name FlowField
extends Node3D

signal path_updated()

@export var flow_node_scene: PackedScene
@export var nodes: Array[FlowNode] = []
@export var goals: Array[FlowNode] = []
@export var starts: Array[FlowNode] = []
@export var nodes_visible: bool = false


func _ready() -> void:
	if !nodes_visible:
		for node: FlowNode in nodes:
			node.visible = false


@warning_ignore("unused_parameter")
func _process(delta: float) -> void:
	if !nodes_visible:
		return
	for node: FlowNode in nodes:
		if node.traversable and node.buildable:
			node.set_color(Color.WEB_GRAY)
		elif node.traversable and !node.buildable:
			node.set_color(Color.CORAL)
		else:
			node.set_color(Color.BLACK)
		if goals.has(node):
			node.set_color(Color.BLUE)
		if starts.has(node):
			node.set_color(Color.PINK)
		if magic_node:
			magic_node.set_color(Color.DEEP_PINK)


func get_closest_traversable_point(pos: Vector3) -> FlowNode:
	var closest_point: FlowNode = null
	var closest_dist: float = 100000.0
	for node: FlowNode in nodes:
		if node.traversable and node.global_position.distance_to(pos) < closest_dist:
			closest_dist = node.global_position.distance_to(pos)
			closest_point = node
	return closest_point


func get_closest_point_point(pos: Vector3) -> FlowNode:
	var closest_point: FlowNode = null
	var closest_dist: float = 100000.0
	for node: FlowNode in nodes:
		if node.global_position.distance_to(pos) < closest_dist:
			closest_dist = node.global_position.distance_to(pos)
			closest_point = node
	return closest_point


func get_closest_buildable_point(pos: Vector3) -> FlowNode:
	var closest_point: FlowNode = null
	var closest_dist: float = 100000.0
	for node: FlowNode in nodes:
		if node.buildable and node.global_position.distance_to(pos) < closest_dist:
			closest_dist = node.global_position.distance_to(pos)
			closest_point = node
	return closest_point


func test_traversability() -> bool:
	for node: FlowNode in starts:
		while node.best_path != null:
			if node.best_path.traversable:
				node = node.best_path
			else:
				return false
	return true


func iterate_search(search_frontier: Array[FlowNode], reached: Array[FlowNode]) -> void:
	var current: FlowNode = search_frontier.pop_front()
	for node: FlowNode in current.connections:
		if !reached.has(node):
			reached.append(node)
			if node.traversable:
				search_frontier.append(node)
			node.best_path = current


func calculate() -> void:
	var reached: Array[FlowNode] = []
	var search_frontier: Array[FlowNode] = []
	for node: FlowNode in goals:
		node.best_path = null
		reached.append(node)
		search_frontier.append(node)
	while search_frontier.size() > 0:
		iterate_search(search_frontier, reached)


var magic_node: FlowNode = null
func traversable_after_blocking_point(point: FlowNode) -> bool:
	magic_node = null
	var reached: Array[FlowNode] = [point]
	var search_frontier: Array[FlowNode] = []
	for node: FlowNode in point.connections:
		if node.best_path == point and node.traversable:
			reached.append(node)
			search_frontier.append(node)
	if search_frontier.size() == 0: # if no neighbors rely on this node, then we're all good
		return true
	while search_frontier.size() > 0:
		var current: FlowNode = search_frontier.pop_front()
		for node: FlowNode in current.connections:
			if !reached.has(node):
				if node.traversable and node.best_path != node and !reached.has(node.best_path):
					#if we havent already seen the node this neighbor goes to,
					#then all our searched nodes could swap to go this direction
					#and the path would still be traversable
					magic_node = node
					return true
				reached.append(node)
				if node.traversable:
					search_frontier.append(node)
	return false


## Connects many nodes to a single single node, if any connections already
## exist, this function disconnects them instead
func connect_many_nodes(common_node: FlowNode, child_nodes: Array[FlowNode]) -> void:
	for node: FlowNode in child_nodes:
		if common_node.connections.has(node):
			disconnect_nodes(common_node, node)
		else:
			connect_nodes(common_node, node)


func toggle_goal(nodes_to_toggle: Array[FlowNode]) -> void:
	for node: FlowNode in nodes_to_toggle:
		if goals.has(node):
			goals.erase(node)
		else:
			goals.append(node)


func toggle_start(nodes_to_toggle: Array[FlowNode]) -> void:
	for node: FlowNode in nodes_to_toggle:
		if starts.has(node):
			starts.erase(node)
		else:
			starts.append(node)


func toggle_traversable(node: FlowNode) -> bool:
	node.traversable = !node.traversable
	calculate()
	#TODO: technically the path only changed if the new path IS traversable
	path_updated.emit()
	return test_traversability()


func toggle_buildable(node: FlowNode) -> void:
	node.buildable = !node.buildable


func create_node(pos: Vector3 = Vector3.ZERO) -> FlowNode:
	var node: FlowNode = flow_node_scene.instantiate()
	node.position = pos
	node.set_color(Color.WEB_GRAY)
	nodes.append(node)
	add_child(node)
	node.owner = self
	return node


func delete_node(node: FlowNode) -> void:
	for neighbor: FlowNode in node.connections:
		node.remove_connection(neighbor)
	nodes.erase(node)
	node.queue_free()


func connect_nodes(node1: FlowNode, node2: FlowNode) -> void:
	if node1 != node2:
		node1.add_connection(node2)
		node2.add_connection(node1)


func disconnect_nodes(node1: FlowNode, node2: FlowNode) -> void:
	if node1 != node2:
		node1.remove_connection(node2)
		node2.remove_connection(node1)


func create_grid(x_size: int, y_size: int, gap: float) -> void:
	var grid: Array[Array] = []
	for x: int in x_size:
		var row: Array[FlowNode] = []
		for y: int in y_size:
			#var start_pos: Vector3 = Vector3.ZERO - (Vector3(gap * x_size, 0, gap * y_size) / 2.0)
			var point_position: Vector3 = Vector3((x - floori(x_size / 2.0)) * gap, 0, (y - floori(y_size / 2.0)) * gap)
			#point_position += global_position
			#row.append(create_node(start_pos + Vector3(gap * x, 0, gap * y)))
			row.append(create_node(point_position))
		grid.append(row)
	for x: int in grid.size():
		for y: int in grid[x].size():
			if y > 0:
				connect_nodes(grid[x][y], grid[x][y - 1])
			if x > 0:
				connect_nodes(grid[x][y], grid[x - 1][y])
			if y < grid[x].size() - 1:
				connect_nodes(grid[x][y], grid[x][y + 1])
			if x < grid.size() - 1:
				connect_nodes(grid[x][y], grid[x + 1][y])
