/*
 * Copyright (c) 2024 lax1dude. All Rights Reserved.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 */

const platfInputName = "platformInput";

async function initPlatformInput(inputImports) {

	const EVENT_INPUT_MOUSE = 0;
	const EVENT_INPUT_KEYBOARD = 1;
	const EVENT_INPUT_TOUCH = 2;
	const EVENT_INPUT_TOUCH_KEYBOARD = 3;
	const EVENT_INPUT_TOUCH_PASTE = 4;
	const EVENT_INPUT_FOCUS = 5;
	const EVENT_INPUT_BLUR = 6;
	const EVENT_INPUT_MOUSE_ENTER = 7;
	const EVENT_INPUT_MOUSE_EXIT = 8;
	const EVENT_INPUT_WINDOW_RESIZE = 9;
	const EVENT_INPUT_GAMEPAD = 10;

	const EVENT_MOUSE_DOWN = 0;
	const EVENT_MOUSE_UP = 1;
	const EVENT_MOUSE_MOVE = 2;
	const EVENT_MOUSE_WHEEL = 3;

	const EVENT_KEY_DOWN = 0;
	const EVENT_KEY_UP = 1;
	const EVENT_KEY_REPEAT = 2;

	const EVENT_KEY_MOD_CONTROL = 1;
	const EVENT_KEY_MOD_SHIFT = 2;
	const EVENT_KEY_MOD_ALT = 4;

	const EVENT_TOUCH_START = 0;
	const EVENT_TOUCH_MOVE = 1;
	const EVENT_TOUCH_END = 2;

	const EVENT_TOUCH_KEYBOARD_ABSOLUTE = 0;
	const EVENT_TOUCH_KEYBOARD_CODEPOINTS = 1;

	const EVENT_RESIZE_WINDOW = 1;
	const EVENT_RESIZE_VISUAL_VIEWPORT = 2;
	const EVENT_RESIZE_DPI = 4;

	const EVENT_GAMEPAD_CONNECTED = 0;
	const EVENT_GAMEPAD_DISCONNECTED = 1;

	var touchKeyboardOpenZone = null;
	var touchOffsetX = 0;
	var touchOffsetY = 0;
	var touchKeyboardForm = null;
	var touchKeyboardField = null;
	var shownTouchKeyboardEventWarning = false;
	var shownLegacyTouchKeyboardWarning = false;
	var showniOSReturnTouchKeyboardWarning = false;
	var lastTouchKeyboardEvtA = 0.0;
	var lastTouchKeyboardEvtB = 0.0;
	var lastTouchKeyboardEvtC = 0.0;

	var lastWindowWidth = -1;
	var lastWindowHeight = -1;
	var lastWindowDPI = -1.0;
	var useVisualViewport = false;
	var lastVisualViewportX = -1;
	var lastVisualViewportY = -1;
	var lastVisualViewportW = -1;
	var lastVisualViewportH = -1;

	var pointerLockSupported = false;
	var pointerLockFlag = false;
	var pointerLockWaiting = false;
	var mouseUngrabTimer = 0;
	var mouseGrabTimer = 0;
	var mouseUngrabTimeout = -1;

	var fullscreenSupported = false;
	var fullscreenQuery = null;
	var keyboardLockSupported = false;
	var lockKeys = false;

	var vsyncSupport = false;
	var vsyncTimeout = -1;

	var gamepadSupported = false;

	const currentEventListeners = {
		contextmenu: null,
		mousedown: null,
		mouseup: null,
		mousemove: null,
		mouseenter: null,
		mouseleave: null,
		touchstart: null,
		touchend: null,
		touchmove: null,
		touchcancel: null,
		gamepadconnected: null,
		gamepaddisconnected: null,
		keydown: null,
		keyup: null,
		touchKeyboardOpenZone_touchstart: null,
		touchKeyboardOpenZone_touchend: null,
		touchKeyboardOpenZone_touchmove: null,
		wheel: null,
		focus: null,
		blur: null,
		pointerlock: null,
		pointerlockerr: null,
		fullscreenChange: null
	};

	touchKeyboardOpenZone = document.createElement("div");
	touchKeyboardOpenZone.classList.add("_eaglercraftX_keyboard_open_zone");
	touchKeyboardOpenZone.style.display = "none";
	touchKeyboardOpenZone.style.position = "absolute";
	touchKeyboardOpenZone.style.backgroundColor = "transparent";
	touchKeyboardOpenZone.style.top = "0px";
	touchKeyboardOpenZone.style.left = "0px";
	touchKeyboardOpenZone.style.width = "0px";
	touchKeyboardOpenZone.style.height = "0px";
	touchKeyboardOpenZone.style.zIndex = "100";
	touchKeyboardOpenZone.style.touchAction = "pan-x pan-y";
	touchKeyboardOpenZone.style.setProperty("-webkit-touch-callout", "none");
	touchKeyboardOpenZone.style.setProperty("-webkit-tap-highlight-color", "rgba(255, 255, 255, 0)");
	parentElement.appendChild(touchKeyboardOpenZone);

	function updateTouchOffset() {
		touchOffsetX = 0;
		touchOffsetY = 0;
		var el = canvasElement;
		while(el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) {
			touchOffsetX += el.offsetLeft - el.scrollLeft;
			touchOffsetY += el.offsetTop - el.scrollTop;
			el = el.offsetParent;
		}
	}

	function reportWindowSize() {
		var newWindowDPI = window.devicePixelRatio;
		if(newWindowDPI < 0.01) newWindowDPI = 1.0;
		updateTouchOffset();
		const w = parentElement.clientWidth;
		const h = parentElement.clientHeight;
		var newWindowWidth = (w * newWindowDPI) | 0;
		var newWindowHeight = (h * newWindowDPI) | 0;
		var newVisualViewportX = 0;
		var newVisualViewportY = 0;
		var newVisualViewportW = newWindowWidth;
		var newVisualViewportH = newWindowHeight;
		if(useVisualViewport) {
			const vv = window.visualViewport;
			const scale = vv.scale;
			newVisualViewportX = (vv.pageLeft * newWindowDPI * scale);
			newVisualViewportY = (vv.pageTop * newWindowDPI * scale);
			newVisualViewportW = (vv.width * newWindowDPI * scale);
			newVisualViewportH = (vv.height * newWindowDPI * scale);
			if(newVisualViewportW < 1) newVisualViewportW = 1;
			if(newVisualViewportH < 1) newVisualViewportH = 1;
			if(newVisualViewportX < 0) {
				newVisualViewportW += newVisualViewportX;
				newVisualViewportX = 0;
			}else if(newVisualViewportX >= newWindowWidth) {
				newVisualViewportX = newWindowWidth - 1;
			}
			if(newVisualViewportY < 0) {
				newVisualViewportH += newVisualViewportY;
				newVisualViewportY = 0;
			}else if(newVisualViewportY >= newWindowHeight) {
				newVisualViewportY = newWindowHeight - 1;
			}
			if((newVisualViewportX + newVisualViewportW) > newWindowWidth) {
				newVisualViewportW = newWindowWidth - newVisualViewportX;
			}
			if((newVisualViewportY + newVisualViewportH) > newWindowHeight) {
				newVisualViewportH = newWindowHeight - newVisualViewportY;
			}
		}
		const eventData = {
			"eventTypeMask": 0
		};
		if(lastWindowDPI !== newWindowDPI) {
			lastWindowDPI = newWindowDPI;
			eventData["eventTypeMask"] |= EVENT_RESIZE_DPI;
			eventData["windowDPI"] = newWindowDPI;
		}
		if(lastWindowWidth !== newWindowWidth || lastWindowHeight !== newWindowHeight) {
			lastWindowWidth = newWindowWidth;
			lastWindowHeight = newWindowHeight;
			eventData["eventTypeMask"] |= EVENT_RESIZE_WINDOW;
			eventData["windowWidth"] = newWindowWidth;
			eventData["windowHeight"] = newWindowHeight;
		}
		if(lastVisualViewportX !== newVisualViewportX || lastVisualViewportY !== newVisualViewportY
				|| lastVisualViewportW !== newVisualViewportW || lastVisualViewportH !== newVisualViewportH) {
			lastVisualViewportX = newVisualViewportX;
			lastVisualViewportY = newVisualViewportY;
			lastVisualViewportW = newVisualViewportW;
			lastVisualViewportH = newVisualViewportH;
			eventData["eventTypeMask"] |= EVENT_RESIZE_VISUAL_VIEWPORT;
			eventData["visualViewportX"] = newVisualViewportX;
			eventData["visualViewportY"] = newVisualViewportY;
			eventData["visualViewportW"] = newVisualViewportW;
			eventData["visualViewportH"] = newVisualViewportH;
		}
		if(eventData["eventTypeMask"] !== 0) {
			pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_WINDOW_RESIZE, eventData);
		}
	}

	reportWindowSize();

	parentElement.addEventListener("contextmenu", currentEventListeners.contextmenu = function(/** Event */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
	});

	canvasElement.addEventListener("mousedown", /** @type {function(Event)} */ (currentEventListeners.mousedown = function(/** MouseEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
		if(pointerLockSupported && pointerLockFlag && document.pointerLockElement !== canvasElement) {
			mouseSetGrabbed(1);
			return;
		}
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_MOUSE, {
			"eventType": EVENT_MOUSE_DOWN,
			"posX": evt.offsetX,
			"posY": evt.offsetY,
			"button": evt.button
		});
	}));

	canvasElement.addEventListener("mouseup", /** @type {function(Event)} */ (currentEventListeners.mouseup = function(/** MouseEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_MOUSE, {
			"eventType": EVENT_MOUSE_UP,
			"posX": evt.offsetX,
			"posY": evt.offsetY,
			"button": evt.button
		});
	}));

	canvasElement.addEventListener("mousemove", /** @type {function(Event)} */ (currentEventListeners.mousemove = function(/** MouseEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_MOUSE, {
			"eventType": EVENT_MOUSE_MOVE,
			"posX": evt.offsetX,
			"posY": evt.offsetY,
			"movementX": evt.movementX,
			"movementY": evt.movementY
		});
	}));

	canvasElement.addEventListener("mouseenter", currentEventListeners.mouseenter = function(/** Event */ evt) {
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_MOUSE_ENTER, null);
	});

	canvasElement.addEventListener("mouseleave", currentEventListeners.mouseleave = function(/** Event */ evt) {
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_MOUSE_EXIT, null);
	});

	/**
	 * @param {TouchList} lstIn
	 * @return {Array}
	 */
	function createTouchList(lstIn) {
		const len = lstIn.length;
		const ret = new Array(len);
		for(var i = 0; i < len; ++i) {
			const itm = lstIn.item(i);
			ret[i] = {
				"pointX": itm.pageX - touchOffsetX,
				"pointY": itm.pageY - touchOffsetY,
				"radius": (itm.radiusX + itm.radiusY) * 0.5,
				"force": itm.force,
				"pointUID": itm.identifier
			};
		}
		return ret;
	}

	canvasElement.addEventListener("touchstart", /** @type {function(Event)} */ (currentEventListeners.touchstart = function(/** TouchEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH, {
			"eventType": EVENT_TOUCH_START,
			"changedTouches": createTouchList(evt.changedTouches),
			"targetTouches": createTouchList(evt.targetTouches)
		});
		touchCloseDeviceKeyboard();
	}));

	canvasElement.addEventListener("touchend", /** @type {function(Event)} */ (currentEventListeners.touchend = function(/** TouchEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH, {
			"eventType": EVENT_TOUCH_END,
			"changedTouches": createTouchList(evt.changedTouches),
			"targetTouches": createTouchList(evt.targetTouches)
		});
	}));

	canvasElement.addEventListener("touchmove", /** @type {function(Event)} */ (currentEventListeners.touchmove = function(/** TouchEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH, {
			"eventType": EVENT_TOUCH_MOVE,
			"changedTouches": createTouchList(evt.changedTouches),
			"targetTouches": createTouchList(evt.targetTouches)
		});
	}));

	canvasElement.addEventListener("touchcancel", /** @type {function(Event)} */ (currentEventListeners.touchcancel = function(/** TouchEvent */ evt) {
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH, {
			"eventType": EVENT_TOUCH_END,
			"changedTouches": createTouchList(evt.changedTouches),
			"targetTouches": createTouchList(evt.targetTouches)
		});
	}));

	canvasElement.addEventListener("wheel", /** @type {function(Event)} */ (currentEventListeners.wheel = function(/** WheelEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_MOUSE, {
			"eventType": EVENT_MOUSE_WHEEL,
			"posX": evt.offsetX,
			"posY": evt.offsetY,
			"wheel": evt.deltaY
		});
	}));

	window.addEventListener("blur", currentEventListeners.blur = function(/** Event */ evt) {
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_BLUR, null);
	});

	window.addEventListener("focus", currentEventListeners.focus = function(/** Event */ evt) {
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_FOCUS, null);
	});

	/**
	 * @param {number} evtType
	 * @param {KeyboardEvent} evt
	 * @return {Object}
	 */
	function createKeyEvent(evtType, evt) {
		return {
			"eventType": evtType,
			"keyCode": (evt.code || null),
			"keyName": (evt.key || null),
			"legacyCode": (typeof evt.which === "number") ? evt.which : ((typeof evt.keyCode === "number") ? evt.keyCode : 0),
			"location": evt.location,
			"mods": ((evt.ctrlKey ? EVENT_KEY_MOD_CONTROL : 0)
				| (evt.shiftKey ? EVENT_KEY_MOD_SHIFT : 0)
				| (evt.metaKey ? EVENT_KEY_MOD_ALT : 0))
		};
	}

	window.addEventListener("keydown", /** @type {function(Event)} */ (currentEventListeners.keydown = function(/** KeyboardEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
		if(evt.key === "F11" && !evt.repeat) {
			toggleFullscreenImpl();
			return;
		}
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_KEYBOARD, createKeyEvent(evt.repeat ? EVENT_KEY_REPEAT : EVENT_KEY_DOWN, evt));
		if(evt.timeStamp && evt.key !== "Unidentified") lastTouchKeyboardEvtA = evt.timeStamp;
	}));

	window.addEventListener("keyup", /** @type {function(Event)} */ (currentEventListeners.keyup = function(/** KeyboardEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
		pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_KEYBOARD, createKeyEvent(EVENT_KEY_UP, evt));
	}));

	touchKeyboardOpenZone.addEventListener("touchstart", /** @type {function(Event)} */ (currentEventListeners.touchKeyboardOpenZone_touchstart = function(/** TouchEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
	}));

	touchKeyboardOpenZone.addEventListener("touchend", /** @type {function(Event)} */ (currentEventListeners.touchKeyboardOpenZone_touchend = function(/** TouchEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
		touchOpenDeviceKeyboard();
	}));

	touchKeyboardOpenZone.addEventListener("touchmove", /** @type {function(Event)} */ (currentEventListeners.touchKeyboardOpenZone_touchmove = function(/** TouchEvent */ evt) {
		evt.preventDefault();
		evt.stopPropagation();
	}));

	pointerLockSupported = (typeof document.exitPointerLock === "function");

	if(pointerLockSupported) {
		document.addEventListener("pointerlockchange", /** @type {function(Event)} */ (currentEventListeners.pointerlock = function(evt) {
			window.setTimeout(function() {
				const grab = (document.pointerLockElement === canvasElement);
				if(!grab) {
					if(pointerLockFlag) {
						mouseUngrabTimer = performance.now() | 0;
					}
				}
				pointerLockFlag = grab;
			}, 60);
			pointerLockWaiting = false;
		}));
		document.addEventListener("pointerlockerror", /** @type {function(Event)} */ (currentEventListeners.pointerlockerr = function(evt) {
			pointerLockWaiting = false;
		}));
	}else {
		eagError("Pointer lock is not supported on this browser");
	}

	fullscreenSupported = (typeof document.exitFullscreen === "function");

	if(fullscreenSupported) {
		fullscreenQuery = window.matchMedia("(display-mode: fullscreen)");
		keyboardLockSupported = !!(navigator.keyboard && navigator.keyboard.lock);
		if(keyboardLockSupported) {
			fullscreenQuery.addEventListener("change", /** @type {function(Event)} */ (currentEventListeners.fullscreenChange = function(evt) {
				if(!fullscreenQuery.matches) {
					navigator.keyboard.unlock();
					lockKeys = false;
				}
			}));
		}
	}else {
		eagError("Fullscreen is not supported on this browser");
	}

	if(typeof window.visualViewport !== "undefined") {
		if(rootElement.tagName.toLowerCase() === "body") {
			useVisualViewport = true;
		}else {
			const bodyTag = document.body;
			if (Math.abs(bodyTag.clientWidth - parentElement.clientWidth) <= 10
					&& Math.abs(bodyTag.clientHeight - parentElement.clientHeight) <= 10) {
				useVisualViewport = true;
			}
		}
	}else {
		useVisualViewport = false;
	}

	if(useVisualViewport) {
		eagInfo("Note: Detected game is embedded in body, some screens may be resized to window.visualViewport instead for a better mobile experience");
	}

	function asyncRequestAnimationFrame() {
		return new Promise(function(resolve) {
			if(vsyncTimeout !== -1) {
				throw new Error("Main thread is already waiting for VSync!");
			}
			const hasCompleted = [ false ];
			window.requestAnimationFrame(function() {
				if(!hasCompleted[0]) {
					hasCompleted[0] = true;
					if(vsyncTimeout !== -1) {
						window.clearTimeout(vsyncTimeout);
						vsyncTimeout = -1;
					}
					resolve();
				}
			});
			vsyncTimeout = window.setTimeout(function() {
				if(!hasCompleted[0]) {
					hasCompleted[0] = true;
					vsyncTimeout = -1;
					resolve();
				}
			}, 20);
		});
	}

	try {
		await asyncRequestAnimationFrame();
		vsyncSupport = true;
	}catch(ex) {
		vsyncSupport = false;
	}

	if(!vsyncSupport) {
		eagError("VSync is not supported on this browser");
	}

	gamepadSupported = (typeof navigator.getGamepads === "function");
	if(gamepadSupported) {
		window.addEventListener("gamepadconnected", /** @type {function(Event)} */ (currentEventListeners.gamepadconnected = function(/** GamepadEvent */ evt) {
			pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_GAMEPAD, {
				"eventType": EVENT_GAMEPAD_CONNECTED
			});
		}));
		window.addEventListener("gamepaddisconnected", /** @type {function(Event)} */ (currentEventListeners.gamepaddisconnected = function(/** GamepadEvent */ evt) {
			pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_GAMEPAD, {
				"eventType": EVENT_GAMEPAD_DISCONNECTED,
				"gamepad": evt.gamepad
			});
		}));
	}else {
		eagError("Gamepad detected as unsupported!");
	}

	/**
	 * @return {boolean}
	 */
	inputImports["keyboardLayoutSupported"] = function() {
		return !!(navigator.keyboard && navigator.keyboard.getLayoutMap);
	};

	/**
	 * @return {Promise<Array>}
	 */
	async function iterateKeyboardLayoutImpl() {
		const ret = [];
		try {
			const layout = await navigator.keyboard.getLayoutMap();
			if(layout && layout["forEach"]) {
				layout["forEach"](function(/** string */ k, /** string */ v) {
					ret.push({
						"key": k,
						"value": v
					});
				});
			}
		}catch(ex) {
		}
		return ret;
	}

	inputImports["iterateKeyboardLayout"] = new WebAssembly.Suspending(iterateKeyboardLayoutImpl);

	/**
	 * @param {number} width
	 * @param {number} height
	 */
	inputImports["updateCanvasSize"] = function(width, height) {
		if(canvasElement.width !== width) {
			canvasElement.width = width;
		}
		if(canvasElement.height !== height) {
			canvasElement.height = height;
		}
	};

	function sleepPromise(ms) {
		return new Promise(function(resolve) {
			setTimeout(resolve, ms);
		});
	}

	var manualSyncTimer = 0;

	/**
	 * @param {number} fpsLimit
	 * @return {Promise}
	 */
	function syncDelay(fpsLimit) {
		if(fpsLimit > 0 && fpsLimit <= 1000) {
			const frameMillis = (1000 / fpsLimit);
			if(manualSyncTimer === 0) {
				manualSyncTimer = performance.now() + frameMillis;
			}else {
				var millis = performance.now();
				var remaining = (manualSyncTimer - millis) | 0;
				if(remaining > 0) {
					if(!runtimeOpts.useDelayOnSwap && allowImmediateContinue) {
						return immediateContinueImpl().then(function() {
							var millis0 = performance.now();
							var remaining0 = (manualSyncTimer - millis0) | 0;
							if(remaining0 > 0) {
								return sleepPromise(remaining0).then(function() {
									var millis1 = performance.now();
									if((manualSyncTimer += frameMillis) < millis1) {
										manualSyncTimer = millis1;
									}
								});
							}else if((manualSyncTimer += frameMillis) < millis0) {
								manualSyncTimer = millis0;
							}
						});
					}else {
						return sleepPromise(remaining).then(function() {
							var millis0 = performance.now();
							if((manualSyncTimer += frameMillis) < millis0) {
								manualSyncTimer = millis0;
							}
						});
					}
				}else if((manualSyncTimer += frameMillis) < millis) {
					manualSyncTimer = millis;
				}
			}
		}else {
			manualSyncTimer = 0;
		}
		
		return swapDelayImpl();
	}

	/**
	 * @param {number} fpsLimit
	 * @param {number} vsync
	 * @return {Promise}
	 */
	function updatePlatformAndSleepImpl(fpsLimit, vsync) {
		reportWindowSize();
		if((typeof document.visibilityState !== "string") || (document.visibilityState === "visible")) {
			if(vsyncSupport && vsync) {
				manualSyncTimer = 0;
				return asyncRequestAnimationFrame();
			}else {
				if(fpsLimit <= 0) {
					manualSyncTimer = 0;
					return swapDelayImpl();
				}else {
					return syncDelay(fpsLimit);
				}
			}
		}else {
			manualSyncTimer = 0;
			return sleepPromise(50);
		}
	}

	inputImports["updatePlatformAndSleep"] = new WebAssembly.Suspending(updatePlatformAndSleepImpl);

	/**
	 * @return {boolean}
	 */
	inputImports["isVSyncSupported"] = function() {
		return vsyncSupport;
	}

	/**
	 * @param {number} grab
	 */
	function mouseSetGrabbed(grab) {
		if(!pointerLockSupported) return;
		pointerLockFlag = !!grab;
		const t = performance.now() | 0;
		mouseGrabTimer = t;
		if(grab) {
			pointerLockWaiting = true;
			try {
				canvasElement.requestPointerLock();
			}catch(ex) {
			}
			if(mouseUngrabTimeout !== -1) window.clearTimeout(mouseUngrabTimeout);
			mouseUngrabTimeout = -1;
			if(t - mouseUngrabTimer < 3000) {
				mouseUngrabTimeout = window.setTimeout(function() {
					try {
						canvasElement.requestPointerLock();
					}catch(ex) {
					}
				}, 3100 - (t - mouseUngrabTimer));
			}
		}else {
			if(mouseUngrabTimeout !== -1) window.clearTimeout(mouseUngrabTimeout);
			mouseUngrabTimeout = -1;
			if(!pointerLockWaiting) {
				try {
					document.exitPointerLock();
				}catch(ex) {
				}
			}
		}
	};

	inputImports["mouseSetGrabbed"] = mouseSetGrabbed;

	/**
	 * @return {boolean}
	 */
	inputImports["isMouseGrabSupported"] = function() {
		return pointerLockSupported;
	};

	/**
	 * @return {boolean}
	 */
	inputImports["isMouseGrabbed"] = function() {
		return pointerLockFlag;
	};

	/**
	 * @return {boolean}
	 */
	inputImports["isPointerLocked"] = function() {
		return pointerLockSupported && (pointerLockWaiting || document.pointerLockElement === canvasElement);
	};

	/**
	 * @return {boolean}
	 */
	inputImports["supportsFullscreen"] = function() {
		return fullscreenSupported;
	};

	function toggleFullscreenImpl() {
		if(!fullscreenSupported) return;
		if(fullscreenQuery.matches) {
			if(keyboardLockSupported) {
				try {
					navigator.keyboard.unlock();
				}catch(ex) {
				}
				lockKeys = false;
			}
			try {
				document.exitFullscreen();
			}catch(ex) {
			}
		}else {
			if(keyboardLockSupported) {
				try {
					navigator.keyboard.lock();
				}catch(ex) {
				}
				lockKeys = true;
			}
			try {
				canvasElement.requestFullscreen();
			}catch(ex) {
			}
		}
	}

	inputImports["toggleFullscreen"] = toggleFullscreenImpl;

	/**
	 * @return {boolean}
	 */
	inputImports["isFullscreen"] = function() {
		return fullscreenSupported && fullscreenQuery.matches;
	};

	function touchOpenDeviceKeyboard() {
		if(!touchIsDeviceKeyboardOpenMAYBE()) {
			if(touchKeyboardField) {
				touchKeyboardField.blur();
				touchKeyboardField.value = "";
				setTimeout(function() {
					if(touchKeyboardField) {
						if(touchKeyboardForm) {
							touchKeyboardForm.removeChild(touchKeyboardField);
						}else {
							touchKeyboardField.remove();
						}
						touchKeyboardField = null;
					}
					if(touchKeyboardForm) {
						parentElement.removeChild(touchKeyboardForm);
						touchKeyboardForm = null;
					}
				}, 10);
				return;
			}
			if(touchKeyboardForm) {
				parentElement.removeChild(touchKeyboardForm);
				touchKeyboardForm = null;
			}
			touchKeyboardForm = document.createElement("form");
			touchKeyboardForm.setAttribute("autocomplete", "off");
			touchKeyboardForm.classList.add("_eaglercraftX_text_input_wrapper");
			touchKeyboardForm.style.position = "absolute";
			touchKeyboardForm.style.top = "0px";
			touchKeyboardForm.style.left = "0px";
			touchKeyboardForm.style.right = "0px";
			touchKeyboardForm.style.bottom = "0px";
			touchKeyboardForm.style.zIndex = "-100";
			touchKeyboardForm.style.margin = "0px";
			touchKeyboardForm.style.padding = "0px";
			touchKeyboardForm.style.border = "none";
			touchKeyboardForm.addEventListener("submit", function(/** Event */ evt) {
				evt.preventDefault();
				evt.stopPropagation();
				const d = evt.timeStamp;
				if(d) {
					if(lastTouchKeyboardEvtA !== 0.0 && (d - lastTouchKeyboardEvtA) < 10.0) {
						return;
					}
					if(lastTouchKeyboardEvtB !== 0.0 && (d - lastTouchKeyboardEvtB) < 10.0) {
						return;
					}
					if(lastTouchKeyboardEvtC !== 0.0 && (d - lastTouchKeyboardEvtC) < 10.0) {
						return;
					}
				}
				if(!showniOSReturnTouchKeyboardWarning) {
					eagInfo("Note: Generating return keystroke from submit event on form, this browser probably doesn't generate keydown/beforeinput/input when enter/return is pressed on the on-screen keyboard");
					showniOSReturnTouchKeyboardWarning = true;
				}
				pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH_KEYBOARD, {
					"eventType": EVENT_TOUCH_KEYBOARD_ABSOLUTE,
					"absoluteCode": 0x1C, // KEY_RETURN
					"absoluteChar": 10 // '\n'
				});
			});
			touchKeyboardField = document.createElement("input");
			touchKeyboardField.type = "password";
			touchKeyboardField.value = " ";
			touchKeyboardField.classList.add("_eaglercraftX_text_input_element");
			touchKeyboardField.setAttribute("autocomplete", "off");
			touchKeyboardField.style.position = "absolute";
			touchKeyboardField.style.top = "0px";
			touchKeyboardField.style.left = "0px";
			touchKeyboardField.style.right = "0px";
			touchKeyboardField.style.bottom = "0px";
			touchKeyboardField.style.zIndex = "-100";
			touchKeyboardField.style.margin = "0px";
			touchKeyboardField.style.padding = "0px";
			touchKeyboardField.style.border = "none";
			touchKeyboardField.style.setProperty("-webkit-touch-callout", "default");
			touchKeyboardField.addEventListener("beforeinput", /** @type {function(Event)} */ (function(/** InputEvent */ evt) {
				if(touchKeyboardField !== evt.target) return;
				if(!shownTouchKeyboardEventWarning) {
					eagInfo("Note: Caught beforeinput event from on-screen keyboard, browser probably does not generate global keydown/keyup events on text fields, or does not respond to cancelling keydown");
					shownTouchKeyboardEventWarning = true;
				}
				const d = evt.timeStamp;
				if(d) {
					if(lastTouchKeyboardEvtA !== 0.0 && (d - lastTouchKeyboardEvtA) < 10.0) {
						return;
					}
					lastTouchKeyboardEvtB = d;
				}
				evt.preventDefault();
				evt.stopPropagation();
				switch(evt.inputType) {
				case "insertParagraph":
				case "insertLineBreak":
					pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH_KEYBOARD, {
						"eventType": EVENT_TOUCH_KEYBOARD_ABSOLUTE,
						"absoluteCode": 0x1C, // KEY_RETURN
						"absoluteChar": 10 // '\n'
					});
					break;
				case "deleteWordBackward":
				case "deleteSoftLineBackward":
				case "deleteHardLineBackward":
				case "deleteEntireSoftLine":
				case "deleteContentBackward":
				case "deleteContent":
					pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH_KEYBOARD, {
						"eventType": EVENT_TOUCH_KEYBOARD_ABSOLUTE,
						"absoluteCode": 0x0E, // KEY_BACK
						"absoluteChar": 0
					});
					break;
				case "deleteWordForward":
				case "deleteSoftLineForward":
				case "deleteHardLineForward":
				case "deleteContentForward":
					pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH_KEYBOARD, {
						"eventType": EVENT_TOUCH_KEYBOARD_ABSOLUTE,
						"absoluteCode": 0xD3, // KEY_DELETE
						"absoluteChar": 0
					});
					break;
				case "insertText":
				case "insertCompositionText":
				case "insertReplacementText": {
					const txt = evt.data;
					if(txt && txt.length > 0) {
						pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH_KEYBOARD, {
							"eventType": EVENT_TOUCH_KEYBOARD_CODEPOINTS,
							"codepoints": txt
						});
					}
					break;
				}
				case "insertFromPaste":
				case "insertFromPasteAsQuotation":
				case "insertFromDrop":
				case "insertFromYank":
				case "insertLink": {
					const txt = evt.data;
					if(txt && txt.length > 0) {
						pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH_PASTE, txt);
					}
					break;
				}
				case "historyUndo":
				case "historyRedo":
				case "deleteByDrag":
				case "deleteByCut":
					break;
				default:
					eagInfo("Ignoring InputEvent.inputType \"{}\" from on-screen keyboard", evt.inputType);
					break;
				}
			}));
			touchKeyboardField.addEventListener("input", function(/** Event */ evt) {
				if(touchKeyboardField !== evt.target) return;
				if(!shownLegacyTouchKeyboardWarning) {
					eagInfo("Note: Caught legacy input events from on-screen keyboard, browser could be outdated and doesn't support beforeinput event, or does not respond to cancelling beforeinput");
					shownLegacyTouchKeyboardWarning = true;
				}
				const d = evt.timeStamp;
				if(d) {
					if(lastTouchKeyboardEvtA !== 0.0 && (d - lastTouchKeyboardEvtA) < 10.0) {
						return;
					}
					if(lastTouchKeyboardEvtB !== 0.0 && (d - lastTouchKeyboardEvtB) < 10.0) {
						return;
					}
					lastTouchKeyboardEvtC = d;
				}
				var val = touchKeyboardField.value;
				var l = val.length;
				if(l === 0) {
					pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH_KEYBOARD, {
						"eventType": EVENT_TOUCH_KEYBOARD_ABSOLUTE,
						"absoluteCode": 0x0E, // KEY_BACK
						"absoluteChar": 0
					});
				}else if(l === 1) {
					pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH_KEYBOARD, {
						"eventType": EVENT_TOUCH_KEYBOARD_CODEPOINTS,
						"codepoints": val
					});
				}else {
					val = val.trim();
					l = val.length;
					if(l === 0) {
						pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH_KEYBOARD, {
							"eventType": EVENT_TOUCH_KEYBOARD_ABSOLUTE,
							"absoluteCode": 0x39, // KEY_SPACE
							"absoluteChar": 32
						});
					}else {
						pushEvent(EVENT_TYPE_INPUT, EVENT_INPUT_TOUCH_KEYBOARD, {
							"eventType": EVENT_TOUCH_KEYBOARD_CODEPOINTS,
							"codepoints": val.charAt(l - 1)
						});
					}
				}
				touchKeyboardField.value = " ";
				touchKeyboardField.setSelectionRange(1, 1);
			});
			touchKeyboardField.addEventListener("focus", function(/** Event */ evt) {
				if(touchKeyboardField !== evt.target) return;
				touchKeyboardField.value = " ";
				touchKeyboardField.setSelectionRange(1, 1);
			});
			touchKeyboardField.addEventListener("select", function(/** Event */ evt) {
				if(touchKeyboardField !== evt.target) return;
				evt.preventDefault();
				evt.stopPropagation();
				touchKeyboardField.value = " ";
				touchKeyboardField.setSelectionRange(1, 1);
			});
			touchKeyboardForm.appendChild(touchKeyboardField);
			parentElement.appendChild(touchKeyboardForm);
			touchKeyboardField.value = " ";
			touchKeyboardField.focus();
			touchKeyboardField.select();
			touchKeyboardField.setSelectionRange(1, 1);
		}else {
			touchCloseDeviceKeyboard();
		}
	}

	/**
	 * @param {number} x
	 * @param {number} y
	 * @param {number} w
	 * @param {number} h
	 */
	inputImports["touchSetOpenKeyboardZone"] = function(x, y, w, h) {
		if(w !== 0 && h !== 0) {
			touchKeyboardOpenZone.style.display = "block";
			touchKeyboardOpenZone.style.top = "" + ((lastWindowHeight - y - h) / lastWindowDPI) + "px";
			touchKeyboardOpenZone.style.left = "" + (x / lastWindowDPI) + "px";
			touchKeyboardOpenZone.style.width = "" + (w / lastWindowDPI) + "px";
			touchKeyboardOpenZone.style.height = "" + (h / lastWindowDPI) + "px";
		}else {
			touchKeyboardOpenZone.style.display = "none";
			touchKeyboardOpenZone.style.top = "0px";
			touchKeyboardOpenZone.style.left = "0px";
			touchKeyboardOpenZone.style.width = "0px";
			touchKeyboardOpenZone.style.height = "0px";
		}
	};

	function touchCloseDeviceKeyboard() {
		if(touchKeyboardField) {
			touchKeyboardField.blur();
			touchKeyboardField.value = "";
			const el = touchKeyboardField;
			const el2 = touchKeyboardForm;
			window.setTimeout(function() {
				if(el2 !== null) {
					el2.removeChild(el);
					el2.remove();
				}else {
					el.remove();
				}
			}, 10);
			touchKeyboardField = null;
			touchKeyboardForm = null;
		}else if(touchKeyboardForm) {
			if(parentElement) {
				parentElement.removeChild(touchKeyboardForm);
			}else {
				touchKeyboardForm.remove();
			}
			touchKeyboardForm = null;
		}
	}

	inputImports["touchCloseDeviceKeyboard"] = touchCloseDeviceKeyboard;

	function touchIsDeviceKeyboardOpenMAYBE() {
		return !!touchKeyboardField && document.activeElement === touchKeyboardField;
	}

	/**
	 * @return {boolean}
	 */
	inputImports["touchIsDeviceKeyboardOpenMAYBE"] = touchIsDeviceKeyboardOpenMAYBE;

	/**
	 * @return {boolean}
	 */
	inputImports["gamepadSupported"] = function() {
		return gamepadSupported;
	};

	/**
	 * @return {boolean}
	 */
	inputImports["isVisualViewport"] = function() {
		return useVisualViewport;
	};

	removeEventHandlers = function() {
		try {
			if(currentEventListeners.contextmenu) {
				parentElement.removeEventListener("contextmenu", /** @type {function(Event)} */ (currentEventListeners.contextmenu));
				currentEventListeners.contextmenu = null;
			}
			if(currentEventListeners.mousedown) {
				canvasElement.removeEventListener("mousedown", /** @type {function(Event)} */ (currentEventListeners.mousedown));
				currentEventListeners.mousedown = null;
			}
			if(currentEventListeners.mouseup) {
				canvasElement.removeEventListener("mouseup", /** @type {function(Event)} */ (currentEventListeners.mouseup));
				currentEventListeners.mouseup = null;
			}
			if(currentEventListeners.mousemove) {
				canvasElement.removeEventListener("mousemove", /** @type {function(Event)} */ (currentEventListeners.mousemove));
				currentEventListeners.mousemove = null;
			}
			if(currentEventListeners.mouseenter) {
				canvasElement.removeEventListener("mouseenter", /** @type {function(Event)} */ (currentEventListeners.mouseenter));
				currentEventListeners.mouseenter = null;
			}
			if(currentEventListeners.mouseleave) {
				canvasElement.removeEventListener("mouseleave", /** @type {function(Event)} */ (currentEventListeners.mouseleave));
				currentEventListeners.mouseleave = null;
			}
			if(currentEventListeners.touchstart) {
				canvasElement.removeEventListener("touchstart", /** @type {function(Event)} */ (currentEventListeners.touchstart));
				currentEventListeners.touchstart = null;
			}
			if(currentEventListeners.touchend) {
				canvasElement.removeEventListener("touchend", /** @type {function(Event)} */ (currentEventListeners.touchend));
				currentEventListeners.touchend = null;
			}
			if(currentEventListeners.touchmove) {
				canvasElement.removeEventListener("touchmove", /** @type {function(Event)} */ (currentEventListeners.touchmove));
				currentEventListeners.touchmove = null;
			}
			if(currentEventListeners.touchcancel) {
				canvasElement.removeEventListener("touchcancel", /** @type {function(Event)} */ (currentEventListeners.touchcancel));
				currentEventListeners.touchcancel = null;
			}
			if(currentEventListeners.gamepadconnected) {
				window.removeEventListener("gamepadconnected", /** @type {function(Event)} */ (currentEventListeners.gamepadconnected));
				currentEventListeners.gamepadconnected = null;
			}
			if(currentEventListeners.gamepaddisconnected) {
				window.removeEventListener("gamepaddisconnected", /** @type {function(Event)} */ (currentEventListeners.gamepaddisconnected));
				currentEventListeners.gamepaddisconnected = null;
			}
			if(currentEventListeners.keydown) {
				window.removeEventListener("keydown", /** @type {function(Event)} */ (currentEventListeners.keydown));
				currentEventListeners.keydown = null;
			}
			if(currentEventListeners.keyup) {
				window.removeEventListener("keyup", /** @type {function(Event)} */ (currentEventListeners.keyup));
				currentEventListeners.keyup = null;
			}
			if(currentEventListeners.touchKeyboardOpenZone_touchstart) {
				touchKeyboardOpenZone.removeEventListener("touchstart", /** @type {function(Event)} */ (currentEventListeners.touchKeyboardOpenZone_touchstart));
				currentEventListeners.touchKeyboardOpenZone_touchstart = null;
			}
			if(currentEventListeners.touchKeyboardOpenZone_touchend) {
				touchKeyboardOpenZone.removeEventListener("touchend", /** @type {function(Event)} */ (currentEventListeners.touchKeyboardOpenZone_touchend));
				currentEventListeners.touchKeyboardOpenZone_touchend = null;
			}
			if(currentEventListeners.touchKeyboardOpenZone_touchmove) {
				touchKeyboardOpenZone.removeEventListener("touchmove", /** @type {function(Event)} */ (currentEventListeners.touchKeyboardOpenZone_touchmove));
				currentEventListeners.touchKeyboardOpenZone_touchmove = null;
			}
			if(currentEventListeners.wheel) {
				canvasElement.removeEventListener("wheel", /** @type {function(Event)} */ (currentEventListeners.wheel));
				currentEventListeners.wheel = null;
			}
			if(currentEventListeners.focus) {
				window.removeEventListener("focus", /** @type {function(Event)} */ (currentEventListeners.focus));
				currentEventListeners.focus = null;
			}
			if(currentEventListeners.blur) {
				window.removeEventListener("blur", /** @type {function(Event)} */ (currentEventListeners.blur));
				currentEventListeners.blur = null;
			}
			if(currentEventListeners.pointerlock) {
				document.removeEventListener("pointerlockchange", /** @type {function(Event)} */ (currentEventListeners.pointerlock));
				currentEventListeners.pointerlock = null;
			}
			if(currentEventListeners.pointerlockerr) {
				document.removeEventListener("pointerlockerror", /** @type {function(Event)} */ (currentEventListeners.pointerlockerr));
				currentEventListeners.pointerlockerr = null;
			}
			if(currentEventListeners.fullscreenChange) {
				fullscreenQuery.removeEventListener("change", /** @type {function(Event)} */ (currentEventListeners.fullscreenChange));
				currentEventListeners.fullscreenChange = null;
			}
		}catch(ex) {
			eagError("Failed to remove event listeners! {}", ex);
		}
		removeEventHandlers = null;
	};
}

function initNoPlatformInput(inputImports) {
	setUnsupportedFunc(inputImports, platfInputName, "keyboardLayoutSupported");
	setUnsupportedFunc(inputImports, platfInputName, "iterateKeyboardLayout");
	setUnsupportedFunc(inputImports, platfInputName, "updateCanvasSize");
	setUnsupportedFunc(inputImports, platfInputName, "updatePlatformAndSleep");
	setUnsupportedFunc(inputImports, platfInputName, "isVSyncSupported");
	setUnsupportedFunc(inputImports, platfInputName, "mouseSetGrabbed");
	setUnsupportedFunc(inputImports, platfInputName, "isMouseGrabSupported");
	setUnsupportedFunc(inputImports, platfInputName, "isMouseGrabbed");
	setUnsupportedFunc(inputImports, platfInputName, "isPointerLocked");
	setUnsupportedFunc(inputImports, platfInputName, "supportsFullscreen");
	setUnsupportedFunc(inputImports, platfInputName, "toggleFullscreen");
	setUnsupportedFunc(inputImports, platfInputName, "isFullscreen");
	setUnsupportedFunc(inputImports, platfInputName, "touchSetOpenKeyboardZone");
	setUnsupportedFunc(inputImports, platfInputName, "touchCloseDeviceKeyboard");
	setUnsupportedFunc(inputImports, platfInputName, "touchIsDeviceKeyboardOpenMAYBE");
	setUnsupportedFunc(inputImports, platfInputName, "gamepadSupported");
	setUnsupportedFunc(inputImports, platfInputName, "isVisualViewport");
}
