extends RefCounted
class_name Promise


enum Status {
	RESOLVED,
	REJECTED
}


signal settled(status: PromiseResult)
signal resolved(value: Variant)
signal rejected(reason: Rejection)


## Generic rejection reason
const PROMISE_REJECTED := "Promise rejected"


var is_settled := false


func _init(callable: Callable):
	resolved.connect(
		func(value: Variant): 
			is_settled = true
			settled.emit(PromiseResult.new(Status.RESOLVED, value)), 
		CONNECT_ONE_SHOT
	)
	rejected.connect(
		func(rejection: Rejection):
			is_settled = true
			settled.emit(PromiseResult.new(Status.REJECTED, rejection)), 
		CONNECT_ONE_SHOT
	)
	
	callable.call_deferred(
		func(value: Variant):
			if not is_settled:
				resolved.emit(value),
		func(rejection: Rejection):
			if not is_settled:
				rejected.emit(rejection)
	)

	
func then(resolved_callback: Callable) -> Promise:
	resolved.connect(
		resolved_callback, 
		CONNECT_ONE_SHOT
	)
	return self
	
	
func catch(rejected_callback: Callable) -> Promise:
	rejected.connect(
		rejected_callback, 
		CONNECT_ONE_SHOT
	)
	return self
	
	
static func from(input_signal: Signal) -> Promise:
	return Promise.new(
		func(resolve: Callable, _reject: Callable):
			var number_of_args := input_signal.get_object().get_signal_list() \
				.filter(func(signal_info: Dictionary) -> bool: return signal_info["name"] == input_signal.get_name()) \
				.map(func(signal_info: Dictionary) -> int: return signal_info["args"].size()) \
				.front() as int
			
			if number_of_args == 0:
				await input_signal
				resolve.call(null)
			else:
				# only one arg in signal is allowed for now
				var result = await input_signal
				resolve.call(result)
	)


static func from_many(input_signals: Array[Signal]) -> Array[Promise]:
	return input_signals.map(
		func(input_signal: Signal): 
			return Promise.from(input_signal)
	)

	
static func all(promises: Array[Promise]) -> Promise:
	return Promise.new(
		func(resolve: Callable, reject: Callable):
			var resolved_promises: Array[bool] = []
			var results := []
			results.resize(promises.size())
			resolved_promises.resize(promises.size())
			resolved_promises.fill(false)
	
			for i in promises.size():
				promises[i].then(
					func(value: Variant):
						results[i] = value
						resolved_promises[i] = true
						if resolved_promises.all(func(value: bool): return value):
							resolve.call(results)
				).catch(
					func(rejection: Rejection):
						reject.call(rejection)
				)
	)
	
	
static func any(promises: Array[Promise]) -> Promise:
	return Promise.new(
		func(resolve: Callable, reject: Callable):
			var rejected_promises: Array[bool] = []
			var rejections: Array[Rejection] = []
			rejections.resize(promises.size())
			rejected_promises.resize(promises.size())
			rejected_promises.fill(false)
	
			for i in promises.size():
				promises[i].then(
					func(value: Variant): 
						resolve.call(value)
				).catch(
					func(rejection: Rejection):
						rejections[i] = rejection
						rejected_promises[i] = true
						if rejected_promises.all(func(value: bool): return value):
							reject.call(PromiseAnyRejection.new(PROMISE_REJECTED, rejections))
				)
	)


class PromiseResult:
	var status: Status
	var payload: Variant
	
	func _init(_status: Status, _payload: Variant):
		status = _status
		payload = _payload
		
		
class Rejection:
	var reason: String
	var stack: Array
	
	func _init(_reason: String):
		reason = _reason
		stack = get_stack() if OS.is_debug_build() else []
		
		
	func as_string() -> String:
		return ("%s\n" % reason) + "\n".join(
			stack.map(
				func(dict: Dictionary) -> String: 
					return "At %s:%i:%s" % [dict["source"], dict["line"], dict["function"]]
		))
	

class PromiseAnyRejection extends Rejection:
	var group: Array[Rejection]
	
	func _init(_reason: String, _group: Array[Rejection]):
		super(_reason)
		group = _group