@tool
extends Node

## Advanced websocket client that automatically reconnects to the server
class_name WebsocketClient


## Called as soon the websocket got a connection
signal connection_established

## Called as soon the websocket closed the connection
signal connection_closed

## Called when a complete message got received
signal message_received(message: PackedByteArray)

## Called when the state of the websocket changed
signal connection_state_changed(state : WebSocketPeer.State)

@export var connection_url: String:
	set(val):
		_logDebug("Set connection to %s" % val)
		connection_url = val

var connection_state : WebSocketPeer.State = WebSocketPeer.STATE_CLOSED:
	set(new_state):
		if new_state != connection_state:
			connection_state_changed.emit(new_state)
			if new_state == WebSocketPeer.STATE_OPEN: connection_established.emit()
			if new_state == WebSocketPeer.STATE_CLOSED: connection_closed.emit()
		connection_state = new_state

## Determines if a connection should be established or not
@export var auto_reconnect: bool:
	set(val):
		auto_reconnect = val
		_logDebug("New auto_reconnect value: %s" % val)

## True if currently connecting to prevent 2 connectionen processes at the same time
var _is_already_connecting: bool
var is_open: bool:
	get(): return _peer.get_ready_state() == WebSocketPeer.STATE_OPEN
var is_closed: bool:
	get(): return _peer.get_ready_state() == WebSocketPeer.STATE_CLOSED

var _peer: WebSocketPeer = WebSocketPeer.new()
var _tries: int


func open_connection() -> void:
	if not is_closed: return
	auto_reconnect = true
	_logInfo("Open connection")
	await _establish_connection()


func wait_connection_established() -> void:
	if is_open: return
	await connection_established

func _establish_connection() -> void:
	if _is_already_connecting || not is_closed: return
	_is_already_connecting = true
	var wait_time = pow(2, _tries)
	_logDebug("Wait %s before connecting" % [wait_time])
	await get_tree().create_timer(wait_time, true, false, true).timeout
	_logInfo("Connecting to %s" % connection_url)
	var err = _peer.connect_to_url(connection_url)
	if err != OK:
		logError("Couldn't connect cause of %s" % [error_string(err)])
	_tries += 1
	_is_already_connecting = false


func _enter_tree() -> void:
	if auto_reconnect: open_connection()


func _exit_tree() -> void:
	if not is_open: return
	_peer.close(1000, "resource got freed")


func _process(delta: float) -> void:
	_poll()


func _poll() -> void:
	if connection_url == "": return

	var state := _peer.get_ready_state()
	if state == WebSocketPeer.STATE_CLOSED and auto_reconnect:
		_establish_connection()
	_peer.poll()
	_handle_state_changes(state)
	connection_state = state
	if state == WebSocketPeer.STATE_OPEN:
		_read_data()


func _handle_state_changes(state: WebSocketPeer.State) -> void:
	if connection_state != WebSocketPeer.STATE_OPEN && state == WebSocketPeer.STATE_OPEN:
		_logInfo("connected")
		_tries = 0

	if connection_state != WebSocketPeer.STATE_CLOSED && state == WebSocketPeer.STATE_CLOSED:
		_logInfo("connection was closed [%s]: %s" % [_peer.get_close_code(), _peer.get_close_reason()])


func _read_data() -> void:
	while (_peer.get_available_packet_count()):
		message_received.emit(_peer.get_packet())


func send_text(message: String) -> Error:
	return _peer.send_text(message)


func close(status: int = 1000, message: String = "Normal Closure") -> void:
	_logDebug("Websocket activly closed")
	auto_reconnect = false
	_peer.close(status, message)


# === LOGGER ===
static var logger: Dictionary = {}
static func set_logger(error: Callable, info: Callable, debug: Callable) -> void:
	logger.debug = debug
	logger.info = info
	logger.error = error

func _logDebug(text: String) -> void:
	logDebug("[%s]: %s" % [connection_url, text])

static func logDebug(text: String) -> void:
	if logger.has("debug"): logger.debug.call(text)

func _logInfo(text: String) -> void:
	logInfo("[%s]: %s" % [connection_url, text])

static func logInfo(text: String) -> void:
	if logger.has("info"): logger.info.call(text)

static func logError(text: String) -> void:
	if logger.has("error"): logger.error.call(text)
