package net.lax1dude.eaglercraft.adapter;

import static net.lax1dude.eaglercraft.adapter.teavm.WebGL2RenderingContext.*;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.json.JSONObject;
import org.teavm.interop.Async;
import org.teavm.interop.AsyncCallback;
import org.teavm.jso.JSBody;
import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.jso.ajax.ReadyStateChangeHandler;
import org.teavm.jso.ajax.XMLHttpRequest;
import org.teavm.jso.browser.Storage;
import org.teavm.jso.browser.TimerHandler;
import org.teavm.jso.browser.Window;
import org.teavm.jso.canvas.CanvasRenderingContext2D;
import org.teavm.jso.canvas.ImageData;
import org.teavm.jso.core.JSString;
import org.teavm.jso.dom.css.CSSStyleDeclaration;
import org.teavm.jso.dom.events.ErrorEvent;
import org.teavm.jso.dom.events.Event;
import org.teavm.jso.dom.events.EventListener;
import org.teavm.jso.dom.events.KeyboardEvent;
import org.teavm.jso.dom.events.MessageEvent;
import org.teavm.jso.dom.events.MouseEvent;
import org.teavm.jso.dom.events.WheelEvent;
import org.teavm.jso.dom.html.HTMLAudioElement;
import org.teavm.jso.dom.html.HTMLCanvasElement;
import org.teavm.jso.dom.html.HTMLDocument;
import org.teavm.jso.dom.html.HTMLElement;
import org.teavm.jso.dom.html.HTMLImageElement;
import org.teavm.jso.dom.html.HTMLVideoElement;
import org.teavm.jso.media.MediaError;
import org.teavm.jso.typedarrays.ArrayBuffer;
import org.teavm.jso.typedarrays.DataView;
import org.teavm.jso.typedarrays.Int32Array;
import org.teavm.jso.typedarrays.Uint8Array;
import org.teavm.jso.typedarrays.Uint8ClampedArray;
import org.teavm.jso.webaudio.AnalyserNode;
import org.teavm.jso.webaudio.AudioBuffer;
import org.teavm.jso.webaudio.AudioBufferSourceNode;
import org.teavm.jso.webaudio.AudioContext;
import org.teavm.jso.webaudio.AudioListener;
import org.teavm.jso.webaudio.AudioNode;
import org.teavm.jso.webaudio.ChannelMergerNode;
import org.teavm.jso.webaudio.DecodeErrorCallback;
import org.teavm.jso.webaudio.DecodeSuccessCallback;
import org.teavm.jso.webaudio.GainNode;
import org.teavm.jso.webaudio.MediaElementAudioSourceNode;
import org.teavm.jso.webaudio.MediaEvent;
import org.teavm.jso.webaudio.MediaStream;
import org.teavm.jso.webaudio.MediaStreamAudioSourceNode;
import org.teavm.jso.webaudio.PannerNode;
import org.teavm.jso.webgl.WebGLBuffer;
import org.teavm.jso.webgl.WebGLFramebuffer;
import org.teavm.jso.webgl.WebGLProgram;
import org.teavm.jso.webgl.WebGLRenderbuffer;
import org.teavm.jso.webgl.WebGLShader;
import org.teavm.jso.webgl.WebGLTexture;
import org.teavm.jso.webgl.WebGLUniformLocation;
import org.teavm.jso.websocket.CloseEvent;
import org.teavm.jso.websocket.WebSocket;
import org.teavm.jso.workers.Worker;
import org.teavm.platform.Platform;
import org.teavm.platform.PlatformRunnable;

import net.lax1dude.eaglercraft.AssetRepository;
import net.lax1dude.eaglercraft.Base64;
import net.lax1dude.eaglercraft.Client;
import net.lax1dude.eaglercraft.EaglerAdapter;
import net.lax1dude.eaglercraft.EaglerImage;
import net.lax1dude.eaglercraft.EaglerInputStream;
import net.lax1dude.eaglercraft.EaglerProfile;
import net.lax1dude.eaglercraft.EarlyLoadScreen;
import net.lax1dude.eaglercraft.ExpiringSet;
import net.lax1dude.eaglercraft.IntegratedServer;
import net.lax1dude.eaglercraft.LANPeerEvent;
import net.lax1dude.eaglercraft.LocalStorageManager;
import net.lax1dude.eaglercraft.PKT;
import net.lax1dude.eaglercraft.RelayQuery;
import net.lax1dude.eaglercraft.RelayServerSocket;
import net.lax1dude.eaglercraft.RelayWorldsQuery;
import net.lax1dude.eaglercraft.ServerQuery;
import net.lax1dude.eaglercraft.adapter.teavm.BufferConverter;
import net.lax1dude.eaglercraft.adapter.teavm.EaglercraftLANClient;
import net.lax1dude.eaglercraft.adapter.teavm.EaglercraftLANServer;
import net.lax1dude.eaglercraft.adapter.teavm.EaglercraftVoiceClient;
import net.lax1dude.eaglercraft.adapter.teavm.MessageChannel;
import net.lax1dude.eaglercraft.adapter.teavm.SelfDefence;
import net.lax1dude.eaglercraft.adapter.teavm.TeaVMUtils;
import net.lax1dude.eaglercraft.adapter.teavm.WebGL2RenderingContext;
import net.lax1dude.eaglercraft.adapter.teavm.WebGLQuery;
import net.lax1dude.eaglercraft.adapter.teavm.WebGLVertexArray;
import net.lax1dude.eaglercraft.glemu.EaglerAdapterGL30;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket00Handshake;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds.LocalWorld;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket69Pong;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacketFFErrorCode;
import net.minecraft.src.MathHelper;

public class EaglerAdapterImpl2 {

	public static final boolean _wisWebGL() {
		return true;
	}
	public static final String _wgetShaderHeader() {
		return "#version 300 es";
	}
	
	@JSBody(params = { }, script = "return window.location.href;")
	private static native String getLocationString();
	
	public static final boolean isSSLPage() {
		return getLocationString().startsWith("https");
	}
	
	public static final InputStream loadResource(String path) {
		byte[] file = loadResourceBytes(path);
		if (file != null) {
			return new EaglerInputStream(file);
		} else {
			return null;
		}
	}

	public static final byte[] loadResourceBytes(String path) {
		return AssetRepository.getResource(path);
	}
	
	public static final String fileContents(String path) {
		byte[] contents = loadResourceBytes(path);
		if(contents == null) {
			return null;
		}else {
			return new String(contents, Charset.forName("UTF-8"));
		}
	}
	
	public static final String[] fileContentsLines(String path) {
		String contents = fileContents(path);
		if(contents == null) {
			return null;
		}else {
			return contents.replace("\r\n", "\n").split("[\r\n]");
		}
	}

	@Async
	public static native String downloadAssetPack(String assetPackageURI);

	private static void downloadAssetPack(String assetPackageURI, final AsyncCallback<String> cb) {
		final XMLHttpRequest request = new XMLHttpRequest();
		request.setResponseType("arraybuffer");
		request.open("GET", assetPackageURI, true);
		request.setOnReadyStateChange(new ReadyStateChangeHandler() {
			@Override
			public void stateChanged() {
				if(request.getReadyState() == XMLHttpRequest.DONE) {
					loadedPackage = TeaVMUtils.wrapByteArrayBuffer((ArrayBuffer)request.getResponse());
					cb.complete("yee");
				}
			}
		});
		request.send();
	}

	@Async
	public static native byte[] downloadURL(String url);

	private static void downloadURL(String url, final AsyncCallback<byte[]> cb) {
		if(url.isEmpty()) {
			cb.complete(new byte[0]);
			return;
		}
		final XMLHttpRequest request = new XMLHttpRequest();
		request.setResponseType("arraybuffer");
		request.open("GET", url, true);
		request.setOnReadyStateChange(new ReadyStateChangeHandler() {
			@Override
			public void stateChanged() {
				if(request.getReadyState() == XMLHttpRequest.DONE) {
					cb.complete(TeaVMUtils.wrapByteArrayBuffer((ArrayBuffer)request.getResponse()));
				}
			}
		});
		request.send();
	}

	@JSBody(params = { "v", "s" }, script = "window[v] = s;")
	public static native void setDebugVar(String v, String s);
	
	@JSBody(params = { }, script = "if(window.navigator.userActivation){return window.navigator.userActivation.hasBeenActive;}else{return false;}")
	public static native boolean hasBeenActive();

	public static HTMLDocument doc = null;
	public static HTMLElement parent = null;
	public static HTMLCanvasElement canvas = null;
	public static WebGL2RenderingContext webgl = null;
	public static FramebufferGL backBuffer = null;
	public static RenderbufferGL backBufferColor = null;
	public static RenderbufferGL backBufferDepth = null;
	public static Window win = null;
	private static byte[] loadedPackage = null;
	private static EventListener<?> contextmenu = null;
	private static EventListener<?> mousedown = null;
	private static EventListener<?> mouseup = null;
	private static EventListener<?> mousemove = null;
	private static EventListener<?> keydown = null;
	private static EventListener<?> keyup = null;
	private static EventListener<?> keypress = null;
	private static EventListener<?> wheel = null;
	private static String[] identifier = new String[0];
	private static String integratedServerScript = "worker_bootstrap.js";
	private static boolean anisotropicFilteringSupported = false;
	private static boolean vsyncSupport = false;
	private static int vsyncTimeout = -1;
	private static boolean useDelayOnSwap = false;
	private static MessageChannel immediateContinueChannel = null;
	private static Runnable currentMsgChannelContinueHack = null;
	
	private static final EagsFileChooser fileChooser = initFileChooser();
	
	public static final String[] getIdentifier() {
		return identifier;
	}

	@JSBody(params = { "v" }, script = "try { return \"\"+window.navigator[v]; } catch(e) { return \"<error>\"; }")
	private static native String getNavString(String var);
	
	public static void onWindowUnload() {
		LocalStorageManager.saveStorageA();
		LocalStorageManager.saveStorageG();
		LocalStorageManager.saveStorageP();
	}

	@JSBody(params = { "m" }, script = "return m.offsetX;")
	private static native int getOffsetX(MouseEvent m);
	
	@JSBody(params = { "m" }, script = "return m.offsetY;")
	private static native int getOffsetY(MouseEvent m);
	
	@JSBody(params = { "e" }, script = "return e.which;")
	private static native int getWhich(KeyboardEvent e);
	
