extends Node

var player_script := preload("res://scripts/Player.gd")
var server_map
@onready var Weapons = GameData.Weapons
var player_model = preload("res://scenes/models/player.tscn")
var peer = ENetMultiplayerPeer.new()
var settings
var clients = {}
var last_client_id = 1
var map_path
var map_root_name

var spawnpoints_os = []
var spawnpoints_cs = []

var cs_score = 0
var os_score = 0
var round_number = 0
var gamemode

func parse_arguments():
	var arguments = {}
	for argument in OS.get_cmdline_args():
		if argument.find("=") > -1:
			var key_value = argument.split("=")
			arguments[key_value[0].lstrip("--")] = key_value[1]
		else:
			# Options without an argument will be present in the dictionary,
			# with the value set to an empty string.
			arguments[argument.lstrip("--")] = ""
	return arguments

func check_map_availability(path):
	var maps = DirAccess.open("res://scenes/maps").get_files()
	var map_name = (path.split("/")[-1]).split(".")[0]
	for map in maps:
		var checking_map_name = str(map.split(".")[0])
		if(checking_map_name == map_name):
			return true
	return false

func check_gamemode_availability(gamemode):
	print(str(settings["game"]))
	var gamemodes = settings["game"]["gamemodes"].keys()
	for gm in gamemodes:
		if gm == gamemode:
			return true
	return false

func _ready():
	var arguments = parse_arguments()
	if "--server" in OS.get_cmdline_args():
		GameData.read_settings()
		settings = GameData.server_settings
		
		################ prepairing map
		var path = "res://scenes/maps/%s.tscn" % arguments["map"] if arguments.has("map") else "res://scenes/maps/%s.tscn" % settings["defaults"]["map"]

		if (check_map_availability(path)):
			map_path = path
		else:
			print("Unknown map %s Available maps:" % path)
			for map in DirAccess.open("res://scenes/maps").get_files():
				print(str(map.split(".")[0]))
			get_tree().quit()
			return
		################ prepairing gamemode
		var gm = str(arguments["gamemode"]) if (arguments.has("gamemode")) else "TDM"
		if(check_gamemode_availability(gm)):
			print("Gamemode exists")
		else:
			print("No")
		StartServer(map_path, gm)
#######################################SERVER####################################

func switch_map(new_map_path):
	print("Switching map to %s" % new_map_path)
	if (not check_map_availability(new_map_path)):
		var default_map = settings["defaults"]["map"]
		print("Error. No map found. Loading default map %s" % default_map)
		new_map_path = "res://scenes/maps/%s.tscn" % default_map
		if (not check_map_availability(new_map_path)):
			print("Error. Default map is not valid. Please, specify a valid map in a config file.")
			return
	map_path = new_map_path
	get_tree().change_scene_to_file(map_path)
	await get_tree().create_timer(0.1).timeout #I know that this isn't a good practice, but I didn't find anything better
	map_root_name = (map_path.split("/")[-1]).split(".")[0]
	server_map = get_tree().root.get_node(map_root_name)

func switch_gamemode(new_gamemode):
	if not check_gamemode_availability(new_gamemode):
		print("No gamemode found")
		return
	gamemode = new_gamemode

func new_game(new_map_path, new_gamemode):
	cs_score = 0
	os_score = 0
	round_number = 0
	await switch_map(new_map_path)
	await switch_gamemode(new_gamemode)

func new_round():
	round_number += 1

func find_playermodel_by_internal_id(internal_id):
	return server_map.get_node("player" + str(internal_id))

func StartServer(map_path, gm):
	await new_game(map_path, gm)

	var port = int(settings["port"])
	var maxclients = int(settings["maxclients"])
	if (peer.create_server(port, maxclients) != OK):
		print("Couldn't create server. Check if another proccess binds port %s" % str(port))
		return
	
	multiplayer.multiplayer_peer = peer
	print("Server started")
	
	peer.connect("peer_connected", _Peer_Connected)
	peer.connect("peer_disconnected", _Peer_Disconnected)

	var spectator = preload("res://scenes/models/spectator.tscn").instantiate()
	server_map.add_child(spectator)
	var spawnpoints = server_map.find_children("spawnpoint*", "" ,true)
	for spawnpoint in spawnpoints:
		if spawnpoint.team == 0: # cs
			spawnpoints_cs.push_back(spawnpoint)
		elif spawnpoint.team > 0: # os
			spawnpoints_os.push_back(spawnpoint)
			
