const { Framework } = require("../core.js");

((f)=>{
	f.CustomPromise = class {
		#state = 'pending';
		#value = undefined;
		// A list of "clients" that need to be notified when a state
		//   change event occurs. These event-consumers are the promises
		//   that are returned by the calls to the `then` method:
		#consumers = [];
		// Only relevant for promises returned by a `then` call:
		#onFulfilled;
		#onRejected;
		
		constructor(executor) {
			try {
				executor(value => this.#resolve(value), 
						 reason => this.#reject(reason));
			} catch(err) { // Return a rejected promise when error occurs
				this.#reject(err);
			}
		}
		
		// 2.1.1.1: provide only two ways to transition
		#fulfill(value) {
			if (this.#state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
			this.#state = 'fulfilled'; // 2.1.1.1: can transition
			this.#value = value; // 2.1.2.2: must have a value
			this.#broadcast();
		}    
		
		#reject(reason) {
			if (this.#state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
			this.#state = 'rejected'; // 2.1.1.1: can transition
			this.#value = reason; // 2.1.3.2: must have a reason
			this.#broadcast();
		}    
		
		// A promise’s then method accepts two arguments:
		then(onFulfilled, onRejected) {
			const consumer = new MyPromise(() => {});
			// 2.2.1.1 ignore onFulfilled if not a function
			consumer.#onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
			// 2.2.1.2 ignore onRejected if not a function
			consumer.#onRejected = typeof onRejected === 'function' ? onRejected : null;
			// 2.2.6.1, 2.2.6.2: .then() may be called multiple times on the same promise
			this.#consumers.push(consumer);
			// It might be that the promise was already resolved... 
			this.#broadcast();
			// 2.2.7: .then() must return a promise
			return consumer;
		}
		
		#broadcast() {
			// 2.2.2.1, 2.2.2.2, 2.2.3.1, 2.2.3.2 called after promise is resolved
			if (this.#state === 'pending') return;
			const fulfilled = this.#state == 'fulfilled';
			const resolver = fulfilled ? this.#resolve : this.#reject;
			// 2.2.4 onFulfilled/onRejected must be called asynchronously
			queueMicrotask(() => {
				// 2.2.6.1, 2.2.6.2 traverse all callbacks in order, 
				//    2.2.2.3, 2.2.3.3 call them only once
				for (const consumer of this.#consumers.splice(0)) {
					try {
						const callback = fulfilled ? consumer.#onFulfilled : consumer.#onRejected;
						// 2.2.1.1, 2.2.1.2 ignore callback if not a function, else
						// 2.2.5 call callback without this-binding
						if (callback) {
							// 2.2.7.1. execute the Promise Resolution Procedure:
							consumer.#resolve(callback(this.#value)); 
						} else {
							// 2.2.7.3 resolve in same way as current promise
							resolver.call(consumer, this.#value);
						}
					} catch (e) {
						// 2.2.7.2
						consumer.#reject(e);
					};
				}
			});
		};
		
		// The Promise Resolution Procedure: will treat values that are thenables/promises
		// and will eventually call either fulfill or reject/throw.
		#resolve(x) {
			// 2.3.1
			if (this === x) {
				throw new TypeError('Circular reference: promise value is promise itself');
			}
			// 2.3.2
			if (x instanceof MyPromise) {
				// 2.3.2.1, 2.3.2.2, 2.3.2.3
				x.then(value => this.#resolve(value), 
					   reason => this.#reject(reason));
			} else if (x === Object(x)) { // 2.3.3
				// firstTime() only returns true the first time it's called
				let firstTime = () => (firstTime = () => false, true);
				try {
					// 2.3.3.1
					const then = x.then;
					if (typeof then === 'function') {
						// 2.3.3.3
						then.call(x, (y) => {
							// 2.3.3.3.3 don't allow multiple calls
											// 2.3.3.3.1 recurse
							if (firstTime()) this.#resolve(y);
						}, (reasonY) => {
							// 2.3.3.3.3 don't allow multiple calls
											// 2.3.3.3.2
							if (firstTime()) this.#reject(reasonY);
						});
					} else {
						// 2.3.3.4
						this.#fulfill(x);
					}
				} catch(e) {
					// 2.3.3.3.4.1 ignore if call was made
					// 2.3.3.2 or 2.3.3.3.4.2
					if (firstTime()) this.#reject(e);
				}
			} else {
				// 2.3.4
				this.#fulfill(x);
			}
		}
	}
})(Framework);