## Conway's Game of Life
##	Rule 1 - Any live cell with fewer than two live neighbours dies (underpopulation)
##	Rule 2 - Any live cell with two or three live neighbours lives
##	Rule 3 - Any live cell with more than three live neighbours dies (overpopulation)
##	Rule 4 - Any dead cell with exactly three live neighbours becomes a live cell (reproduction)
extends Node2D


enum CellStates {
	DEAD,
	ALIVE,
}

## UI
@onready var background_ui: Control = $UI/Background

@onready var debug_ui: MarginContainer = $UI/Debug
@onready var debug_generation_counter: Label = $UI/Debug/VBoxContainer/GenerationCounter
@onready var debug_living_cells_counter: Label = $UI/Debug/VBoxContainer/LivingCellsCounter
@onready var debug_world_seed: Label = $UI/Debug/VBoxContainer/WorldSeed

@onready var generation_ui: MarginContainer = $UI/WorldGeneration
@onready var generation_seed: LineEdit = $UI/WorldGeneration/VBoxContainer/Seed/Input
@onready var generation_speed: HSlider = $UI/WorldGeneration/VBoxContainer/GenerationSpeed/Input
@onready var generation_world_size_x: LineEdit = $UI/WorldGeneration/VBoxContainer/WorldSize/Input_x
@onready var generation_world_size_y: LineEdit = $UI/WorldGeneration/VBoxContainer/WorldSize/Input_y
@onready var start_paused_button: CheckBox = $UI/WorldGeneration/VBoxContainer/FinalRow/StartPausedButton

@onready var messages_label: Label = $UI/Messages/Label

## Other
@onready var camera: Camera2D = $Camera
@onready var generation_timer: Timer = $GenerationTimer

@export var world_seed: int ## The seed utilized for generation of the world
@export var cell_size: Vector2 = Vector2(16, 16) ## Size of each cell image
@export var cell_texture: Texture2D ## Testure for each cell
@export var world_size: Vector2 = Vector2(32, 32) ## The size of the world

var cell_instance
var evolution_is_stalled: bool = false
var generation: int = 1
var is_paused: bool = false
var total_living: int = 0

var cell_ids: Array = [] # Store cell RIDs
var cell_states: Array = []

var previous_generation_hashes: Array = [] # Store the generation related to the hashes
var previous_generation_count: int = 10 # Number of generations to check for evolution stalling

# Number of collisions to consider evolution stalled
#  The higher the number, the less likely to catch oscillators
#  Consider the previous_generation_count as this may need to increase for larger counts
#  4 Collisions out of 10 previous_generation_count seems reasonable although it may not be quite enough
var collision_count_limit: int = 4

var timer_speeds: Array = [0.1, 0.08, 0.05, 0.03, 0.01] # The speeds for the generation_timer for selection from the UI


func _ready() -> void:
	update_generation_ui()
	debug_ui.visible = false
	background_ui.visible = true
	generation_ui.visible = true

func _input(event: InputEvent) -> void:
	if event.is_action_pressed("Pause"):
		toggle_pause()
	if is_paused and event.is_action_pressed("Next Generation"):
		process_generation()
		debug_generation_counter.text = "Generation: %s" % generation
		debug_living_cells_counter.text = "Living Cells: %s" % total_living
	if event.is_action_pressed("Menu"):
		toggle_pause()
		update_generation_ui()
		generation_ui.visible = is_paused


#
# UI
#
## Update the Generation UI with the current settings
func update_generation_ui() -> void:
	if world_seed:
		generation_seed.text = str(world_seed)
	else:
		generation_seed.text = str(randi())
	generation_world_size_x.text = str(world_size.x)
	generation_world_size_y.text = str(world_size.y)
	start_paused_button.button_pressed = false


#
# Conway Specific
#
func start_conway() -> void:
	clear_world()
	generate_world()
	update_previous_generation_hashes()

	generation = 1
	evolution_is_stalled = false
	debug_world_seed.text = "World Seeed: %s" % world_seed
	debug_generation_counter.text = "Generation: %s" % generation
	debug_living_cells_counter.text = "Living Cells: %s" % total_living

	# Center the camera on the world
	camera.position = Vector2(
		world_size.x * cell_size.x / 2,
		world_size.y * cell_size.y / 2
	)
	camera.zoom = Vector2.ONE

	if not is_paused: generation_timer.start()


## Check a cell against the Conway rules and return True if cell should be alive
##  The logic in this could be cleaned up pretty easily. Only verbose for understanding.
func cell_is_alive(pos: Vector2) -> bool:
	var neighbors := count_living_neighbors(pos)
	var currently_alive = cell_states[pos.x][pos.y] == CellStates.ALIVE

	# Rule 1 - Any live cell with fewer than two live neighbours dies (underpopulation)
	if currently_alive and neighbors < 2:
		return false
	# Rule 2 - Any live cell with two or three live neighbours lives
	elif currently_alive and (neighbors == 2 or neighbors == 3):
		return true
	# Rule 3 - Any live cell with more than three live neighbours dies (overpopulation)
	elif currently_alive and neighbors > 3:
		return false
	# Rule 4 - Any dead cell with exactly three live neighbours becomes a live cell (reproduction)
	elif not currently_alive and neighbors == 3:
		return true

	return false