func send_everyone_except(client_id, args):
	if (typeof(args[0]) == 4): #string
		for current_client_id in clients.keys():
			if (str(current_client_id) == str(client_id)): continue
			if (args.size() == 1): rpc_id(int(current_client_id), args[0])
			elif (args.size() == 2): rpc_id(int(current_client_id), args[0], args[1])
			elif (args.size() == 3): rpc_id(int(current_client_id), args[0], args[1], args[2])
			else: rpc_id(int(current_client_id), args[0], args[1], args[2], args[3])
	else:
		for current_client_id in clients.keys():
			if (str(current_client_id) == str(client_id)): continue
			if (args.size() == 1): args[0].rpc_id(int(current_client_id))
			elif (args.size() == 2): args[0].rpc_id(int(current_client_id), args[1])
			elif (args.size() == 3): args[0].rpc_id(int(current_client_id), args[1], args[2])
			else: args[0].rpc_id(int(current_client_id), args[1], args[2], args[3])

func find_weapon_by_number(number):
	var found_weapon
	for weapon in settings["game"]["weapons"].keys():
		if (settings["game"]["weapons"][weapon]["number"] == number):
			found_weapon = settings["game"]["weapons"][weapon].duplicate()
			break
	return found_weapon

func find_class_type_by_number(number):
	var found_class
	var classtypes = settings["game"]["classTypes"]
	for classtype in classtypes:
		if classtypes[classtype]["number"] == number:
			found_class = classtypes[classtype].duplicate()
			break
	return found_class

func _Peer_Connected(client_id):
	print("User " + str(client_id) + " has conected")
	var internal_id = last_client_id + 1
	last_client_id += 1
	
	clients[client_id] = player_script.new().properties.duplicate()
	var client = clients[client_id]
	client["position"] = Vector3(0, 10, 0)
	client["internal_id"] = internal_id
	client["is_playable"] = false
	client["current_weapon"] = settings["game"]["weapons"]["knife"].duplicate()
	
	print("New client's properties: " + str(clients[client_id]))
	
	var puppet = player_model.instantiate()
	#var CB3D = puppet.find_child("CharacterBody3D")
	puppet.set_properties(clients[client_id])
	
	server_map.add_child(puppet)

@rpc("any_peer", "reliable", "call_remote")
func client_ready(client_id):
	var client = clients[client_id]
	var internal_id = client["internal_id"]
	
	send_everyone_except(client_id, ["spawn_puppet", clients[client_id]])

	client["ready"] = true
	var client_playermodel = find_playermodel_by_internal_id(internal_id)
	if (client["class_type"] == 0):
		var class_spawnpoint = find_class_type_by_number(client["class_type"])["spawnpoint"]
		client["position"] = Vector3(class_spawnpoint[0], class_spawnpoint[1], class_spawnpoint[2])
	else:
		var index = abs(client["class_type"])
		if (client["class_type"] > 0):
			client["position"] = spawnpoints_os.pick_random().get_class_spawnpoint(index)
		elif (client["class_type"] < 0):
			client["position"] = spawnpoints_cs.pick_random().get_class_spawnpoint(index)
	client_playermodel.set_properties(client)
	client_playermodel.teleport.rpc_id(client_id, Vector3(client["position"].x, client["position"].y, client["position"].z))

func _Peer_Disconnected(client_id):
	print("User " + str(client_id) + " has disconnected")
	var client = clients[client_id]
	var internal_id = client["internal_id"]
	rpc("despawn_puppet", internal_id)
	var puppet = server_map.get_node("player" + str(internal_id))
	server_map.remove_child(puppet)
	clients.erase(client_id)