	public static final void initializeContext(HTMLElement rootElement, String assetPackageURI, String serverWorkerURI) {
		parent = rootElement;
		String s = parent.getAttribute("style");
		parent.setAttribute("style", (s == null ? "" : s)+"overflow-x:hidden;overflow-y:hidden;");
		win = Window.current();
		doc = win.getDocument();
		integratedServerScript = serverWorkerURI;
		double r = win.getDevicePixelRatio();
		int iw = parent.getClientWidth();
		int ih = parent.getClientHeight();
		int sw = (int)(r * iw);
		int sh = (int)(r * ih);
		canvas = (HTMLCanvasElement)doc.createElement("canvas");
		CSSStyleDeclaration canvasStyle = canvas.getStyle();
		canvasStyle.setProperty("width", "100%");
		canvasStyle.setProperty("height", "100%");
		canvasStyle.setProperty("image-rendering", "pixelated");
		canvas.setWidth(sw);
		canvas.setHeight(sh);
		rootElement.appendChild(canvas);
		try {
			doc.exitPointerLock();
		}catch(Throwable t) {
			Client.showIncompatibleScreen("Mouse cursor lock is not available on this device!");
			throw new RuntimeException("Mouse cursor lock is not available on this device!");
		}
		SelfDefence.init(canvas);
		webgl = (WebGL2RenderingContext) canvas.getContext("webgl2", youEagler());
		if(webgl == null) {
			Client.showIncompatibleScreen("WebGL 2.0 is not supported on this device!");
			throw new RuntimeException("WebGL 2.0 is not supported in your browser ("+getNavString("userAgent")+")");
		}

		setupBackBuffer();
		resizeBackBuffer(sw, sh);
		
		anisotropicFilteringSupported = webgl.getExtension("EXT_texture_filter_anisotropic") != null;
		
		win.addEventListener("contextmenu", contextmenu = new EventListener<MouseEvent>() {
			@Override
			public void handleEvent(MouseEvent evt) {
				evt.preventDefault();
				evt.stopPropagation();
			}
		});
		canvas.addEventListener("mousedown", mousedown = new EventListener<MouseEvent>() {
			@Override
			public void handleEvent(MouseEvent evt) {
				int b = evt.getButton();
				buttonStates[b == 1 ? 2 : (b == 2 ? 1 : b)] = true;
				mouseEvents.add(evt);
				evt.preventDefault();
				evt.stopPropagation();
			}
		});
		canvas.addEventListener("mouseup", mouseup = new EventListener<MouseEvent>() {
			@Override
			public void handleEvent(MouseEvent evt) {
				int b = evt.getButton();
				buttonStates[b == 1 ? 2 : (b == 2 ? 1 : b)] = false;
				mouseEvents.add(evt);
				evt.preventDefault();
				evt.stopPropagation();
			}
		});
		canvas.addEventListener("mousemove", mousemove = new EventListener<MouseEvent>() {
			@Override
			public void handleEvent(MouseEvent evt) {
				mouseX = (int)(getOffsetX(evt) * win.getDevicePixelRatio());
				mouseY = (int)((canvas.getClientHeight() - getOffsetY(evt)) * win.getDevicePixelRatio());
				mouseDX += evt.getMovementX();
				mouseDY += -evt.getMovementY();
				if(hasBeenActive()) {
					mouseEvents.add(evt);
				}
				evt.preventDefault();
				evt.stopPropagation();
			}
		});
		win.addEventListener("keydown", keydown = new EventListener<KeyboardEvent>() {
			@Override
			public void handleEvent(KeyboardEvent evt) {
				//keyStates[remapKey(evt.getKeyCode())] = true;
				keyStates[remapKey(getWhich(evt))] = true;
				keyEvents.add(evt);
				evt.preventDefault();
				evt.stopPropagation();
			}
		});
		win.addEventListener("keyup", keyup = new EventListener<KeyboardEvent>() {
			@Override
			public void handleEvent(KeyboardEvent evt) {
				//keyStates[remapKey(evt.getKeyCode())] = false;
				keyStates[remapKey(getWhich(evt))] = false;
				keyEvents.add(evt);
				evt.preventDefault();
				evt.stopPropagation();
			}
		});
		win.addEventListener("keypress", keypress = new EventListener<KeyboardEvent>() {
			@Override
			public void handleEvent(KeyboardEvent evt) {
				if(enableRepeatEvents && evt.isRepeat()) keyEvents.add(evt);
				evt.preventDefault();
				evt.stopPropagation();
			}
		});
		canvas.addEventListener("wheel", wheel = new EventListener<WheelEvent>() {
			@Override
			public void handleEvent(WheelEvent evt) {
				mouseEvents.add(evt);
				evt.preventDefault();
				evt.stopPropagation();
			}
		});
		win.addEventListener("blur", new EventListener<WheelEvent>() {
			@Override
			public void handleEvent(WheelEvent evt) {
				isWindowFocused = false;
			}
		});
		win.addEventListener("focus", new EventListener<WheelEvent>() {
			@Override
			public void handleEvent(WheelEvent evt) {
				isWindowFocused = true;
			}
		});
		onBeforeCloseRegister();
		
		checkImmediateContinueSupport();
		
		vsyncTimeout = -1;
		vsyncSupport = false;

		try {
			asyncRequestAnimationFrame();
			vsyncSupport = true;
		}catch(Throwable t) {
			System.err.println("VSync is not supported on this browser!");
		}
		
		initFileChooser();
		
		EarlyLoadScreen.paintScreen();
		
		voiceClient = startVoiceClient();
		rtcLANClient = startRTCLANClient();
		
		//todo: safely skip startRTCLANServer() if the integrated server is disabled:
		
		//if(integratedServerScript != null) {
			rtcLANServer = startRTCLANServer();
		//}
		
		downloadAssetPack(assetPackageURI);
		
		try {
			AssetRepository.install(loadedPackage);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		if(mouseEvents.isEmpty() && keyEvents.isEmpty() && !hasBeenActive()) {
			EarlyLoadScreen.paintEnable();
			
			while(mouseEvents.isEmpty() && keyEvents.isEmpty()) {
				sleep(100);
			}
		}
		
		audioctx = new AudioContext();
		masterVolumeNode = audioctx.createGain();
		masterVolumeNode.getGain().setValue(1.0f);
		masterVolumeNode.connect(audioctx.getDestination());
		musicVolumeNode = audioctx.createGain();
		musicVolumeNode.getGain().setValue(1.0f);
		musicVolumeNode.connect(audioctx.getDestination());
		
		mouseEvents.clear();
		keyEvents.clear();
		
		Window.setInterval(new TimerHandler() {
			@Override
			public void onTimer() {
				if(!videosBuffer.isEmpty()) {
					long now = steadyTimeMillis();
					Iterator<BufferedVideo> vids = videosBuffer.values().iterator();
					while(vids.hasNext()) {
						BufferedVideo v = vids.next();
						if(now - v.requestedTime > v.ttl) {
							v.videoElement.setSrc("");
							vids.remove();
						}
					}
				}
			}
		}, 5000);
	}

	@JSBody(params = { }, script = "return window.startVoiceClient();")
	private static native EaglercraftVoiceClient startVoiceClient();

	private static interface EagsFileChooser extends JSObject {

		@JSProperty
		HTMLElement getInputElement();

		void openFileChooser(String ext, String mime);

		@JSProperty
		ArrayBuffer getFileChooserResult();

		@JSProperty
		void setFileChooserResult(ArrayBuffer val);

		@JSProperty
		String getFileChooserResultName();

		@JSProperty
		void setFileChooserResultName(String str);

	}

	@JSBody(params = { }, script = 
			"var ret = {\r\n" + 
			"inputElement: null,\r\n" + 
			"openFileChooser: function(ext, mime){\r\n" + 
			"var el = ret.inputElement = document.createElement(\"input\");\r\n" + 
			"el.type = \"file\";\r\n" + 
			"el.multiple = false;\r\n" + 
			"el.addEventListener(\"change\", function(evt){\r\n" + 
			"var f = ret.inputElement.files;\r\n" + 
			"if(f.length == 0){\r\n" + 
			"ret.fileChooserResult = null;\r\n" + 
			"}else{\r\n" + 
			"ret.fileChooserResult = null;\r\n" + 
			"ret.fileChooserResultName = null;\r\n" + 
			"var phile = f[0];\r\n" +
			"phile.arrayBuffer().then(function(res) {\r\n" + 
			"ret.fileChooserResult = res;\r\n" + 
			"ret.fileChooserResultName = phile.name;console.log(ret);\r\n" + 
			"});\r\n" + 
			"}\r\n" + 
			"});\r\n" + 
			"el.accept = \".\" + ext;\r\n" +
			"el.click();\r\n" + 
			"},\r\n" + 
			"fileChooserResult: null,\r\n" + 
			"fileChooserResultName: null\r\n" + 
			"}; return ret;")
	private static native EagsFileChooser initFileChooser();
	
	public static final void destroyContext() {
		
	}
	
	public static final void removeEventHandlers() {
		try {
			win.removeEventListener("contextmenu", contextmenu);
			canvas.removeEventListener("mousedown", mousedown);
			canvas.removeEventListener("mouseup", mouseup);
			canvas.removeEventListener("mousemove", mousemove);
			win.removeEventListener("keydown", keydown);
			win.removeEventListener("keyup", keyup);
			win.removeEventListener("keypress", keypress);
			canvas.removeEventListener("wheel", wheel);
		}catch(Throwable t) {
		}
		try {
			String screenImg = canvas.toDataURL("image/png");
			canvas.delete();
			HTMLImageElement newImage = (HTMLImageElement) doc.createElement("img");
			newImage.setSrc(screenImg);
			newImage.setWidth(parent.getClientWidth());
			newImage.setHeight(parent.getClientHeight());
			parent.appendChild(newImage);
		}catch(Throwable t) {
		}
	}

	private static LinkedList<MouseEvent> mouseEvents = new LinkedList<>();
	private static LinkedList<KeyboardEvent> keyEvents = new LinkedList<>();

	private static int mouseX = 0;
	private static int mouseY = 0;
	private static double mouseDX = 0.0D;
	private static double mouseDY = 0.0D;
	private static int width = 0;
	private static int height = 0;
	private static boolean enableRepeatEvents = false;
	private static boolean isWindowFocused = true;
	
	@JSBody(params = { }, script = "return {antialias: false, depth: true, powerPreference: \"high-performance\", desynchronized: true, preserveDrawingBuffer: false, premultipliedAlpha: false, alpha: false};")
	public static native JSObject youEagler();
	
	@JSBody(params = { }, script = "return { willReadFrequently: true };")
	public static native JSObject youEagler2();
	
	public static final int _wGL_TEXTURE_2D = TEXTURE_2D;
	public static final int _wGL_DEPTH_TEST = DEPTH_TEST;
	public static final int _wGL_LEQUAL = LEQUAL;
	public static final int _wGL_GEQUAL = GEQUAL;
	public static final int _wGL_GREATER = GREATER;
	public static final int _wGL_LESS = LESS;
	public static final int _wGL_BACK = BACK;
	public static final int _wGL_FRONT = FRONT;
	public static final int _wGL_FRONT_AND_BACK = FRONT_AND_BACK;
	public static final int _wGL_COLOR_BUFFER_BIT = COLOR_BUFFER_BIT;
	public static final int _wGL_DEPTH_BUFFER_BIT = DEPTH_BUFFER_BIT;
	public static final int _wGL_BLEND = BLEND;
	public static final int _wGL_RGBA = RGBA;
	public static final int _wGL_RGB = RGB;
	public static final int _wGL_RGB8 = RGB8;
	public static final int _wGL_RGBA8 = RGBA8;
	public static final int _wGL_RED = RED;
	public static final int _wGL_R8 = R8;
	public static final int _wGL_UNSIGNED_BYTE = UNSIGNED_BYTE;
	public static final int _wGL_UNSIGNED_SHORT = UNSIGNED_SHORT;
	public static final int _wGL_SRC_ALPHA = SRC_ALPHA;
	public static final int _wGL_ONE_MINUS_SRC_ALPHA = ONE_MINUS_SRC_ALPHA;
	public static final int _wGL_ONE_MINUS_DST_COLOR = ONE_MINUS_DST_COLOR;
	public static final int _wGL_ONE_MINUS_SRC_COLOR = ONE_MINUS_SRC_COLOR;
	public static final int _wGL_ZERO = ZERO;
	public static final int _wGL_CULL_FACE = CULL_FACE;
	public static final int _wGL_TEXTURE_MIN_FILTER = TEXTURE_MIN_FILTER;
	public static final int _wGL_TEXTURE_MAG_FILTER = TEXTURE_MAG_FILTER;
	public static final int _wGL_LINEAR = LINEAR;
	public static final int _wGL_EQUAL = EQUAL;
	public static final int _wGL_SRC_COLOR = SRC_COLOR;
	public static final int _wGL_ONE = ONE;
	public static final int _wGL_NEAREST = NEAREST;
	public static final int _wGL_CLAMP = CLAMP_TO_EDGE;
	public static final int _wGL_TEXTURE_WRAP_S = TEXTURE_WRAP_S;
	public static final int _wGL_TEXTURE_WRAP_T = TEXTURE_WRAP_T;
	public static final int _wGL_REPEAT = REPEAT;
	public static final int _wGL_DST_COLOR = DST_COLOR;
	public static final int _wGL_DST_ALPHA = DST_ALPHA;
	public static final int _wGL_FLOAT = FLOAT;
	public static final int _wGL_SHORT = SHORT;
	public static final int _wGL_TRIANGLES = TRIANGLES;
	public static final int _wGL_TRIANGLE_STRIP = TRIANGLE_STRIP;
	public static final int _wGL_TRIANGLE_FAN = TRIANGLE_FAN;
	public static final int _wGL_LINE_STRIP = LINE_STRIP;
	public static final int _wGL_LINES = LINES;
	public static final int _wGL_PACK_ALIGNMENT = PACK_ALIGNMENT;
	public static final int _wGL_UNPACK_ALIGNMENT = UNPACK_ALIGNMENT;
	public static final int _wGL_TEXTURE0 = TEXTURE0;
	public static final int _wGL_TEXTURE1 = TEXTURE1;
	public static final int _wGL_TEXTURE2 = TEXTURE2;
	public static final int _wGL_TEXTURE3 = TEXTURE3;
	public static final int _wGL_VIEWPORT = VIEWPORT;
	public static final int _wGL_VERTEX_SHADER = VERTEX_SHADER;
	public static final int _wGL_FRAGMENT_SHADER = FRAGMENT_SHADER;
	public static final int _wGL_ARRAY_BUFFER = ARRAY_BUFFER;
	public static final int _wGL_ELEMENT_ARRAY_BUFFER = ELEMENT_ARRAY_BUFFER;
	public static final int _wGL_STATIC_DRAW = STATIC_DRAW;
	public static final int _wGL_DYNAMIC_DRAW = DYNAMIC_DRAW;
	public static final int _wGL_STREAM_DRAW = STREAM_DRAW;
	public static final int _wGL_INVALID_ENUM = INVALID_ENUM;
	public static final int _wGL_INVALID_VALUE= INVALID_VALUE;
	public static final int _wGL_INVALID_OPERATION = INVALID_OPERATION;
	public static final int _wGL_OUT_OF_MEMORY = OUT_OF_MEMORY;
	public static final int _wGL_CONTEXT_LOST_WEBGL = CONTEXT_LOST_WEBGL;
	public static final int _wGL_FRAMEBUFFER_COMPLETE = FRAMEBUFFER_COMPLETE;
	public static final int _wGL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT = FRAMEBUFFER_INCOMPLETE_ATTACHMENT;
	public static final int _wGL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT = FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
	public static final int _wGL_COLOR_ATTACHMENT0 = COLOR_ATTACHMENT0;
	public static final int _wGL_DEPTH_STENCIL_ATTACHMENT = DEPTH_STENCIL_ATTACHMENT;
	public static final int _wGL_DEPTH_STENCIL = DEPTH_STENCIL;
	public static final int _wGL_NEAREST_MIPMAP_LINEAR = NEAREST_MIPMAP_LINEAR; 
	public static final int _wGL_LINEAR_MIPMAP_LINEAR = LINEAR_MIPMAP_LINEAR; 
	public static final int _wGL_LINEAR_MIPMAP_NEAREST = LINEAR_MIPMAP_NEAREST; 
	public static final int _wGL_NEAREST_MIPMAP_NEAREST = NEAREST_MIPMAP_NEAREST;
	public static final int _wGL_TEXTURE_MAX_LEVEL = TEXTURE_MAX_LEVEL; 
	public static final int _wGL_UNSIGNED_INT_24_8 = UNSIGNED_INT_24_8;
	public static final int _wGL_UNSIGNED_INT = UNSIGNED_INT;
	public static final int _wGL_ANY_SAMPLES_PASSED = ANY_SAMPLES_PASSED; 
	public static final int _wGL_QUERY_RESULT = QUERY_RESULT;
	public static final int _wGL_QUERY_RESULT_AVAILABLE = QUERY_RESULT_AVAILABLE;
	public static final int _wGL_TEXTURE_MAX_ANISOTROPY = TEXTURE_MAX_ANISOTROPY_EXT;
	public static final int _wGL_DEPTH24_STENCIL8 = DEPTH24_STENCIL8;
	public static final int _wGL_DEPTH_COMPONENT32F = DEPTH_COMPONENT32F;
	public static final int _wGL_DEPTH_ATTACHMENT = DEPTH_ATTACHMENT;
	public static final int _wGL_MULTISAMPLE = -1;
	public static final int _wGL_LINE_SMOOTH = -1;
	public static final int _wGL_READ_FRAMEBUFFER = READ_FRAMEBUFFER;
	public static final int _wGL_DRAW_FRAMEBUFFER = DRAW_FRAMEBUFFER;
	public static final int _wGL_FRAMEBUFFER = FRAMEBUFFER;
	public static final int _wGL_POLYGON_OFFSET_FILL = POLYGON_OFFSET_FILL;
	
	public static final class TextureGL { 
		protected final WebGLTexture obj;
		public int w = -1;
		public int h = -1;
		public boolean nearest = true;
		public boolean anisotropic = false;
		protected TextureGL(WebGLTexture obj) { 
			this.obj = obj; 
		} 
	} 
	public static final class BufferGL { 
		protected final WebGLBuffer obj; 
		protected BufferGL(WebGLBuffer obj) { 
			this.obj = obj; 
		} 
	} 
	public static final class ShaderGL { 
		protected final WebGLShader obj; 
		protected ShaderGL(WebGLShader obj) { 
			this.obj = obj; 
		} 
	}
	private static int progId = 0;
	public static final class ProgramGL { 
		protected final WebGLProgram obj; 
		protected final int hashcode; 
		protected ProgramGL(WebGLProgram obj) { 
			this.obj = obj; 
			this.hashcode = ++progId;
		} 
	} 
	public static final class UniformGL { 
		protected final WebGLUniformLocation obj; 
		protected UniformGL(WebGLUniformLocation obj) { 
			this.obj = obj; 
		} 
	} 
	public static final class BufferArrayGL { 
		protected final WebGLVertexArray obj; 
		public boolean isQuadBufferBound; 
		protected BufferArrayGL(WebGLVertexArray obj) { 
			this.obj = obj; 
			this.isQuadBufferBound = false; 
		} 
	} 
	public static final class FramebufferGL { 
		protected final WebGLFramebuffer obj; 
		protected FramebufferGL(WebGLFramebuffer obj) { 
			this.obj = obj; 
		} 
	} 
	public static final class RenderbufferGL { 
		protected final WebGLRenderbuffer obj; 
		protected RenderbufferGL(WebGLRenderbuffer obj) { 
			this.obj = obj; 
		} 
	} 
	public static final class QueryGL { 
		protected final WebGLQuery obj; 
		protected QueryGL(WebGLQuery obj) { 
			this.obj = obj; 
		} 
	}
	
	public static final boolean anisotropicFilteringSupported() {
		return anisotropicFilteringSupported;
	}
	public static final void _wglEnable(int p1) {
		webgl.enable(p1);
	}
	public static final void _wglClearDepth(float p1) {
		webgl.clearDepth(p1);
	}
	public static final void _wglDepthFunc(int p1) {
		webgl.depthFunc(p1);
	}
	public static final void _wglCullFace(int p1) {
		webgl.cullFace(p1);
	}
	private static int[] viewportCache = new int[4];
	public static final void _wglViewport(int p1, int p2, int p3, int p4) {
		viewportCache[0] = p1; viewportCache[1] = p2;
		viewportCache[2] = p3; viewportCache[3] = p4;
		webgl.viewport(p1, p2, p3, p4);
	}
	public static final void _wglClear(int p1) {
		webgl.clear(p1);
	}
	public static final void _wglClearColor(float p1, float p2, float p3, float p4) {
		webgl.clearColor(p1, p2, p3, p4);
	}
	public static final void _wglDisable(int p1) {
		webgl.disable(p1);
	}
	public static final int _wglGetError() {
		return webgl.getError();
	}
	public static final void _wglFlush() {
		//webgl.flush();
	}
	public static final void _wglTexImage2D(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, ByteBuffer p9) {
		webgl.texImage2D(p1, p2, p3, p4, p5, p6, p7, p8, p9 != null ? BufferConverter.convertByteBufferUnsigned(p9) : null);
	}
	public static final void _wglBlendFunc(int p1, int p2) {
		webgl.blendFunc(p1, p2);
	}
	public static final void _wglBlendFuncSeparate(int p1, int p2, int p3, int p4) {
		webgl.blendFuncSeparate(p1, p2, p3, p4);
	}
	public static final void _wglBlendColor(float r, float g, float b, float a) {
		webgl.blendColor(r, g, b, a);
	}
	public static final void _wglDepthMask(boolean p1) {
		webgl.depthMask(p1);
	}
	public static final void _wglColorMask(boolean p1, boolean p2, boolean p3, boolean p4) {
		webgl.colorMask(p1, p2, p3, p4);
	}
	public static final void _wglBindTexture(int p1, TextureGL p2) {
		webgl.bindTexture(p1, p2 == null ? null : p2.obj);
	}
	public static final void _wglCopyTexSubImage2D(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8) {
		webgl.copyTexSubImage2D(p1, p2, p3, p4, p5, p6, p7, p8);
	}
	public static final void _wglTexParameteri(int p1, int p2, int p3) {
		webgl.texParameteri(p1, p2, p3);
	}
	public static final void _wglTexParameterf(int p1, int p2, float p3) {
		webgl.texParameterf(p1, p2, p3);
	}
	public static final void _wglTexImage2D(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, IntBuffer p9) {
		webgl.texImage2D(p1, p2, p3, p4, p5, p6, p7, p8, p9 != null ? BufferConverter.convertIntBufferUnsigned(p9) : null);
	}
	public static final void _wglTexSubImage2D(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, IntBuffer p9) {
		webgl.texSubImage2D(p1, p2, p3, p4, p5, p6, p7, p8, p9 != null ? BufferConverter.convertIntBufferUnsigned(p9) : null);
	}
	public static final void _wglDeleteTextures(TextureGL p1) {
		webgl.deleteTexture(p1.obj);
	}
	public static final void _wglDrawArrays(int p1, int p2, int p3) {
		webgl.drawArrays(p1, p2, p3);
	}
	public static final void _wglDrawElements(int p1, int p2, int p3, int p4) {
		webgl.drawElements(p1, p2, p3, p4);
	}
	public static final TextureGL _wglGenTextures() {
		return new TextureGL(webgl.createTexture());
	}
	public static final void _wglTexSubImage2D(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, ByteBuffer p9) {
		webgl.texSubImage2D(p1, p2, p3, p4, p5, p6, p7, p8, p9 != null ? BufferConverter.convertByteBufferUnsigned(p9) : null);
	}
	public static final void _wglActiveTexture(int p1) {
		webgl.activeTexture(p1);
	}
	public static final ProgramGL _wglCreateProgram() {
		return new ProgramGL(webgl.createProgram());
	}
	public static final ShaderGL _wglCreateShader(int p1) {
		return new ShaderGL(webgl.createShader(p1));
	}
	public static final void _wglAttachShader(ProgramGL p1, ShaderGL p2) {
		webgl.attachShader(p1.obj, p2.obj);
	}
	public static final void _wglDetachShader(ProgramGL p1, ShaderGL p2) {
		webgl.detachShader(p1.obj, p2.obj);
	}
	public static final void _wglCompileShader(ShaderGL p1) {
		webgl.compileShader(p1.obj);
	}
	public static final void _wglLinkProgram(ProgramGL p1) {
		webgl.linkProgram(p1.obj);
	}
	public static final void _wglShaderSource(ShaderGL p1, String p2) {
		webgl.shaderSource(p1.obj, p2);
	}
	public static final String _wglGetShaderInfoLog(ShaderGL p1) {
		return webgl.getShaderInfoLog(p1.obj);
	}
	public static final String _wglGetProgramInfoLog(ProgramGL p1) {
		return webgl.getProgramInfoLog(p1.obj);
	}
	public static final boolean _wglGetShaderCompiled(ShaderGL p1) {
		return webgl.getShaderParameteri(p1.obj, COMPILE_STATUS) == 1;
	}
	public static final boolean _wglGetProgramLinked(ProgramGL p1) {
		return webgl.getProgramParameteri(p1.obj, LINK_STATUS) == 1;
	}
	public static final void _wglDeleteShader(ShaderGL p1) {
		webgl.deleteShader(p1.obj);
	}
	public static final void _wglDeleteProgram(ProgramGL p1) {
		webgl.deleteProgram(p1.obj);
	}
	public static final BufferGL _wglCreateBuffer() {
		return new BufferGL(webgl.createBuffer());
	}
	public static final void _wglDeleteBuffer(BufferGL p1) {
		webgl.deleteBuffer(p1.obj);
	}
	public static final void _wglBindBuffer(int p1, BufferGL p2) {
		webgl.bindBuffer(p1, p2 == null ? null : p2.obj);
	}
	public static final void _wglBufferData0(int p1, IntBuffer p2, int p3) {
		webgl.bufferData(p1, p2 != null ? BufferConverter.convertIntBufferUnsigned(p2) : null, p3);
	}
	public static final void _wglBufferSubData0(int p1, int p2, IntBuffer p3) {
		webgl.bufferSubData(p1, p2, p3 != null ? BufferConverter.convertIntBufferUnsigned(p3) : null);
	}
	public static final void _wglBufferData(int p1, Object p2, int p3) {
		webgl.bufferData(p1, (Int32Array)p2, p3);
	}
	public static final void _wglBufferData00(int p1, long len, int p3) {
		webgl.bufferData(p1, (int)len, p3);
	}
	public static final void _wglBufferSubData(int p1, int p2, Object p3) {
		webgl.bufferSubData(p1, p2, (Int32Array)p3);
	}
	public static final void _wglBindAttribLocation(ProgramGL p1, int p2, String p3) {
		webgl.bindAttribLocation(p1.obj, p2, p3);
	}
	public static final void _wglEnableVertexAttribArray(int p1) {
		webgl.enableVertexAttribArray(p1);
	}
	public static final void _wglDisableVertexAttribArray(int p1) {
		webgl.disableVertexAttribArray(p1);
	}
	public static final UniformGL _wglGetUniformLocation(ProgramGL p1, String p2) {
		WebGLUniformLocation u = webgl.getUniformLocation(p1.obj, p2);
		return u == null ? null : new UniformGL(u);
	}
	public static final void _wglBindAttributeLocation(ProgramGL p1, int p2, String p3) {
		webgl.bindAttribLocation(p1.obj, p2, p3);
	}
	public static final void _wglUniform1f(UniformGL p1, float p2) {
		if(p1 != null) webgl.uniform1f(p1.obj, p2);
	}
	public static final void _wglUniform2f(UniformGL p1, float p2, float p3) {
		if(p1 != null) webgl.uniform2f(p1.obj, p2, p3);
	}
	public static final void _wglUniform3f(UniformGL p1, float p2, float p3, float p4) {
		if(p1 != null) webgl.uniform3f(p1.obj, p2, p3, p4);
	}
	public static final void _wglUniform4f(UniformGL p1, float p2, float p3, float p4, float p5) {
		if(p1 != null) webgl.uniform4f(p1.obj, p2, p3, p4, p5);
	}
	public static final void _wglUniform1i(UniformGL p1, int p2) {
		if(p1 != null) webgl.uniform1i(p1.obj, p2);
	}
	public static final void _wglUniform2i(UniformGL p1, int p2, int p3) {
		if(p1 != null) webgl.uniform2i(p1.obj, p2, p3);
	}
	public static final void _wglUniform3i(UniformGL p1, int p2, int p3, int p4) {
		if(p1 != null) webgl.uniform3i(p1.obj, p2, p3, p4);
	}
	public static final void _wglUniform4i(UniformGL p1, int p2, int p3, int p4, int p5) {
		if(p1 != null) webgl.uniform4i(p1.obj, p2, p3, p4, p5);
	}
	public static final void _wglUniformMat2fv(UniformGL p1, float[] mat) {
		if(p1 != null) webgl.uniformMatrix2fv(p1.obj, false, TeaVMUtils.unwrapFloatArray(mat));
	}
	public static final void _wglUniformMat3fv(UniformGL p1, float[] mat) {
		if(p1 != null) webgl.uniformMatrix3fv(p1.obj, false, TeaVMUtils.unwrapFloatArray(mat));
	}
	public static final void _wglUniformMat4fv(UniformGL p1, float[] mat) {
		if(p1 != null) webgl.uniformMatrix4fv(p1.obj, false, TeaVMUtils.unwrapFloatArray(mat));
	}
	private static int currentProgram = -1;
	public static final void _wglUseProgram(ProgramGL p1) {
		if(p1 != null && currentProgram != p1.hashcode) {
			currentProgram = p1.hashcode;
			webgl.useProgram(p1.obj);
		}
	}
	public static final void _wglGetParameter(int p1, int size, int[] ret) {
		if(p1 == _wGL_VIEWPORT) {
			ret[0] = viewportCache[0];
			ret[1] = viewportCache[1];
			ret[2] = viewportCache[2];
			ret[3] = viewportCache[3];
		}
	}
	public static final void _wglPolygonOffset(float p1, float p2) {
		webgl.polygonOffset(p1, p2);
	}
	public static final void _wglVertexAttribPointer(int p1, int p2, int p3, boolean p4, int p5, int p6) {
		webgl.vertexAttribPointer(p1, p2, p3, p4, p5, p6);
	}
	public static final void _wglBindFramebuffer(int p1, FramebufferGL p2) {
		webgl.bindFramebuffer(p1, p2 == null ? backBuffer.obj : p2.obj);
	}
	public static final void _wglReadBuffer(int p1) {
		webgl.readBuffer(p1);
	}
	public static final FramebufferGL _wglCreateFramebuffer() {
		return new FramebufferGL(webgl.createFramebuffer());
	}
	public static final void _wglDeleteFramebuffer(FramebufferGL p1) {
		webgl.deleteFramebuffer(p1.obj);
	}
	public static final void _wglFramebufferTexture2D(int p1, TextureGL p2) {
		webgl.framebufferTexture2D(FRAMEBUFFER, p1, TEXTURE_2D, p2 == null ? null : p2.obj, 0);
	}
	public static final void _wglFramebufferTexture2D(int p1, TextureGL p2, int p3) {
		webgl.framebufferTexture2D(FRAMEBUFFER, p1, TEXTURE_2D, p2 == null ? null : p2.obj, p3);
	}
	public static final QueryGL _wglCreateQuery() { 
		return new QueryGL(webgl.createQuery()); 
	}
	public static final void _wglBeginQuery(int p1, QueryGL p2) { 
		webgl.beginQuery(p1, p2.obj); 
	}
	public static final void _wglEndQuery(int p1) { 
		webgl.endQuery(p1); 
	}
	public static final void _wglDeleteQuery(QueryGL p1) { 
		webgl.deleteQuery(p1.obj);
	}
	public static final int _wglGetQueryObjecti(QueryGL p1, int p2) { 
		return webgl.getQueryParameter(p1.obj, p2);
	}
	public static final BufferArrayGL _wglCreateVertexArray() {
		return new BufferArrayGL(webgl.createVertexArray());
	}
	public static final void _wglDeleteVertexArray(BufferArrayGL p1) {
		webgl.deleteVertexArray(p1.obj);
	}
	public static final void _wglBindVertexArray(BufferArrayGL p1) {
		webgl.bindVertexArray(p1 == null ? null : p1.obj);
	}
	public static final void _wglDrawBuffer(int p1) {
		webgl.drawBuffers(new int[] { p1 });
	}
	public static final RenderbufferGL _wglCreateRenderBuffer() {
		return new RenderbufferGL(webgl.createRenderbuffer());
	}
	public static final void _wglBindRenderbuffer(RenderbufferGL p1) {
		webgl.bindRenderbuffer(RENDERBUFFER, p1 == null ? null : p1.obj);
	}
	public static final void _wglRenderbufferStorage(int p1, int p2, int p3) {
		webgl.renderbufferStorage(RENDERBUFFER, p1, p2, p3);
	}
	public static final void _wglFramebufferRenderbuffer(int p1, RenderbufferGL p2) {
		webgl.framebufferRenderbuffer(FRAMEBUFFER, p1, RENDERBUFFER, p2 == null ? null : p2.obj);
	}
	public static final void _wglDeleteRenderbuffer(RenderbufferGL p1) {
		webgl.deleteRenderbuffer(p1.obj);
	}
	public static final void _wglRenderbufferStorageMultisample(int p1, int p2, int p3, int p4) {
		webgl.renderbufferStorageMultisample(RENDERBUFFER, p1, p2, p3, p4);
	}
	public static final void _wglBlitFramebuffer(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, int p9, int p10) {
		webgl.blitFramebuffer(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10);
	}
	public static final int _wglGetAttribLocation(ProgramGL p1, String p2) {
		return webgl.getAttribLocation(p1.obj, p2);
	}
	
	@JSBody(params = { "ctx", "p" }, script = "return ctx.getTexParameter(0x0DE1, p) | 0;")
	private static final native int __wglGetTexParameteri(WebGL2RenderingContext ctx, int p);
	public static final int _wglGetTexParameteri(int p1) {
		return __wglGetTexParameteri(webgl, p1);
	}
	@JSBody(params = { "ctx", "p" }, script = "return (0.0 + ctx.getTexParameter(0x0DE1, p));")
	private static final native float __wglGetTexParameterf(WebGL2RenderingContext ctx, int p);
	public static final float _wglGetTexParameterf(int p1) {
		return __wglGetTexParameterf(webgl, p1);
	}
	public static final boolean isWindows() {
		return getNavString("platform").toLowerCase().contains("win");
	}
	public static final boolean glNeedsAnisotropicFix() {
		return anisotropicFilteringSupported && DetectAnisotropicGlitch.hasGlitch();
	}

	private static HTMLCanvasElement imageLoadCanvas = null;
	private static CanvasRenderingContext2D imageLoadContext = null;

	@JSBody(params = { "buf", "mime" }, script = "return URL.createObjectURL(new Blob([buf], {type: mime}));")
	private static native String getDataURL(ArrayBuffer buf, String mime);
	
	@JSBody(params = { "url" }, script = "URL.revokeObjectURL(url);")
	private static native void freeDataURL(String url);
	
	public static final EaglerImage loadPNG(byte[] data) {
		return loadPNG0(TeaVMUtils.unwrapArrayBuffer(data));
	}
	
	@JSBody(params = { "cccc", "ennn" }, script = "cccc.imageSmoothingEnabled = ennn;")
	private static native void setImageSmoothingMode(CanvasRenderingContext2D cc, boolean en);
	
	@Async
	private static native EaglerImage loadPNG0(ArrayBuffer data);
	
	private static void loadPNG0(ArrayBuffer data, final AsyncCallback<EaglerImage> ret) {
		final HTMLImageElement toLoad = (HTMLImageElement) doc.createElement("img");
		toLoad.addEventListener("load", new EventListener<Event>() {
			@Override
			public void handleEvent(Event evt) {
				if(imageLoadCanvas == null) {
					imageLoadCanvas = (HTMLCanvasElement) doc.createElement("canvas");
				}
				if(imageLoadCanvas.getWidth() < toLoad.getWidth()) {
					imageLoadCanvas.setWidth(toLoad.getWidth());
				}
				if(imageLoadCanvas.getHeight() < toLoad.getHeight()) {
					imageLoadCanvas.setHeight(toLoad.getHeight());
				}
				if(imageLoadContext == null) {
					imageLoadContext = (CanvasRenderingContext2D) imageLoadCanvas.getContext("2d", youEagler2());
					setImageSmoothingMode(imageLoadContext, false);
				}
				imageLoadContext.clearRect(0, 0, toLoad.getWidth(), toLoad.getHeight());
				imageLoadContext.drawImage(toLoad, 0, 0, toLoad.getWidth(), toLoad.getHeight());
				ImageData pxlsDat = imageLoadContext.getImageData(0, 0, toLoad.getWidth(), toLoad.getHeight());
				Uint8ClampedArray pxls = pxlsDat.getData();
				int totalPixels = pxlsDat.getWidth() * pxlsDat.getHeight();
				freeDataURL(toLoad.getSrc());
				if(pxls.getByteLength() < totalPixels << 2) {
					ret.complete(null);
					return;
				}
				DataView dv = new DataView(pxls.getBuffer());
				int[] pixels = new int[totalPixels];
				for(int i = 0, j; i < pixels.length; ++i) {
					j = dv.getUint32(i << 2, false);
					pixels[i] = ((j >> 8) & 0xFFFFFF) | ((j & 0xFF) << 24);
				}
				ret.complete(new EaglerImage(pixels, pxlsDat.getWidth(), pxlsDat.getHeight(), true));
			}
		});
		toLoad.addEventListener("error", new EventListener<Event>() {
			@Override
			public void handleEvent(Event evt) {
				freeDataURL(toLoad.getSrc());
				ret.complete(null);
			}
		});
		String src = getDataURL(data, "image/png");
		if(src == null) {
			ret.complete(null);
		}else {
			toLoad.setSrc(src);
		}
	}

	private static HTMLVideoElement currentVideo = null;
	private static TextureGL videoTexture = null;
	private static boolean videoIsLoaded = false;
	private static boolean videoTexIsInitialized = false;
	private static int frameRate = 33;
	private static long frameTimer = 0l;
	
	public static final boolean isVideoSupported() {
		return true;
	}
	public static final void loadVideo(String src, boolean autoplay) {
		loadVideo(src, autoplay, null, null);
	}
	public static final void loadVideo(String src, boolean autoplay, String setJavascriptPointer) {
		loadVideo(src, autoplay, setJavascriptPointer, null);
	}
	
	@JSBody(params = { "ptr", "el" }, script = "window[ptr] = el;")
	private static native void setVideoPointer(String ptr, HTMLVideoElement el);
	@JSBody(params = { "ptr", "el" }, script = "window[ptr](el);")
	private static native void callVideoLoadEvent(String ptr, HTMLVideoElement el);
	
	private static MediaElementAudioSourceNode currentVideoAudioSource = null;
	
	private static GainNode currentVideoAudioGain = null;
	private static float currentVideoAudioGainValue = 1.0f;
	
	private static PannerNode currentVideoAudioPanner = null;
	private static float currentVideoAudioX = 0.0f;
	private static float currentVideoAudioY = 0.0f;
	private static float currentVideoAudioZ = 0.0f;
	
	public static final void loadVideo(String src, boolean autoplay, String setJavascriptPointer, final String javascriptOnloadFunction) {
		videoIsLoaded = false;
		videoTexIsInitialized = false;
		if(videoTexture == null) {
			videoTexture = _wglGenTextures();
		}
		if(currentVideo != null) {
			currentVideo.pause();
			currentVideo.setSrc("");
		}
		
		BufferedVideo vid = videosBuffer.get(src);
		
		if(vid != null) {
			currentVideo = vid.videoElement;
			videosBuffer.remove(src);
		}else {
			currentVideo = (HTMLVideoElement) win.getDocument().createElement("video");
			currentVideo.setAttribute("crossorigin", "anonymous");
			currentVideo.setAutoplay(autoplay);
		}
		
		if(setJavascriptPointer != null) {
			setVideoPointer(setJavascriptPointer, currentVideo);
		}
		
		currentVideo.addEventListener("playing", new EventListener<Event>() {
			@Override
			public void handleEvent(Event evt) {
				videoIsLoaded = true;
				if(javascriptOnloadFunction != null) {
					callVideoLoadEvent(javascriptOnloadFunction, currentVideo);
				}
			}
		});
		
		if(vid == null) {
			currentVideo.setControls(false);
			currentVideo.setSrc(src);
		}else {
			if(autoplay) {
				currentVideo.play();
			}
		}
		
		if(currentVideoAudioSource != null) {
			currentVideoAudioSource.disconnect();
		}
		
		currentVideoAudioSource = audioctx.createMediaElementSource(currentVideo);
		
		if(currentVideoAudioGainValue < 0.0f) {
			currentVideoAudioSource.connect(masterVolumeNode);
		}else {
			if(currentVideoAudioGain == null) {
				currentVideoAudioGain = audioctx.createGain();
				currentVideoAudioGain.getGain().setValue(currentVideoAudioGainValue > 1.0f ? 1.0f : currentVideoAudioGainValue);
			}
			
			currentVideoAudioSource.connect(currentVideoAudioGain);
			
			if(currentVideoAudioPanner == null) {
				currentVideoAudioPanner = audioctx.createPanner();
				currentVideoAudioPanner.setRolloffFactor(1f);
				currentVideoAudioPanner.setDistanceModel("linear");
				currentVideoAudioPanner.setPanningModel("HRTF");
				currentVideoAudioPanner.setConeInnerAngle(360f);
				currentVideoAudioPanner.setConeOuterAngle(0f);
				currentVideoAudioPanner.setConeOuterGain(0f);
				currentVideoAudioPanner.setOrientation(0f, 1f, 0f);
				currentVideoAudioPanner.setPosition(currentVideoAudioX, currentVideoAudioY, currentVideoAudioZ);
				currentVideoAudioPanner.setMaxDistance(currentVideoAudioGainValue * 16f + 0.1f);
				currentVideoAudioGain.connect(currentVideoAudioPanner);
				currentVideoAudioPanner.connect(audioctx.getDestination());
			}
		}
		
	}
	
	private static class BufferedVideo {
		
		protected final HTMLVideoElement videoElement;
		protected final String url;
		protected final long requestedTime;
		protected final int ttl;
		
		public BufferedVideo(HTMLVideoElement videoElement, String url, int ttl) {
			this.videoElement = videoElement;
			this.url = url;
			this.requestedTime = steadyTimeMillis();
			this.ttl = ttl;
		}
		
	}
	
	private static final HashMap<String, BufferedVideo> videosBuffer = new HashMap<>();
	
	public static final void bufferVideo(String src, int ttl) {
		if(!videosBuffer.containsKey(src)) {
			HTMLVideoElement video = (HTMLVideoElement) win.getDocument().createElement("video");
			video.setAutoplay(false);
			video.setAttribute("crossorigin", "anonymous");
			video.setPreload("auto");
			video.setControls(false);
			video.setSrc(src);
			videosBuffer.put(src, new BufferedVideo(video, src, ttl));
		}
	}
	
	public static final void unloadVideo() {
		if(videoTexture != null) {
			_wglDeleteTextures(videoTexture);
			videoTexture = null;
		}
		if(currentVideo != null) {
			currentVideo.pause();
			currentVideo.setSrc("");
			currentVideo = null;
		}
		if(currentVideoAudioSource != null) {
			currentVideoAudioSource.disconnect();
		}
	}
	public static final boolean isVideoLoaded() {
		return videoTexture != null && currentVideo != null && videoIsLoaded;
	}
	public static final boolean isVideoPaused() {
		return currentVideo == null || currentVideo.isPaused();
	}
	public static final void setVideoPaused(boolean pause) {
		if(currentVideo != null) {
			if(pause) {
				currentVideo.pause();
			}else {
				currentVideo.play();
			}
		}
	}
	public static final void setVideoLoop(boolean loop) {
		if(currentVideo != null) {
			currentVideo.setLoop(loop);
		}
	}
	public static final void setVideoVolume(float x, float y, float z, float v) {
		currentVideoAudioX = x;
		currentVideoAudioY = y;
		currentVideoAudioZ = z;
		if(v < 0.0f) {
			if(currentVideoAudioGainValue >= 0.0f && currentVideoAudioSource != null) {
				currentVideoAudioSource.disconnect();
				currentVideoAudioSource.connect(masterVolumeNode);
			}
			currentVideoAudioGainValue = v;
		}else {
			if(currentVideoAudioGain != null) {
				currentVideoAudioGain.getGain().setValue(v > 1.0f ? 1.0f : v);
				if(currentVideoAudioGainValue < 0.0f && currentVideoAudioSource != null) {
					currentVideoAudioSource.disconnect();
					currentVideoAudioSource.connect(currentVideoAudioGain);
				}
			}
			currentVideoAudioGainValue = v;
			if(currentVideoAudioPanner != null) {
				currentVideoAudioPanner.setMaxDistance(v * 16f + 0.1f);
				currentVideoAudioPanner.setPosition(x, y, z);
			}
		}
	}
	
	@JSBody(
		params = {"ctx", "target", "internalformat", "format", "type", "video"},
		script = "ctx.texImage2D(target, 0, internalformat, format, type, video);"
	)
	private static native void html5VideoTexImage2D(WebGL2RenderingContext ctx, int target, int internalformat, int format, int type, HTMLVideoElement video);

	@JSBody(
		params = {"ctx", "target", "format", "type", "video"},
		script = "ctx.texSubImage2D(target, 0, 0, 0, format, type, video);"
	)
	private static native void html5VideoTexSubImage2D(WebGL2RenderingContext ctx, int target, int format, int type, HTMLVideoElement video);
	
	public static final void updateVideoTexture() {
		long ms = steadyTimeMillis();
		if(ms - frameTimer < frameRate && videoTexIsInitialized) {
			return;
		}
		frameTimer = ms;
		if(currentVideo != null && videoTexture != null && videoIsLoaded) {
			try {
				_wglBindTexture(_wGL_TEXTURE_2D, videoTexture);
				if(videoTexIsInitialized) {
					html5VideoTexSubImage2D(webgl, _wGL_TEXTURE_2D, _wGL_RGBA, _wGL_UNSIGNED_BYTE, currentVideo);
				}else {
					html5VideoTexImage2D(webgl, _wGL_TEXTURE_2D, _wGL_RGBA, _wGL_RGBA, _wGL_UNSIGNED_BYTE, currentVideo);
					_wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_WRAP_S, _wGL_CLAMP);
					_wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_WRAP_T, _wGL_CLAMP);
					_wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_MIN_FILTER, _wGL_LINEAR);
					_wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_MAG_FILTER, _wGL_LINEAR);
					videoTexIsInitialized = true;
				}
			}catch(Throwable t) {
				// rip
			}
		}
	}
	public static final void bindVideoTexture() {
		if(videoTexture != null) {
			_wglBindTexture(_wGL_TEXTURE_2D, videoTexture);
		}
	}
	public static final int getVideoWidth() {
		if(currentVideo != null && videoIsLoaded) {
			return currentVideo.getWidth();
		}else {
			return -1;
		}
	}
	public static final int getVideoHeight() {
		if(currentVideo != null && videoIsLoaded) {
			return currentVideo.getHeight();
		}else {
			return -1;
		}
	}
	public static final float getVideoCurrentTime() {
		if(currentVideo != null && videoIsLoaded) {
			return (float) currentVideo.getCurrentTime();
		}else {
			return -1.0f;
		}
	}
	public static final void setVideoCurrentTime(float seconds) {
		if(currentVideo != null && videoIsLoaded) {
			currentVideo.setCurrentTime(seconds);
		}
	}
	public static final float getVideoDuration() {
		if(currentVideo != null && videoIsLoaded) {
			return (float) currentVideo.getDuration();
		}else {
			return -1.0f;
		}
	}

	public static final int VIDEO_ERR_NONE = -1;
	public static final int VIDEO_ERR_ABORTED = 1;
	public static final int VIDEO_ERR_NETWORK = 2;
	public static final int VIDEO_ERR_DECODE = 3;
	public static final int VIDEO_ERR_SRC_NOT_SUPPORTED = 4;

	public static final int getVideoError() {
		if(currentVideo != null && videoIsLoaded) {
			MediaError err = currentVideo.getError();
			if(err != null) {
				return err.getCode();
			}else {
				return -1;
			}
		}else {
			return -1;
		}
	}
	
	public static final void setVideoFrameRate(float fps) {
		frameRate = (int)(1000.0f / fps);
		if(frameRate < 1) {
			frameRate = 1;
		}
	}

	private static HTMLImageElement currentImage = null;
	private static TextureGL imageTexture = null;
	private static boolean imageIsLoaded = false;
	private static boolean imageTexIsInitialized = false;
	private static int imageFrameRate = 33;
	private static long imageFrameTimer = 0l;

	public static final boolean isImageSupported() {
		return true;
	}
	public static final void loadImage(String src) {
		loadImage(src, null);
	}
	public static final void loadImage(String src, String setJavascriptPointer) {
		loadImage(src, setJavascriptPointer, null);
	}

	@JSBody(params = { "ptr", "el" }, script = "window[ptr] = el;")
	private static native void setImagePointer(String ptr, HTMLImageElement el);
	@JSBody(params = { "ptr", "el" }, script = "window[ptr](el);")
	private static native void callImageLoadEvent(String ptr, HTMLImageElement el);

	public static final void loadImage(String src, String setJavascriptPointer, final String javascriptOnloadFunction) {
		imageIsLoaded = false;
		imageTexIsInitialized = false;
		if(imageTexture == null) {
			imageTexture = _wglGenTextures();
		}
		if(currentImage != null) {
			currentImage.setSrc("");
		}

		BufferedImageElem img = imagesBuffer.get(src);

		if(img != null) {
			currentImage = img.imageElement;
			imagesBuffer.remove(src);
		}else {
			currentImage = (HTMLImageElement) win.getDocument().createElement("img");
			currentImage.setAttribute("crossorigin", "anonymous");
		}

		if(setJavascriptPointer != null) {
			setImagePointer(setJavascriptPointer, currentImage);
		}

		currentImage.addEventListener("load", new EventListener<Event>() {
			@Override
			public void handleEvent(Event evt) {
				imageIsLoaded = true;
				if(javascriptOnloadFunction != null) {
					callImageLoadEvent(javascriptOnloadFunction, currentImage);
				}
			}
		});

		if(img == null) {
			currentImage.setSrc(src);
		}
	}

	private static class BufferedImageElem {

		protected final HTMLImageElement imageElement;
		protected final String url;
		protected final long requestedTime;
		protected final int ttl;

		public BufferedImageElem(HTMLImageElement imageElement, String url, int ttl) {
			this.imageElement = imageElement;
			this.url = url;
			this.requestedTime = steadyTimeMillis();
			this.ttl = ttl;
		}

	}

	private static final HashMap<String, BufferedImageElem> imagesBuffer = new HashMap<>();

	public static final void bufferImage(String src, int ttl) {
		if(!imagesBuffer.containsKey(src)) {
			HTMLImageElement image = (HTMLImageElement) win.getDocument().createElement("img");
			image.setAttribute("crossorigin", "anonymous");
			image.setSrc(src);
			imagesBuffer.put(src, new BufferedImageElem(image, src, ttl));
		}
	}

	public static final void unloadImage() {
		if(imageTexture != null) {
			_wglDeleteTextures(imageTexture);
			imageTexture = null;
		}
		if(currentImage != null) {
			currentImage.setSrc("");
			currentImage = null;
		}
	}
	public static final boolean isImageLoaded() {
		return imageTexture != null && currentImage != null && imageIsLoaded;
	}

	@JSBody(
			params = {"ctx", "target", "internalformat", "format", "type", "image"},
			script = "ctx.texImage2D(target, 0, internalformat, format, type, image);"
	)
	private static native void html5ImageTexImage2D(WebGL2RenderingContext ctx, int target, int internalformat, int format, int type, HTMLImageElement image);

	@JSBody(
			params = {"ctx", "target", "format", "type", "image"},
			script = "ctx.texSubImage2D(target, 0, 0, 0, format, type, image);"
	)
	private static native void html5ImageTexSubImage2D(WebGL2RenderingContext ctx, int target, int format, int type, HTMLImageElement image);

	public static final void updateImageTexture() {
		long ms = steadyTimeMillis();
		if(ms - imageFrameTimer < imageFrameRate && imageTexIsInitialized) {
			return;
		}
		imageFrameTimer = ms;
		if(currentImage != null && imageTexture != null && imageIsLoaded) {
			try {
				_wglBindTexture(_wGL_TEXTURE_2D, imageTexture);
				if(imageTexIsInitialized) {
					html5ImageTexSubImage2D(webgl, _wGL_TEXTURE_2D, _wGL_RGBA, _wGL_UNSIGNED_BYTE, currentImage);
				}else {
					html5ImageTexImage2D(webgl, _wGL_TEXTURE_2D, _wGL_RGBA, _wGL_RGBA, _wGL_UNSIGNED_BYTE, currentImage);
					_wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_WRAP_S, _wGL_CLAMP);
					_wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_WRAP_T, _wGL_CLAMP);
					_wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_MIN_FILTER, _wGL_LINEAR);
					_wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_MAG_FILTER, _wGL_LINEAR);
					imageTexIsInitialized = true;
				}
			}catch(Throwable t) {
				// rip
			}
		}
	}
	public static final void bindImageTexture() {
		if(imageTexture != null) {
			_wglBindTexture(_wGL_TEXTURE_2D, imageTexture);
		}
	}
	public static final int getImageWidth() {
		if(currentImage != null && imageIsLoaded) {
			return currentImage.getWidth();
		}else {
			return -1;
		}
	}
	public static final int getImageHeight() {
		if(currentImage != null && imageIsLoaded) {
			return currentImage.getHeight();
		}else {
			return -1;
		}
	}

	public static final void setImageFrameRate(float fps) {
		frameRate = (int)(1000.0f / fps);
		if(frameRate < 1) {
			frameRate = 1;
		}
	}
	
	private static MouseEvent currentEvent = null;
	private static KeyboardEvent currentEventK = null;
	private static boolean[] buttonStates = new boolean[8];
	private static boolean[] keyStates = new boolean[256];
	public static final boolean mouseNext() {
		currentEvent = null;
		return !mouseEvents.isEmpty() && (currentEvent = mouseEvents.remove(0)) != null;
	}
	public static final int mouseGetEventButton() {
		if(currentEvent == null || currentEvent.getType().equals(MouseEvent.MOUSEMOVE)) return -1;
		int b = currentEvent.getButton();
		return b == 1 ? 2 : (b == 2 ? 1 : b);
	}
	public static final boolean mouseGetEventButtonState() {
		return currentEvent == null ? false : currentEvent.getType().equals(MouseEvent.MOUSEDOWN);
	}
	public static final boolean mouseIsButtonDown(int p1) {
		return buttonStates[p1];
	}
	public static final int mouseGetEventDWheel() {
		return ("wheel".equals(currentEvent.getType())) ? (((WheelEvent)currentEvent).getDeltaY() == 0.0D ? 0 : (((WheelEvent)currentEvent).getDeltaY() > 0.0D ? -1 : 1)) : 0;
	}
	public static final void mouseSetCursorPosition(int x, int y) {
		
	}
	private static long mouseUngrabTimer = 0l;
	private static int mouseUngrabTimeout = 0;
	public static final void mouseSetGrabbed(boolean grabbed) {
		if(grabbed) {
			canvas.requestPointerLock();
			long t = steadyTimeMillis();
			if(mouseUngrabTimeout != 0) Window.clearTimeout(mouseUngrabTimeout);
			mouseUngrabTimeout = 0;
			if(t - mouseUngrabTimer < 3000l) {
				mouseUngrabTimeout = Window.setTimeout(new TimerHandler() {
					@Override
					public void onTimer() {
						canvas.requestPointerLock();
					}
				}, 3000 - (int)(t - mouseUngrabTimer));
			}
		}else {
			mouseUngrabTimer = steadyTimeMillis();
			if(mouseUngrabTimeout != 0) Window.clearTimeout(mouseUngrabTimeout);
			mouseUngrabTimeout = 0;
			doc.exitPointerLock();
		}
	}
	public static final int mouseGetDX() {
		double dx = mouseDX;
		mouseDX = 0.0D;
		return (int)dx;
	}
	public static final int mouseGetDY() {
		double dy = mouseDY;
		mouseDY = 0.0D;
		return (int)dy;
	}
	public static final int mouseGetX() {
		return mouseX;
	}
	public static final int mouseGetY() {
		return mouseY;
	}
	public static final int mouseGetEventX() {
		return currentEvent == null ? -1 : (int)(currentEvent.getClientX() * win.getDevicePixelRatio());
	}
	public static final int mouseGetEventY() {
		return currentEvent == null ? -1 : (int)((canvas.getClientHeight() - currentEvent.getClientY()) * win.getDevicePixelRatio());
	}
	public static final boolean keysNext() {
		if(unpressCTRL) { //un-press ctrl after copy/paste permission
			keyEvents.clear();
			currentEventK = null;
			keyStates[29] = false;
			keyStates[157] = false;
			keyStates[28] = false;
			keyStates[219] = false;
			keyStates[220] = false;
			unpressCTRL = false;
			return false;
		}
		currentEventK = null;
		return !keyEvents.isEmpty() && (currentEventK = keyEvents.remove(0)) != null;
	}
	public static final int getEventKey() {
		return currentEventK == null ? -1 : remapKey(getWhich(currentEventK));
	}
	public static final char getEventChar() {
		if(currentEventK == null) return '\0';
		String s = currentEventK.getKey();
		return currentEventK == null ? ' ' : (char) (s.length() > 1 ? '\0' : s.charAt(0));
	}
	public static final boolean getEventKeyState() {
		return currentEventK == null? false : !currentEventK.getType().equals("keyup");
	}
	public static final boolean isKeyDown(int p1) {
		if(unpressCTRL) { //un-press ctrl after copy/paste permission
			keyStates[28] = false;
			keyStates[29] = false;
			keyStates[157] = false;
			keyStates[219] = false;
			keyStates[220] = false;
		}
		return keyStates[p1];
	}
	public static final String getKeyName(int p1) {
		return (p1 >= 0 && p1 < 256) ? LWJGLKeyNames[p1] : "null";
	}
	public static final void setFullscreen(boolean p1) {
		Window.alert("use F11 to enter fullscreen");
	}
	public static final boolean shouldShutdown() {
		return false;
	}
	public static final boolean isVSyncSupported() {
		return vsyncSupport;
	}
	@JSBody(params = { "doc" }, script = "return (typeof doc.visibilityState !== \"string\") || (doc.visibilityState === \"visible\");")
	private static native boolean getVisibilityState(JSObject doc);
	private static final long[] syncTimer = new long[1];
	public static final void updateDisplay(int fpsLimit, boolean vsync) {
		double r = win.getDevicePixelRatio();
		int w = parent.getClientWidth();
		int h = parent.getClientHeight();
		int w2 = (int)(w * r);
		int h2 = (int)(h * r);
		if(canvas.getWidth() != w2) {
			canvas.setWidth(w2);
		}
		if(canvas.getHeight() != h2) {
			canvas.setHeight(h2);
		}
		webgl.bindFramebuffer(FRAMEBUFFER, null);
		webgl.bindFramebuffer(READ_FRAMEBUFFER, backBuffer.obj);
		webgl.bindFramebuffer(DRAW_FRAMEBUFFER, null);
		webgl.blitFramebuffer(0, 0, backBufferWidth, backBufferHeight, 0, 0, w2, h2, COLOR_BUFFER_BIT, NEAREST);
		webgl.bindFramebuffer(FRAMEBUFFER, backBuffer.obj);
		resizeBackBuffer(w2, h2);

		if(getVisibilityState(win.getDocument())) {
			if(vsyncSupport && vsync) {
				syncTimer[0] = 0l;
				asyncRequestAnimationFrame();
			}else {
				if(fpsLimit <= 0) {
					syncTimer[0] = 0l;
					swapDelayTeaVM();
				}else {
					if(!EaglerAdapterGL30.sync(fpsLimit, syncTimer)) {
						swapDelayTeaVM();
					}
				}
			}
		}else {
			syncTimer[0] = 0l;
			sleep(50);
		}
	}
	@Async
	private static native void asyncRequestAnimationFrame();
	private static void asyncRequestAnimationFrame(AsyncCallback<Void> cb) {
		if(vsyncTimeout != -1) {
			cb.error(new IllegalStateException("Already waiting for vsync!"));
			return;
		}
		final boolean[] hasTimedOut = new boolean[] { false };
		final int[] timeout = new int[] { -1 };
		Window.requestAnimationFrame((d) -> {
			if(!hasTimedOut[0]) {
				hasTimedOut[0] = true;
				if(vsyncTimeout != -1) {
					if(vsyncTimeout == timeout[0]) {
						try {
							Window.clearTimeout(vsyncTimeout);
						}catch(Throwable t) {
						}
						vsyncTimeout = -1;
					}
					cb.complete(null);
				}
			}
		});
		vsyncTimeout = timeout[0] = Window.setTimeout(() -> {
			if(!hasTimedOut[0]) {
				hasTimedOut[0] = true;
				if(vsyncTimeout != -1) {
					vsyncTimeout = -1;
					cb.complete(null);
				}
			}
		}, 50);
	}
	private static final void swapDelayTeaVM() {
		if(!useDelayOnSwap && immediateContinueChannel != null) {
			immediateContinueTeaVM0();
		}else {
			sleep(0);
		}
	}
	public static final void immediateContinue() {
		if(immediateContinueChannel != null) {
			immediateContinueTeaVM0();
		}else {
			sleep(0);
		}
	}
	public static final boolean immediateContinueSupported() {
		return immediateContinueChannel != null;
	}
	private static final JSString emptyJSString = JSString.valueOf("");
	@Async
	private static native void immediateContinueTeaVM0();
	private static void immediateContinueTeaVM0(final AsyncCallback<Void> cb) {
		if(currentMsgChannelContinueHack != null) {
			cb.error(new IllegalStateException("Main thread is already waiting for an immediate continue callback!"));
			return;
		}
		currentMsgChannelContinueHack = () -> {
			cb.complete(null);
		};
		try {
			immediateContinueChannel.getPort2().postMessage(emptyJSString);
		}catch(Throwable t) {
			currentMsgChannelContinueHack = null;
			System.err.println("Caught error posting immediate continue, using setTimeout instead");
			Window.setTimeout(() -> cb.complete(null), 0);
		}
	}
	private static final int IMMEDIATE_CONT_SUPPORTED = 0;
	private static final int IMMEDIATE_CONT_FAILED_NOT_ASYNC = 1;
	private static final int IMMEDIATE_CONT_FAILED_NOT_CONT = 2;
	private static final int IMMEDIATE_CONT_FAILED_EXCEPTIONS = 3;
	private static void checkImmediateContinueSupport() {
		immediateContinueChannel = null;
		int stat = checkImmediateContinueSupport0();
		if(stat == IMMEDIATE_CONT_SUPPORTED) {
			return;
		}else if(stat == IMMEDIATE_CONT_FAILED_NOT_ASYNC) {
			System.err.println("MessageChannel fast immediate continue hack is incompatible with this browser due to actually continuing immediately!");
		}else if(stat == IMMEDIATE_CONT_FAILED_NOT_CONT) {
			System.err.println("MessageChannel fast immediate continue hack is incompatible with this browser due to startup check failing!");
		}else if(stat == IMMEDIATE_CONT_FAILED_EXCEPTIONS) {
			System.err.println("MessageChannel fast immediate continue hack is incompatible with this browser due to exceptions!");
		}
		immediateContinueChannel = null;
	}
	private static int checkImmediateContinueSupport0() {
		try {
			if(!MessageChannel.supported()) {
				return IMMEDIATE_CONT_SUPPORTED;
			}
			immediateContinueChannel = new MessageChannel();
			immediateContinueChannel.getPort1().addEventListener("message", new EventListener<MessageEvent>() {
				@Override
				public void handleEvent(MessageEvent evt) {
					Runnable toRun = currentMsgChannelContinueHack;
					currentMsgChannelContinueHack = null;
					if(toRun != null) {
						toRun.run();
					}
				}
			});
			immediateContinueChannel.getPort1().start();
			immediateContinueChannel.getPort2().start();
			final boolean[] checkMe = new boolean[1];
			checkMe[0] = false;
			currentMsgChannelContinueHack = () -> {
				checkMe[0] = true;
			};
			immediateContinueChannel.getPort2().postMessage(emptyJSString);
			if(checkMe[0]) {
				currentMsgChannelContinueHack = null;
				if(immediateContinueChannel != null) {
					safeShutdownChannel(immediateContinueChannel);
				}
				immediateContinueChannel = null;
				return IMMEDIATE_CONT_FAILED_NOT_ASYNC;
			}
			sleep(10);
			currentMsgChannelContinueHack = null;
			if(!checkMe[0]) {
				if(immediateContinueChannel != null) {
					safeShutdownChannel(immediateContinueChannel);
				}
				immediateContinueChannel = null;
				return IMMEDIATE_CONT_FAILED_NOT_CONT;
			}else {
				return IMMEDIATE_CONT_SUPPORTED;
			}
		}catch(Throwable t) {
			currentMsgChannelContinueHack = null;
			if(immediateContinueChannel != null) {
				safeShutdownChannel(immediateContinueChannel);
			}
			immediateContinueChannel = null;
			return IMMEDIATE_CONT_FAILED_EXCEPTIONS;
		}
	}
	private static void safeShutdownChannel(MessageChannel chan) {
		try {
			chan.getPort1().close();
		}catch(Throwable tt) {
		}
		try {
			chan.getPort2().close();
		}catch(Throwable tt) {
		}
	}
	public static final void setupBackBuffer() {
		backBuffer = _wglCreateFramebuffer();
		_wglBindFramebuffer(_wGL_FRAMEBUFFER, null);
		backBufferColor = _wglCreateRenderBuffer();
		_wglBindRenderbuffer(backBufferColor);
		_wglFramebufferRenderbuffer(_wGL_COLOR_ATTACHMENT0, backBufferColor);
		backBufferDepth = _wglCreateRenderBuffer();
		_wglBindRenderbuffer(backBufferDepth);
		_wglFramebufferRenderbuffer(_wGL_DEPTH_ATTACHMENT, backBufferDepth);
	}
	private static int backBufferWidth = -1;
	private static int backBufferHeight = -1;
	public static final void resizeBackBuffer(int w, int h) {
		if(w != backBufferWidth || h != backBufferHeight) {
			_wglBindRenderbuffer(backBufferColor);
			_wglRenderbufferStorage(_wGL_RGBA8, w, h);
			_wglBindRenderbuffer(backBufferDepth);
			_wglRenderbufferStorage(_wGL_DEPTH_COMPONENT32F, w, h);
			backBufferWidth = w;
			backBufferHeight = h;
		}
	}
	public static final float getContentScaling() {
		 return (float)win.getDevicePixelRatio();
	}
	public static final void enableRepeatEvents(boolean b) {
		enableRepeatEvents = b;
	}
	
	@JSBody(params = { }, script = "return document.pointerLockElement != null;")
	public static native boolean isPointerLocked();
	
	private static boolean pointerLockFlag = false;
	
	public static final boolean isFocused() {
		boolean yee = isPointerLocked();
		boolean dee = pointerLockFlag;
		pointerLockFlag = yee;
		if(!dee && yee) {
			mouseDX = 0.0D;
			mouseDY = 0.0D;
		}
		return isWindowFocused && !(dee && !yee);
	}
	public static final int getScreenWidth() {
		return win.getScreen().getAvailWidth();
	}
	public static final int getScreenHeight() {
		return win.getScreen().getAvailHeight();
	}
	public static final int getCanvasWidth() {
		return canvas.getWidth();
	}
	public static final int getCanvasHeight() {
		return canvas.getHeight();
	}
	public static final void setDisplaySize(int x, int y) {
		
	}
	
	private static final DateFormat dateFormatSS = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss");
	public static final void saveScreenshot() {
		webgl.finish();
		HTMLCanvasElement retardedCanvas = (HTMLCanvasElement)doc.createElement("canvas");
		retardedCanvas.setWidth(canvas.getWidth());
		retardedCanvas.setHeight(canvas.getHeight());
		CanvasRenderingContext2D cc = (CanvasRenderingContext2D)retardedCanvas.getContext("2d", youEagler2());
		setImageSmoothingMode(cc, false);
		cc.setFillStyle("black");
		cc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
		cc.drawImage(canvas, 0, 0, canvas.getWidth(), canvas.getHeight());
		saveScreenshot("screenshot_" + dateFormatSS.format(new Date()).toString() + ".png", retardedCanvas);
	}
	
	@JSBody(params = { "name", "cvs" }, script = "var a=document.createElement(\"a\");a.href=cvs.toDataURL(\"image/png\");a.download=name;a.click();")
	private static native void saveScreenshot(String name, HTMLCanvasElement cvs);

	public static enum RateLimit {
		NONE, FAILED, BLOCKED, FAILED_POSSIBLY_LOCKED, LOCKED, NOW_LOCKED;
	}

	private static final Set<String> rateLimitedAddresses = new HashSet<>();
	private static final Set<String> blockedAddresses = new HashSet<>();
	
	private static WebSocket sock = null;
	private static boolean sockIsConnecting = false;
	private static boolean sockIsConnected = false;
	private static boolean sockIsAlive = false;
	private static LinkedList<byte[]> readPackets = new LinkedList<>();
	private static RateLimit rateLimitStatus = null;
	private static String currentSockURI = null;
	
	public static final RateLimit getRateLimitStatus() {
		RateLimit l = rateLimitStatus;
		rateLimitStatus = null;
		return l;
	}
	public static final void logRateLimit(String addr, RateLimit l) {
		if(l == RateLimit.BLOCKED) {
			blockedAddresses.add(addr);
		}else {
			rateLimitedAddresses.add(addr);
		}
	}
	public static final RateLimit checkRateLimitHistory(String addr) {
		if(blockedAddresses.contains(addr)) {
			return RateLimit.LOCKED;
		}else if(rateLimitedAddresses.contains(addr)) {
			return RateLimit.BLOCKED;
		}else {
			return RateLimit.NONE;
		}
	}
	
	@Async
	public static native String connectWebSocket(String sockURI);
	
	private static void connectWebSocket(String sockURI, final AsyncCallback<String> cb) {
		sockIsConnecting = true;
		sockIsConnected = false;
		sockIsAlive = false;
		rateLimitStatus = null;
		currentSockURI = sockURI;
		try {
			sock = new WebSocket(sockURI);
		} catch(Throwable t) {
			sockIsConnecting = false;
			sockIsAlive = false;
			return;
		}
		sock.setBinaryType("arraybuffer");
		sock.addEventListener("open", new EventListener<Event>() {
			@Override
			public void handleEvent(Event evt) {
				sockIsConnecting = false;
				sockIsAlive = false;
				sockIsConnected = true;
				readPackets.clear();
				cb.complete("okay");
			}
		});
		sock.addEventListener("close", new EventListener<CloseEvent>() {
			@Override
			public void handleEvent(CloseEvent evt) {
				sock = null;
				if(sockIsConnecting) {
					if(rateLimitStatus == null) {
						if(blockedAddresses.contains(currentSockURI)) {
							rateLimitStatus = RateLimit.LOCKED;
						}else if(rateLimitedAddresses.contains(currentSockURI)) {
							rateLimitStatus = RateLimit.FAILED_POSSIBLY_LOCKED;
						}else {
							rateLimitStatus = RateLimit.FAILED;
						}
					}
				}else if(!sockIsAlive) {
					if(rateLimitStatus == null) {
						if(blockedAddresses.contains(currentSockURI)) {
							rateLimitStatus = RateLimit.LOCKED;
						}else if(rateLimitedAddresses.contains(currentSockURI)) {
							rateLimitStatus = RateLimit.BLOCKED;
						}
					}
				}
				boolean b = sockIsConnecting;
				sockIsConnecting = false;
				sockIsConnected = false;
				sockIsAlive = false;
				if(b) cb.complete("fail");
			}
		});
		sock.addEventListener("message", new EventListener<MessageEvent>() {
			@Override
			public void handleEvent(MessageEvent evt) {
				sockIsAlive = true;
				if(isString(evt.getData())) {
					String stat = evt.getDataAsString();
					if(stat.equalsIgnoreCase("BLOCKED")) {
						if(rateLimitStatus == null) {
							rateLimitStatus = RateLimit.BLOCKED;
						}
						rateLimitedAddresses.add(currentSockURI);
					}else if(stat.equalsIgnoreCase("LOCKED")) {
						if(rateLimitStatus == null) {
							rateLimitStatus = RateLimit.NOW_LOCKED;
						}
						rateLimitedAddresses.add(currentSockURI);
						blockedAddresses.add(currentSockURI);
					}
					sockIsConnecting = false;
					sockIsConnected = false;
					sock.close();
					return;
				}
				readPackets.add(TeaVMUtils.wrapByteArrayBuffer(evt.getDataAsArray()));
			}
		});
	}
	
	public static final boolean startConnection(String uri) {
		String res = connectWebSocket(uri);
		return !"fail".equals(res);
	}
	public static final void endConnection() {
		if(sock == null || sock.getReadyState() == 3) {
			sockIsConnecting = false;
		}
		if(sock != null && !sockIsConnecting) sock.close();
	}
	public static final boolean connectionOpen() {
		if(IntegratedServer.doesChannelExist(EaglerProfile.username) && IntegratedServer.isWorldRunning()) {
			return true;
		}
		if(!EaglerAdapter.clientLANClosed()) {
			return true;
		}
		if(sock == null || sock.getReadyState() == 3) {
			sockIsConnecting = false;
		}
		return sock != null && !sockIsConnecting && sock.getReadyState() != 3;
	}
	@JSBody(params = { "sock", "buffer" }, script = "sock.send(buffer);")
	private static native void nativeBinarySend(WebSocket sock, ArrayBuffer buffer);
	public static final void writePacket(byte[] packet) {
		if(sock != null && !sockIsConnecting) {
			nativeBinarySend(sock, TeaVMUtils.unwrapArrayBuffer(packet));
		}
	}
	public static final byte[] readPacket() {
		if(!readPackets.isEmpty()) {
			return readPackets.remove(0);
		}else {
			return null;
		}
	}
	public static final byte[] loadLocalStorage(String key) {
		try {
			Storage strg = win.getLocalStorage();
			if(strg != null) {
				String s = strg.getItem("_eaglercraft."+key);
				if(s != null) {
					return Base64.decodeBase64(s);
				}else {
					return null;
				}
			}else {
				return null;
			}
		}catch(Throwable t) {
			return null;
		}
	}
	public static final void saveLocalStorage(String key, byte[] data) {
		try {
			Storage strg = win.getLocalStorage();
			if(strg != null) {
				strg.setItem("_eaglercraft."+key, Base64.encodeBase64String(data));
			}
		}catch(Throwable t) {
		}
	}
	public static final void openLink(String url) {
		SelfDefence.openWindowIgnore(url, "_blank");
	}
	public static final void redirectTo(String url) {
		win.getLocation().setFullURL(url);
	}
	
	@JSBody(params = { }, script = "window.onbeforeunload = function(){javaMethods.get('net.lax1dude.eaglercraft.adapter.EaglerAdapterImpl2.onWindowUnload()V').invoke();return false;};")
	private static native void onBeforeCloseRegister();

	public static final void openFileChooser(String ext, String mime) {
		fileChooser.openFileChooser(ext, mime);
	}
	
	public static final boolean getFileChooserResultAvailable() {
		return fileChooser.getFileChooserResult() != null;
	}
	
	public static final byte[] getFileChooserResult() {
		ArrayBuffer b = getFileChooserResult0();
		if(b == null) return null;
		return TeaVMUtils.wrapByteArrayBuffer(b);
	}
	
	public static final void clearFileChooserResult() {
		getFileChooserResult0();
	}

	private static final ArrayBuffer getFileChooserResult0() {
		ArrayBuffer ret = fileChooser.getFileChooserResult();
		fileChooser.setFileChooserResult(null);
		return ret;
	}

	public static final String getFileChooserResultName() {
		return fileChooser.getFileChooserResultName();
	}

	public static final void setListenerPos(float x, float y, float z, float vx, float vy, float vz, float pitch, float yaw) {
		float var2 = MathHelper.cos(-yaw * 0.017453292F);
		float var3 = MathHelper.sin(-yaw * 0.017453292F);
		float var4 = -MathHelper.cos(pitch * 0.017453292F);
		float var5 = MathHelper.sin(pitch * 0.017453292F);
		AudioListener l = audioctx.getListener();
		l.setPosition(x, y, z);
		l.setOrientation(-var3 * var4, -var5, -var2 * var4, 0.0f, 1.0f, 0.0f);
	}
	
	private static int playbackId = 0;
	private static int audioElementId = 0;
	private static final HashMap<String,AudioBufferX> loadedSoundFiles = new HashMap<>();
	private static AudioContext audioctx = null;
	private static GainNode masterVolumeNode = null;
	private static GainNode musicVolumeNode = null;
	private static float playbackOffsetDelay = 0.03f;
	
	public static final void setPlaybackOffsetDelay(float f) {
		playbackOffsetDelay = f;
	}

	private static final void setGainlessAudioVolume(float oldGain, float f, boolean music) {
		if (f != oldGain) {
			for (AudioSourceNodeX a : activeSoundEffects.values()) {
				if (a.music == music && a instanceof MediaElementAudioSourceNodeX && a.gain == null) {
					HTMLAudioElement aud = ((MediaElementAudioSourceNodeX) a).audio;
					float newVolume = 0.5F;
					if (oldGain == 0) {
						aud.setMuted(false);
						newVolume = f * aud.getVolume();
					} else if (f == 0) {
						aud.setMuted(true);
						newVolume = aud.getVolume() / oldGain;
					} else {
						newVolume = f * aud.getVolume() / oldGain;
					}
					aud.setVolume(newVolume > 1.0f ? 1.0f : newVolume);
				}
			}
		}
	}

	public static final void setMasterVolume(float f) {
		setGainlessAudioVolume(masterVolumeNode.getGain().getValue(), f, false);
		masterVolumeNode.getGain().setValue(f);
	}

	public static final void setMusicVolume(float f) {
		setGainlessAudioVolume(musicVolumeNode.getGain().getValue(), f, true);
		musicVolumeNode.getGain().setValue(f);
	}
	
	@Async
	public static native AudioBuffer decodeAudioAsync(ArrayBuffer buffer);
	
	private static void decodeAudioAsync(ArrayBuffer buffer, final AsyncCallback<AudioBuffer> cb) {
		audioctx.decodeAudioData(buffer, new DecodeSuccessCallback() {
			@Override
			public void onSuccess(AudioBuffer decodedData) {
				cb.complete(decodedData);
			}
		}, new DecodeErrorCallback() {
			@Override
			public void onError(JSObject error) {
				cb.complete(null);
			}
		});
	}
	
	private static final HashMap<Integer,AudioSourceNodeX> activeSoundEffects = new HashMap<>();

	private static class AudioBufferX {
		private final AudioBuffer buffer;
		private AudioBufferX(AudioBuffer buffer) {
			this.buffer = buffer;
		}
	}

	private static class AudioSourceNodeX {
		private final PannerNode panner;
		private final GainNode gain;
		private final boolean music;
		private AudioSourceNodeX(PannerNode panner, GainNode gain, boolean music) {
			this.panner = panner;
			this.gain = gain;
			this.music = music;
		}
	}

	private static class AudioBufferSourceNodeX extends AudioSourceNodeX {
		private final AudioBufferSourceNode source;
		private AudioBufferSourceNodeX(AudioBufferSourceNode source, PannerNode panner, GainNode gain, boolean music) {
			super(panner, gain, music);
			this.source = source;
		}
	}

	private static class MediaElementAudioSourceNodeX extends AudioSourceNodeX {
		private final MediaElementAudioSourceNode source;
		private final HTMLAudioElement audio;
		private MediaElementAudioSourceNodeX(MediaElementAudioSourceNode source, HTMLAudioElement audio, PannerNode panner, GainNode gain, boolean music) {
			super(panner, gain, music);
			this.source = source;
			this.audio = audio;
		}
	}

	@JSBody(params = { "playing", "volume" }, script = "window.dispatchEvent(new CustomEvent('eagTitleMusic', { detail: { playing: playing, volume: volume } }));return;")
	public static native void fireTitleMusicEvent(boolean playing, float volume);
	
	private static final AudioBuffer getBufferFor(String fileName) {
		AudioBufferX ret = loadedSoundFiles.get(fileName);
		if(ret == null) {
			byte[] file = loadResourceBytes(fileName);
			if(file == null) return null;
			Uint8Array buf = new Uint8Array(file.length);
			buf.set(file);
			ret = new AudioBufferX(decodeAudioAsync(buf.getBuffer()));
			loadedSoundFiles.put(fileName, ret);
		}
		return ret.buffer;
	}
	public static final int beginPlayback(String fileName, float x, float y, float z, float volume, float pitch) {
		return beginPlayback(fileName, x, y, z, volume, pitch, false);
	}
	public static final int beginPlayback(String fileNamePre, float x, float y, float z, float volume, float pitch, boolean music) {
		if(fileNamePre.startsWith("/")) fileNamePre = fileNamePre.substring(1);
		String fileName = AssetRepository.fileNameOverrides.getOrDefault(fileNamePre, fileNamePre);
		AudioNode s;
		HTMLAudioElement audioElement = null;
		String lowerFileName = fileName.toLowerCase();
		boolean usingUrl = AssetRepository.fileNameOverrides.containsKey(fileNamePre) || lowerFileName.startsWith("http://") || lowerFileName.startsWith("https://") || lowerFileName.startsWith("blob:") || lowerFileName.startsWith("data:");
		if (usingUrl) {
			audioElement = (HTMLAudioElement) win.getDocument().createElement("audio");
			audioElement.setAutoplay(true);
			audioElement.setCrossOrigin("anonymous");
			audioElement.setSrc(fileName);
			s = audioctx.createMediaElementSource(audioElement);
			audioElement.setPlaybackRate(pitch);
		} else {
			AudioBuffer b = getBufferFor(fileName);
			if(b == null) return -1;
			s = audioctx.createBufferSource();
			((AudioBufferSourceNode) s).setBuffer(b);
			((AudioBufferSourceNode) s).getPlaybackRate().setValue(pitch);
		}
		ChannelMergerNode c = audioctx.createChannelMerger(1);
		PannerNode p = audioctx.createPanner();
		p.setPosition(x, y, z);
		p.setMaxDistance(volume * 16f + 0.1f);
		p.setRolloffFactor(1f);
		//p.setVelocity(0f, 0f, 0f);
		p.setDistanceModel("linear");
		p.setPanningModel("HRTF");
		p.setConeInnerAngle(360f);
		p.setConeOuterAngle(0f);
		p.setConeOuterGain(0f);
		p.setOrientation(0f, 1f, 0f);
		GainNode g = audioctx.createGain();
		g.getGain().setValue(volume > 1.0f ? 1.0f : volume);
		s.connect(c);
		c.connect(g);
		g.connect(p);
		p.connect(music ? musicVolumeNode : masterVolumeNode);
		if (!usingUrl) {
			((AudioBufferSourceNode) s).start(0.0d, playbackOffsetDelay);
		}
		final int theId = ++playbackId;
		if (usingUrl) {
			activeSoundEffects.put(theId, new MediaElementAudioSourceNodeX((MediaElementAudioSourceNode) s, audioElement, p, g, music));
			audioElement.addEventListener("canplay", new EventListener<Event>() {
				@Override
				public void handleEvent(Event evt) {
					if (activeSoundEffects.containsKey(theId)) {
						((MediaElementAudioSourceNodeX) activeSoundEffects.get(theId)).audio.play();
					}
				}
			});
			audioElement.addEventListener("ended", new EventListener<Event>() {
				@Override
				public void handleEvent(Event evt) {
					((MediaElementAudioSourceNodeX) activeSoundEffects.remove(theId)).audio.setSrc("");
				}
			});
		} else {
			activeSoundEffects.put(theId, new AudioBufferSourceNodeX((AudioBufferSourceNode) s, p, g, music));
			((AudioBufferSourceNode) s).setOnEnded(new EventListener<MediaEvent>() {
				@Override
				public void handleEvent(MediaEvent evt) {
					activeSoundEffects.remove(theId);
				}
			});
		}
		return theId;
	}
	public static final int beginPlaybackStatic(String fileName, float volume, float pitch) {
		return beginPlaybackStatic(fileName, volume, pitch, false);
	}
	public static final int beginPlaybackStatic(String fileNamePre, float volume, float pitch, boolean music) {
		if(fileNamePre.startsWith("/")) fileNamePre = fileNamePre.substring(1);
		String fileName = AssetRepository.fileNameOverrides.getOrDefault(fileNamePre, fileNamePre);
		AudioNode s = null;
		GainNode g = null;
		HTMLAudioElement audioElement = null;
		String lowerFileName = fileName.toLowerCase();
		boolean usingUrl = AssetRepository.fileNameOverrides.containsKey(fileNamePre) || lowerFileName.startsWith("http://") || lowerFileName.startsWith("https://") || lowerFileName.startsWith("blob:") || lowerFileName.startsWith("data:");
		if (usingUrl) {
			audioElement = (HTMLAudioElement) win.getDocument().createElement("audio");
			audioElement.setAutoplay(true);
			// audioElement.setCrossOrigin("anonymous");
			audioElement.setSrc(fileName);
			audioElement.setPlaybackRate(pitch);
		} else {
			AudioBuffer b = getBufferFor(fileName);
			if(b == null) return -1;
			s = audioctx.createBufferSource();
			((AudioBufferSourceNode) s).setBuffer(b);
			((AudioBufferSourceNode) s).getPlaybackRate().setValue(pitch);
			g = audioctx.createGain();
			g.getGain().setValue(volume > 1.0f ? 1.0f : volume);
			s.connect(g);
			g.connect(music ? musicVolumeNode : masterVolumeNode);
			((AudioBufferSourceNode) s).start(0.0d, playbackOffsetDelay);
		}

		final int theId = ++playbackId;
		if (usingUrl) {
			activeSoundEffects.put(theId, new MediaElementAudioSourceNodeX(null, audioElement, null, null, music));
			audioElement.addEventListener("canplay", new EventListener<Event>() {
				@Override
				public void handleEvent(Event evt) {
					if (activeSoundEffects.containsKey(theId)) {
						((MediaElementAudioSourceNodeX) activeSoundEffects.get(theId)).audio.play();
					}
				}
			});
			audioElement.addEventListener("ended", new EventListener<Event>() {
				@Override
				public void handleEvent(Event evt) {
					((MediaElementAudioSourceNodeX) activeSoundEffects.remove(theId)).audio.setSrc("");
				}
			});
		} else {
			activeSoundEffects.put(theId, new AudioBufferSourceNodeX(((AudioBufferSourceNode) s), null, g, music));
			((AudioBufferSourceNode) s).setOnEnded(new EventListener<MediaEvent>() {
				@Override
				public void handleEvent(MediaEvent evt) {
					activeSoundEffects.remove(theId);
				}
			});
		}
		return theId;
	}
	public static final void setPitch(int id, float pitch) {
		AudioSourceNodeX a = activeSoundEffects.get(id);
		if(a != null) {
			if (a instanceof AudioBufferSourceNodeX) {
				((AudioBufferSourceNodeX) a).source.getPlaybackRate().setValue(pitch);
			} else if (a instanceof MediaElementAudioSourceNodeX) {
				((MediaElementAudioSourceNodeX) a).audio.setPlaybackRate(pitch);
			}
		}
	}
	public static final void setVolume(int id, float volume) {
		AudioSourceNodeX a = activeSoundEffects.get(id);
		if(a != null) {
			if (a instanceof MediaElementAudioSourceNodeX && a.gain == null) {
				HTMLAudioElement audioElem = ((MediaElementAudioSourceNodeX) a).audio;
				float gainValue = (a.music ? musicVolumeNode : masterVolumeNode).getGain().getValue();
				float newVolume;
				if (gainValue == 0) {
					audioElem.setMuted(true);
					newVolume = volume;
				} else {
					audioElem.setMuted(false);
					newVolume = gainValue * volume;
				}
				audioElem.setVolume(newVolume > 1.0f ? 1.0f : volume);
			} else {
				a.gain.getGain().setValue(volume > 1.0f ? 1.0f : volume);
				if (a.panner != null) a.panner.setMaxDistance(volume * 16f + 0.1f);
			}
		}
	}
	public static final void moveSound(int id, float x, float y, float z, float vx, float vy, float vz) {
		AudioSourceNodeX a = activeSoundEffects.get(id);
		if(a != null && a.panner != null) {
			a.panner.setPosition(x, y, z);
			//a.panner.setVelocity(vx, vy, vz);
		}
	}
	public static final void endSound(int id) {
		AudioSourceNodeX a = activeSoundEffects.get(id);
		if(a != null) {
			if (a instanceof AudioBufferSourceNodeX) {
				((AudioBufferSourceNodeX) a).source.stop();
			} else if (a instanceof MediaElementAudioSourceNodeX) {
				((MediaElementAudioSourceNodeX) a).audio.pause();
				((MediaElementAudioSourceNodeX) a).audio.setSrc("");
			}
			activeSoundEffects.remove(id);
		}
	}
	public static final boolean isPlaying(int id) {
		return activeSoundEffects.containsKey(id);
	}
	public static final void openConsole() {
		Window.alert("Still under development");
	}

	private static EaglercraftVoiceClient voiceClient = null;
	
	private static boolean voiceAvailableStat = false;
	private static boolean voiceSignalHandlersInitialized = false;

	private static Consumer<byte[]> returnSignalHandler = null;

	private static final HashMap<String, AnalyserNode> voiceAnalysers = new HashMap<>();
	private static final HashMap<String, GainNode> voiceGains = new HashMap<>();
	private static final HashMap<String, PannerNode> voicePanners = new HashMap<>();
	private static final HashSet<String> nearbyPlayers = new HashSet<>();

	public static void clearVoiceAvailableStatus() {
		voiceAvailableStat = false;
	}

	public static void setVoiceSignalHandler(Consumer<byte[]> signalHandler) {
		returnSignalHandler = signalHandler;
	}

	public static final int VOICE_SIGNAL_ALLOWED = 0;
	public static final int VOICE_SIGNAL_REQUEST = 0;
	public static final int VOICE_SIGNAL_CONNECT = 1;
	public static final int VOICE_SIGNAL_DISCONNECT = 2;
	public static final int VOICE_SIGNAL_ICE = 3;
	public static final int VOICE_SIGNAL_DESC = 4;
	public static final int VOICE_SIGNAL_GLOBAL = 5;

	public static void handleVoiceSignal(byte[] data) {
		try {
			DataInputStream streamIn = new DataInputStream(new EaglerInputStream(data));
			int sig = streamIn.read();
			switch(sig) {
				case VOICE_SIGNAL_GLOBAL:
					String[] voicePlayers = new String[streamIn.readInt()];
					for(int i = 0; i < voicePlayers.length; i++) voicePlayers[i] = streamIn.readUTF();
					for (String username : voicePlayers) {
						// notice that literally everyone except for those already connected using voice chat will receive the request; however, ones using proximity will simply ignore it.
						sendVoiceRequestIfNeeded(username);
					}
					break;
				case VOICE_SIGNAL_ALLOWED:
					voiceAvailableStat = streamIn.read() == 1;
					String[] servs = new String[streamIn.read()];
					for(int i = 0; i < servs.length; i++) {
						servs[i] = streamIn.readUTF();
					}
					voiceClient.setICEServers(servs);
					break;
				case VOICE_SIGNAL_CONNECT:
					String peerId = streamIn.readUTF();
					try {
						boolean offer = streamIn.readBoolean();
						voiceClient.signalConnect(peerId, offer);
					} catch (EOFException e) { // this is actually a connect ANNOUNCE, not an absolute "yes please connect" situation
						if (false && !nearbyPlayers.contains(peerId)) return;
						// send request to peerId
						sendVoiceRequest(peerId);
					}
					break;
				case VOICE_SIGNAL_DISCONNECT:
					String peerId2 = streamIn.readUTF();
					voiceClient.signalDisconnect(peerId2, true);
					break;
				case VOICE_SIGNAL_ICE:
					String peerId3 = streamIn.readUTF();
					String candidate = streamIn.readUTF();
					voiceClient.signalICECandidate(peerId3, candidate);
					break;
				case VOICE_SIGNAL_DESC:
					String peerId4 = streamIn.readUTF();
					String descJSON = streamIn.readUTF();
					voiceClient.signalDescription(peerId4, descJSON);
					break;
				default:
					System.err.println("Unknown voice signal packet '" + sig + "'!");
					break;
			}
		}catch(IOException ex) {
			ex.printStackTrace();
		}
	}
	
	public static final boolean voiceAvailable() {
		return voiceClient.voiceClientSupported() && voiceClient.getReadyState() != EaglercraftVoiceClient.READYSTATE_ABORTED;
	}
	public static final boolean voiceAllowed() {
		return voiceAvailableStat;
	}
	public static final boolean voiceRelayed() {
		return false;
	}

	public static final void addNearbyPlayer(String username) {
		recentlyNearbyPlayers.remove(username);
		if (nearbyPlayers.add(username)) {
			sendVoiceRequestIfNeeded(username);
		}
	}

	private static final void sendVoiceRequest(String username) {
		try {
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			DataOutputStream dos = new DataOutputStream(baos);
			dos.write(VOICE_SIGNAL_REQUEST);
			dos.writeUTF(username);
			returnSignalHandler.accept(baos.toByteArray());
		} catch (IOException ignored) {  }
	}

	private static final void sendVoiceRequestIfNeeded(String username) {

	}

	private static final ExpiringSet<String> recentlyNearbyPlayers = new ExpiringSet<>(5000, new ExpiringSet.ExpiringEvent<String>() {
		@Override
		public void onExpiration(String username) {
			if (!nearbyPlayers.contains(username)) voiceClient.signalDisconnect(username, false);
		}
	});

	public static final void removeNearbyPlayer(String username) {

	}

	public static final void cleanupNearbyPlayers(HashSet<String> existingPlayers) {
		nearbyPlayers.stream().filter(un -> !existingPlayers.contains(un)).collect(Collectors.toSet()).forEach(EaglerAdapterImpl2::removeNearbyPlayer);
	}

	public static final void updateVoicePosition(String username, double x, double y, double z) {
		if (voicePanners.containsKey(username)) voicePanners.get(username).setPosition((float) x, (float) y, (float) z);
	}

	public static final void sendInitialVoice() {
		returnSignalHandler.accept(new byte[] { VOICE_SIGNAL_CONNECT });
		for (String username : nearbyPlayers) sendVoiceRequest(username);
	}

	private static boolean talkStatus = false;
	public static final void activateVoice(boolean talk) {
		if(talkStatus != talk) {
			voiceClient.activateVoice(talk);
		}
		talkStatus = talk;
	}

	private static int proximity = 16;
	public static final void setVoiceProximity(int prox) {
		for (PannerNode panner : voicePanners.values()) panner.setMaxDistance(getVoiceListenVolume() * 2 * prox + 0.1f);
		proximity = prox;
	}
	public static final int getVoiceProximity() {
		return proximity;
	}

	private static float volumeListen = 0.5f;
	public static final void setVoiceListenVolume(float f) {
		for (String username : voiceGains.keySet()) {
			GainNode gain = voiceGains.get(username);
			float val = f;
			if(val > 0.5f) val = 0.5f + (val - 0.5f) * 3.0f;
			if(val > 2.0f) val = 2.0f;
			if(val < 0.0f) val = 0.0f;
			gain.getGain().setValue(val * 2.0f);
			if (voicePanners.containsKey(username)) voicePanners.get(username).setMaxDistance(f * 2 * getVoiceProximity() + 0.1f);
		}
		volumeListen = f;
	}
	public static final float getVoiceListenVolume() {
		return volumeListen;
	}

	private static float volumeSpeak = 0.5f;
	public static final void setVoiceSpeakVolume(float f) {
		if(volumeSpeak != f) {
			voiceClient.setMicVolume(f);
		}
		volumeSpeak = f;
	}
	public static final float getVoiceSpeakVolume() {
		return volumeSpeak;
	}

	private static final Set<String> mutedSet = new HashSet<>();
	private static final Set<String> speakingSet = new HashSet<>();
	public static final Set<String> getVoiceListening() {
		return voiceGains.keySet();
	}
	public static final Set<String> getVoiceSpeaking() {
		return speakingSet;
	}
	public static final void setVoiceMuted(String username, boolean mute) {
		voiceClient.mutePeer(username, mute);
		if(mute) {
			mutedSet.add(username);
		}else {
			mutedSet.remove(username);
		}
	}
	public static final Set<String> getVoiceMuted() {
		return mutedSet;
	}
	public static final List<String> getVoiceRecent() {
		return new ArrayList<>(voiceGains.keySet());
	}

	public static final void tickVoice() {
		recentlyNearbyPlayers.checkForExpirations();
		speakingSet.clear();
		for (String username : voiceAnalysers.keySet()) {
			AnalyserNode analyser = voiceAnalysers.get(username);
			Uint8Array array = new Uint8Array(analyser.getFrequencyBinCount());
			analyser.getByteFrequencyData(array);
			int len = array.getLength();
			for (int i = 0; i < len; i++) {
				if (array.get(i) >= 0.1f) {
					speakingSet.add(username);
					break;
				}
			}
		}
	}
	
	
	public static final void doJavascriptCoroutines() {
		
	}
	public static final long maxMemory() {
		return 1024*1024*1024;
	}
	public static final long totalMemory() {
		return 1024*1024*1024;
	}
	public static final long freeMemory() {
		return 0l;
	}
	public static final void exit() {
		
	}
	
	@JSBody(params = { }, script = "return window.navigator.userAgent;")
	public static native String getUserAgent();
	
	private static String[] LWJGLKeyNames = new String[] {"NONE", "ESCAPE", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "MINUS", "EQUALS", "BACK", "TAB", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "LBRACKET", "RBRACKET", "RETURN", "LCONTROL", "A", "S", "D", "F", "G", "H", "J", "K", "L", "SEMICOLON", "APOSTROPHE", "GRAVE", "LSHIFT", "BACKSLASH", "Z", "X", "C", "V", "B", "N", "M", "COMMA", "PERIOD", "SLASH", "RSHIFT", "MULTIPLY", "LMENU", "SPACE", "CAPITAL", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "NUMLOCK", "SCROLL", "NUMPAD7", "NUMPAD8", "NUMPAD9", "SUBTRACT", "NUMPAD4", "NUMPAD5", "NUMPAD6", "ADD", "NUMPAD1", "NUMPAD2", "NUMPAD3", "NUMPAD0", "DECIMAL", "null", "null", "null", "F11", "F12", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "F13", "F14", "F15", "F16", "F17", "F18", "null", "null", "null", "null", "null", "null", "KANA", "F19", "null", "null", "null", "null", "null", "null", "null", "CONVERT", "null", "NOCONVERT", "null", "YEN", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "NUMPADEQUALS", "null", "null", "CIRCUMFLEX", "AT", "COLON", "UNDERLINE", "KANJI", "STOP", "AX", "UNLABELED", "null", "null", "null", "null", "NUMPADENTER", "RCONTROL", "null", "null", "null", "null", "null", "null", "null", "null", "null", "SECTION", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "NUMPADCOMMA", "null", "DIVIDE", "null", "SYSRQ", "RMENU", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "FUNCTION", "PAUSE", "null", "HOME", "UP", "PRIOR", "null", "LEFT", "null", "RIGHT", "null", "END", "DOWN", "NEXT", "INSERT", "DELETE", "null", "null", "null", "null", "null", "null", "CLEAR", "LMETA", "RMETA", "APPS", "POWER", "SLEEP", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null"};
	
	private static int[] LWJGLKeyCodes = new int[] {
			/* 0 */ -1, /* 1 */ -1, /* 2 */ -1, /* 3 */ -1, /* 4 */ -1,
			/* 5 */ -1, /* 6 */ -1, /* 7 */ -1, /* 8 */ 14, /* 9 */ 15,
			/* 10 */ -1, /* 11 */ -1, /* 12 */ -1, /* 13 */ 28, /* 14 */ -1,
			/* 15 */ -1, /* 16 */ 42, /* 17 */ 29, /* 18 */ 56, /* 19 */ -1,
			/* 20 */ -1, /* 21 */ -1, /* 22 */ -1, /* 23 */ -1, /* 24 */ -1,
			/* 25 */ -1, /* 26 */ -1, /* 27 */ 1, /* 28 */ -1, /* 29 */ -1,
			/* 30 */ -1, /* 31 */ -1, /* 32 */ 57, /* 33 */ 210, /* 34 */ 201,
			/* 35 */ 207, /* 36 */ 199, /* 37 */ 203, /* 38 */ 200, /* 39 */ 205,
			/* 40 */ 208, /* 41 */ 205, /* 42 */ 208, /* 43 */ -1, /* 44 */ -1,
			/* 45 */ 210, /* 46 */ 211, /* 47 */ 211, /* 48 */ 11, /* 49 */ 2,
			/* 50 */ 3, /* 51 */ 4, /* 52 */ 5, /* 53 */ 6, /* 54 */ 7,
			/* 55 */ 8, /* 56 */ 9, /* 57 */ 10, /* 58 */ -1, /* 59 */ -1,
			/* 60 */ -1, /* 61 */ -1, /* 62 */ -1, /* 63 */ -1, /* 64 */ -1,
			/* 65 */ 30, /* 66 */ 48, /* 67 */ 46, /* 68 */ 32, /* 69 */ 18,
			/* 70 */ 33, /* 71 */ 34, /* 72 */ 35, /* 73 */ 23, /* 74 */ 36,
			/* 75 */ 37, /* 76 */ 38, /* 77 */ 50, /* 78 */ 49, /* 79 */ 24,
			/* 80 */ 25, /* 81 */ 16, /* 82 */ 19, /* 83 */ 31, /* 84 */ 20,
			/* 85 */ 22, /* 86 */ 47, /* 87 */ 17, /* 88 */ 45, /* 89 */ 21,
			/* 90 */ 44, /* 91 */ -1, /* 92 */ -1, /* 93 */ -1, /* 94 */ -1,
			/* 95 */ -1, /* 96 */ -1, /* 97 */ -1, /* 98 */ -1, /* 99 */ -1,
			/* 100 */ -1, /* 101 */ -1, /* 102 */ -1, /* 103 */ -1, /* 104 */ -1,
			/* 105 */ -1, /* 106 */ -1, /* 107 */ -1, /* 108 */ -1, /* 109 */ 12,
			/* 110 */ 52, /* 111 */ 53, /* 112 */ -1, /* 113 */ -1, /* 114 */ -1,
			/* 115 */ -1, /* 116 */ -1, /* 117 */ -1, /* 118 */ -1, /* 119 */ -1,
			/* 120 */ -1, /* 121 */ -1, /* 122 */ -1, /* 123 */ -1, /* 124 */ -1,
			/* 125 */ -1, /* 126 */ -1, /* 127 */ -1, /* 128 */ -1, /* 129 */ -1,
			/* 130 */ -1, /* 131 */ -1, /* 132 */ -1, /* 133 */ -1, /* 134 */ -1,
			/* 135 */ -1, /* 136 */ -1, /* 137 */ -1, /* 138 */ -1, /* 139 */ -1,
			/* 140 */ -1, /* 141 */ -1, /* 142 */ -1, /* 143 */ -1, /* 144 */ -1,
			/* 145 */ -1, /* 146 */ -1, /* 147 */ -1, /* 148 */ -1, /* 149 */ -1,
			/* 150 */ -1, /* 151 */ -1, /* 152 */ -1, /* 153 */ -1, /* 154 */ -1,
			/* 155 */ -1, /* 156 */ -1, /* 157 */ -1, /* 158 */ -1, /* 159 */ -1,
			/* 160 */ -1, /* 161 */ -1, /* 162 */ -1, /* 163 */ -1, /* 164 */ -1,
			/* 165 */ -1, /* 166 */ -1, /* 167 */ -1, /* 168 */ -1, /* 169 */ -1,
			/* 170 */ -1, /* 171 */ -1, /* 172 */ -1, /* 173 */ -1, /* 174 */ -1,
			/* 175 */ -1, /* 176 */ -1, /* 177 */ -1, /* 178 */ -1, /* 179 */ -1,
			/* 180 */ -1, /* 181 */ -1, /* 182 */ -1, /* 183 */ -1, /* 184 */ -1,
			/* 185 */ -1, /* 186 */ 39, /* 187 */ 13, /* 188 */ 51, /* 189 */ 12,
			/* 190 */ 52, /* 191 */ 53, /* 192 */ -1, /* 193 */ -1, /* 194 */ -1,
			/* 195 */ -1, /* 196 */ -1, /* 197 */ -1, /* 198 */ -1, /* 199 */ -1,
			/* 200 */ -1, /* 201 */ -1, /* 202 */ -1, /* 203 */ -1, /* 204 */ -1,
			/* 205 */ -1, /* 206 */ -1, /* 207 */ -1, /* 208 */ -1, /* 209 */ -1,
			/* 210 */ -1, /* 211 */ -1, /* 212 */ -1, /* 213 */ -1, /* 214 */ -1,
			/* 215 */ -1, /* 216 */ -1, /* 217 */ -1, /* 218 */ -1, /* 219 */ 26,
			/* 220 */ 43, /* 221 */ 27, /* 222 */ 40
	};

	public static final int _wArrayByteLength(Object obj) {
		return ((Int32Array)obj).getByteLength();
	}
	
	public static final Object _wCreateLowLevelIntBuffer(int len) {
		return new Int32Array(len);
	}
	
	private static int appendbufferindex = 0;
	private static Int32Array appendbuffer = new Int32Array(new ArrayBuffer(525000*4));

	public static final void _wAppendLowLevelBuffer(Object arr) {
		Int32Array a = ((Int32Array)arr);
		if(appendbufferindex + a.getLength() < appendbuffer.getLength()) {
			appendbuffer.set(a, appendbufferindex);
			appendbufferindex += a.getLength();
		}
	}
	
	public static final Object _wGetLowLevelBuffersAppended() {
		Int32Array ret = new Int32Array(appendbuffer.getBuffer(), 0, appendbufferindex);
		appendbufferindex = 0;
		return ret;
	}
	
	private static int remapKey(int k) {
		return (k > LWJGLKeyCodes.length || k < 0) ? -1 : LWJGLKeyCodes[k];
	}
	
	public static final boolean isIntegratedServerAvailable() {
		return integratedServerScript != null;
	}
	
	@JSFunctor
	private static interface WorkerBinaryPacketHandler extends JSObject {
		public void onMessage(String channel, ArrayBuffer buf);
	}
	
	private static final HashMap<String,List<PKT>> workerMessageQueue = new HashMap<>();
	
	private static Worker server = null;
	private static boolean serverAlive = false;
	
	private static class WorkerBinaryPacketHandlerImpl implements WorkerBinaryPacketHandler {
		
		public void onMessage(String channel, ArrayBuffer buf) {
			if(channel == null) {
				System.err.println("Recieved IPC packet with null channel");
				return;
			}
			
			serverAlive = true;
			synchronized(workerMessageQueue) {
				List<PKT> existingQueue = workerMessageQueue.get(channel);
				
				if(existingQueue == null) {
					System.err.println("Recieved IPC packet with unknown '" + channel + "' channel");
					return;
				}
				
				if(buf == null) {
					System.err.println("Recieved IPC packet with null buffer");
					return;
				}
				
				existingQueue.add(new PKT(channel, TeaVMUtils.wrapByteArrayBuffer(buf)));
			}
		}
		
	}
	
	@JSBody(params = { "w", "wb" }, script = "w.onmessage = function(o) { wb(o.data.ch, o.data.dat); };")
	private static native void registerPacketHandler(Worker w, WorkerBinaryPacketHandler wb);

	@JSBody(params = { "w", "ch", "dat" }, script = "w.postMessage({ ch: ch, dat : dat });")
	private static native void sendWorkerPacket(Worker w, String channel, ArrayBuffer arr);
	
	@JSBody(params = { "w", "dbName" }, script = "w.postMessage({ worldDatabaseName : dbName });")
	private static native void sendWorkerStartPacket(Worker w, String dbName);
	
	private static String worldDatabaseName = "MAIN";
	
	public static final void beginLoadingIntegratedServer() {
		if(server != null) {
			server.terminate();
		}
		workerMessageQueue.put("IPC", new LinkedList<PKT>());
		server = new Worker(integratedServerScript);
		server.onError(new EventListener<ErrorEvent>() {
			@Override
			public void handleEvent(ErrorEvent evt) {
				System.err.println("Worker Error: " + evt.getError());
			}
		});
		sendWorkerStartPacket(server, worldDatabaseName);
		registerPacketHandler(server, new WorkerBinaryPacketHandlerImpl());
	}
	
	public static final void setWorldDatabaseName(String name) {
		worldDatabaseName = name;
	}
	
	public static final boolean isIntegratedServerAlive() {
		return serverAlive && server != null;
	}
	
	public static final void terminateIntegratedServer() {
		if(server != null) {
			server.terminate();
			server = null;
			serverAlive = false;
		}
	}
	
	public static final void sendToIntegratedServer(String channel, byte[] pkt) {
		sendWorkerPacket(server, channel, TeaVMUtils.unwrapArrayBuffer(pkt));
		//System.out.println("[Client][WRITE][" + channel + "]: " + pkt.length);
	}
	
	public static final void enableChannel(String channel) {
		synchronized(workerMessageQueue) {
			if(workerMessageQueue.containsKey(channel)) {
				System.err.println("Tried to enable existing channel '" + channel + "' again");
			}else {
				System.out.println("[Client][ENABLE][" + channel + "]");
				workerMessageQueue.put(channel, new LinkedList<>());
			}
		}
	}
	
	public static final void disableChannel(String channel) {
		synchronized(workerMessageQueue) {
			if(workerMessageQueue.remove(channel) == null) {
				System.err.println("Tried to disable unknown channel '" + channel + "'");
			}
			System.out.println("[Client][DISABLE][" + channel + "]");
		}
	}
	
	public static final PKT recieveFromIntegratedServer(String channel) {
		synchronized(workerMessageQueue) {
			List<PKT> list = workerMessageQueue.get(channel);
			if(list == null) {
				System.err.println("Tried to read from unknown channel '" + channel + "'");
				return null;
			}else {
				return list.size() > 0 ? list.remove(0) : null;
			}
		}
	}
	
	@JSBody(params = { "name", "buf" }, script =
		"var hr = window.URL.createObjectURL(new Blob([buf], {type: \"octet/stream\"}));" +
		"var a = document.createElement(\"a\");" +
		"a.href = hr; a.download = name; a.click();" +
		"window.URL.revokeObjectURL(hr);")
	private static final native void downloadBytesImpl(String str, ArrayBuffer buf);
	
	public static final void downloadBytes(String str, byte[] dat) {
		downloadBytesImpl(str, TeaVMUtils.unwrapArrayBuffer(dat));
	}
	
	@JSFunctor
	private static interface StupidFunctionResolveString extends JSObject {
		void resolveStr(String s);
	}
	
	private static boolean unpressCTRL = false;
	
	@Async
	public static native String getClipboard();
	
	private static void getClipboard(final AsyncCallback<String> cb) {
		final long start = steadyTimeMillis();
		getClipboard0(new StupidFunctionResolveString() {
			@Override
			public void resolveStr(String s) {
				if(steadyTimeMillis() - start > 500l) {
					unpressCTRL = true;
				}
				cb.complete(s);
			}
		});
	}
	
	@JSBody(params = { "cb" }, script = "if(!window.navigator.clipboard) cb(null); else window.navigator.clipboard.readText().then(function(s) { cb(s); }, function(s) { cb(null); });")
	private static native void getClipboard0(StupidFunctionResolveString cb);
	
	@JSBody(params = { "str" }, script = "if(window.navigator.clipboard) window.navigator.clipboard.writeText(str);")
	public static native void setClipboard(String str);
	
	@JSBody(params = { "obj" }, script = "return typeof obj === \"string\";")
	private static native boolean isString(JSObject obj);
	
	private static class ServerQueryImpl implements ServerQuery {
		
		private final LinkedList<QueryResponse> queryResponses = new LinkedList<>();
		private final LinkedList<byte[]> queryResponsesBytes = new LinkedList<>();
		private final String type;
		private boolean open;
		private boolean alive;
		private String uriString;
		private long pingStart;
		private long pingTimer;
		
		private final WebSocket sock;
		
		private ServerQueryImpl(String type_, String uri) {
			type = type_;
			uriString = uri;
			alive = false;
			pingStart = -1l;
			pingTimer = -1l;
			WebSocket s = null;
			try {
				s = new WebSocket(uri);
				s.setBinaryType("arraybuffer");
				open = true;
			}catch(Throwable t) {
				open = false;
				if(EaglerAdapterImpl2.blockedAddresses.contains(uriString)) {
					queryResponses.add(new QueryResponse(true, -1l));
				}else if(EaglerAdapterImpl2.rateLimitedAddresses.contains(uriString)) {
					queryResponses.add(new QueryResponse(false, -1l));
				}
				sock = null;
				return;
			}
			sock = s;
			if(open) {
				sock.addEventListener("open", new EventListener<Event>() {
					@Override
					public void handleEvent(Event evt) {
						pingStart = steadyTimeMillis();
						sock.send("Accept: " + type);
					}
				});
				sock.addEventListener("close", new EventListener<CloseEvent>() {
					@Override
					public void handleEvent(CloseEvent evt) {
						open = false;
						if(!alive) {
							if(EaglerAdapterImpl2.blockedAddresses.contains(uriString)) {
								queryResponses.add(new QueryResponse(true, pingTimer));
							}else if(EaglerAdapterImpl2.rateLimitedAddresses.contains(uriString)) {
								queryResponses.add(new QueryResponse(false, pingTimer));
							}
						}
					}
				});
				sock.addEventListener("message", new EventListener<MessageEvent>() {
					@Override
					public void handleEvent(MessageEvent evt) {
						alive = true;
						if(pingTimer == -1) {
							pingTimer = steadyTimeMillis() - pingStart;
						}
						if(isString(evt.getData())) {
							try {
								String str = evt.getDataAsString();
								if(str.equalsIgnoreCase("BLOCKED")) {
									EaglerAdapterImpl2.rateLimitedAddresses.add(uriString);
									queryResponses.add(new QueryResponse(false, pingTimer));
									sock.close();
									return;
								}else if(str.equalsIgnoreCase("LOCKED")) {
									EaglerAdapterImpl2.blockedAddresses.add(uriString);
									queryResponses.add(new QueryResponse(true, pingTimer));
									sock.close();
									return;
								}else {
									QueryResponse q = new QueryResponse(new JSONObject(str), pingTimer);
									if(q.rateLimitStatus != null) {
										if(q.rateLimitStatus == RateLimit.BLOCKED) {
											EaglerAdapterImpl2.rateLimitedAddresses.add(uriString);
										}else if(q.rateLimitStatus == RateLimit.LOCKED) {
											EaglerAdapterImpl2.blockedAddresses.add(uriString);
										}
										sock.close();
									}
									queryResponses.add(q);
								}
							}catch(Throwable t) {
								System.err.println("Query response could not be parsed: " + t.toString());
							}
						}else {
							queryResponsesBytes.add(TeaVMUtils.wrapByteArrayBuffer(evt.getDataAsArray()));
						}
					}
				});
				Window.setTimeout(new TimerHandler() {
					@Override
					public void onTimer() {
						if(open && sock.getReadyState() != 1) {
							if(sock.getReadyState() == 0) {
								sock.close();
							}
							open = false;
						}
					}
				}, 5000l);
			}
		}

		@Override
		public boolean isQueryOpen() {
			return open;
		}

		@Override
		public void close() {
			open = false;
			sock.close();
		}

		@Override
		public void send(String str) {
			sock.send(str);
		}

		@Override
		public int responseAvailable() {
			return queryResponses.size();
		}

		@Override
		public int responseBinaryAvailable() {
			return queryResponsesBytes.size();
		}

		@Override
		public QueryResponse getResponse() {
			return queryResponses.size() > 0 ? queryResponses.remove(0) : null;
		}

		@Override
		public byte[] getBinaryResponse() {
			return queryResponsesBytes.size() > 0 ? queryResponsesBytes.remove(0) : null;
		}
		
	}

	public static final ServerQuery openQuery(String type, String uri) {
		return new ServerQueryImpl(type, uri);
	}
	
	private static String serverToJoinOnLaunch = null;
	
	public static final void setServerToJoinOnLaunch(String s) {
		serverToJoinOnLaunch = s;
	}
	
	public static final String getServerToJoinOnLaunch() {
		return serverToJoinOnLaunch;
	}

	private static boolean endianWasChecked = false;
	private static boolean isBigEndian = false;
	private static boolean isLittleEndian = false;
	
	public static final boolean isBigEndian() {
		if(!endianWasChecked) {
			int checkIntegerA = 0xFF000000;
			int checkIntegerB = 0x000000FF;
			
			ArrayBuffer buf = new ArrayBuffer(4);
			Int32Array bufW = new Int32Array(buf);
			Uint8Array bufR = new Uint8Array(buf);
			
			bufW.set(0, checkIntegerA);

			boolean knownBig1 = false;
			if(bufR.get(0) == (short)0xFF && bufR.get(1) == (short)0 && bufR.get(2) == (short)0 && bufR.get(3) == (short)0) {
				knownBig1 = true;
			}
			
			boolean knownLittle1 = false;
			if(bufR.get(0) == (short)0 && bufR.get(1) == (short)0 && bufR.get(2) == (short)0 && bufR.get(3) == (short)0xFF) {
				knownLittle1 = true;
			}
			
			bufW.set(0, checkIntegerB);
			
			boolean knownBig2 = false;
			if(bufR.get(0) == (short)0 && bufR.get(1) == (short)0 && bufR.get(2) == (short)0 && bufR.get(3) == (short)0xFF) {
				knownBig2 = true;
			}

			boolean knownLittle2 = false;
			if(bufR.get(0) == (short)0xFF && bufR.get(1) == (short)0 && bufR.get(2) == (short)0 && bufR.get(3) == (short)0) {
				knownLittle2 = true;
			}
			
			if(knownBig1 == knownBig2 && knownLittle1 == knownLittle2 && knownBig1 != knownLittle1) {
				isBigEndian = knownBig1;
				isLittleEndian = knownLittle1;
			}
			
			if(isBigEndian) {
				System.out.println("This browser is BIG endian!");
			}else if(isLittleEndian) {
				System.out.println("This browser is LITTLE endian!");
			}else {
				System.out.println("The byte order of this browser is inconsistent!");
				System.out.println(" - the sequence FF000000 was " + (knownBig1 ? "" : "not ") + "big endian.");
				System.out.println(" - the sequence FF000000 was " + (knownLittle1 ? "" : "not ") + "little endian.");
				System.out.println(" - the sequence 000000FF was " + (knownBig2 ? "" : "not ") + "big endian.");
				System.out.println(" - the sequence 000000FF was " + (knownLittle2 ? "" : "not ") + "little endian.");
			}
			
			endianWasChecked = true;
		}
		return !isLittleEndian;
	}
	
	private static final Map<String,Long> relayQueryLimited = new HashMap<>();
	private static final Map<String,Long> relayQueryBlocked = new HashMap<>();
	
	private static class RelayQueryImpl implements RelayQuery {

		private final WebSocket sock;
		private final String uri;

		private boolean open;
		private boolean failed;
		
		private boolean hasRecievedAnyData = false;
		
		private int vers = -1;
		private String comment = "<no comment>";
		private String brand = "<no brand>";
		
		private long connectionOpenedAt;
		private long connectionPingStart = -1;
		private long connectionPingTimer = -1;
		
		private RateLimit rateLimitStatus = RateLimit.NONE;
		
		private VersionMismatch versError = VersionMismatch.UNKNOWN;
		
		private RelayQueryImpl(String uri) {
			this.uri = uri;
			WebSocket s = null;
			try {
				connectionOpenedAt = steadyTimeMillis();
				s = new WebSocket(uri);
				s.setBinaryType("arraybuffer");
				open = true;
				failed = false;
			}catch(Throwable t) {
				connectionOpenedAt = 0l;
				sock = null;
				open = false;
				failed = true;
				return;
			}
			sock = s;
			sock.addEventListener("open", new EventListener<Event>() {
				@Override
				public void handleEvent(Event evt) {
					try {
						connectionPingStart = steadyTimeMillis();
						nativeBinarySend(sock, TeaVMUtils.unwrapArrayBuffer(
								IPacket.writePacket(new IPacket00Handshake(0x03, IntegratedServer.preferredRelayVersion, ""))
						));
					} catch (IOException e) {
						System.err.println(e.toString());
						sock.close();
						failed = true;
					}
				}
			});
			sock.addEventListener("message", new EventListener<MessageEvent>() {
				@Override
				public void handleEvent(MessageEvent evt) {
					if(evt.getData() != null && !isString(evt.getData())) {
						hasRecievedAnyData = true;
						byte[] arr = TeaVMUtils.wrapByteArrayBuffer(evt.getDataAsArray());
						if(arr.length == 2 && arr[0] == (byte)0xFC) {
							long millis = steadyTimeMillis();
							if(arr[1] == (byte)0x00 || arr[1] == (byte)0x01) {
								rateLimitStatus = RateLimit.BLOCKED;
								relayQueryLimited.put(RelayQueryImpl.this.uri, millis);
							}else if(arr[1] == (byte)0x02) {
								rateLimitStatus = RateLimit.NOW_LOCKED;
								relayQueryLimited.put(RelayQueryImpl.this.uri, millis);
								relayQueryBlocked.put(RelayQueryImpl.this.uri, millis);
							}else {
								rateLimitStatus = RateLimit.LOCKED;
								relayQueryBlocked.put(RelayQueryImpl.this.uri, millis);
							}
							failed = true;
							open = false;
							sock.close();
						}else {
							if(open) {
								try {
									IPacket pkt = IPacket.readPacket(new DataInputStream(new EaglerInputStream(arr)));
									if(pkt instanceof IPacket69Pong) {
										IPacket69Pong ipkt = (IPacket69Pong)pkt;
										versError = RelayQuery.VersionMismatch.COMPATIBLE;
										if(connectionPingTimer == -1) {
											connectionPingTimer = steadyTimeMillis() - connectionPingStart;
										}
										vers = ipkt.protcolVersion;
										comment = ipkt.comment;
										brand = ipkt.brand;
										open = false;
										failed = false;
										sock.close();
									}else if(pkt instanceof IPacketFFErrorCode) {
										IPacketFFErrorCode ipkt = (IPacketFFErrorCode)pkt;
										if(ipkt.code == IPacketFFErrorCode.TYPE_PROTOCOL_VERSION) {
											String s = ipkt.desc.toLowerCase();
											if(s.contains("outdated client") || s.contains("client outdated")) {
												versError = RelayQuery.VersionMismatch.CLIENT_OUTDATED;
											}else if(s.contains("outdated server") || s.contains("server outdated") ||
													s.contains("outdated relay") || s.contains("server relay")) {
												versError = RelayQuery.VersionMismatch.RELAY_OUTDATED;
											}else {
												versError = RelayQuery.VersionMismatch.UNKNOWN;
											}
										}
										System.err.println(uri + ": Recieved query error code " + ipkt.code + ": " + ipkt.desc);
										open = false;
										failed = true;
										sock.close();
									}else {
										throw new IOException("Unexpected packet '" + pkt.getClass().getSimpleName() + "'");
									}
								} catch (IOException e) {
									System.err.println("Relay Query Error: " + e.toString());
									e.printStackTrace();
									open = false;
									failed = true;
									sock.close();
								}
							}
						}
					}
				}
			});
			sock.addEventListener("close", new EventListener<CloseEvent>() {
				@Override
				public void handleEvent(CloseEvent evt) {
					open = false;
					if(!hasRecievedAnyData) {
						failed = true;
						Long l = relayQueryBlocked.get(uri);
						if(l != null) {
							if(steadyTimeMillis() - l.longValue() < 400000l) {
								rateLimitStatus = RateLimit.LOCKED;
								return;
							}
						}
						l = relayQueryLimited.get(uri);
						if(l != null) {
							if(steadyTimeMillis() - l.longValue() < 900000l) {
								rateLimitStatus = RateLimit.BLOCKED;
								return;
							}
						}
					}
				}
			});
		}

		@Override
		public boolean isQueryOpen() {
			return open;
		}

		@Override
		public boolean isQueryFailed() {
			return failed;
		}

		@Override
		public RateLimit isQueryRateLimit() {
			return rateLimitStatus;
		}

		@Override
		public void close() {
			if(sock != null && open) {
				sock.close();
			}
			open = false;
		}

		@Override
		public int getVersion() {
			return vers;
		}

		@Override
		public String getComment() {
			return comment;
		}

		@Override
		public String getBrand() {
			return brand;
		}

		@Override
		public long getPing() {
			return connectionPingTimer < 1 ? 1 : connectionPingTimer;
		}

		@Override
		public VersionMismatch getCompatible() {
			return versError;
		}
		
	}
	
	private static class RelayQueryRatelimitDummy implements RelayQuery {
		
		private final RateLimit type;
		
		private RelayQueryRatelimitDummy(RateLimit type) {
			this.type = type;
		}

		@Override
		public boolean isQueryOpen() {
			return false;
		}

		@Override
		public boolean isQueryFailed() {
			return true;
		}

		@Override
		public RateLimit isQueryRateLimit() {
			return type;
		}

		@Override
		public void close() {
		}

		@Override
		public int getVersion() {
			return IntegratedServer.preferredRelayVersion;
		}

		@Override
		public String getComment() {
			return "this query was rate limited";
		}

		@Override
		public String getBrand() {
			return "lax1dude";
		}

		@Override
		public long getPing() {
			return 0l;
		}

		@Override
		public VersionMismatch getCompatible() {
			return VersionMismatch.COMPATIBLE;
		}
		
	}
	
	public static final RelayQuery openRelayQuery(String addr) {
		long millis = steadyTimeMillis();
		
		Long l = relayQueryBlocked.get(addr);
		if(l != null && millis - l.longValue() < 60000l) {
			return new RelayQueryRatelimitDummy(RateLimit.LOCKED);
		}
		
		l = relayQueryLimited.get(addr);
		if(l != null && millis - l.longValue() < 10000l) {
			return new RelayQueryRatelimitDummy(RateLimit.BLOCKED);
		}
		
		return new RelayQueryImpl(addr);
	}
	
	private static class RelayWorldsQueryImpl implements RelayWorldsQuery {
		
		private final WebSocket sock;
		private final String uri;

		private boolean open;
		private boolean failed;
		
		private boolean hasRecievedAnyData = false;
		private RateLimit rateLimitStatus = RateLimit.NONE;
		
		private RelayQuery.VersionMismatch versError = RelayQuery.VersionMismatch.UNKNOWN;
		
		private List<LocalWorld> worlds = null;
		
		private RelayWorldsQueryImpl(String uri) {
			this.uri = uri;
			WebSocket s = null;
			try {
				s = new WebSocket(uri);
				s.setBinaryType("arraybuffer");
				open = true;
				failed = false;
			}catch(Throwable t) {
				sock = null;
				open = false;
				failed = true;
				return;
			}
			sock = s;
			sock.addEventListener("open", new EventListener<Event>() {
				@Override
				public void handleEvent(Event evt) {
					try {
						nativeBinarySend(sock, TeaVMUtils.unwrapArrayBuffer(
								IPacket.writePacket(new IPacket00Handshake(0x04, IntegratedServer.preferredRelayVersion, ""))
						));
					} catch (IOException e) {
						System.err.println(e.toString());
						sock.close();
						open = false;
						failed = true;
					}
				}
			});
			sock.addEventListener("message", new EventListener<MessageEvent>() {
				@Override
				public void handleEvent(MessageEvent evt) {
					if(evt.getData() != null && !isString(evt.getData())) {
						hasRecievedAnyData = true;
						byte[] arr = TeaVMUtils.wrapByteArrayBuffer(evt.getDataAsArray());
						if(arr.length == 2 && arr[0] == (byte)0xFC) {
							long millis = steadyTimeMillis();
							if(arr[1] == (byte)0x00 || arr[1] == (byte)0x01) {
								rateLimitStatus = RateLimit.BLOCKED;
								relayQueryLimited.put(RelayWorldsQueryImpl.this.uri, millis);
							}else if(arr[1] == (byte)0x02) {
								rateLimitStatus = RateLimit.NOW_LOCKED;
								relayQueryLimited.put(RelayWorldsQueryImpl.this.uri, millis);
								relayQueryBlocked.put(RelayWorldsQueryImpl.this.uri, millis);
							}else {
								rateLimitStatus = RateLimit.LOCKED;
								relayQueryBlocked.put(RelayWorldsQueryImpl.this.uri, millis);
							}
							open = false;
							failed = true;
							sock.close();
						}else {
							if(open) {
								try {
									IPacket pkt = IPacket.readPacket(new DataInputStream(new EaglerInputStream(arr)));
									if(pkt instanceof IPacket07LocalWorlds) {
										worlds = ((IPacket07LocalWorlds)pkt).worldsList;
										sock.close();
										open = false;
										failed = false;
									}else if(pkt instanceof IPacketFFErrorCode) {
										IPacketFFErrorCode ipkt = (IPacketFFErrorCode)pkt;
										if(ipkt.code == IPacketFFErrorCode.TYPE_PROTOCOL_VERSION) {
											String s = ipkt.desc.toLowerCase();
											if(s.contains("outdated client") || s.contains("client outdated")) {
												versError = RelayQuery.VersionMismatch.CLIENT_OUTDATED;
											}else if(s.contains("outdated server") || s.contains("server outdated") ||
													s.contains("outdated relay") || s.contains("server relay")) {
												versError = RelayQuery.VersionMismatch.RELAY_OUTDATED;
											}else {
												versError = RelayQuery.VersionMismatch.UNKNOWN;
											}
										}
										System.err.println(uri + ": Recieved query error code " + ipkt.code + ": " + ipkt.desc);
										open = false;
										failed = true;
										sock.close();
									}else {
										throw new IOException("Unexpected packet '" + pkt.getClass().getSimpleName() + "'");
									}
								} catch (IOException e) {
									System.err.println("Relay World Query Error: " + e.toString());
									e.printStackTrace();
									open = false;
									failed = true;
									sock.close();
								}
							}
						}
					}
				}
			});
			sock.addEventListener("close", new EventListener<CloseEvent>() {
				@Override
				public void handleEvent(CloseEvent evt) {
					open = false;
					if(!hasRecievedAnyData) {
						failed = true;
						Long l = relayQueryBlocked.get(uri);
						if(l != null) {
							if(steadyTimeMillis() - l.longValue() < 400000l) {
								rateLimitStatus = RateLimit.LOCKED;
								return;
							}
						}
						l = relayQueryLimited.get(uri);
						if(l != null) {
							if(steadyTimeMillis() - l.longValue() < 900000l) {
								rateLimitStatus = RateLimit.BLOCKED;
								return;
							}
						}
					}
				}
			});
		}

		@Override
		public boolean isQueryOpen() {
			return open;
		}

		@Override
		public boolean isQueryFailed() {
			return failed;
		}

		@Override
		public RateLimit isQueryRateLimit() {
			return rateLimitStatus;
		}

		@Override
		public void close() {
			if(open && sock != null) {
				sock.close();
			}
			open = false;
		}

		@Override
		public List<LocalWorld> getWorlds() {
			return worlds;
		}

		@Override
		public RelayQuery.VersionMismatch getCompatible() {
			return versError;
		}
		
	}
	
	private static class RelayWorldsQueryRatelimitDummy implements RelayWorldsQuery {
		
		private final RateLimit rateLimit;
		
		private RelayWorldsQueryRatelimitDummy(RateLimit rateLimit) {
			this.rateLimit = rateLimit;
		}

		@Override
		public boolean isQueryOpen() {
			return false;
		}

		@Override
		public boolean isQueryFailed() {
			return true;
		}

		@Override
		public RateLimit isQueryRateLimit() {
			return rateLimit;
		}

		@Override
		public void close() {
		}

		@Override
		public List<LocalWorld> getWorlds() {
			return new ArrayList<>(0);
		}

		@Override
		public RelayQuery.VersionMismatch getCompatible() {
			return RelayQuery.VersionMismatch.COMPATIBLE;
		}
	}
	
	public static final RelayWorldsQuery openRelayWorldsQuery(String addr) {
		long millis = steadyTimeMillis();
		
		Long l = relayQueryBlocked.get(addr);
		if(l != null && millis - l.longValue() < 60000l) {
			return new RelayWorldsQueryRatelimitDummy(RateLimit.LOCKED);
		}
		
		l = relayQueryLimited.get(addr);
		if(l != null && millis - l.longValue() < 10000l) {
			return new RelayWorldsQueryRatelimitDummy(RateLimit.BLOCKED);
		}
		
		return new RelayWorldsQueryImpl(addr);
	}
	
	private static class RelayServerSocketImpl implements RelayServerSocket {
		
		private final WebSocket sock;
		private final String uri;

		private boolean open;
		private boolean closed;
		private boolean failed;
		
		private boolean hasRecievedAnyData;

		private final List<Throwable> exceptions = new LinkedList<>();
		private final List<IPacket> packets = new LinkedList<>();
		
		private RelayServerSocketImpl(String uri, int timeout) {
			this.uri = uri;
			WebSocket s = null;
			try {
				s = new WebSocket(uri);
				s.setBinaryType("arraybuffer");
				open = false;
				closed = false;
				failed = false;
			}catch(Throwable t) {
				exceptions.add(t);
				sock = null;
				open = false;
				closed = true;
				failed = true;
				return;
			}
			sock = s;
			sock.addEventListener("open", new EventListener<Event>() {
				@Override
				public void handleEvent(Event evt) {
					open = true;
				}
			});
			sock.addEventListener("message", new EventListener<MessageEvent>() {
				@Override
				public void handleEvent(MessageEvent evt) {
					if(evt.getData() != null && !isString(evt.getData())) {
						hasRecievedAnyData = true;
						byte[] arr = TeaVMUtils.wrapByteArrayBuffer(evt.getDataAsArray());
						try {
							packets.add(IPacket.readPacket(new DataInputStream(new EaglerInputStream(arr))));
						} catch (IOException e) {
							exceptions.add(e);
							System.err.println("Relay Socket Error: " + e.toString());
							e.printStackTrace();
							open = false;
							failed = true;
							closed = true;
							sock.close();
						}
					}
				}
			});
			sock.addEventListener("close", new EventListener<CloseEvent>() {
				@Override
				public void handleEvent(CloseEvent evt) {
					if(!hasRecievedAnyData) {
						failed = true;
					}
					open = false;
					closed = true;
				}
			});
			Window.setTimeout(new TimerHandler() {

				@Override
				public void onTimer() {
					if(!open && !closed) {
						closed = true;
						sock.close();
					}
				}
				
			}, timeout);
		}

		@Override
		public boolean isOpen() {
			return open;
		}

		@Override
		public boolean isClosed() {
			return closed;
		}

		@Override
		public void close() {
			if(open && sock != null) {
				sock.close();
			}
			open = false;
			closed = true;
		}

		@Override
		public boolean isFailed() {
			return failed;
		}

		@Override
		public Throwable getException() {
			if(exceptions.size() > 0) {
				return exceptions.remove(0);
			}else {
				return null;
			}
		}

		@Override
		public void writePacket(IPacket pkt) {
			try {
				nativeBinarySend(sock, TeaVMUtils.unwrapArrayBuffer(IPacket.writePacket(pkt)));
			} catch (Throwable e) {
				System.err.println("Relay connection error: " + e.toString());
				e.printStackTrace();
				exceptions.add(e);
				failed = true;
				open = false;
				closed = true;
				sock.close();
			}
		}

		@Override
		public IPacket readPacket() {
			if(packets.size() > 0) {
				return packets.remove(0);
			}else {
				return null;
			}
		}

		@Override
		public IPacket nextPacket() {
			if(packets.size() > 0) {
				return packets.get(0);
			}else {
				return null;
			}
		}
		
		@Override
		public RateLimit getRatelimitHistory() {
			if(relayQueryBlocked.containsKey(uri)) {
				return RateLimit.LOCKED;
			}
			if(relayQueryLimited.containsKey(uri)) {
				return RateLimit.BLOCKED;
			}
			return RateLimit.NONE;
		}
		
		@Override
		public String getURI() {
			return uri;
		}
		
	}
	
	private static class RelayServerSocketRatelimitDummy implements RelayServerSocket {
		
		private final RateLimit limit;
		
		private RelayServerSocketRatelimitDummy(RateLimit limit) {
			this.limit = limit;
		}
		
		@Override
		public boolean isOpen() {
			return false;
		}

		@Override
		public boolean isClosed() {
			return true;
		}

		@Override
		public void close() {
		}

		@Override
		public boolean isFailed() {
			return true;
		}

		@Override
		public Throwable getException() {
			return null;
		}

		@Override
		public void writePacket(IPacket pkt) {
		}

		@Override
		public IPacket readPacket() {
			return null;
		}

		@Override
		public IPacket nextPacket() {
			return null;
		}
		
		@Override
		public RateLimit getRatelimitHistory() {
			return limit;
		}
		
		@Override
		public String getURI() {
			return "<disconnected>";
		}
		
	}
	
	public static final RelayServerSocket openRelayConnection(String addr, int timeout) {
		long millis = steadyTimeMillis();
		
		Long l = relayQueryBlocked.get(addr);
		if(l != null && millis - l.longValue() < 60000l) {
			return new RelayServerSocketRatelimitDummy(RateLimit.LOCKED);
		}
		
		l = relayQueryLimited.get(addr);
		if(l != null && millis - l.longValue() < 10000l) {
			return new RelayServerSocketRatelimitDummy(RateLimit.BLOCKED);
		}
		
		return new RelayServerSocketImpl(addr, timeout);
	}

	private static EaglercraftLANClient rtcLANClient = null;
	
	@JSBody(params = { }, script = "return window.startLANClient();")
	private static native EaglercraftLANClient startRTCLANClient();

	private static boolean clientLANinit = false;
	private static final List<byte[]> clientLANPacketBuffer = new ArrayList<>();
	
	private static String clientICECandidate = null;
	private static String clientDescription = null;
	private static boolean clientDataChannelOpen = false;
	private static boolean clientDataChannelClosed = true;
	
	public static final boolean clientLANSupported() {
		return rtcLANClient.LANClientSupported();
	}
	
	public static final int clientLANReadyState() {
		return rtcLANClient.getReadyState();
	}
	
	public static final void clientLANCloseConnection() {
		rtcLANClient.signalRemoteDisconnect(false);
	}
	
	public static final void clientLANSendPacket(byte[] pkt) {
		rtcLANClient.sendPacketToServer(TeaVMUtils.unwrapArrayBuffer(pkt));
	}
	
	public static final byte[] clientLANReadPacket() {
		return clientLANPacketBuffer.size() > 0 ? clientLANPacketBuffer.remove(0) : null;
	}
	
	public static final void clientLANSetICEServersAndConnect(String[] servers) {
		if(!clientLANinit) {
			clientLANinit = true;
			rtcLANClient.setDescriptionHandler(new EaglercraftLANClient.DescriptionHandler() {
				@Override
				public void call(String description) {
					clientDescription = description;
				}
			});
			rtcLANClient.setICECandidateHandler(new EaglercraftLANClient.ICECandidateHandler() {
				@Override
				public void call(String candidate) {
					clientICECandidate = candidate;
				}
			});
			rtcLANClient.setRemoteDataChannelHandler(new EaglercraftLANClient.ClientSignalHandler() {
				@Override
				public void call() {
					clientDataChannelClosed = false;
					clientDataChannelOpen = true;
				}
			});
			rtcLANClient.setRemotePacketHandler(new EaglercraftLANClient.RemotePacketHandler() {
				@Override
				public void call(ArrayBuffer buffer) {
					clientLANPacketBuffer.add(TeaVMUtils.wrapByteArrayBuffer(buffer));
				}
			});
			rtcLANClient.setRemoteDisconnectHandler(new EaglercraftLANClient.ClientSignalHandler() {
				@Override
				public void call() {
					clientDataChannelClosed = true;
				}
			});
		}
		rtcLANClient.setICEServers(servers);
		if(clientLANReadyState() == rtcLANClient.READYSTATE_CONNECTED || clientLANReadyState() == rtcLANClient.READYSTATE_CONNECTING) {
			rtcLANClient.signalRemoteDisconnect(true);
		}
		rtcLANClient.initializeClient();
		rtcLANClient.signalRemoteConnect();
	}

	public static final void clearLANClientState() {
		clientICECandidate = null;
		clientDescription = null;
		clientDataChannelOpen = false;
		clientDataChannelClosed = true;
	}
	
	public static final String clientLANAwaitICECandidate() {
		if(clientICECandidate != null) {
			String ret = clientICECandidate;
			clientICECandidate = null;
			return ret;
		}else {
			return null;
		}
	}
	
	public static final String clientLANAwaitDescription() {
		if(clientDescription != null) {
			String ret = clientDescription;
			clientDescription = null;
			return ret;
		}else {
			return null;
		}
	}
	
	public static final boolean clientLANAwaitChannel() {
		if(clientDataChannelOpen) {
			clientDataChannelOpen = false;
			return true;
		}else {
			return false;
		}
	}
	
	public static final boolean clientLANClosed() {
		return clientDataChannelClosed;
	}
	
	public static final void clientLANSetICECandidate(String candidate) {
		rtcLANClient.signalRemoteICECandidate(candidate);
	}
	
	public static final void clientLANSetDescription(String description) {
		rtcLANClient.signalRemoteDescription(description);
	}
	
	private static EaglercraftLANServer rtcLANServer = null;

	@JSBody(params = { }, script = "return window.startLANServer();")
	private static native EaglercraftLANServer startRTCLANServer();

	private static boolean serverLANinit = false;
	private static final List<LANPeerEvent> serverLANEventBuffer = new LinkedList<>();
	
	public static final boolean serverLANSupported() {
		return rtcLANServer.LANServerSupported();
	}
	
	public static final void serverLANInitializeServer(String[] servers) {
		serverLANEventBuffer.clear();
		rtcLANServer.setICEServers(servers);
		rtcLANServer.initializeServer();
		if(!serverLANinit) {
			serverLANinit = true;
			rtcLANServer.setDescriptionHandler(new EaglercraftLANServer.DescriptionHandler() {
				@Override
				public void call(String peerId, String description) {
					serverLANEventBuffer.add(new LANPeerEvent.LANPeerDescriptionEvent(peerId, description));
				}
			});
			rtcLANServer.setICECandidateHandler(new EaglercraftLANServer.ICECandidateHandler() {
				@Override
				public void call(String peerId, String candidate) {
					serverLANEventBuffer.add(new LANPeerEvent.LANPeerICECandidateEvent(peerId, candidate));
				}
			});
			rtcLANServer.setRemoteClientDataChannelHandler(new EaglercraftLANServer.ClientSignalHandler() {
				@Override
				public void call(String peerId) {
					serverLANEventBuffer.add(new LANPeerEvent.LANPeerDataChannelEvent(peerId));
				}
			});
			rtcLANServer.setRemoteClientPacketHandler(new EaglercraftLANServer.PeerPacketHandler() {
				@Override
				public void call(String peerId, ArrayBuffer buffer) {
					serverLANEventBuffer.add(new LANPeerEvent.LANPeerPacketEvent(peerId, TeaVMUtils.wrapByteArrayBuffer(buffer)));
				}
			});
			rtcLANServer.setRemoteClientDisconnectHandler(new EaglercraftLANServer.ClientSignalHandler() {
				@Override
				public void call(String peerId) {
					serverLANEventBuffer.add(new LANPeerEvent.LANPeerDisconnectEvent(peerId));
				}
			});
		}
	}

	public static final void serverLANCloseServer() {
		rtcLANServer.signalRemoteDisconnect("");
	}
	
	public static final LANPeerEvent serverLANGetEvent(String clientId) {
		if(serverLANEventBuffer.size() > 0) {
			Iterator<LANPeerEvent> i = serverLANEventBuffer.iterator();
			while(i.hasNext()) {
				LANPeerEvent evt = i.next();
				if(evt.getPeerId().equals(clientId)) {
					i.remove();
					return evt;
				}
			}
			return null;
		}else {
			return null;
		}
	}
	
	public static final List<LANPeerEvent> serverLANGetAllEvent(String clientId) {
		if(serverLANEventBuffer.size() > 0) {
			List<LANPeerEvent> lst = null;
			Iterator<LANPeerEvent> i = serverLANEventBuffer.iterator();
			while(i.hasNext()) {
				LANPeerEvent evt = i.next();
				if(evt.getPeerId().equals(clientId)) {
					i.remove();
					if(lst == null) {
						lst = new ArrayList<>();
					}
					lst.add(evt);
				}
			}
			return lst;
		}else {
			return null;
		}
	}

	private static final int fragmentSize = 0xFF00;

	public static final void serverLANWritePacket(String peer, byte[] data) {
		if (data.length > fragmentSize) {
			for (int i = 0; i < data.length; i += fragmentSize) {
				byte[] fragData = new byte[((i + fragmentSize > data.length) ? (data.length % fragmentSize) : fragmentSize) + 1];
				System.arraycopy(data, i, fragData, 1, fragData.length - 1);
				fragData[0] = (i + fragmentSize < data.length) ? (byte) 1 : (byte) 0;
				rtcLANServer.sendPacketToRemoteClient(peer, TeaVMUtils.unwrapArrayBuffer(fragData));
			}
		} else {
			byte[] sendData = new byte[data.length + 1];
			sendData[0] = 0;
			System.arraycopy(data, 0, sendData, 1, data.length);
			rtcLANServer.sendPacketToRemoteClient(peer, TeaVMUtils.unwrapArrayBuffer(sendData));
		}
	}
	
	public static final void serverLANCreatePeer(String peer) {
		rtcLANServer.signalRemoteConnect(peer);
	}
	
	public static final void serverLANPeerICECandidates(String peer, String iceCandidates) {
		rtcLANServer.signalRemoteICECandidate(peer, iceCandidates);
	}
	
	public static final void serverLANPeerDescription(String peer, String description) {
		rtcLANServer.signalRemoteDescription(peer, description);
	}
	
	public static final void serverLANDisconnectPeer(String peer) {
		rtcLANServer.signalRemoteDisconnect(peer);
	}
	
	public static final int countPeers() {
		return rtcLANServer.countPeers();
	}

	private static final JSObject steadyTimeFunc = getSteadyTimeFunc();

	@JSBody(params = { }, script = "return ((typeof performance !== \"undefined\") && (typeof performance.now === \"function\"))"
			+ "? performance.now.bind(performance)"
			+ ": (function(epochStart){ return function() { return Date.now() - epochStart; }; })(Date.now());")
	private static native JSObject getSteadyTimeFunc();

	@JSBody(params = { "steadyTimeFunc" }, script = "return steadyTimeFunc();")
	private static native double steadyTimeMillis0(JSObject steadyTimeFunc);

	public static long steadyTimeMillis() {
		return (long)steadyTimeMillis0(steadyTimeFunc);
	}

	public static long nanoTime() {
		return (long)(steadyTimeMillis0(steadyTimeFunc) * 1000000.0);
	}

	@Async
	public static native void sleep(int millis);

	private static void sleep(int millis, final AsyncCallback<Void> callback) {
		Platform.schedule(new DumbSleepHandler(callback), millis);
	}

	private static class DumbSleepHandler implements PlatformRunnable {
		private final AsyncCallback<Void> callback;
		private DumbSleepHandler(AsyncCallback<Void> callback) {
			this.callback = callback;
		}
		@Override
		public void run() {
			callback.complete(null);
		}
	}

}
