class_name HTTPServer extends TCP_Server


# Public constants

const Method = preload("res://addons/http_server/method.gd")
const Request = preload("res://addons/http_server/request.gd")
const Response = preload("res://addons/http_server/response.gd")
const Status = preload("res://addons/http_server/status.gd")


# Private variables

var __endpoints: Dictionary = {
	# key: [Int, String], array with 0 index representing method, 1 index representing endpoint
	# value: [FuncRef, Array], index 0 = reference to function to call, index 1 = binds to pass to func
}
var __fallback: FuncRef = null
var __server: TCP_Server = null


# Public methods

func endpoint(type: int, endpoint: String, function: FuncRef, binds: Array = []) -> void:
	var endpoint_hash: Array = [type, endpoint]
	if endpoint_hash in __endpoints:
		print(
			"[ERR] Endpoint already defined type: %s, endpoint: %s" % [
				Method.type_to_identifier(type),
				endpoint,
			]
		)
		return

	__endpoints[endpoint_hash] = [function, binds]


func fallback(function: FuncRef) -> void:
	__fallback = function


func take_connection() -> StreamPeerTCP:
	if !is_listening():
		print(
			"[ERR] Server is not listening, please initialize and listen before calling `take_connection`"
		)
		return null

	var connection: StreamPeerTCP = .take_connection()

	if connection:
		__process_connection(connection)

	return connection


# Private methods

func __process_connection(connection: StreamPeerTCP) -> void:
	var content: PoolByteArray = PoolByteArray([])

	while true:
		var bytes = connection.get_available_bytes()
		if bytes == 0:
			break

		var data = connection.get_partial_data(bytes)
		content.append_array(data[1])

	if content.empty():
		return

	var content_string: String = content.get_string_from_utf8()
	var content_parts: Array = content_string.split("\r\n")

	if content_parts.empty():
		connection.put_data(__response_from_status(Status.BAD_REQUEST).to_utf8())
		return

	var request_line = content_parts[0]
	var request_line_parts = request_line.split(" ")

	var method: String = request_line_parts[0]
	var endpoint: String = request_line_parts[1]

	var headers: Dictionary = {}
	var header_index: int = content_parts.find("")

	if header_index == -1:
		print(
			"[ERR] Error parsing request data: %s" % [String(content)]
		)
		connection.put_data(__response_from_status(Status.BAD_REQUEST).to_utf8())
		return

	for i in range(1, header_index):
		var header_parts: Array = content_parts[i].split(":", true, 1)
		var header = header_parts[0].strip_edges().to_lower()
		var value = header_parts[1].strip_edges()

		headers[header] = value

	var body: String = ""
	if header_index != content_parts.size() - 1:
		var body_parts: Array = content_parts.slice(header_index + 1, content_parts.size())
		body = PoolStringArray(body_parts).join("\r\n")

	var response: Response = __process_request(method, endpoint, headers, body)
	connection.put_data(response.get_data())


func __process_request(method: String, endpoint: String, headers: Dictionary, body: String) -> Response:
	var type: int = Method.description_to_type(method)

	var request: Request = Request.new(
		type,
		endpoint,
		headers,
		body
	)

	var endpoint_func: FuncRef = null
	var endpoint_parts: PoolStringArray = endpoint.split("/", false)
	var binds

	# special case for if endpoint is just root
	if endpoint == "/":
		var endpoint_hash: Array = [type, "/"]
		if __endpoints.has(endpoint_hash):
			endpoint_func = __endpoints[endpoint_hash][0]
			binds = __endpoints[endpoint_hash][1]
	else:
		while (!endpoint_func && !endpoint_parts.empty()):
			var endpoint_hash: Array = [type, "/" + endpoint_parts.join("/")]
			if __endpoints.has(endpoint_hash):
				endpoint_func = __endpoints[endpoint_hash][0]
				binds = __endpoints[endpoint_hash][1]
			else:
				endpoint_parts.remove(endpoint_parts.size() - 1)


	if !endpoint_func:
		print(
			"[WRN] Recieved request for unknown endpoint, method: %s, endpoint: %s" % [method, endpoint]
		)
		if __fallback:
			endpoint_func = __fallback
		else:
			return __response_from_status(Status.NOT_FOUND)

	var response: Response = Response.new()


	if !endpoint_func.is_valid():
		print(
			"[ERR] FuncRef for endpoint not valid, method: %s, endpoint: %s" % [method, endpoint]
		)
	else:
		print(
			"[INF] Recieved request method: %s, endpoint: %s" % [method, endpoint]
		)

		if !binds:
			endpoint_func.call_func(request, response)
		else:
			endpoint_func.call_func(request, response, binds)

	return response


func __response_from_status(code: int) -> Response:
	var response: Response = Response.new()
	response.status(code)

	return response