@rpc ("any_peer", "reliable", "call_remote")
func get_character_properties(client_id):
	if !clients.has(client_id):
		return
	var to_send = clients[client_id]
	rpc_id(client_id, "set_character_properties", to_send.duplicate())

@rpc("any_peer", "call_remote", "unreliable")
func sync_client(client_id, position, rotation):
	var client = clients[client_id]
	var internal_id = str(client["internal_id"])
	var client_playermodel = find_playermodel_by_internal_id(internal_id)
	client_playermodel.position = position
	client_playermodel.find_child("Head").rotation.y = rotation.y
	client_playermodel.find_child("Head").find_child("Camera").rotation.x = rotation.x
	client["position"] = position
	client["rotation"] = rotation
	send_everyone_except(client_id, [client_playermodel.sync_puppet, internal_id, position, rotation])

@rpc ("any_peer", "call_remote", "reliable")
func get_client_list(client_id):
	for current_client_id in clients.keys():
		if (current_client_id == client_id): continue
		rpc_id(client_id, "spawn_puppet", clients[current_client_id])

@rpc ("any_peer", "call_remote", "reliable")
func get_server_settings(client_id):
	print("Sending game settings")
	var client = clients[client_id]
	var internal_id = str(client["internal_id"])
	var client_playermodel = find_playermodel_by_internal_id(internal_id)
	client_playermodel.set_game_settings.rpc_id(client_id, settings["game"])

@rpc ("any_peer", "call_remote", "reliable")
func set_nickname(client_id, nickname):
	print("Got nickname from client: " + str(nickname))
	clients[client_id]["nickname"] = nickname

@rpc("any_peer", "call_remote", "reliable")
func shot(client_id):
	var client = clients[client_id]
	var internal_id = client["internal_id"]
	var current_weapon = settings["game"]["weapons"].find_key(find_weapon_by_number(client["current_weapon"]["number"]))
	var current_weapon_settings = settings["game"]["weapons"][current_weapon]

	var client_playermodel = find_playermodel_by_internal_id(internal_id)
	var raycast:RayCast3D = client_playermodel.get_node("Head/Camera/viewRaycast")
	var weapon_raycast:RayCast3D = client_playermodel.get_node("Head/Camera/Hand/" + str(current_weapon) + "/raycast")
	raycast.target_position.z = -current_weapon_settings["range"]
	raycast.force_raycast_update()
	raycast.force_update_transform()
	var target_point = raycast.get_collision_point()
	
	weapon_raycast.target_position = weapon_raycast.to_local(target_point) * 1.1
	weapon_raycast.rotation.y = atan((weapon_raycast.position.x - client_playermodel.position.x) / ((-current_weapon_settings["range"]) - (abs(weapon_raycast.position.z - client_playermodel.position.z))))
	
	weapon_raycast.target_position.y += current_weapon_settings["spreading"] * randf() * sin(randf() * 2 * PI)
	weapon_raycast.target_position.z += current_weapon_settings["spreading"] * randf() * cos(randf() * 2 * PI)
	
	weapon_raycast.force_raycast_update()
	weapon_raycast.force_update_transform()
	
	var target = weapon_raycast.get_collider()
	
	if (target is CharacterBody3D):
		var target_internal_id = int(target.name.get_slice("player", 1))
		var target_client
		
		for checking_client_id in clients.keys():
			if (clients[checking_client_id]["internal_id"] == target_internal_id):
				target_client = clients[checking_client_id]
		var target_client_id = int(clients.find_key(target_client))
		
		var shape_num = weapon_raycast.get_collider_shape()
		var shapes = ["head", "body", "hand", "leg"]
		var shape = choose_collision_shape(target, shapes, shape_num)
		var damage
		for s in shapes:
			if s in shape.name:
				damage = current_weapon_settings["damage"][s]
				break
		
		var time_since_last_shot = (Time.get_ticks_msec() - client["last_shot"]) / 1000.
		
		if (time_since_last_shot < current_weapon_settings["fireRate"] and client["last_shot"] > 0):
			return
		
		if (client["current_weapon"]["magazine"] == 0):
			return
		client["last_shot"] = Time.get_ticks_msec()
		
		target_client["HP"] -= damage
		if (target_client["HP"] <= 0):
			var index = abs(client["class_type"])
			var respawn = Vector3(0, 10 ,0)
			if (target_client["class_type"] > 0):
				respawn = spawnpoints_os.pick_random().get_class_spawnpoint(index)
			elif (target_client["class_type"] < 0):
				respawn = spawnpoints_cs.pick_random().get_class_spawnpoint(index)
				
			target_client["position"] = respawn
			target_client["HP"] = 100
			target.teleport.rpc_id(target_client_id, target_client["position"])
		target.set_hp.rpc_id(target_client_id, target_client["HP"])
	elif (target is StaticBody3D):
		var shapes = ["head", "body"]
		var shape_num = weapon_raycast.get_collider_shape()
		var shape = choose_collision_shape(target, shapes, shape_num)
		target.shot.rpc_id(client_id, weapon_raycast.get_collision_point())
		send_everyone_except(client_id, [target.shot, weapon_raycast.get_collision_point()])
