extends Node

signal task_done

const THREAD_COUNT: int = 4

var _threads: Array = []
var _commands: Array = []
# Using a Dictionary here because there is no Set
var _tasks_in_progress: Dictionary = {}
var _next_task_id: int = 0
var _finishing: bool = false
var _wait_semaphore := Semaphore.new()
var _commands_mutex := Mutex.new()
var _tasks_in_progress_mutex := Mutex.new()


func _ready() -> void:
	for _count in range(THREAD_COUNT):
		var th := Thread.new()
		_threads.append(th)
		th.start(self, '_thread_wait')


func _exit_tree() -> void:
	_finishing = true
	for _count in range(THREAD_COUNT):
		_wait_semaphore.post()
	for th in _threads:
		if th.is_active():
			th.wait_to_finish()


func queue_task(instance: Object, method: String, args: Array=[],
		callback: FuncRef=null, pass_id: bool=false, pass_result: bool=true,
		callback_args: Array=[]) -> int:
	assert(instance.has_method(method))
	var task_id: int = _next_task_id
	_next_task_id += 1
	
	_commands_mutex.lock()
	_commands.append({"id": task_id, "instance": instance, "method": method,
			"args": args, "callback": callback, "pass_id": pass_id,
			"pass_result": pass_result, "callback_args": callback_args})
	_tasks_in_progress_mutex.lock()
	_tasks_in_progress[task_id] = null
	_tasks_in_progress_mutex.unlock()
	_commands_mutex.unlock()
	
	_wait_semaphore.post()
	return task_id


func wait_for_task_completion(task_id: int):
	var result = null
	
	if not (task_id in _tasks_in_progress):
		return result
	
	while true:
		var task_data: Dictionary = yield(self, "task_done")
		if task_data["command"]["id"] == task_id:
			result = task_data["result"]
			break
	return result


func _thread_wait() -> void:
	while not _finishing:
		_wait_semaphore.wait()
		
		_commands_mutex.lock()
		if _commands.size() == 0:
			_commands_mutex.unlock()
			continue
		var command: Dictionary = _commands.pop_front()
		_commands_mutex.unlock()
		
		var instance: Object = command["instance"]
		var method: String = command["method"]
		var args: Array = command["args"]
		var result = null
		if is_instance_valid(instance):
			result = instance.callv(method, args)
			if result is GDScriptFunctionState:
				result = yield(result, "completed")
		
		# Use `call_deferred()` so that it is run in the main thread
		call_deferred("_complete_task", command, result)


func _complete_task(command: Dictionary, result) -> void:
	var task_id: int = command["id"]
	_tasks_in_progress_mutex.lock()
	_tasks_in_progress.erase(task_id)
	_tasks_in_progress_mutex.unlock()
	
	emit_signal("task_done", {"command": command, "result": result})
	_execute_callback(command, result)


func _execute_callback(command: Dictionary, result) -> void:
	var id: int = command["id"]
	var callback: FuncRef = command["callback"]
	var pass_id: bool = command["pass_id"]
	var pass_result: bool = command["pass_result"]
	var callback_args: Array = command["callback_args"]
	if callback != null and callback.is_valid():
		if pass_result:
			callback_args.push_front(result)
		if pass_id:
			callback_args.push_front(id)
		callback.call_funcv(callback_args)
