tool
extends Node2D

class_name GameMap

var debug_areas := false

var ship_direction := 0
var ship_speed := 0

var current_ship_position := Vector2.ZERO
var current_ship_subpos := Vector2.ZERO
var current_ship_target = Vector2.ZERO

var current_ship_direction := 0.0
var current_ship_speed := 0.0

var deepspace_mat: ShaderMaterial = null
var warp_state := false

puppet var pup_ship_position := Vector2.ZERO
puppet var pup_ship_subpos := Vector2.ZERO
puppet var pup_ship_target := Vector2.ZERO
puppet var pup_ship_direction := 0.0
puppet var pup_ship_speed := 0.0

const MAX_ACCELERATION := 0.03
const MAX_STEERING := 0.06
const EPSILON := 1e-3
const SCROLL_MULTIPLIER := 1e4
const ENGINE_MULTIPLIER := 5000
const NO_SPEED_THRESHOLD := 0.01

const ProbeElectricity := preload("res://Actors/Systems/Electricity/ElectricProbe.tscn")

onready var tilemaps := [ $tiles/base, $tiles/cables, $tiles/floor, $tiles/walls ]

onready var pois := $pois

export var unlit := false setget set_unlit
onready var darkness := $darkness

export var shadow_intensity := 0.2

func _ready():
	if Engine.editor_hint:
		return
	set_unlit(true)
	
	if not is_network_master():
		return

	# Run autotile conversions and generate occlusions
	$tiles/walls.run_conversions()

	# Electricity setup
	make_electric_probes($tiles/cables, "Wire")
	
	# Set engines to expected power level
	set_engine_strength(current_ship_speed)
	
	deepspace_mat = $deepspace.material as ShaderMaterial

func _process(delta: float):
	if Engine.editor_hint:
		return
	
	var engines := get_engine_count()
	var max_speed := get_max_speed()

	# Ease ship speed/direction changes
	if abs(current_ship_direction) > PI*2:
		current_ship_direction -= PI*2 * sign(current_ship_direction)
	var direction_delta := ship_direction - current_ship_direction
	if direction_delta < 0:
		direction_delta += PI*2
	if direction_delta > PI:
		direction_delta -= PI*2

	if abs(direction_delta) < 1e-3:
		current_ship_direction = ship_direction
	else:
		# Avoid stuttering by not turning until there's enough reason to
		if abs(direction_delta) > 0.1:
			current_ship_direction += MAX_STEERING * engines * delta * sign(direction_delta)
	var speed_delta := ship_speed - current_ship_speed
	if abs(speed_delta) < EPSILON:
		current_ship_speed = ship_speed
	else:
		current_ship_speed += MAX_ACCELERATION * max_speed * delta * sign(speed_delta)
		set_engine_strength(current_ship_speed * ENGINE_MULTIPLIER)

	deepspace_mat.set_shader_param("scroll_speed", current_ship_speed * 5000)

func _physics_process(delta):
	if Engine.editor_hint:
		return
	if is_network_master():
		var adj_position := current_ship_position+current_ship_subpos
		if current_ship_target != null:
			var distance := adj_position.distance_to(current_ship_target)
			if distance > NO_SPEED_THRESHOLD:
				ship_direction = current_ship_target.angle_to_point(adj_position)
				var max_speed := get_max_speed()
				if max_speed > 0:
					var deceleration_time = current_ship_speed / (MAX_ACCELERATION * max_speed)
					if current_ship_speed * deceleration_time < distance:
						ship_speed = max_speed
					else:
						ship_speed = 0.0
				else:
					ship_speed = 0.0
				rset("pup_ship_direction", ship_direction)
				rset("pup_ship_speed", ship_speed)
			else:
				ship_speed = 0.0
				rset("pup_ship_speed", ship_speed)
			if current_ship_speed > EPSILON:
				var pos_delta: Vector2 = (Vector2.RIGHT * current_ship_speed * delta).rotated(current_ship_direction)
				current_ship_subpos += pos_delta
				if current_ship_subpos.x >= 1.0:
					var int_part := floor(current_ship_subpos.x)
					current_ship_position.x += int_part
					current_ship_subpos.x -= int_part
				if current_ship_subpos.y >= 1.0:
					var int_part := floor(current_ship_subpos.y)
					current_ship_position.y += int_part
					current_ship_subpos.y -= int_part
				rset("pup_ship_position", current_ship_position)
				rset("pup_ship_subpos", current_ship_subpos)
				if not warp_state and current_ship_speed > 0.05:
					# Engage FTL
					rpc("warp_enter")
				elif warp_state and current_ship_speed < 0.02:
					# Diseangage FTL
					rpc("warp_exit")
		else:
			ship_speed = 0.0
			rset("pup_ship_speed", ship_speed)
	else:
		ship_direction = pup_ship_direction
		ship_speed = pup_ship_speed
		current_ship_position = pup_ship_position
		current_ship_subpos = pup_ship_subpos