func choose_collision_shape(target, shapes, shape_num):
	var collision_shapes:Array
	for s in target.get_children():
		if s is CollisionShape3D:
			collision_shapes.push_back(s)
	var shape
	for i in range(0, collision_shapes.size()):
		if i == shape_num:
			shape = collision_shapes[i]
			break
	return shape
@rpc("reliable", "call_remote", "any_peer")
func change_weapon(client_id, new_weapon_number):
	var client = clients[client_id]
	client["reloading"] = false
	var internal_id = str(client["internal_id"])
	for weapon in Weapons.keys():
		if (settings["game"]["weapons"][weapon]["number"] == new_weapon_number):
			client["current_weapon"] = settings["game"]["weapons"][weapon].duplicate()
			break
	var client_playermodel = find_playermodel_by_internal_id(internal_id)
	client_playermodel.change_weapon(new_weapon_number)
	send_everyone_except(client_id, [client_playermodel.change_weapon_puppet, internal_id, new_weapon_number])

@rpc("any_peer", "call_remote", "reliable")
func client_reloading(client_id):
	var client = clients[client_id]
	var weapons_list = settings["game"]["weapons"]
	var current_client_weapon = client["current_weapon"]
	var current_weapon_example = weapons_list.find_key(find_weapon_by_number(current_client_weapon["number"]))
	var current_weapon_settings = weapons_list[current_weapon_example]
	var reload_time = current_weapon_settings["reload"]
	client["reloading"] = true
	reloading_complete(client_id, reload_time)

func reloading_complete(client_id, reloading_time):
	await get_tree().create_timer(reloading_time).timeout
	var client = clients[client_id]
	if (!client["reloading"]): # if a client has interrupted the reloading
		return
	
	var weapons_list = settings["game"]["weapons"]
	var current_client_weapon = client["current_weapon"]
	var current_weapon_example = weapons_list.find_key(find_weapon_by_number(current_client_weapon["number"]))
	var current_weapon_settings = weapons_list[current_weapon_example]
	var to_reload = current_weapon_settings["magazine"] - current_client_weapon["magazine"]
	if (to_reload <= current_client_weapon["ammo"]):
		current_client_weapon["magazine"] += to_reload
		current_client_weapon["ammo"] -= to_reload
	else:
		current_client_weapon["magazine"] += current_client_weapon["ammo"]
		current_client_weapon["ammo"] = 0
	
@rpc("reliable", "any_peer", "call_remote")
func get_map(client_id):
	rpc_id(client_id, "receive_map", map_path)

@rpc("reliable", "any_peer", "call_remote")
func choose_class(client_id, class_id):
	var client = clients[client_id]
	client["class_type"] = class_id
	#here are must be checks if the teams are balanced. WIP.
	switch_class.rpc_id(client_id, class_id)

