// CFX JS runtime
/// <reference path="./natives_server.d.ts" />

const EXT_FUNCREF = 10;
const EXT_LOCALFUNCREF = 11;

(function (global) {
	let boundaryIdx = 1;
	let lastBoundaryStart = null;
	const isDuplicityVersion = IsDuplicityVersion();
	const currentResourceName = GetCurrentResourceName();

	// temp
	global.FormatStackTrace = function (args, argLength) {
		return Citizen.invokeNativeByHash(0, 0xd70c3bca, args, argLength, Citizen.resultAsString());
	}

	function getBoundaryFunc(pushFunc, id) {
		return (func, ...args) => {
			const boundary = id || (boundaryIdx++);
			pushFunc(boundary);

			function wrap(...args) {
				return func(...args);
			}

			Object.defineProperty(wrap, 'name', { writable: true });
			wrap.name = `__cfx_wrap_${boundary}`;

			return wrap.call(boundary, ...args);
		};
	}

	global.runWithBoundaryStart = getBoundaryFunc(boundary => {
		Citizen.submitBoundaryStart(boundary);
		lastBoundaryStart = boundary;
	});
	const runWithBoundaryEnd = getBoundaryFunc(Citizen.submitBoundaryEnd);

	let refIndex = 0;
	const nextRefIdx = () => refIndex++;
	const refFunctionsMap = new Map();

	const codec = msgpack.createCodec({
		uint8array: true,
		preset: false,
		binarraybuffer: true
	});

	const pack = data => msgpack.encode(data, { codec });
	const unpack = data => msgpack.decode(data, { codec });

	// store for use by natives.js
	global.msgpack_pack = pack;
	global.msgpack_unpack = unpack;

	/**
	 * @param {Function} refFunction
	 * @returns {string}
	 */
	Citizen.makeRefFunction = (refFunction) => {
		const ref = nextRefIdx();

		refFunctionsMap.set(ref, {
			callback: refFunction,
			refCount: 0
		});

		return Citizen.canonicalizeRef(ref);
	};

	function refFunctionPacker(refFunction) {
		const ref = Citizen.makeRefFunction(refFunction);

		return ref;
	}

	function refFunctionUnpacker(refSerialized) {
		const fnRef = Citizen.makeFunctionReference(refSerialized);
		const invoker = GetInvokingResource();

		return function (...args) {
			return runWithBoundaryEnd(() => {
				let retvals = null;
				try {
					retvals = unpack(fnRef(pack(args)));
				} catch (e) {
				}

				if (retvals === null) {
					let errorMessage = `Error in nested ref call for ${currentResourceName}. `
					// invoker can be null, we don't want to give an even worse
					// error by erroring here :P
					if (invoker) {
						errorMessage += `${currentResourceName} tried to call a function reference in ${invoker} but the reference wasn't valid. `
						if (GetResourceState(invoker) !== "started") {
							errorMessage += `And ${invoker} isn't started, was the resource restarted mid call?`
						} else {
							errorMessage += `(did ${invoker} restart recently?)`
						}
					}

					throw new Error(errorMessage);
				}

				switch (retvals.length) {
					case 0:
						return undefined;
					case 1:
						const rv = retvals[0];
						if (rv && rv['__cfx_async_retval']) {
							return new Promise((res, rej) => {
								rv['__cfx_async_retval']((v, e) => {
									if (e != null)
										rej(e);
									else {
										switch(v.length) {
											case 0:
												res(undefined);
											case 1:
												res(v[0]);
											default:
												res(v);
										}
									}
								});
							});
						}
						return rv;
					default:
						return retvals;
				}
			});
		};
	}

	const AsyncFunction = (async () => {}).constructor;
	codec.addExtPacker(EXT_FUNCREF, AsyncFunction, refFunctionPacker);
	codec.addExtPacker(EXT_FUNCREF, Function, refFunctionPacker);
	codec.addExtUnpacker(EXT_FUNCREF, refFunctionUnpacker);
	codec.addExtUnpacker(EXT_LOCALFUNCREF, refFunctionUnpacker);

	/**
	 * Deletes ref function
	 * 
	 * @param {int} ref
	 */
	Citizen.setDeleteRefFunction(function (ref) {
		if (refFunctionsMap.has(ref)) {
			const data = refFunctionsMap.get(ref);

			if (--data.refCount <= 0) {
				refFunctionsMap.delete(ref);
			}
		}
	});

	/**
	 * Invokes ref function
	 * 
	 * @param {int} ref 
	 * @param {UInt8Array} args 
	 */
	Citizen.setCallRefFunction(function (ref, argsSerialized) {
		if (!refFunctionsMap.has(ref)) {
			console.error('Invalid ref call attempt:', ref);

			return pack([]);
		}

		try {
			return runWithBoundaryStart(() => {
				const rv = refFunctionsMap.get(ref).callback(...unpack(argsSerialized));
				if (rv instanceof Promise) {
					return pack([{'__cfx_async_retval': (cb) => {
						rv.then(v => {
							if (cb != null)
								cb([v], null);
						}).catch(err => {
							if (cb != null) {
								let msg = '';
								if (err) {
									if (err.message) {
										msg = err.message.toString();
									} else {
										msg = err.toString();
									}
								}
								cb(null, msg);
							}
						});
					}}]);
				}
				return pack([rv]);
			});
		} catch (e) {
			global.printError('call ref', e);

			return pack(null);
		}
	});

	/**
	 * Duplicates ref function
	 * 
	 * @param {int} ref
	 */
	Citizen.setDuplicateRefFunction(function (ref) {
		if (refFunctionsMap.has(ref)) {
			const refFunction = refFunctionsMap.get(ref);
			++refFunction.refCount;

			return ref;
		}

		return -1;
	});

	// Events
	const emitter = new EventEmitter2();
	const rawEmitter = new EventEmitter2();
	const netSafeEventNames = new Set();

	// Raw events
	global.addRawEventListener = rawEmitter.on.bind(rawEmitter);
	global.addRawEventHandler = global.addRawEventListener;

	// Raw events configuration
	global.setMaxRawEventListeners = rawEmitter.setMaxListeners.bind(rawEmitter);

	// Client events
	global.addEventListener = (name, callback, netSafe = false) => {
		if (netSafe) {
			netSafeEventNames.add(name);
		}

		RegisterResourceAsEventHandler(name);

		emitter.on(name, callback);
	};
	global.on = global.addEventListener;

	// Event Emitter configuration
	global.setMaxEventListeners = emitter.setMaxListeners.bind(emitter);

	// Net events
	global.addNetEventListener = (name, callback) => global.addEventListener(name, callback, true);
	global.onNet = global.addNetEventListener;

	global.removeEventListener = emitter.off.bind(emitter);

	// Convenience aliases for Lua similarity
	global.AddEventHandler = global.addEventListener;
	global.RegisterNetEvent = (name) => void netSafeEventNames.add(name);
	global.RegisterServerEvent = global.RegisterNetEvent;
	global.RemoveEventHandler = global.removeEventListener;

	// Event triggering
	global.emit = (name, ...args) => {
		const dataSerialized = pack(args);

		runWithBoundaryEnd(() => {
			TriggerEventInternal(name, dataSerialized, dataSerialized.length);
		});
	};

	global.TriggerEvent = global.emit;

	if (isDuplicityVersion) {
		global.emitNet = (name, source, ...args) => {
			const dataSerialized = pack(args);

			TriggerClientEventInternal(name, source, dataSerialized, dataSerialized.length);
		};

		global.TriggerClientEvent = global.emitNet;

		global.TriggerLatentClientEvent = (name, source, bps, ...args) => {
			const dataSerialized = pack(args);

			TriggerLatentClientEventInternal(name, source, dataSerialized, dataSerialized.length, bps);
		};

		global.getPlayerIdentifiers = (player) => {
			const numIds = GetNumPlayerIdentifiers(player);
			let t = [];
			for (let i = 0; i < numIds; i++) {
				t[i] = GetPlayerIdentifier(player, i);
			}
			return t;
		};

		global.getPlayerTokens = (player) => {
			const numIds = GetNumPlayerTokens(player);
			let t = [];
			for (let i = 0; i < numIds; i++) {
				t[i] = GetPlayerToken(player, i);
			}
			return t;
		};

		global.getPlayers = () => {
			const num = GetNumPlayerIndices();
			let t = [];

			for (let i = 0; i < num; i++) {
				t[i] = GetPlayerFromIndex(i);
			}

			return t;
		};
	} else {
		global.SendNUIMessage = (data) => {
			const dataJson = JSON.stringify(data)
			SendNuiMessage(dataJson)
		}

		global.emitNet = (name, ...args) => {
			const dataSerialized = pack(args);

			TriggerServerEventInternal(name, dataSerialized, dataSerialized.length);
		};

		global.TriggerServerEvent = global.emitNet;

		global.TriggerLatentServerEvent = (name, bps, ...args) => {
			const dataSerialized = pack(args);

			TriggerLatentServerEventInternal(name, dataSerialized, dataSerialized.length, bps);
		};
	}

	let currentStackDumpError = null;

	function prepareStackTrace(error, trace) {
		const frames = [];
		let skip = false;

		if (error.bs) {
			skip = true;
		}

		if (!error.be) {
			error.be = lastBoundaryStart;
		}

		for (const frame of trace) {
			const functionName = frame.methodName;

			if (functionName && functionName.startsWith('__cfx_wrap_')) {
				const boundary = functionName.substring('__cfx_wrap_'.length) | 0;

				if (boundary == error.bs) {
					skip = false;
				}

				if (boundary == error.be) {
					break;
				}
			}

			if (skip) {
				continue;
			}

			const fn = frame.file;

			if (fn && !fn.startsWith('citizen:/')) {
				const isConstruct = false;
				const isEval = false;
				const isNative = false;
				const methodName = functionName;
				const type = frame.typeName;

				let frameName = '';

				if (isNative) {
					frameName = 'native';
				} else if (isEval) {
					frameName = `eval at ${frame.getEvalOrigin()}`;
				} else if (isConstruct) {
					frameName = `new ${functionName}`;
				} else if (methodName && functionName && methodName !== functionName) {
					frameName = `${type}${functionName} [as ${methodName}]`;
				} else if (methodName || functionName) {
					frameName = `${type}${functionName ? functionName : methodName}`;
				}

				frames.push({
					file: fn,
					line: frame.lineNumber | 0,
					name: frameName
				});
			}
		}

		return frames;
	}

	class StackDumpError {
		constructor(bs, be) {
			this.bs = bs;
			this.be = be;

			Error.captureStackTrace(this);
		}
	}

	function getError(where, e) {
		const stackBlob = global.msgpack_pack(prepareStackTrace(e, parseStack(e.stack)));
		const fst = global.FormatStackTrace(stackBlob, stackBlob.length);

		if (fst !== null && fst !== undefined) {
			return '^1SCRIPT ERROR in ' + where + ': ' + e.toString() + "^7\n" + fst;
		}

		return '';
	}

	global.printError = function (where, e) {
		console.log(getError(where, e));
	}

	Citizen.setStackTraceFunction(function (bs, be) {
		const sde = new StackDumpError(bs, be);
		const rv = pack(prepareStackTrace(sde, parseStack(sde.stack)));

		return rv;
	});

	let errorQueue = [];

	function processErrorQueue() {
		for (const error of errorQueue) {
			console.log(getError('promise (unhandled rejection)', error.error));
		}

		errorQueue = [];
	}

	Citizen.setUnhandledPromiseRejectionFunction(function (event, promise, value) {
		// unhandled
		// we might get a `1` in which case it actually was handled and we should.. un-print
		if (event === 0) {
			let error = '';

			if (value instanceof Error) {
				error = value;
			} else {
				error = new Error((value || '').toString());
			}

			// grab the stack early so it'll remain valid
			const stack = error.stack;

			errorQueue.push({
				error,
				stack,
				promise
			});

			global.setImmediate(processErrorQueue);
		} else if (event === 1) {
			errorQueue = errorQueue.filter(a => a.promise !== promise);
		}
	});

	/**
	 * @param {string} name
	 * @param {UInt8Array} payloadSerialized
	 * @param {string} source
	 */
	Citizen.setEventFunction(function (name, payloadSerialized, source) {
		runWithBoundaryStart(() => {
			global.source = source;

			if (source.startsWith('net')) {
				if (emitter.listeners(name).length > 0 && !netSafeEventNames.has(name)) {
					console.error(`Event ${name} was not safe for net`);

					global.source = null;
					return;
				}

				global.source = parseInt(source.substr(4));
			} else if (isDuplicityVersion && source.startsWith('internal-net')) {
				global.source = parseInt(source.substr(13));
			}

			// Running raw event listeners
			try {
				rawEmitter.emit(name, payloadSerialized, source);
			} catch (e) {
				console.error('Unhandled error during running raw event listeners', e);
			}

			const listeners = emitter.listeners(name);
			if (listeners.length == 0) {
				global.source = null;
				return;
			}

			const payload = unpack(payloadSerialized) || [];
			if(!Array.isArray(payload)) {
				global.source = null;
				return;
			}

			// Running normal event listeners
			for (const listener of listeners) {
				try {
					const retval = listener.apply(null, payload);

					if (retval instanceof Promise) {
						(async () => {
							try {
								await retval;
							} catch (e) {
								console.error('Unhandled promise failure:', e);
							}
						})();
					}
				} catch (e) {
					global.printError('event `' + name + '\'', e);
				}
			}

			global.source = null;
		});
	});

	// Compatibility layer for legacy exports
	const exportsCallbackCache = {};
	const exportKey = isDuplicityVersion ? 'server_export' : 'export';
	const eventType = isDuplicityVersion ? 'Server' : 'Client';

	const getExportEventName = (resource, name) => `__cfx_export_${resource}_${name}`;

	on(`on${eventType}ResourceStart`, (resource) => {
		if (resource === currentResourceName) {
			const numMetaData = GetNumResourceMetadata(resource, exportKey) || 0;

			for (let i = 0; i < numMetaData; i++) {
				const exportName = GetResourceMetadata(resource, exportKey, i);

				on(getExportEventName(resource, exportName), (setCB) => {
					if (global[exportName]) {
						setCB(global[exportName]);
					}
				});
			}
		}
	});

	on(`on${eventType}ResourceStop`, (resource) => {
		exportsCallbackCache[resource] = {};
	});

	// export invocation
	const createExports = () => {
		return new Proxy(() => { }, {
			get(t, k) {
				const resource = k;

				return new Proxy({}, {
					get(t, k) {
						if (!exportsCallbackCache[resource]) {
							exportsCallbackCache[resource] = {};
						}

						if (!exportsCallbackCache[resource][k]) {
							emit(getExportEventName(resource, k), (exportData) => {
								exportsCallbackCache[resource][k] = exportData;
							});

							if (!exportsCallbackCache[resource][k]) {
								throw new Error(`No such export ${k} in resource ${resource}`);
							}
						}

						return (...args) => {
							try {
								return exportsCallbackCache[resource][k](...args);
							} catch (e) {
								throw new Error(`An error occurred while calling export ${k} of resource ${resource} - see above for details`);
							}
						};
					},

					set() {
						throw new Error('cannot set values on an export resource');
					}
				});
			},

			apply(t, self, args) {
				if (args.length !== 2) {
					throw new Error('this needs 2 arguments');
				}

				const [exportName, func] = args;

				on(getExportEventName(currentResourceName, exportName), (setCB) => {
					setCB(func);
				});
			},

			set() {
				throw new Error('cannot set values on exports');
			}
		});
	};

	global.exports = createExports();

	const EXT_ENTITY = 41;
	const EXT_PLAYER = 42;

	global.NewStateBag = (es) => {
		return new Proxy({}, {
			get(_, k) {
				if (k === 'set') {
					return (s, v, r) => {
						const payload = msgpack_pack(v);
						SetStateBagValue(es, s, payload, payload.length, r);
					};
				}

				return GetStateBagValue(es, k);
			},

			set(_, k, v) {
				const payload = msgpack_pack(v);
				SetStateBagValue(es, k, payload, payload.length, isDuplicityVersion);
				return true; // If the set() method returns false, and the assignment happened in strict-mode code, a TypeError will be thrown.
			},
		});
	};

	global.GlobalState = NewStateBag('global');

	function getEntityStateBagId(entityGuid) {
		if (isDuplicityVersion || NetworkGetEntityIsNetworked(entityGuid)) {
			return `entity:${NetworkGetNetworkIdFromEntity(entityGuid)}`;
		} else {
			EnsureEntityStateBag(entityGuid);
			return `localEntity:${entityGuid}`;
		}
	}

	const entityTM = {
		get(t, k) {
			if (k === 'state') {
				const es = getEntityStateBagId(t.__data);

				if (isDuplicityVersion) {
					EnsureEntityStateBag(t.__data);
				}

				return NewStateBag(es);
			}

			return null;
		},

		set() {
			throw new Error('Not allowed at this time.');
		},

		__ext: EXT_ENTITY,

		__pack: () => {
			return String(NetworkGetNetworkIdFromEntity(this.__data));
		},

		__unpack: (data, t) => {
			const ref = NetworkGetEntityFromNetworkId(Number(data));
			return new Proxy({ __data: ref }, entityTM);
		},
	};

	const playerTM = {
		get(t, k) {
			if (k === 'state') {
				const pid = t.__data === -1 ? GetPlayerServerId(PlayerId()) : t.__data;

				const es = `player:${pid}`;

				return NewStateBag(es);
			}

			return null;
		},

		set() {
			throw new Error('Not allowed at this time.');
		},

		__ext: EXT_PLAYER,

		__pack: () => {
			return String(this.__data);
		},

		__unpack: (data, t) => {
			const ref = Number(data);
			return new Proxy({ __data: ref }, playerTM);
		},
	};

	global.Entity = (ent) => {
		if (typeof ent === 'number') {
			return new Proxy({ __data: ent }, entityTM);
		}

		return ent;
	};

	global.Player = (ent) => {
		if (typeof ent === 'number' || typeof ent === 'string') {
			return new Proxy({ __data: Number(ent) }, playerTM);
		}

		return ent;
	};

	if (!isDuplicityVersion) {
		global.LocalPlayer = Player(-1);
	}

	/*
	BEGIN
	https://github.com/errwischt/stacktrace-parser/blob/0121cc6e7d57495437818676f6b69be7d34c2fa7/src/stack-trace-parser.js

	MIT License

	Copyright (c) 2014-2019 Georg Tavonius

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
	*/

	const UNKNOWN_FUNCTION = '<unknown>';

	/**
	 * This parses the different stack traces and puts them into one format
	 * This borrows heavily from TraceKit (https://github.com/csnover/TraceKit)
	 */
	function parseStack(stackString) {
		const lines = stackString.split('\n');

		return lines.reduce((stack, line) => {
			const parseResult =
				parseChrome(line) ||
				parseWinjs(line) ||
				parseGecko(line) ||
				parseNode(line) ||
				parseJSC(line);

			if (parseResult) {
				stack.push(parseResult);
			}

			return stack;
		}, []);
	}

	const chromeRe = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\/|[a-z]:\\|\\\\).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
	const chromeEvalRe = /\((\S*)(?::(\d+))(?::(\d+))\)/;

	function parseChrome(line) {
		const parts = chromeRe.exec(line);

		if (!parts) {
			return null;
		}

		const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line
		const isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line

		const submatch = chromeEvalRe.exec(parts[2]);
		if (isEval && submatch != null) {
			// throw out eval line/column and use top-most line/column number
			parts[2] = submatch[1]; // url
			parts[3] = submatch[2]; // line
			parts[4] = submatch[3]; // column
		}

		const methodParts = (parts[1] || UNKNOWN_FUNCTION).split(/\./, 2);
		const typeName = methodParts.length == 2 ? (methodParts[0] + '.') : '';
		const methodName = methodParts[methodParts.length - 1];

		return {
			file: !isNative ? parts[2] : null,
			methodName,
			typeName,
			arguments: isNative ? [parts[2]] : [],
			lineNumber: parts[3] ? +parts[3] : null,
			column: parts[4] ? +parts[4] : null,
		};
	}

	const winjsRe = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i;

	function parseWinjs(line) {
		const parts = winjsRe.exec(line);

		if (!parts) {
			return null;
		}

		return {
			file: parts[2],
			methodName: parts[1] || UNKNOWN_FUNCTION,
			arguments: [],
			lineNumber: +parts[3],
			column: parts[4] ? +parts[4] : null,
		};
	}

	const geckoRe = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i;
	const geckoEvalRe = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i;

	function parseGecko(line) {
		const parts = geckoRe.exec(line);

		if (!parts) {
			return null;
		}

		const isEval = parts[3] && parts[3].indexOf(' > eval') > -1;

		const submatch = geckoEvalRe.exec(parts[3]);
		if (isEval && submatch != null) {
			// throw out eval line/column and use top-most line number
			parts[3] = submatch[1];
			parts[4] = submatch[2];
			parts[5] = null; // no column when eval
		}

		return {
			file: parts[3],
			methodName: parts[1] || UNKNOWN_FUNCTION,
			arguments: parts[2] ? parts[2].split(',') : [],
			lineNumber: parts[4] ? +parts[4] : null,
			column: parts[5] ? +parts[5] : null,
		};
	}

	const javaScriptCoreRe = /^\s*(?:([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$/i;

	function parseJSC(line) {
		const parts = javaScriptCoreRe.exec(line);

		if (!parts) {
			return null;
		}

		return {
			file: parts[3],
			methodName: parts[1] || UNKNOWN_FUNCTION,
			arguments: [],
			lineNumber: +parts[4],
			column: parts[5] ? +parts[5] : null,
		};
	}

	const nodeRe = /^\s*at (?:((?:\[object object\])?[^\\/]+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i;

	function parseNode(line) {
		const parts = nodeRe.exec(line);

		if (!parts) {
			return null;
		}

		const methodParts = (parts[1] || UNKNOWN_FUNCTION).split(/\./, 2);
		const typeName = methodParts.length == 2 ? (methodParts[0] + '.') : '';
		const methodName = methodParts[methodParts.length - 1];

		return {
			file: parts[2],
			typeName,
			methodName,
			arguments: [],
			lineNumber: +parts[3],
			column: parts[4] ? +parts[4] : null,
		};
	}
	// END	
})(this || globalThis);
