function getBrowserSize() {
    var actualWidth = window.innerWidth ||
                      document.documentElement.clientWidth ||
                      document.body.clientWidth ||
                      document.body.offsetWidth;
    var actualHeight = window.innerHeight ||
                      document.documentElement.clientHeight ||
                      document.body.clientHeight ||
                      document.body.offsetHeight;
    return { "width": actualWidth, "height": actualHeight };
}

function getContext(canvas) {
    var settings = { preserveDrawingBuffer: true };
    gl = canvas.getContext("webgl", settings) ||
         canvas.getContext("experimental-webgl", settings);
    return gl;
}

function physicalSizeRatio() {
    var div = document.createElement("div");
    div.style.width = "1mm";
    div.style.height = "1mm";
    var body = document.getElementsByTagName("body")[0];
    body.appendChild(div);
    var physicalWidth = document.defaultView.getComputedStyle(div, null).getPropertyValue('width');
    var physicalHeight = document.defaultView.getComputedStyle(div, null).getPropertyValue('height');
    body.removeChild(div);
    return {
        "width": parseFloat(physicalWidth),
        "height": parseFloat(physicalHeight)
    };
}

window.onload = function () {
    var DEBUG = 0;
    var MOUSETRACKING = 0;
    var LOADINGSCREEN = 1;
    var canvas;
    var socket = new WebSocket("ws://" + host + ":" + port);
    socket.binaryType = "arraybuffer";
    var CONNECT_SERIAL = 666;
    var gl;
    var startTime = new Date();
    // There is no way to get proper vsync since we have no idea when the real
    // swap happens under the hood. What we can do is to delay the response for
    // the eglSwapBuffer call, i.e. block the client for the given number of
    // milliseconds on each swap.
    var SWAP_DELAY = 16; //${swap_delay};
    var contextData = { }; // context -> { shaderMap, programMap, ... }
    var currentContext = 0;
    var currentWindowId = "";
    var windowData = {};
    var currentZIndex = 1;
    var textDecoder;
    var initialLoadingCanvas;
    var supportsTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints;

    if (typeof TextDecoder !== 'undefined') {
        textDecoder = new TextDecoder("utf8");
    } else {
    textDecoder = {
            "decode": function (buffer)
            {
                var string = String.fromCharCode.apply(String, buffer);
                return string;
            }
        };
    }
    var supportedFunctions;

    var sendObject = function (obj) { socket.send(JSON.stringify(obj)); };

    var connect = function () {
        var size = getBrowserSize();
        var width = size.width;
        var height = size.height;
        var physicalSize = physicalSizeRatio();

        var object = { "type": "connect",
            "width": width, "height": height,
            "physicalWidth": width / physicalSize.width,
            "physicalHeight": height / physicalSize.height
        };
        sendObject(object);
        initialLoadingCanvas = createLoadingCanvas('loadingCanvas', 0, 0, width, height);
    };

    var sendResponse = function (id, value) {
        if (DEBUG)
            console.log("Response to " + id + " = " + value);
        sendObject({ "type": "gl_response", "id": id, "value": value });
    };

    var createLoadingCanvas = function(name, x, y, width, height) {
        var canvas = document.createElement("canvas");
        canvas.id = "loading_" + name;
        canvas.style.position = "absolute";
        canvas.style.left = x + "px";
        canvas.style.top = y + "px";
        canvas.style.width = width + "px";
        canvas.style.height = height + "px";
        canvas.style.zIndex = currentZIndex++;
        canvas.style.background = "black";
        canvas.width = width;
        canvas.height = height;
        var body = document.getElementsByTagName("body")[0];
        body.appendChild(canvas);

        if (!LOADINGSCREEN)
            return canvas;
        var gl = canvas.getContext("webgl");

        var loadingVertexShaderSource =
            "attribute vec2 a_position; void main(){ gl_Position = vec4(a_position, 0, 1); }";
        var loadingFragmentShaderSource =
            "precision mediump float;\n"+
            "#define PI 3.14159265\n"+
            "#define QT_COLOR vec4(0.255,0.804,0.321,1.0)\n"+
            "#define GRAY vec4(0.953,0.953,0.957,1.0)\n"+
            "uniform float u_time;\n"+
            "uniform vec2 u_size;\n"+
            "vec4 loadingIndicator(vec2 uv, float time)\n"+
            "{\n"+
            "    float l = length(uv);\n"+
            "    float theta = atan(uv.y,uv.x)/PI/2.0 + 0.5;\n"+
            "    float radius = 0.2;\n"+
            "    float thickness = 0.02;\n"+
            "    float edge = 3.0 / length(u_size.xy);\n"+
            "    float t = fract(-time*2.0);\n"+
            "    float circleVal = smoothstep(radius - edge - thickness, radius - thickness, l) * \n"+
            "                     (1.0 - smoothstep(radius + thickness, radius + edge + thickness, l));\n"+
            "    float indicatorVal = smoothstep(0.245,0.25,fract(theta - t)) * (1.0 - smoothstep(0.995,1.0,fract(theta - t)));\n"+
            "    return (1.0 - indicatorVal) * QT_COLOR + indicatorVal * circleVal * GRAY + vec4(1.0,1.0,1.0,1.0) * (1.0 - circleVal);\n"+
            "}\n"+
            "void main()\n" +
            "{\n"+
            "    vec2 aspect = vec2(u_size.x/u_size.y, 1.0);\n"+
            "    vec2 origin = aspect*vec2(0.5, 0.5);\n"+
            "    vec2 uv = aspect*gl_FragCoord.xy;\n"+
            "    gl_FragColor = loadingIndicator(uv/u_size - origin,u_time); \n"+
            "}\n";

        var vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, loadingVertexShaderSource);
        gl.compileShader(vertexShader);

        var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShader, loadingFragmentShaderSource);
        gl.compileShader(fragmentShader);

        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        gl.useProgram(program);

        // look up where the vertex data needs to go.
        var positionLocation = gl.getAttribLocation(program, "a_position");
        var timeLocation = gl.getUniformLocation(program, "u_time");
        var sizeLocation = gl.getUniformLocation(program, "u_size");

        // Create a buffer and put a single clipspace rectangle in
        // it (2 triangles)
        var buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array([
                -1.0, -1.0,
                 1.0, -1.0,
                -1.0,  1.0,
                -1.0,  1.0,
                 1.0, -1.0,
                 1.0,  1.0]),
            gl.STATIC_DRAW);
        gl.enableVertexAttribArray(positionLocation);
        gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
        gl.uniform2fv(sizeLocation, new Float32Array([canvas.width, canvas.height]));
        var time = 0.0;

        function draw() {
            gl.uniform1f(timeLocation, time);
            time += 0.01;
            gl.drawArrays(gl.TRIANGLES, 0, 6);
        }
        canvas.timerId = setInterval(draw, 16);
        return canvas;
    };

    var createCanvas = function (name, x, y, width, height, title) {
        var body = document.getElementsByTagName("body")[0];
        if (initialLoadingCanvas) {
            clearInterval(initialLoadingCanvas.timerId);
            body.removeChild(initialLoadingCanvas);
            initialLoadingCanvas = undefined;
        }
        var canvas = document.createElement("canvas");
        canvas.id = name;
        canvas.style.position = "absolute";
        canvas.style.left = x + "px";
        canvas.style.top = y + "px";
        canvas.style.width = width + "px";
        canvas.style.height = height + "px";
        canvas.style.zIndex = currentZIndex++;
        canvas.width = width;
        canvas.height = height;
        body.appendChild(canvas);

        var qtButtons = 0;
        var sendMouseEvent = function (buttons, layerX, layerY, clientX, clientY, name) {
            var object = { "type": "mouse",
                "buttons": buttons,
                "layerX": layerX, "layerY": layerY, "clientX": clientX, "clientY": clientY,
                "time": new Date().getTime().toString(),
                "name": name
            };
            sendObject(object);
        };

        var mapButton = function (b) {
            var qtb = 1;
            if (b === 1)
                qtb = 4;
            else if (b === 2)
                qtb = 2;
            return qtb;
        };

        canvas.onmousedown = function (event) {
            /* jslint bitwise: true */
            if (supportsTouch && event.mozInputSource == MOZ_SOURCE_TOUCH)
                return;
            qtButtons |= mapButton(event.button);
            sendMouseEvent(qtButtons, event.layerX, event.layerY, event.clientX, event.clientY,
                           name);
        };

        canvas.onmousemove = function (event) {
            if (supportsTouch && event.mozInputSource == MOZ_SOURCE_TOUCH)
                return;
            if (MOUSETRACKING || event.buttons > 0)
                sendMouseEvent(qtButtons, event.layerX, event.layerY, event.clientX, event.clientY,
                               name);
        };

        canvas.onmouseup = function (event) {
            /* jslint bitwise: true */
            if (supportsTouch && event.mozInputSource == MOZ_SOURCE_TOUCH)
                return;
            qtButtons &= ~mapButton(event.button);
            sendMouseEvent(qtButtons, event.layerX, event.layerY, event.clientX, event.clientY,
                           name);
        };

        function handleMouseWheel(event) {
            var deltaY = 0;
            if (!event)
                deltaY = window.event;
            if (event.deltaY)
                deltaY = event.deltaY;
            else if (event.detail)
                deltaY = event.detail * 40;
            if (deltaY) {
                var object = { "type": "wheel",
                    "layerX": event.layerX, "layerY": event.layerY,
                    "clientX": event.clientX, "clientY": event.clientY,
                    "deltaX": event.deltaX, "deltaY": deltaY, "deltaZ": event.deltaZ,
                    "time": new Date().getTime(),
                    "name": name
                };
                sendObject(object);
            }
            if (event.preventDefault)
                event.preventDefault();
            event.returnValue = false;
        }

        // Internet Explorer, Opera, Chrome and Safari
        canvas.addEventListener('mousewheel', handleMouseWheel, { passive: true });
        // Firefox
        canvas.addEventListener('DOMMouseScroll', handleMouseWheel, { passive: true });

        function handleTouch(event) {
            var object = {
                "type": "touch",
                "name": name,
                "time": new Date().getTime().toString(),
                "event": event.type,
                "changedTouches": [],
                "stationaryTouches": [],
            };
            var addTouch = function(changedTouch, isChanged) {
                var touch = {
                    "clientX": changedTouch.clientX,
                    "clientY": changedTouch.clientY,
                    "force": changedTouch.force,
                    "identifier": changedTouch.identifier,
                    "pageX": changedTouch.pageX,
                    "pageY": changedTouch.pageY,
                    "radiusX": changedTouch.radiusX,
                    "radiusY": changedTouch.radiusY,
                    "screenX": changedTouch.screenX,
                    "screenY": changedTouch.screenY,
                    "normalPositionX": changedTouch.screenX / screen.width,
                    "normalPositionY": changedTouch.screenY / screen.height,
                };
                if (isChanged)
                    object.changedTouches.push(touch);
                else
                    object.stationaryTouches.push(touch);
            };

            for (var i = 0; i < event.changedTouches.length; ++i) {
                var changedTouch = event.changedTouches[i];
                addTouch(changedTouch, true);
            }

            for (var i = 0; i < event.targetTouches.length; ++i) {
                var targetTouch = event.targetTouches[i];
                if (object.changedTouches.findIndex(function(touch){
                    return touch.identifier === targetTouch.identifier;
                }) === -1) {
                    addTouch(targetTouch, false);
                }
            }

            sendObject(object);

            if (event.preventDefault && event.cancelable)
                event.preventDefault();
            event.returnValue = false;
        }

        canvas.addEventListener("touchstart", handleTouch, { passive: true });
        canvas.addEventListener("touchend", handleTouch, { passive: true });
        canvas.addEventListener("touchcancel", handleTouch, { passive: true });
        canvas.addEventListener("touchmove", handleTouch, { passive: true });

        canvas.oncontextmenu = function (event) {
            event.preventDefault();
        };


        var gl = getContext(canvas);
        /* jslint bitwise: true */
        gl.clear([ gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT]);
        windowData[name] = {
            "canvas": canvas,
            "gl": gl,
            "loadingCanvas": createLoadingCanvas(name, x, y, width, height)
        };

        var defaultValuesObject = { "type": "default_context_parameters", "name": name,
            "7939": "GL_OES_element_index_uint GL_OES_standard_derivatives " + // GL_EXTENSIONS
                    "GL_OES_depth_texture GL_OES_packed_depth_stencil" };
        [
            gl.BLEND,
            gl.DEPTH_TEST,
            gl.MAX_TEXTURE_SIZE,
            gl.MAX_VERTEX_ATTRIBS,
            gl.RENDERER,
            gl.SCISSOR_TEST,
            gl.STENCIL_TEST,
            gl.UNPACK_ALIGNMENT,
            gl.VENDOR,
            gl.VERSION,
            gl.VIEWPORT
        ].forEach(function (value) {
            defaultValuesObject[value] = gl.getParameter(value);
        });
        sendObject(defaultValuesObject);

        gl._attachShader = gl.attachShader;
        gl.attachShader = function(program, shader) {
            var d = contextData[currentContext];
            gl._attachShader(d.programMap[program], d.shaderMap[shader].shader);
        };

        gl._bindAttribLocation = gl.bindAttribLocation;
        gl.bindAttribLocation = function(program, index, name) {
            var d = contextData[currentContext];
            gl._bindAttribLocation(d.programMap[program], index, name);
        };

        gl._bindBuffer = gl.bindBuffer;
        gl.bindBuffer = function(target, buffer) {
            var d = contextData[currentContext];
            gl._bindBuffer(target, buffer ? d.bufferMap[buffer] : null);
        };

        gl._bindFramebuffer = gl.bindFramebuffer;
        gl.bindFramebuffer = function(target, framebuffer) {
            var d = contextData[currentContext];
            gl._bindFramebuffer(target, framebuffer ? d.framebufferMap[framebuffer] : null);
        };

        gl._bindRenderbuffer = gl.bindRenderbuffer;
        gl.bindRenderbuffer = function(target, renderbuffer) {
            var d = contextData[currentContext];
            gl._bindRenderbuffer(target, renderbuffer ? d.renderbufferMap[renderbuffer] : null);
            d.boundRenderbuffer = renderbuffer;
        };

        gl._bindTexture = gl.bindTexture;
        gl.bindTexture = function(target, texture) {
            gl._bindTexture(target, texture ? mapTexture(currentContext, texture) : null);
        };

        gl._bufferData = gl.bufferData;
        gl.bufferData = function(target, usage, size, data) {
            gl._bufferData(target, data === null || data.length === 0 ? size : data, usage);
        };

        gl._clearColor = gl.clearColor;
        gl.clearColor = function (red, green, blue, alpha) {
            gl._clearColor(red, green, blue, alpha);
        };

        gl.clearDepthf = function(depth) {
            gl.clearDepth(depth);
        };

        gl._compileShader = gl.compileShader;
        gl.compileShader = function(remoteShader) {
            var d = contextData[currentContext];
            gl._compileShader(d.shaderMap[remoteShader].shader);
        };

        gl._createProgram = gl.createProgram;
        gl.createProgram = function() {
            var d = contextData[currentContext];
            var remoteProgram = d.nextProgramId++;
            var localProgram = gl._createProgram();
            d.programMap[remoteProgram] = localProgram;
            return remoteProgram;
        };

        gl._createShader = gl.createShader;
        gl.createShader = function(type) {
            var d = contextData[currentContext];
            var remoteShader = d.nextShaderId++;
            var localShader = gl._createShader(type);
            d.shaderMap[remoteShader] = { };
            d.shaderMap[remoteShader].shader = localShader;
            d.shaderMap[remoteShader].source = "";
            return remoteShader;
        };

        gl.deleteBuffers = function(n) {
            var d = contextData[currentContext];
            for (var i = 0; i < n; ++i)
                gl.deleteBuffer(d.bufferMap[arguments[1 +i]]);
        };

        gl.deleteFramebuffers = function(framebuffers) {
            var d = contextData[currentContext];
            for (var i in framebuffers)
                gl.deleteFramebuffer(d.framebufferMap[framebuffers[i]]);
        };

        gl._deleteProgram = gl.deleteProgram;
        gl.deleteProgram = function(program) {
            var d = contextData[currentContext];
            gl._deleteProgram(d.programMap[program]);
        };

        gl.deleteRenderbuffers = function(renderbuffers) {
            var d = contextData[currentContext];
            for (var i in renderbuffers)
                gl.deleteRenderbuffer(d.renderbufferMap[renderbuffers[i]]);
        };

        gl._deleteShader = gl.deleteShader;
        gl.deleteShader = function(remoteShader) {
            var d = contextData[currentContext];
            gl._deleteShader(d.shaderMap[remoteShader].shader);
        };

        gl.deleteTextures = function(textures) {
            for (var i in textures)
                gl.deleteTexture(mapTexture(currentContext, textures[i]));
        };

        gl._drawElements = gl.drawElements;
        gl.drawElements = function(mode, count, type, n, indices, offset) {
            if (!arguments[3].length)
                gl._drawElements(mode, count, type, offset);
            else
                console.error("fixme: Client-side drawElements not supported");
        };

        gl._framebufferRenderbuffer = gl.framebufferRenderbuffer;
        gl.framebufferRenderbuffer = function(target, attachment, renderbuffertarget, renderbuffer)
        {
            var d = contextData[currentContext];
            // With packed depth-stencil Quick tries to attach the same renderbuffer to both
            // the depth and stencil attachment points. WebGL does not allow this. Instead,
            // we need to attach to the DEPTH_STENCIL attachment point.
            if (d.renderbufferFormat[d.boundRenderbuffer] === gl.DEPTH_STENCIL) {
                if (attachment === gl.STENCIL_ATTACHMENT) {
                    attachment = gl.DEPTH_STENCIL_ATTACHMENT;
                } else {
                    // Ignore this call. Qt Quick will send a new call with STENCIL_ATTACHMENT
                    // parameter, it will be replaced by DEPTH_STENCIL_ATTACHMENT to work-around the
                    // browser limitation.
                    return;
                }
            }
            gl._framebufferRenderbuffer(target, attachment, renderbuffertarget,
                                        d.renderbufferMap[renderbuffer]);
        };

        gl.genBuffers = function(n) {
            var d = contextData[currentContext];
            var data = [];
            for (var i = 0; i < n; ++i) {
                var remoteBuf = d.nextBufferId++;
                var localBuf = gl.createBuffer();
                data.push(remoteBuf);
                d.bufferMap[remoteBuf] = localBuf;
            }
            return data;
        };

        gl.genFramebuffers = function(n) {
            var d = contextData[currentContext];
            var data = [];
            for (var i = 0; i < n; ++i) {
                var remoteFramebuffer = d.nextFramebufferId++;
                var localFramebuffer = gl.createFramebuffer();
                d.framebufferMap[remoteFramebuffer] = localFramebuffer;
                data.push(remoteFramebuffer);
            }
            return data;
        };

        gl.genRenderbuffers = function(n) {
            var d = contextData[currentContext];
            var data = [];
            for (var i = 0; i < n; ++i) {
                var remoteRenderBuffer = d.nextRenderBufferId++;
                var localRenderBuffer = gl.createRenderbuffer();
                d.renderbufferMap[remoteRenderBuffer] = localRenderBuffer;
                data.push(remoteRenderBuffer);
            }
            return data;
        };

        gl.getAttachedShaders = function(program, maxCount) {
            var d = contextData[currentContext];
            var shaders = d.attachedShaderMap[program];
            var data = [];
            for (var shader in shaders) {
                for (var remoteShaderId in shaderMap) {
                    if (shaderMap[remoteShaderId].shader === shader)
                        data.push(remoteShaderId);
                }
            }
            return data;
        };

        gl._getAttribLocation = gl.getAttribLocation;
        gl.getAttribLocation = function(program, name) {
            if (typeof program === "object")
                return gl._getAttribLocation(program, name);
            var d = contextData[currentContext];
            return gl._getAttribLocation(d.programMap[program], name);
        };

        gl.getBooleanv = gl.getParameter;
        gl.getIntegerv = gl.getParameter;

        gl.getProgramiv = function(program, pname) {
            var d = contextData[currentContext];
            if (pname === 0x8B84) // INFO_LOG_LENGTH
                return gl._getProgramInfoLog(d.programMap[program]).length;
            else
                return gl.getProgramParameter(d.programMap[program], pname);
        };

        gl._getShaderInfoLog = gl.getShaderInfoLog;
        gl.getShaderInfoLog = function(shader) {
            if (typeof shader === "object")
                return gl._getShaderInfoLog(shader);
            var d = contextData[currentContext];
            return gl._getShaderInfoLog(d.shaderMap[shader].shader);
        };

        gl.getShaderiv = function(shader, pname) {
            var d = contextData[currentContext];
            if (pname === 0x8B88)
                return d.shaderMap[shader].source.length;
            else
                return gl.getShaderParameter(d.shaderMap[shader].shader, pname);
        };

        gl._getShaderSource = gl.getShaderSource;
        gl.getShaderSource = function(remoteShader) {
            var d = contextData[currentContext];
            return gl._getShaderSource(d.shaderMap[remoteShader].shader);
        };

        gl.getString = function(pname) {
            return gl.getParameter(pname);
        };

        gl._getProgramInfoLog = gl.getProgramInfoLog;
        gl.getProgramInfoLog = function(remoteProgram) {
            var d = contextData[currentContext];
            var localProgram = d.programMap[remoteProgram];
            return gl._getProgramInfoLog(localProgram);
        };

        gl._getUniformLocation = gl.getUniformLocation;
        gl.getUniformLocation = function(program, name) {
            if (typeof program === "object") {
                return gl._getUniformLocation(program, name);
            } else {
                var d = contextData[currentContext];
                var p = gl._getUniformLocation(d.programMap[program], name);
                var location = -1;
                if (p) {
                    location = d.nextLocation++;
                    d.uniformLocationMap[location] = p;
                }
                return location;
            }
        };

        gl.genTextures = function(n) {
            var d = contextData[currentContext];
            var data = [];
            for (var i = 0; i < n; ++i) {
                var remoteTexture = d.nextTextureId++;
                var localTexture = gl.createTexture();
                d.textureMap[remoteTexture] = localTexture;
                data.push(remoteTexture);
            }
            return data;
        };

        gl._framebufferTexture2D = gl.framebufferTexture2D;
        gl.framebufferTexture2D = function(target, attachment, texTarget, texture, level) {
            var d = contextData[currentContext];
            gl._framebufferTexture2D(target, attachment, texTarget, d.textureMap[texture], level);
        };

        gl._isRenderbuffer = gl.isRenderbuffer;
        gl.isRenderbuffer = function(renderbuffer) {
            if (typeof renderbuffer === "object")
                return gl._isRenderBuffer(renderbuffer);
            var d = contextData[currentContext];
            return gl._isRenderbuffer(d.renderbufferMap[renderbuffer]);
        };

        gl._linkProgram = gl.linkProgram;
        gl.linkProgram = function(program) {
            var d = contextData[currentContext];
            gl._linkProgram(d.programMap[program]);
        };

        gl._renderbufferStorage = gl.renderbufferStorage;
        gl.renderbufferStorage = function(target, internalFormat, width, height) {
            var d = contextData[currentContext];
            if (internalFormat === 0x88F0) // GL_DEPTH24_STENCIL8_OES
                internalFormat = 0x84F9; // GL_DEPTH_STENCIL_OES
            d.renderbufferFormat[d.boundRenderbuffer] = internalFormat;
            gl._renderbufferStorage(target, internalFormat, width, height);
        };

        gl._texImage2D = gl.texImage2D;
        gl.texImage2D = function(target, level, internalFormat, width, height, border, format, type,
                                 data) {
            var dataSize = data ? data.byteLength : 0;
            var pixels;
            if (data === null || dataSize === 0)
                pixels = null;
            else if (type === gl.UNSIGNED_BYTE)
                pixels = data;
            else if (type === g.UNSIGNED_SHORT_5_6_5 || type === UNSIGNED_SHORT_4_4_4_4 ||
                       type === UNSIGNED_SHORT_5_5_5_1)
                pixels = new DataView(new Uint16Array(data));
            else
                console.error("gl.texImage2D: Unsupported type");
            gl._texImage2D(target, level, internalFormat, width, height, border, format, type,
                           pixels);
        };

        gl._texSubImage2D = gl.texSubImage2D;
        gl.texSubImage2D = function(target, level, xoffset, yoffset, width, height, format, type, data) {
            var dataSize = data ? data.byteLength : 0;
            var pixels;
            if (data === null || dataSize === 0)
                pixels = null;
            if (type === gl.UNSIGNED_BYTE)
                pixels = data;
            else if (type === gl.UNSIGNED_SHORT_5_6_5 || type === gl.UNSIGNED_SHORT_4_4_4_4 ||
                     type === gl.UNSIGNED_SHORT_5_5_5_1 || type === ext.HALF_FLOAT_OES)
                pixels = new Uint16Array(pixels);
            else if (type === gl.FLOAT)
                pixels = new Float32Array(pixels);
            else
                console.error("gl.texSubImage2D: Unsupported type");
            gl._texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
        };

        gl._shaderSource = gl.shaderSource;
        gl.shaderSource = function(shader, source) {
            var d = contextData[currentContext];
            d.shaderMap[shader].source = source;
            gl._shaderSource(d.shaderMap[shader].shader, d.shaderMap[shader].source);
        };

        gl._uniform1f = gl.uniform1f;
        gl.uniform1f = function(location, x) {
            var d = contextData[currentContext];
            gl._uniform1f(d.uniformLocationMap[location], x);
        };

        gl._uniform1fv = gl.uniform1fv;
        gl.uniform1fv = function(location, value) {
            var d = contextData[currentContext];
            gl._uniform1fv(d.uniformLocationMap[location], value);
        };

        gl._uniform2fv = gl.uniform2fv;
        gl.uniform2fv = function(location, value) {
            var d = contextData[currentContext];
            gl._uniform2fv(d.uniformLocationMap[location], value);
        };

        gl._uniform3fv = gl.uniform3fv;
        gl.uniform3fv = function(location, value) {
            var d = contextData[currentContext];
            gl._uniform3fv(d.uniformLocationMap[location], value);
        };

        gl._uniform1i = gl.uniform1i;
        gl.uniform1i = function(location, value) {
            var d = contextData[currentContext];
            gl._uniform1i(d.uniformLocationMap[location], value);
        };

        gl._uniform4fv = gl.uniform4fv;
        gl.uniform4fv = function(location, value) {
            var d = contextData[currentContext];
            gl._uniform4fv(d.uniformLocationMap[location], value);
        };

        gl._uniformMatrix3fv = gl.uniformMatrix3fv;
        gl.uniformMatrix3fv = function(location, transpose, data) {
            var d = contextData[currentContext];
            gl._uniformMatrix3fv(d.uniformLocationMap[location], transpose, data);
        };

        gl._uniformMatrix4fv = gl.uniformMatrix4fv;
        gl.uniformMatrix4fv = function(location, transpose, data) {
            var d = contextData[currentContext];
            gl._uniformMatrix4fv(d.uniformLocationMap[location], transpose, data);
        };

        gl._useProgram = gl.useProgram;
        gl.useProgram = function(program) {
            var d = contextData[currentContext];
            gl._useProgram(program !== 0 ? d.programMap[program] : null);
        };

        gl._vertexAttrib1fv = gl.vertexAttrib1fv;
        gl.vertexAttrib1fv = function (index, v0) {
            var values = new Float32Array([v0]);
            gl._vertexAttrib1fv(index, values);
        };

        gl._vertexAttrib2fv = gl.vertexAttrib2fv;
        gl.vertexAttrib2fv = function (index, v0, v1) {
            var values = new Float32Array([v0, v1]);
            gl._vertexAttrib2fv(index, values);
        };

        gl._vertexAttrib3fv = gl.vertexAttrib3fv;
        gl.vertexAttrib3fv = function (index, v0, v1, v2) {
            var values = new Float32Array([v0, v1, v2]);
            gl._vertexAttrib3fv(index, values);
        };

        gl._drawArrays = gl.drawArrays;
        gl.drawArrays = function (mode, first, count/*, size*/) {
            var d = contextData[currentContext];
            var subDataParts = [];
            var bufferSize = 0;
            var offset = 0;
            if (!d.drawArrayBuf)
                d.drawArrayBuf = gl.createBuffer();
            gl._bindBuffer(gl.ARRAY_BUFFER, d.drawArrayBuf);
            for (var i = 4; i < arguments.length; i += 6) {
                var subData = {
                    "index": arguments[i + 0],
                    "size": arguments[i + 1],
                    "type": arguments[i + 2],
                    "normalized": arguments[i + 3],
                    "stride": arguments[i + 4],
                    "offset": 0,
                    "data": arguments[i + 5],
                };
                subDataParts.push(subData);
                bufferSize += subData.data.length;
            }
            gl._bufferData(gl.ARRAY_BUFFER, bufferSize, gl.STATIC_DRAW);
            for (var part in subDataParts) {
                gl.bufferSubData(gl.ARRAY_BUFFER, offset, subDataParts[part].data);
                gl._vertexAttribPointer(subDataParts[part].index,
                                        subDataParts[part].size,
                                        subDataParts[part].type,
                                        subDataParts[part].normalized,
                                        subDataParts[part].stride,
                                        offset);
                offset += subDataParts[part].data.length;
            }
            gl._drawArrays(mode, first, count);
        };

        gl._vertexAttribPointer = gl.vertexAttribPointer;
        gl.vertexAttribPointer = function (index, size, type, normalized, stride, pointer) {
            gl._vertexAttribPointer(index, size, type, normalized, stride, pointer);
        };
    };

    var commandsNeedingResponse = {
        "swapBuffers": undefined,
        "checkFramebufferStatus": undefined,
        "createProgram": undefined,
        "createShader": undefined,
        "genBuffers": undefined,
        "genFramebuffers": undefined,
        "genRenderbuffers": undefined,
        "genTextures": undefined,
        "getAttachedShaders": undefined,
        "getAttribLocation": undefined,
        "getBooleanv": undefined,
        "getError": undefined,
        "getFramebufferAttachmentParameteriv": undefined,
        "getIntegerv": undefined,
        "getParameter": undefined,
        "getProgramInfoLog": undefined,
        "getProgramiv": undefined,
        "getRenderbufferParameteriv": undefined,
        "getShaderiv": undefined,
        "getShaderPrecisionFormat": undefined,
        "getString": undefined,
        "getTexParameterfv": undefined,
        "getTexParameteriv": undefined,
        "getUniformfv": undefined,
        "getUniformLocation": undefined,
        "getUniformiv": undefined,
        "getVertexAttribfv": undefined,
        "getVertexAttribiv": undefined,
        "getShaderSource": undefined,
        "getShaderInfoLog": undefined,
        "isRenderbuffer": undefined
    };

    var ensureContextData = function (context) {
        if (!(context in contextData)) {
            contextData[context] = {
                shaderMap: { },
                programMap: { },
                textureMap: { },
                framebufferMap: { },
                renderbufferMap: { },
                renderbufferFormat: { },
                boundRenderbuffer: 0,
                bufferMap: { },
                uniformLocationMap: { },
                nextLocation: 1,
                nextBufferId: 1,
                nextProgramId: 1,
                nextShaderId: 1,
                nextFramebufferId: 1,
                nextRenderBufferId: 1,
                nextTextureId: 1,
                pendingBinary: [],
                drawArrayBuf: null,
                drawArrayBufSize: 0,
                glCommands: [],
                attribData: []
            };
        }
    };

    var mapTexture = function (context, localId) {
        var d = contextData[context];
        if (localId in d.textureMap)
            return d.textureMap[localId];
        // ### do we need sharing?
        console.error("Texture " + localId + " is not found in context " + context);
        return 0;
    };

    var execGL = function (context) {
        var d = contextData[context];
        if (DEBUG)
            console.log("executing " + d.glCommands.length + " commands");
        while (d.glCommands.length) {
            var obj = d.glCommands.shift();
            if (DEBUG) {
                var parameters = [];
                for (var key in obj.parameters) {
                    if (typeof obj.parameters[key] === 'number' &&
                        d.glConstants[obj.parameters[key]] !== undefined) {
                        parameters[key] = d.glConstants[obj.parameters[key]] + ' (' +
                                          obj.parameters[key] + ')';
                    } else {
                        parameters[key] = obj.parameters[key];
                    }
                }
                console.log("Calling: gl." + obj.function, parameters);
            }
            var response = gl[obj.function].apply(gl, obj.parameters);
            if (response !== undefined)
                sendResponse(obj.id, response);
        }
    };

    var injectGL = function (context, funcName, parameters) {
        contextData[context].glCommands.push({ "function": funcName, "parameters": parameters });
    };

    var handleBinaryMessage = function (event) {
        var view = new DataView(event.data);
        var offset = 0;
        var obj = { "parameters": [] };
        obj.function = supportedFunctions[view.getUint8(offset)];
        offset += 1;
        if (obj.function in commandsNeedingResponse) {
            obj.id = view.getUint32(offset);
            offset += 4;
        }
        if (obj.function === "makeCurrent")
            obj.parameterCount = 4;
        else if (obj.function === "swapBuffers")
            obj.parameterCount = 0;
        else if (obj.function == "drawArrays")
            obj.parameterCount = null; // glDrawArrays has a variable number of arguments
        else
            obj.parameterCount = gl[obj.function].length;
        function deserialize(container, count) {
            for (var i = 0; count != null ? i < count : offset + 4 < event.data.byteLength; ++i) {
                var character = view.getUint8(offset);
                offset += 1;
                var parameterType = String.fromCharCode(character);
                if (parameterType === 'i') {
                    container.push(view.getInt32(offset));
                    offset += 4;
                } else if (parameterType === 'u') {
                    container.push(view.getUint32(offset));
                    offset += 4;
                } else if (parameterType === 'd') {
                    container.push(view.getFloat64(offset));
                    offset += 8;
                } else if (parameterType === 'b') {
                    container.push(view.getUint8(offset) === 1);
                    offset += 1;
                    break;
                } else if (parameterType === 's') {
                    var stringSize = view.getUint32(offset);
                    offset += 4;
                    var string = textDecoder.decode(new Uint8Array(event.data, offset, stringSize));
                    container.push(string);
                    offset += stringSize;
                } else if (parameterType === 'x') {
                    var dataSize = view.getUint32(offset);
                    offset += 4;
                    var data = new Uint8Array(event.data, offset, dataSize);
                    var bytesRead = data.byteLength;
                    if (bytesRead !== dataSize)
                        console.error("invalid data");
                    container.push(data);
                    offset += dataSize;
                } else if (parameterType === 'n') {
                    container.push(null);
                } else if (parameterType === 'a') {
                    var dataSize = view.getUint8(offset);
                    offset += 1;
                    deserialize(container[container.push([]) - 1], dataSize);
                } else {
                    console.error("Unsupported type :" + character);
                }
            }
        }
        deserialize(obj.parameters, obj.parameterCount);
        var magic = view.getUint32(offset);
        if (magic !== 0xbaadf00d) // sentinel expected at end of buffer
            console.error('Invalid magic');
        offset += 4;
        if (offset !== event.data.byteLength)
            console.error("Invalid buffer");

        if (!("function" in obj)) {
            console.error("Function not found");
        } else if (obj.function === "makeCurrent") {
            var winId = obj.parameters[3];
            if (winId in windowData) {
                canvas = windowData[winId].canvas;
                canvas.width = canvas.style.width = obj.parameters[1];
                canvas.height = canvas.style.height = obj.parameters[2];
                gl = windowData[winId].gl;
                currentWindowId = winId;
                currentContext = obj.parameters[0];
                if (currentContext)
                    ensureContextData(currentContext);
                if (DEBUG) {
                    console.log("Current context is now " + currentContext);
                    var d = contextData[currentContext];
                    if (d.glConstants === undefined) {
                        d.glConstants = {};
                        for (var key in gl) {
                            if (typeof gl[key] === 'number') {
                                d.glConstants[gl[key]] = 'gl.' + key;
                            }
                        }
                    }
                }
            }
        } else if (obj.function === "swapBuffers") {
            var data =  windowData[currentWindowId];
            if (data.loadingCanvas) {
                var body = document.getElementsByTagName("body")[0];
                clearInterval(data.loadingCanvas.timerId);
                body.removeChild(data.loadingCanvas);
                data.loadingCanvas = undefined;
            }

            if (DEBUG)
                var t0 = performance.now();
            execGL(currentContext);
            if (startTime) {
                console.log((new Date() - startTime) + "ms to first frame.");
                startTime = undefined;
            }
            var frameTime = performance.now() - t0;
            if (DEBUG)
                console.log("Swap time: " + frameTime + " ms.");
            setTimeout((function () { sendResponse(obj.id, 1); }),
                       Math.max(SWAP_DELAY - frameTime, 0));
            // have preserved swap and now we need to clear for real
        } else {
            handleGlesMessage(obj);
        }
    };

    var handleGlesMessage = function (obj) {
        // A GLES call. Unfortunately WebGL swaps when the control gets back to
        // the event loop. This is totally retarded. So we queue the commands up
        // and issue them when receiving a function that needs a response. This
        // does not solve the problem and still needs preserved swap to be safe
        // since eglSwapBuffers is not the only command that expects a response,
        // but is more efficient than issuing everything from here.
        var d = contextData[currentContext];
        if (d)
            d.glCommands.push(obj);
        if (obj.function in commandsNeedingResponse)
            execGL(currentContext);
    };

    socket.onopen = function (event) {
        console.log("Socket Open");
        (function(){
            var doCheck = true;
            var check = function(){
                var size = getBrowserSize();
                var width = size.width;
                var height = size.height;
                var physicalSize = physicalSizeRatio();
                if (DEBUG)
                    console.log("Resizing canvas to " + width + " x " + height);
                sendObject({ "type": "canvas_resize",
                    "width": width, "height": height,
                    "physicalWidth": width / physicalSize.width,
                    "physicalHeight": height / physicalSize.height
                });
            };
            window.addEventListener("resize",(function(){
                if(doCheck){
                    check();
                    doCheck = false;
                    setTimeout((function(){
                        doCheck = true;
                        check();
                    }), 1000);
                }
            }));
        })();
        connect();
    };
    socket.onclose = function (event) {
        console.log("Socket Closed (" + event.code + "): " + event.reason);
        window.location.reload();
    };
    socket.onerror = function (error) {
        console.log("Socket error: " + error.toString());
    };
    socket.onmessage = function (event) {
        if (event.data instanceof ArrayBuffer) {
            handleBinaryMessage(event);
            return;
        }
        var obj;
        try {
            obj = JSON.parse(event.data);
        } catch (e) {
            console.error("Failed to parse " + event.data + ": " + e.toString());
            return;
        }
        if (!("type" in obj)) {
            console.error("Message type not found");
        } else if (obj.type === "create_canvas") {
            createCanvas(obj.winId, obj.x, obj.y, obj.width, obj.height, obj.title);
            if (obj.title && obj.title.length)
                document.title = obj.title;
        } else if (obj.type === "destroy_canvas") {
            var canvas = document.getElementById(obj.winId);
            var body = document.getElementsByTagName("body")[0];
            body.removeChild(canvas);
        } else if (obj.type === "clipboard_updated") {
            // Opens a new window/tab and shows the current remote clipboard. There is no way to
            // copy some text to the local clipboard without user interaction.
            window.open("/clipboard", "clipboard");
        } else if (obj.type === "open_url") {
            window.open(obj.url);
        } else if (obj.type === "change_title") {
            document.title = obj.text;
        } else if (obj.type === "connect") {
            supportedFunctions = obj.supportedFunctions;
            var sysinfo = obj.sysinfo;
            if (obj.debug)
                DEBUG = 1;
            if (obj.mouseTracking)
                MOUSETRACKING = 1;
            if (obj.loadingScreen === "0")
                LOADINGSCREEN = 0;
            console.log(sysinfo);
        } else {
            console.error("Unknown message type");
        }
    };

    var setupInput = function () {
        var keyHandler = function (event) {
            var object = { "type": event.type,
                "char": event.char,
                "key": event.key,
                "which": event.which,
                "location": event.location,
                "repeat": event.repeat,
                "locale": event.locale,
                "ctrlKey": event.ctrlKey, "shiftKey": event.shiftKey, "altKey": event.altKey,
                "metaKey": event.metaKey,
                "string": String.fromCharCode(event.which ||
                                               event.keyCode),
                "keyCode": event.keyCode, "charCode": event.charCode, "code": event.code,
                "time": new Date().getTime(),
            };
            sendObject(object);
        }

        document.addEventListener('keypress', keyHandler, true);
        document.addEventListener('keydown', keyHandler, true);
        document.addEventListener('keyup', keyHandler, true);
    };
    setupInput();
};
