extends Node

signal ms_updated(action, data)
signal left()

# Hosting info
const SERVER_PORT := 5513
const MAX_PLAYERS := 30
var port := SERVER_PORT

# Throttle network updates of often-changed variables by this many physics frames
const SYSTEMS_UPDATE_INTERVAL := 10

# Master server data
const MASTER_SERVER_ADDR := "fgms.zyg.ovh"
const MASTER_SERVER_UDP_PORT := 9434
const MS_GAME_CODE := "odyssey-0-a2"
const GOTM_OVERRIDE := false

# Master server entry
var ms_active := false
var ms_key := ""
var server_name := ""

var hosting := false

export var player_name := ""

var player_info := {}
var round_info := {}

onready var scene_manager := $"/root/SceneManager"

func _ready():
	randomize()
	player_name = Names.get_random_name()

func bind_events():
	get_tree().connect("network_peer_connected", self, "_player_connected")
	get_tree().connect("network_peer_disconnected", self, "_player_disconnected")
	get_tree().connect("connected_to_server", self, "_connected_ok")
	get_tree().connect("connection_failed", self, "_connected_fail")
	get_tree().connect("server_disconnected", self, "_server_disconnected")

func punch_nat():
	var socketUDP = PacketPeerUDP.new()
	if socketUDP.listen(SERVER_PORT) != OK:
		push_error("error listening on port: " + str(SERVER_PORT))
	socketUDP.set_dest_address(MASTER_SERVER_ADDR, MASTER_SERVER_UDP_PORT)
	socketUDP.put_packet("hi server!".to_ascii())
	# Poll for answer from server, since godot doesn't have events for UDP packets (ノへ￣、)
	var failed = 0
	while failed < 5:
		yield(get_tree().create_timer(.3), "timeout")
		if socketUDP.get_available_packet_count() < 1:
			failed += 1
			print("no reply (attempt #%d)" % failed)
			continue
		port = socketUDP.get_var(false)
		print("Received ", port)
		break
	socketUDP.close()

func discover_upnp():
	var upnp := UPNP.new()
	upnp.discover(2000, 2, "InternetGatewayDevice")
	return upnp.add_port_mapping(SERVER_PORT)

func host(map_name: String = "odyssey"):
	scene_manager.enter_loader()
	scene_manager.loading_text = "Starting server"
	
	# Wait just a sec to draw
	yield(get_tree().create_timer(0.3), "timeout")

	# Run port forwarding/nat punchthrough if the platform doesn't do it already
	print("Running UPNP magicks")
	if discover_upnp() == UPNP.UPNP_RESULT_SUCCESS:
		print("UPNP mapping added")
	else:
		push_warning("UPNP magicks fail, punching NAT in the face")
		yield(punch_nat(), "completed")
	
	server_name = "%s's server" % player_name
	player_info[1] = { "name": player_name }
	round_info = { "map": map_name }

	bind_events()
	var peer := NetworkedMultiplayerENet.new()
	peer.compression_mode = NetworkedMultiplayerENet.COMPRESS_FASTLZ
	var server_res := peer.create_server(port, MAX_PLAYERS)
	if server_res != OK:
		match server_res:
			ERR_CANT_CREATE:
				push_error("Can't create server")
			ERR_ALREADY_IN_USE:
				push_error("Already in use")
		return
	get_tree().network_peer = peer
	print("Hosting")
	hosting = true

	scene_manager.loading_text = null

	scene_manager.load_scene("res://Scenes/Game.tscn", [
		"res://Scenes/Maps/%s.tscn" % map_name
	])
	
	# Add to master server after hosting
	create_ms_entry()

func join(server):
	scene_manager.enter_loader()
	scene_manager.loading_text = "Joining server"

	# Wait just a sec to draw
	yield(get_tree().create_timer(0.3), "timeout")

	bind_events()
	var peer = NetworkedMultiplayerENet.new()
	peer.compression_mode = NetworkedMultiplayerENet.COMPRESS_FASTLZ
	
	var addr = server.address
	
	peer.create_client(addr, SERVER_PORT)
	get_tree().network_peer = peer
	print("Connecting to %s" % addr)

