# 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 Multiplayer
extends Node

signal packet(packet: Dictionary)
signal connection_closed()

static var VERSION_MAJOR: int = 13
static var VERSION_MINOR: int = 0

var connected := false
var socket: WebSocketPeer
var keep_alive := Timer.new()

func _ready():
	add_child(keep_alive)
	keep_alive.wait_time = 1.
	keep_alive.timeout.connect(send_keep_alive)

func connect_to_urls(urls: Array[String]):
	if urls.is_empty():
		connection_closed.emit("No connection address available.")
		return
	
	var error_info: Dictionary[String, int] = {}
	
	# Create a WebSocketPeer for each url
	var peers: Array[WebSocketPeer] = []
	for url: String in urls:
		var ws := WebSocketPeer.new()
		ws.inbound_buffer_size = 1024 * 1024 * 4
		var err := ws.connect_to_url(url)
		if err == OK: peers.append(ws)
		else: error_info[url] = err
	
	# Now keep polling until one of them is succesful, or we run out of peers.
	# Peers are removed from the peers array when they fail to connect.
	var open_peer_found := false
	while not peers.is_empty() and not open_peer_found:
		await get_tree().physics_frame
		for peer: WebSocketPeer in peers:
			peer.poll()
			var state := peer.get_ready_state()
			match state:
				WebSocketPeer.STATE_CLOSED:
					print("URL %s failed" % peer.get_requested_url())
					error_info[peer.get_requested_url()] = peer.get_close_code()
					peers.erase(peer)
				WebSocketPeer.STATE_OPEN:
					# We found a connection that works. Close all others.
					print("URL %s connected!" % peer.get_requested_url())
					socket = peer
					var other_peers := peers.filter(func (p): return p != peer)
					for p: WebSocketPeer in other_peers:
						p.close()
					open_peer_found = true
					break
				_: pass
	
	if not open_peer_found:
		var err_msg: String = tr("c.error.could_not_connect")
		for url: String in error_info.keys():
			err_msg += "\nURL %s failed with code %d" % [url, error_info[url]]
		connection_closed.emit(err_msg)
		return
	
	connected = true
	keep_alive.start()

func _notification(what):
	if what == NOTIFICATION_PREDELETE and socket != null:
		socket.close()
		connected = false

func _process(_delta):
	if connected:
		socket.poll()
		var state = socket.get_ready_state()
		while socket.get_available_packet_count():
			handle_packet(socket.get_packet())
		if state == WebSocketPeer.STATE_CLOSED:
			connection_closed.emit("c.error.connection_closed")
			connected = false

func fix_packet_types(val: Variant):
	match typeof(val):
		TYPE_FLOAT: return val
		TYPE_STRING: return val
		TYPE_BOOL: return val
		TYPE_ARRAY: return val.map(fix_packet_types)
		TYPE_DICTIONARY:
			var new_dict = {}
			for k in val.keys():
				if val[k] is Array and val[k].size() == 2:
					# A Vector2 is represented as an array with 2 elements in our protocol.
					# We need to convert it to Godot's Vector2 type for easier handling.
					if k in ["tile"]: new_dict[k] = Vector2i(val[k][0], val[k][1]) # TODO: Are these still necessary?
					elif k in ["pos", "position", "dir"]: new_dict[k] = Vector2(val[k][0], val[k][1])
					else: new_dict[k] = fix_packet_types(val[k])
				else: new_dict[k] = fix_packet_types(val[k])
			return new_dict

func handle_packet(coded):
	var p = decode_packet(coded)
	if p == null:
		return
	
	p = fix_packet_types(p)

	match p["type"]:
		"version":
			var major = p["major"]
			var minor = p["minor"]
			if major != VERSION_MAJOR or minor > VERSION_MINOR:
				socket.close()
				connected = false
				connection_closed.emit(tr("c.error.version_mismatch").format([major, minor, VERSION_MAJOR, VERSION_MINOR]))
		_: packet.emit(p)

func send_join(player_name: String, character_style: Dictionary):
	send_packet({
		"type": "join",
		"name": player_name,
		"character": character_style
	})

func send_movement(player, pos: Vector2, direction: Vector2, boost: bool):
	send_packet({
		"type": "movement",
		"player": player,
		"pos": [pos.x, pos.y],
		"dir": [direction.x, direction.y],
		"boost": boost
	})
	

func send_tile_interact(player, pos: Vector2i, edge: bool, hand: int):
	@warning_ignore("incompatible_ternary")
	send_packet({
		"type": "interact",
		"player": player,
		"target": {"tile": [pos.x, pos.y]} if edge else null,
		"hand": hand,
	})

func send_player_interact(player, target_player, target_hand: int, edge: bool, hand: int):
	@warning_ignore("incompatible_ternary")
	send_packet({
		"type": "interact",
		"player": player,
		"target": {"player": [target_player, target_hand]} if edge else null,
		"hand": hand,
	})

func send_chat(player, message: String):
	send_packet({
		"type": "communicate",
		"player": player,
		"persist": false,
		"message": {
			"text": message
		}
	})

func send_replay_tick(dt: float):
	send_packet({
		"type": "replay_tick",
		"dt": dt
	})

func send_idle(paused: bool):
	send_packet({
		"type": "idle",
		"paused": paused,
	})

func send_leave(player):
	send_packet({
		"type": "leave",
		"player": player,
	})

func send_ready():
	send_packet({
		"type": "ready"
	})

func send_start_game_vote(player_id: float, map: String, hand_count = 1, bots = []):
	var config := {
		"map": map
	}
	if hand_count != null: config["hand_count"] = hand_count
	if bots != null: config["bots"] = bots
	send_packet({
		"type": "initiate_vote",
		"player": player_id,
		"subject": {
			"action": "start_game",
			"config": config
		}
	})

func send_keep_alive() -> void:
	send_packet({
		"type": "keepalive"
	})

func send_packet(p):
	var json = JSON.stringify(p)
	if socket.get_ready_state() != WebSocketPeer.State.STATE_OPEN:
		push_warning("Can not send packet: Socket not open")
		return
	socket.send_text(json)

func decode_packet(bytes: PackedByteArray):
	var json = JSON.new()
	var in_str = bytes.get_string_from_utf8()
	var error = json.parse(in_str)
	if error == OK:
		return json.data
	else:
		print("Decode of packet failed: %s in %s" % [json.get_error_message(), in_str])
		return null