##########################################CLIENT#######################

var player
var menu = preload("res://scenes/HUD/menu.tscn")
var current_map_instance
var choose_team_hud
var playermodel

@rpc ("reliable", "call_remote")
func set_character_properties(p):
	print("Setting player properties to " + str(p))
	player = p

@rpc("authority", "reliable", "call_remote")
func spawn_puppet(properties):
	var puppet = player_model.instantiate()
	#var CB3D = puppet.find_child("CharacterBody3D")
	properties["ready"] = true
	puppet.set_properties(properties.duplicate())
	
	current_map_instance.add_child(puppet)

@rpc("authority", "reliable", "call_remote")
func despawn_puppet(internal_id):
	var puppet = current_map_instance.get_node("player" + str(internal_id))
	current_map_instance.remove_child(puppet)

@rpc("authority", "reliable", "call_remote")
func receive_map(p):
	map_path = p
	map_root_name = (map_path.split("/")[-1]).split(".")[0]

@rpc("authority", "reliable", "call_remote")
func switch_class(class_id):
	print("Switching to team: " + str(class_id))
	if (class_id == 128): # team is full, you must chose another
		current_map_instance.remove_child(choose_team_hud)
		current_map_instance.add_child(choose_team_hud)
	
	player["class_type"] = class_id
	rpc_id(1, "client_ready", multiplayer.get_unique_id())
	current_map_instance.remove_child(choose_team_hud)
	playermodel.set_property("is_playable", true)
	playermodel.set_property("ready", true)

func spawn_player():
	playermodel = player_model.instantiate()
	var properties = player_script.new().properties.duplicate()
	#playermodel = player_node.find_child("player" + str(properties["internal_id"]))
	
	properties = player
	properties["is_playable"] = false
	properties["ready"] = false
	playermodel.set_properties(properties)
	
	current_map_instance.add_child(playermodel)

func ConnectToServer(ip, port):
	peer.create_client(ip, port)
	multiplayer.multiplayer_peer = peer
	
	if not multiplayer.connected_to_server.is_connected(_Connection_Succseeded): 
		multiplayer.connected_to_server.connect(_Connection_Succseeded)
	if not multiplayer.connection_failed.is_connected(_Connection_Failed): 
		multiplayer.connection_failed.connect(_Connection_Failed)
	if not multiplayer.server_disconnected.is_connected(_Server_Disconnected):
		multiplayer.server_disconnected.connect(_Server_Disconnected)

func _Connection_Failed():
	print("Failed to connect to server")
	multiplayer.multiplayer_peer = null
	peer = ENetMultiplayerPeer.new()
	multiplayer.multiplayer_peer = peer

func _Connection_Succseeded():
	print("Succsessfully connected to the server")
	var nickname = GameData.client_settings["nickname"]
	rpc_id(1, "set_nickname", multiplayer.get_unique_id(), nickname)
	rpc_id(1, "get_character_properties", multiplayer.get_unique_id())
	rpc_id(1, "get_map", multiplayer.get_unique_id())
	await get_tree().create_timer(0.2).timeout # костыль
	
	get_tree().change_scene_to_file(map_path)
	await get_tree().create_timer(1).timeout # костыль
	current_map_instance = get_tree().root.get_node(map_root_name)
	
	rpc_id(1, "get_client_list", multiplayer.get_unique_id())
	await get_tree().create_timer(0.05).timeout # костыль # tp
	rpc_id(1, "get_server_settings", multiplayer.get_unique_id())
	
	await spawn_player()
	
	choose_team_hud = preload("res://scenes/HUD/choose_team.tscn").instantiate()
	current_map_instance.add_child(choose_team_hud)

func _Server_Disconnected():
	print("Server has disconnected")
	Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
	get_tree().change_scene_to_file("res://scenes/HUD/menu.tscn")
	multiplayer.multiplayer_peer = null
	peer = ENetMultiplayerPeer.new()
	multiplayer.multiplayer_peer = peer