## Count all neighbors surrounding a cell
func count_living_neighbors(pos: Vector2) -> int:
	var count := 0

	@warning_ignore("incompatible_ternary")
	var x_min = 0 if pos.x - 1 < 0 else pos.x - 1
	@warning_ignore("incompatible_ternary")
	var y_min = 0 if pos.y - 1 < 0 else pos.y - 1
	var x_max = world_size.x if pos.x + 2 > world_size.x else pos.x + 2
	var y_max = world_size.y if pos.y + 2 > world_size.y else pos.y + 2

	for x in range(x_min, x_max):
		for y in range(y_min, y_max):
			if x == pos.x and y == pos.y: continue # Current cell - Don't count
			if cell_states[x][y] == CellStates.ALIVE:
				count += 1
	return count

func end_world_generation() -> void:
	# Determine which generation first stalled
	var current_world_hash: int = cell_states.hash()
	var colliding = previous_generation_hashes.filter(func(gen): return gen.hash == current_world_hash)
	var stalled_generation = colliding[0].generation

	messages_label.text = "Detected Stalled Evolution Starting at Generation %s" % stalled_generation
	messages_label.visible = true
	evolution_is_stalled = true

## Get the number of previous generation results that collide with the current hash
func get_colliding_hash_count(input_hash: int) -> int:
	var count = 0
	for gen in previous_generation_hashes:
		count += int(gen.hash == input_hash)
	return count

## Loop through world to generate cell states, hide/show cells depending on state
func process_generation() -> void:
	if evolution_is_stalled: return

	update_previous_generation_hashes()

	if previous_generation_hashes[previous_generation_hashes.size()-1].collisions >= collision_count_limit:
		return end_world_generation()

	generation += 1
	total_living = 0

	var rs := RenderingServer

	var new_states: Array= []
	for x in range(world_size.x):
		new_states.append([])
		new_states[x].resize(world_size.x)
		for y in range(world_size.y):
			var pos = Vector2(x, y)
			var is_alive = cell_is_alive(pos)
			total_living += int(is_alive)
			new_states[x][y] = int(is_alive)
			rs.canvas_item_set_visible(cell_ids[x][y], is_alive)

	cell_states = new_states

## Update the previous run hash and generation ids
##  We only store the last `previous_generation_count` number of results
func update_previous_generation_hashes() -> void:
	if previous_generation_hashes.size() < previous_generation_count:
		previous_generation_hashes.resize(previous_generation_count)
		previous_generation_hashes.fill({"generation": 0, "hash": 0, "collisions": 0})

	# Remove the oldest generation information
	previous_generation_hashes.remove_at(0)

	var generation_hash := cell_states.hash()
	previous_generation_hashes.append({
		"generation": generation,
		"hash": generation_hash,
		"collisions": get_colliding_hash_count(generation_hash),
	})

## Toggle Pause UI and Start/Stop Generation Timer
func toggle_pause() -> void:
	is_paused = !is_paused
	background_ui.visible = is_paused

	if is_paused: generation_timer.stop()
	else: generation_timer.start()


## Loop over cell_ids and free the resource in the RenderingServer
##   Clear cell_ids and cell_states
func clear_world() -> void:
	for row in cell_ids:
		for id in row:
			RenderingServer.free_rid(id)
	cell_ids = []
	cell_states = []

## Create the cell using the rendering server
##  This is only performed on initial world generation
func create_cell(pos: Vector2, visible_cell: bool = true) -> RID:
	var rs = RenderingServer

	cell_instance = rs.canvas_item_create()
	rs.canvas_item_set_parent(cell_instance, get_canvas_item())

	var rect: Rect2 = Rect2(-cell_size.x/2, -cell_size.y/2, cell_size.x, cell_size.y) # Centered with cell size
	rs.canvas_item_add_texture_rect(cell_instance, rect, cell_texture)

	var pos_fixed = Vector2(pos.x * cell_size.x, pos.y * cell_size.y)
	var trans = Transform2D(0, pos_fixed)
	rs.canvas_item_set_transform(cell_instance, trans)

	rs.canvas_item_set_visible(cell_instance, visible_cell)

	return cell_instance

## Generate the world with cells in random states (alive or dead)
func generate_world() -> void:
	for x in range(world_size.x):
		cell_ids.append([])
		cell_ids[x].resize(world_size.x)
		cell_states.append([])
		cell_states[x].resize(world_size.x)
		for y in range(world_size.y):
			var is_alive: int = randi_range(CellStates.DEAD, CellStates.ALIVE)
			total_living += is_alive
			cell_states[x][y] = is_alive
			cell_ids[x][y] = create_cell(Vector2(x, y), bool(is_alive))


func _on_generation_timer_timeout() -> void:
	process_generation()

	debug_generation_counter.text = "Generation: %s" % generation
	debug_living_cells_counter.text = "Living Cells: %s" % total_living

	generation_timer.start()

func _on_generation_speed_drag_ended(value_changed: bool) -> void:
	var speed = generation_speed.value - 1
	if value_changed:
		generation_timer.wait_time = timer_speeds[speed]

func _on_quit_button_pressed() -> void:
	get_tree().quit()

func _on_run_button_pressed() -> void:
	if generation_seed.text.strip_edges():
		world_seed = int(generation_seed.text)
	else:
		world_seed = randi()
	world_size = Vector2(int(generation_world_size_x.text), int(generation_world_size_y.text))

	debug_ui.visible = true
	generation_ui.visible = false
	background_ui.visible = false
	messages_label.visible = false
	seed(world_seed)

	start_conway()

func _on_start_paused_button_toggled(button_pressed: bool) -> void:
	is_paused = button_pressed

func _on_world_seed_generate_pressed() -> void:
	generation_seed.text = str(randi())


func _exit_tree() -> void:
	clear_world()