func leave():
	var peer := get_tree().network_peer
	if get_tree().is_network_server():
		# Tell MS we're leaving
		if ms_active:
			_ms_request("remove", { "key": ms_key })
			yield(self, "ms_updated")
	elif peer != null:
		#TODO Send leave message
		pass
	get_tree().network_peer = null
	emit_signal("left")

func _player_connected(id):
	rpc_id(id, "register_player", player_name)
	if get_tree().is_network_server():
		rpc_id(id, "_handshake", { "round": round_info, "players": player_info })

func _player_disconnected(id):
	print("%s (%d) disconnected" % [player_info[id].name, id])
	player_info.erase(id)

func _connected_ok():
	print("Connected to server")
	scene_manager.loading_text = null
	scene_manager.load_scene("res://Scenes/Game.tscn", [])

func _server_disconnected():
	print("Disconnected from server")

func _connected_fail():
	push_warning("Connection failed")

remote func register_player(username: String):
	var id := get_tree().get_rpc_sender_id()
	player_info[id] = { name=username }
	print("%s (%d) connected" % [player_info[id].name, id])

remote func _handshake(infos):
	round_info = infos["round"]
	player_info = infos["players"]

func _ms_request(endpoint: String, data):
	var http_request := HTTPRequest.new()
	add_child(http_request)
	http_request.connect("request_completed", self, "_ms_response", [endpoint])
	print_debug("Telling ms to %s" % endpoint)
	var error = http_request.request(
		"https://%s/%s" % [MASTER_SERVER_ADDR, endpoint],
		["Content-Type: application/json"],
		true, HTTPClient.METHOD_POST, JSON.print(data))
	if error != OK:
		push_error("An error occurred in the HTTP request.")

func ms_get_entries():
	var http_request := HTTPRequest.new()
	add_child(http_request)
	http_request.connect("request_completed", self, "_ms_response", ["list_games"])
	var error = http_request.request(
		"https://%s/%s" % [MASTER_SERVER_ADDR, MS_GAME_CODE],
		["Content-Type: application/json"],
		true, HTTPClient.METHOD_GET)
	if error != OK:
		push_error("An error occurred in the HTTP request.")

func get_game_data():
	return {
		"name": server_name,
		"port": port,
		"max_players": MAX_PLAYERS,
		"players": player_info.size()
	}

func create_ms_entry():
	_ms_request("new", { 
		"game_id": MS_GAME_CODE,
		"data": get_game_data()
	})

func update_ms_entry():
	if ms_active:
		_ms_request("update", { 
			"key": ms_key,
			"data": get_game_data()
		})

var time_left = 30
func _process(delta):
	if not ms_active:
		return
	time_left -= delta
	if time_left < 0:
		update_ms_entry()
		time_left = 30

func _ms_response(_result: int, response_code: int, _headers: PoolStringArray, body: PoolByteArray, action: String):
	print_debug("MS said %s" % response_code)
	if response_code > 299:
		push_error("ms action '%s' returned error: %s - %s" % [action, response_code, body.get_string_from_utf8()])
		return
	var json := JSON.parse(body.get_string_from_utf8())
	match action:
		"new":
			if json.result.ok:
				ms_active = true
				ms_key = json.result.key
	emit_signal("ms_updated", action, json.result)

func _notification(what):
	# Are we quittin son?
	if what == MainLoop.NOTIFICATION_WM_QUIT_REQUEST:
		leave()
		yield(self, "left")

func get_current_map():
	match round_info.map:
		"odyssey":
			return GameWorld.Map.ODYSSEY
		"runtime":
			return GameWorld.Map.RUNTIME
		_:
			return GameWorld.Map.EMPTY
