class_name AStarGraph3D extends Node3D

@export var grid_size: Vector2i = Vector2i(15, 7)
@export var point_gap: float = 2.0
var non_build_locations: Array = []
var astar: AStar3D = AStar3D.new()

#TODO generalize this better
@export var start: Node3D
@export var end: Node3D
@export var spawner: EnemySpawner
@export var visualized_path: VisualizedPath
@export var tower_path: Node
var tower_base_scene: PackedScene = load("res://Scenes/tower_base.tscn")
var tower_frame_scene: PackedScene = load("res://Scenes/tower_frame.tscn")
var tower_bases: Array = []
var tower_base_ids: Dictionary = {}
var tower_frames: Array = []
var wall_id: int = 0


func toggle_point(point_id: int, caller_id: int) -> void:
	networked_toggle_point.rpc(point_id, caller_id)


func remove_wall(wall: TowerBase) -> void:
	networked_remove_wall.rpc(wall.point_id)
	toggle_point(wall.point_id, multiplayer.get_unique_id())


func point_is_build_location(point_id: int) -> bool:
	return !non_build_locations.has(point_id)


func test_path_if_point_toggled(point_id: int) -> bool:
	if astar.is_point_disabled(point_id):
		astar.set_point_disabled(point_id, false)
	else:
		astar.set_point_disabled(point_id, true)
	var result: bool = find_path()
	if astar.is_point_disabled(point_id):
		astar.set_point_disabled(point_id, false)
	else:
		astar.set_point_disabled(point_id, true)
	return result


@rpc("reliable", "any_peer", "call_local")
func networked_toggle_point(point_id: int, caller_id: int) -> void:
	if astar.is_point_disabled(point_id):
		astar.set_point_disabled(point_id, false)
	else:
		astar.set_point_disabled(point_id, true)
	find_path()
	enable_non_path_tower_frames()
	if is_multiplayer_authority() and astar.is_point_disabled(point_id):
		networked_spawn_wall.rpc(astar.get_point_position(point_id), wall_id, caller_id)
		wall_id += 1


func get_north_point(point_id: int) -> int:
	var x: int = floori(float(point_id) / float(grid_size.y))
	var y: int = point_id % grid_size.y
	if x - 1 >= 0: #if the north point id could possibly exist as a neighbor
		return (x - 1) * grid_size.y + y
	return -1


func get_south_point(point_id: int) -> int:
	var x: int = floori(float(point_id) / float(grid_size.y))
	var y: int = point_id % grid_size.y
	if x + 1 <= grid_size.x - 1: #if the south point id could possibly exist as a neighbor
		return (x + 1) * grid_size.y + y
	return -1


func get_west_point(point_id: int) -> int:
	var x: int = floori(float(point_id) / float(grid_size.y))
	var y: int = point_id % grid_size.y
	if y + 1 <= grid_size.y - 1: #if the east point id could possibly exist as a neighbor
		return x * grid_size.y + y + 1
	return -1


func get_east_point(point_id: int) -> int:
	var x: int = floori(float(point_id) / float(grid_size.y))
	var y: int = point_id % grid_size.y
	if y - 1 >= 0: #if the west point id could possibly exist as a neighbor
		return x * grid_size.y + y - 1
	return -1


func count_valid_neighbours(point_id: int) -> int:
	if !point_id:
		return 0
	var valid_neighbours: int = 0
	var north_point: int = get_north_point(point_id)
	var south_point: int = get_south_point(point_id)
	var east_point: int = get_east_point(point_id)
	var west_point: int = get_west_point(point_id)
	
	if north_point and !astar.is_point_disabled(north_point):
		valid_neighbours += 1
	else: #add the spawn point which is always valid
		valid_neighbours += 1
	
	if south_point and !astar.is_point_disabled(south_point):
		valid_neighbours += 1
	else: #add the goal point which is always valid
		valid_neighbours += 1
	
	if east_point and !astar.is_point_disabled(east_point):
		valid_neighbours += 1
	
	if west_point and !astar.is_point_disabled(west_point):
		valid_neighbours += 1
	return valid_neighbours


func disable_all_tower_frames() -> void:
	for frame: Node3D in tower_frames:
		frame.set_visible(false)


func enable_non_path_tower_frames() -> void:
	for frame: Node3D in tower_frames:
		if !astar.is_point_disabled(tower_frames.find(frame)):
			frame.set_visible(true)
	disable_path_tower_frames()


func disable_path_tower_frames() -> void:
	for id: int in astar.get_id_path(astar.get_point_count() - 2, astar.get_point_count() - 1):
		if id < (grid_size.x * grid_size.y) and !test_path_if_point_toggled(id):
			tower_frames[id].set_visible(false)


