extends TileMap

@export var width := 60
@export var height := 32
@export var fill_percentage := 0.65

@export var solid_threshold := 7
@export var nonsolid_threshold := 4

@export var start_area_size := 5
@export var start_area_corner_size := 2

@export var cave_gen_iterations := 3
@export var cave_mine_size_threshold := 80

var tile_array : Array

func generate() -> Dictionary:
	var start_time = Time.get_unix_time_from_system()
	
	randomize()
	setup_tile_data_array()
	fill_random_noise()
	
	for i in cave_gen_iterations:
		change_cells_by_neighbor_thresholds()
	set_borders_solid()
	prepare_player_start_area()
	connect_caves(get_caves())
	for i in cave_gen_iterations:
		change_cells_by_neighbor_thresholds()
	set_borders_solid()
	prepare_player_start_area()
	
	tile_array_to_terrain()
	
	return {"time": Time.get_unix_time_from_system() - start_time, "free_tiles": get_free_tiles()}

func get_free_tiles() -> Array:
	var free_tiles = []
	for x in width:
		for y in height:
			if get_tile_non_solid(x, y): free_tiles.push_back(Vector2(x, y) * self.cell_quadrant_size * self.scale + (Vector2(self.cell_quadrant_size, self.cell_quadrant_size) * self.scale / 2))
	return free_tiles

func setup_tile_data_array():
	tile_array.clear()
	tile_array = []
	for x in width:
		tile_array.append([])
		for y in height:
			tile_array[x].append([])

func fill_random_noise():
	for x in width:
		for y in height:
			if randf() < fill_percentage:
				set_tile_solid(x,y)
			else:
				set_tile_non_solid(x,y)
	pass

func set_borders_solid():
	for x in width:
		set_tile_solid(x,0)
		set_tile_solid(x,height-1)
	for y in height:
		set_tile_solid(0,y)
		set_tile_solid(width-1,y)
	pass

func prepare_player_start_area():
	@warning_ignore("integer_division")
	var center = Vector2i(width / 2, height / 2)
	for x in range(center.x - start_area_size, center.x + start_area_size):
		
		# Getting a P factor for the corner "radius" Decission
		var p := 0
		if x <= center.x - start_area_size + start_area_corner_size:
			p = center.x - start_area_size  + start_area_corner_size - x
		if x >= center.x + start_area_size - start_area_corner_size:
			p =  x - ( center.x + start_area_size)  + start_area_corner_size + 1
		var p2 = p
		
		for y in range(center.y - start_area_size, center.y + start_area_size):
			# Decide if the tile is part of the Corner or not
			if !(p2 > 0 or p2 <= - (start_area_size*2 - p*2)):
				set_tile_non_solid(x,y)
			p2 -= 1
	pass

func change_cells_by_neighbor_thresholds():
	for x in range(1, width-1):
		for y in range(1, height-1):
			# Count Solid Neighbor Cells
			var count := 0
			#y-1
			if get_tile_solid(x-1, y-1): count +=1
			if get_tile_solid(x, y-1): count +=1
			if get_tile_solid(x+1, y-1): count +=1
			#y
			if get_tile_solid(x-1, y): count +=1
			if get_tile_solid(x+1, y): count +=1
			#y+1
			if get_tile_solid(x-1, y+1): count +=1
			if get_tile_solid(x, y+1): count +=1
			if get_tile_solid(x+1, y+1): count +=1
			
			# Check Threshold
			if count < nonsolid_threshold:
				set_tile_non_solid(x,y)
			if count >= solid_threshold:
				set_tile_solid(x,y)
	pass

func get_caves() -> Array:
	# get caves
	var caves := []

	for x in range (2, width-2):
		for y in range (2, height-2):
			if get_tile_non_solid(x, y):
				flood_fill(x,y, caves)

	for cave in caves:
		for tile in cave:
			set_tile_non_solid(tile.x,tile.y)
	return caves

