# Hurry Curry! - a game about cooking
# Copyright (C) 2026 Hurry Curry! Contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3 of the License only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
class_name Map
extends Node3D

class TileInfo:
	func _init(position_, tiles_, tile_instances_, tile_parent_) -> void:
		position = position_; tiles = tiles_; tile_instances = tile_instances_; tile_parent = tile_parent_
	var position: Vector2i
	var tiles: Array # Array[String]
	var tile_instances: Array[Tile]
	var tile_parent: Node3D
	var interact_tile: Tile
	
const NEIGHBOR_OFFSETS: Array[Vector2i] = [Vector2i.UP, Vector2i.LEFT, Vector2i.DOWN, Vector2i.RIGHT]

var tile_by_pos: Dictionary[Vector2i, TileInfo] = {}
var autoflush = false
var currently_baked = false
var floor_node := MeshInstance3D.new()
var tile_factory := TileFactory.new()

func get_tiles_at(pos: Vector2i): # -> Array[String]?
	var e = tile_by_pos.get(pos)
	if e == null: return null
	return e.tiles
func get_topmost_instance(pos: Vector2i): # -> Tile?
	var e = tile_by_pos.get(pos)
	if e == null: return null
	return e.tile_instances[-1]
func get_tile_item(pos: Vector2i): # -> Item?
	var e = get_topmost_instance(pos)
	if e == null: return null
	return e.item

func set_all_tiles(changes: Dictionary[Vector2i, Array], srv: Game.ServerContext = null):
	for pos: Vector2i in changes:
		set_tiles(Vector2i(pos.x, pos.y), changes[pos], changes, srv)

func set_tiles(pos: Vector2i, tiles: Array = [], pending_changes: Dictionary[Vector2i, Array] = {}, srv: Game.ServerContext = null): # tiles: Array[String]
	var tile_info = tile_by_pos.get(pos)
	if tile_info != null:
		for inst: Tile in tile_info.tile_instances:
			for tile: String in tiles:
				# TODO: Don't return, but handle changes which weren't handled by the instance below.
				if inst.change(tile): return # Instance handled change itself!
	_remove_tile(pos)
	if not tiles.is_empty(): _add_tiles(pos, tiles, pending_changes, srv)
	if autoflush: flush()

func _add_tiles(pos: Vector2i, tiles: Array, pending_changes: Dictionary[Vector2i, Array], srv: Game.ServerContext) -> void:
	# Find neighbor tile names
	var neighbors: Array[Array] = [] # Array[Array[String]]
	for offset: Vector2i in NEIGHBOR_OFFSETS:
		var neighbor_pos: Vector2i = pos + offset
		if pending_changes.has(neighbor_pos):
			neighbors.append(pending_changes[neighbor_pos])
		elif tile_by_pos.has(neighbor_pos):
			neighbors.append(tile_by_pos[neighbor_pos])
		else: neighbors.append([])

	var tiles_parent = Node3D.new()
	tiles_parent.name = str(pos)
	var tile_instances = tile_factory.produce(tiles_parent, tiles, pos, neighbors, srv)
	tile_by_pos[pos] = TileInfo.new(pos, tiles, tile_instances, tiles_parent)
	add_child(tiles_parent)

func _remove_tile(pos: Vector2i):
	var tile_info = tile_by_pos.get(pos)
	if tile_info == null: return

	var topmost_instance = get_topmost_instance(pos)
	if topmost_instance.item != null:
		topmost_instance.item.queue_free()
	
	for instance: Tile in tile_info.tile_instances:
		if instance is FloorLike:
			var floor_mesher = tile_factory.floor_meshers.get(instance.fm_id())
			if floor_mesher != null:
				floor_mesher.remove_tile(pos)
		instance.queue_free()
	tile_by_pos.erase(pos)

@onready var voxelgi: VoxelGI = $VoxelGI

func _ready():
	Settings.hook_changed("graphics.gi", self, apply_gi_setting)
	floor_node.material_override = preload("res://map/tiles/floor_material.tres")
	for fm in tile_factory.floor_meshers.values():
		add_child(fm.mesh_instance)
	add_child(floor_node)

func flush() -> void:
	for fm in tile_factory.floor_meshers.values():
		fm.flush()
	gi_bake()

func apply_gi_setting(state):
	if state == "voxelgi" and not currently_baked:
		gi_bake()
	else:
		currently_baked = false
		voxelgi.data = null

func gi_bake():
	if Settings.read("graphics.gi") != "voxelgi": return
	print("Map: Rebaking VoxelGI")
	currently_baked = true
	gi_bake_blocking()

func gi_bake_blocking():
	var map_extents = extents()
	var extent_min = map_extents[0]
	var extent_max = map_extents[1]
	var center = (extent_max + extent_min) / 2
	var size = extent_max - extent_min
	voxelgi.position = Vector3(center.x, 3., center.y)
	voxelgi.size = Vector3(size.x, 8., size.y)
	print("Baking now!")
	var start = Time.get_ticks_msec()
	voxelgi.bake()
	voxelgi.visible = true
	print("Bake done. elapsed=", Time.get_ticks_msec() - start)

func extents() -> Array[Vector2]:
	var extent_min = Vector2(0,0)
	var extent_max = Vector2(0,0)
	for e: TileInfo in tile_by_pos.values():
		extent_min.x = min(extent_min.x, e.position.x)
		extent_min.y = min(extent_min.y, e.position.y)
		extent_max.x = max(extent_max.x, e.position.x)
		extent_max.y = max(extent_max.y, e.position.y)
	return [extent_min, extent_max + Vector2(1., 1.)]