func get_engine_count() -> int:
	var working_engines := 0
	for child in $engines.get_children():
		var engine := child as GameObjectEngine
		if engine.force > 0:
			working_engines += 1
	return working_engines

func get_max_speed() -> float:
	var max_speed := 0.0
	for child in $engines.get_children():
		var engine := child as GameObjectEngine
		max_speed += engine.force
	return max_speed

# Serialization / Deserialization

func serialize() -> Dictionary:
	# Get tilemap data
	var tilemapdata := {}
	for tilemap in tilemaps:
		var data := []
		for cell in tilemap.get_used_cells():
			data.append([cell, tilemap.get_cellv(cell)])
		tilemapdata[tilemap.name] = data

	return {
		"tilemaps": tilemapdata,
		"engines": serialize_children($engines),
		"objects": serialize_children($objects),
		"lights": serialize_children($lights),
		"sockets": serialize_children($sockets),
		"pois": serialize_children($pois),
		"areas": serialize_children($areas),
		"ship": {
			"position": current_ship_position,
			"target": current_ship_target,
			"set_speed": ship_speed,
			"set_direction": ship_direction,
			"speed": current_ship_speed,
			"direction": current_ship_direction
		}
	}

func deserialize(data: Dictionary) -> void:
	# Create maps
	for tilemap in data["tilemaps"]:
		var tilemap_node = get_node(tilemap) as TileMap
		for tile in data["tilemaps"][tilemap]:
			tilemap_node.set_cellv(tile[0], tile[1])
		tilemap_node.update_bitmask_region()
		tilemap_node.update_dirty_quadrants()

	# Create objects
	deserialize_children($engines, data["engines"])
	deserialize_children($objects, data["objects"])
	deserialize_children($lights, data["lights"])
	deserialize_children($pois, data["pois"])
	deserialize_children($sockets, data["sockets"])
	deserialize_children($sockets, data["sockets"])
	deserialize_children($areas, data["areas"], true)
	
	# Set ship parameters
	current_ship_position = data["ship"]["position"]
	current_ship_target = data["ship"]["target"]
	ship_speed = data["ship"]["set_speed"]
	ship_direction = data["ship"]["set_direction"]
	current_ship_speed = data["ship"]["speed"]
	current_ship_direction = data["ship"]["direction"]

	# Run autotile conversions and generate occlusions
	$tiles/walls.run_conversions()

func serialize_children(node: Node) -> Dictionary:
	var data := {}
	for child in node.get_children():
		data[child.name] = {
			"filename": child.filename,
			"data": child.serialize(),
			"transform": child.transform
		}
	return data

func deserialize_children(node: Node, data: Dictionary, deserialize_first: bool = false) -> void:
	for node_name in data:
		var node_data := data[node_name] as Dictionary
		var node_scene = load(node_data.filename).instance()
		node_scene.name = node_name
		node_scene.transform = node_data.transform
		if deserialize_first:
			node_scene.deserialize(node_data.data)
		node.add_child(node_scene, true)
		if not deserialize_first:
			node_scene.deserialize(node_data.data)

# Lighting

func set_unlit(val: bool):
	unlit = val
	if darkness:
		darkness.visible = not val

# Engine related functions

func set_engine_strength(val: float):
	# Set energy strength to current speed
	for child in $engines.get_children():
		if child is GameObjectEngine:
			var engine := child as GameObjectEngine
			engine.strength = val

remotesync func warp_enter():
	$deepspace/AnimationPlayer.play("warp-enter")
	warp_state = true

remotesync func warp_exit():
	$deepspace/AnimationPlayer.play("warp-exit")
	warp_state = false

# Tileset related functions

func make_electric_probes(tilemap: TileMap, tile_name: String):
	var tile_id := tilemap.tile_set.find_tile_by_name(tile_name)
	for cell in tilemap.get_used_cells_by_id(tile_id):
		var coord := tilemap.map_to_world(cell)
		var probe := ProbeElectricity.instance()
		probe.position = coord
		tilemap.add_child(probe)

func get_pois(type_filter, class_filter) -> Array:
	var filtered := []
	for child in $pois.get_children():
		if type_filter != null and child.poitype != type_filter:
			continue
		if class_filter != null and child.poiclass != class_filter:
			continue
		filtered.append(child)
	return filtered