func flood_fill(tilex, tiley, caves) -> Array:
	var cave := []
	var to_fill := [Vector2i(tilex, tiley)]
	while to_fill:
		var tile = to_fill.pop_back()

		if !cave.has(tile):
			cave.append(tile)
			set_tile_solid(tile.x,tile.y)

			#check adjacent cells
			var north = Vector2i(tile.x, tile.y+1)
			var south = Vector2i(tile.x, tile.y-1)
			var east  = Vector2i(tile.x+1, tile.y)
			var west  = Vector2i(tile.x-1, tile.y)

			for dir in [north,south,east,west]:
				if  get_tile_non_solid(dir.x, dir.y):
					if !to_fill.has(dir) and !cave.has(dir):
						to_fill.append(dir)
	if cave.size() >= cave_mine_size_threshold:
		caves.append(cave)
	return caves

func choose_rand(choices):
	randomize()

	var rand_index = randi() % choices.size()
	return choices[rand_index]

func connect_caves(caves):
	var prev_cave = null
	var tunnel_caves = caves.duplicate()

	for cave in tunnel_caves:
		if prev_cave:
			var new_point  = choose_rand(cave)
			var prev_point = choose_rand(prev_cave)

			# ensure not the same point
			if new_point != prev_point:
				create_tunnel(new_point, prev_point, cave)

		prev_cave = cave
	pass
	
func create_tunnel(point1, point2, cave):
	randomize()          # for randf
	var max_steps = 500  # so game won't hang if walk fails
	var steps = 0
	var drunk_x = point2[0]
	var drunk_y = point2[1]

	while steps < max_steps and !cave.has(Vector2i(drunk_x, drunk_y)):
		steps += 1

		# set initial dir weights
		var n       = 1.0
		var s       = 1.0
		var e       = 1.0
		var w       = 1.0
		var weight  = 1

		# weight the random walk against edges
		if drunk_x < point1.x:
			e += weight
		elif drunk_x > point1.x:
			w += weight
		if drunk_y < point1.y:
			s += weight
		elif drunk_y > point1.y:
			n += weight

		# normalize probabilities so they form a range from 0 to 1
		var total = n + s + e + w
		n /= total
		s /= total
		e /= total
		w /= total

		var dx
		var dy

		# choose_rand the direction
		var choice = randf()

		if 0 <= choice and choice < n:
			dx = 0
			dy = -1
		elif n <= choice and choice < (n+s):
			dx = 0
			dy = 1
		elif (n+s) <= choice and choice < (n+s+e):
			dx = 1
			dy = 0
		else:
			dx = -1
			dy = 0

		# ensure not to walk past edge of map
		if (2 < drunk_x + dx and drunk_x + dx < width-2) and \
			(2 < drunk_y + dy and drunk_y + dy < height-2):
			drunk_x += dx
			drunk_y += dy
			
			if get_tile_solid(drunk_x, drunk_y):
				set_tile_non_solid(drunk_x, drunk_y)
				#make tunnel wider
				set_tile_non_solid(drunk_x+1, drunk_y)
				set_tile_non_solid(drunk_x+1, drunk_y+1)
	pass

func tile_array_to_terrain():
	for x in width:
		for y in height:
			match tile_array[x][y]:
				0: set_terrain_tile_non_solid(x,y)
				1: set_terrain_tile_solid(x,y)
	pass

# Getter
func get_tile_solid(x : int, y : int) -> bool:
	return tile_array[x][y] == 1
	
func get_tile_non_solid(x : int, y : int) -> bool:
	return tile_array[x][y] == 0
	
# Setter
func set_tile_solid(x : int, y : int):
	tile_array[x][y] = 1
	pass
	
func set_tile_non_solid(x : int, y : int):
	tile_array[x][y] = 0
	pass

func set_terrain_tile_solid(x : int, y : int):
	self.set_cells_terrain_connect(0, [Vector2i(x,y)], 0, 0, false)
	pass
	
func set_terrain_tile_non_solid(x : int, y : int):
	self.set_cells_terrain_connect(0, [Vector2i(x,y)], 0, 1, false)
	pass