@rpc("reliable", "call_local")
func networked_spawn_wall(pos: Vector3, name_id: int, caller_id: int) -> void:
	var base: TowerBase = tower_base_scene.instantiate() as TowerBase
	base.position = pos
	base.name = "Wall" + str(name_id)
	base.owner_id = caller_id
	var point_id: int = astar.get_closest_point(pos, true)
	base.point_id = point_id
	tower_base_ids[point_id] = base
	tower_bases.append(base)
	tower_path.add_child(base)
	var north_point: int = get_north_point(point_id)
	var south_point: int = get_south_point(point_id)
	var east_point: int = get_east_point(point_id)
	var west_point: int = get_west_point(point_id)
	if north_point >= 0 and tower_base_ids.has(north_point) and astar.is_point_disabled(north_point):
		base.set_north_wall(true)
		tower_base_ids[north_point].set_south_wall(true)
	if south_point >= 0 and tower_base_ids.has(south_point) and astar.is_point_disabled(south_point):
		base.set_south_wall(true)
		tower_base_ids[south_point].set_north_wall(true)
	if east_point >= 0 and tower_base_ids.has(east_point) and astar.is_point_disabled(east_point):
		base.set_east_wall(true)
		tower_base_ids[east_point].set_west_wall(true)
	if west_point >= 0 and tower_base_ids.has(west_point) and astar.is_point_disabled(west_point):
		base.set_west_wall(true)
		tower_base_ids[west_point].set_east_wall(true)


@rpc("reliable", "call_local", "any_peer")
func networked_remove_wall(new_wall_id: int) -> void:
	var wall: TowerBase = tower_base_ids[new_wall_id]
	Game.connected_players_nodes[wall.owner_id].currency += Data.wall_cost
	tower_bases.erase(wall)
	tower_base_ids.erase(new_wall_id)
	wall.queue_free()
	var north_point: int = get_north_point(new_wall_id)
	var south_point: int = get_south_point(new_wall_id)
	var east_point: int = get_east_point(new_wall_id)
	var west_point: int = get_west_point(new_wall_id)
	if north_point >= 0 and tower_base_ids.has(north_point) and astar.is_point_disabled(north_point):
		tower_base_ids[north_point].set_south_wall(false)
	if south_point >= 0 and tower_base_ids.has(south_point) and astar.is_point_disabled(south_point):
		tower_base_ids[south_point].set_north_wall(false)
	if east_point >= 0 and tower_base_ids.has(east_point) and astar.is_point_disabled(east_point):
		tower_base_ids[east_point].set_west_wall(false)
	if west_point >= 0 and tower_base_ids.has(west_point) and astar.is_point_disabled(west_point):
		tower_base_ids[west_point].set_east_wall(false)


func build_random_maze(block_limit: int) -> void:
	var untested_point_ids: Array = []
	for index: int in (grid_size.x * grid_size.y):
		untested_point_ids.append(index)
	if block_limit <= 0 or block_limit > untested_point_ids.size():
		block_limit = untested_point_ids.size()
	for index: int in block_limit:
		var random_point: int = untested_point_ids.pick_random()
		untested_point_ids.erase(random_point)
		if test_path_if_point_toggled(random_point):
			networked_toggle_point.rpc(random_point, multiplayer.get_unique_id())


func place_random_towers(tower_limit: int) -> void:
	var untowered_bases: Array = tower_bases.duplicate()
	if tower_limit <= 0 or tower_limit > untowered_bases.size():
		tower_limit = untowered_bases.size()
	for index: int in tower_limit:
		var random_base: TowerBase = untowered_bases.pick_random() as TowerBase
		untowered_bases.erase(random_base)
		random_base.add_card(Data.cards.pick_random(), multiplayer.get_unique_id())


func find_path() -> bool:
	var path: PackedVector3Array = astar.get_point_path(astar.get_point_count() - 2, astar.get_point_count() - 1)
	if !path.is_empty():
		var curve: Curve3D = Curve3D.new()
		for point: Vector3 in path:
			curve.add_point(point)
		spawner.path.curve = curve
		spawner.path.spawn_visualizer_points()
		return true
	return false


func make_grid() -> void:
	for x: int in grid_size.x:
		for y: int in grid_size.y:
			var point_position: Vector3 = Vector3((x - floori(grid_size.x / 2.0)) * point_gap, 0.5, (y - floori(grid_size.y / 2.0)) * point_gap)
			astar.add_point(int(x * grid_size.y + y), point_position)
			var frame: Node3D = tower_frame_scene.instantiate()
			frame.position = point_position
			tower_frames.append(frame)
			add_child(frame)
	
	for x: int in grid_size.x:
		for y: int in grid_size.y:
			var point_id: int = grid_size.y * x + y
			if x > 0:
				var north_point_id: int = grid_size.y * (x - 1) + y
				astar.connect_points(point_id, north_point_id, false)
			if x < grid_size.x - 1:
				var south_point_id: int = grid_size.y * (x + 1) + y
				astar.connect_points(point_id, south_point_id, false)
			if y > 0:
				var east_point_id: int = grid_size.y * x + (y - 1)
				astar.connect_points(point_id, east_point_id, false)
			if y < grid_size.y - 1:
				var west_point_id: int = grid_size.y * x + (y + 1)
				astar.connect_points(point_id, west_point_id, false)
	
	non_build_locations.append(astar.get_point_count())
	astar.add_point(astar.get_point_count(), start.global_position)
	for x: int in grid_size.y:
		astar.connect_points(int(astar.get_point_count() - 1), x)
	non_build_locations.append(astar.get_point_count())
	astar.add_point(astar.get_point_count(), end.global_position)
	for x: int in grid_size.y:
		astar.connect_points(astar.get_point_count() - 1, int(grid_size.y * (grid_size.x - 1) + x))
