text: /*
   * Copyright (C) 2013 Glyptodon LLC
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy
   * of this software and associated documentation files (the "Software"), to deal
   * in the Software without restriction, including without limitation the rights
   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   * copies of the Software, and to permit persons to whom the Software is
   * furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
   * THE SOFTWARE.
   */
  
  var Guacamole = Guacamole || {};
  
  /**
   * Guacamole protocol client. Given a {@link Guacamole.Tunnel},
   * automatically handles incoming and outgoing Guacamole instructions via the
   * provided tunnel, updating its display using one or more canvas elements.
   * 
   * @constructor
   * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
   *                                  Guacamole instructions.
   */
  Guacamole.Client = function(tunnel) {
  
      var guac_client = this;
  
      var STATE_IDLE          = 0;
      var STATE_CONNECTING    = 1;
      var STATE_WAITING       = 2;
      var STATE_CONNECTED     = 3;
      var STATE_DISCONNECTING = 4;
      var STATE_DISCONNECTED  = 5;
  
      var currentState = STATE_IDLE;
      
      var currentTimestamp = 0;
      var pingInterval = null;
  
      /**
       * Translation from Guacamole protocol line caps to Layer line caps.
       * @private
       */
      var lineCap = {
          0: "butt",
          1: "round",
          2: "square"
      };
  
      /**
       * Translation from Guacamole protocol line caps to Layer line caps.
       * @private
       */
      var lineJoin = {
          0: "bevel",
          1: "miter",
          2: "round"
      };
  
      /**
       * The underlying Guacamole display.
       */
      var display = new Guacamole.Display();
  
      /**
       * All available layers and buffers
       */
      var layers = {};
      
      // No initial parsers
      var parsers = [];
  
      // No initial audio channels 
      var audio_channels = [];
  
      // No initial streams 
      var streams = [];
  
      // Pool of available stream indices
      var stream_indices = new Guacamole.IntegerPool();
  
      // Array of allocated output streams by index
      var output_streams = [];
  
      function setState(state) {
          if (state != currentState) {
              currentState = state;
              if (guac_client.onstatechange)
                  guac_client.onstatechange(currentState);
          }
      }
  
      function isConnected() {
          return currentState == STATE_CONNECTED
              || currentState == STATE_WAITING;
      }
  
      /**
       * Returns the underlying display of this Guacamole.Client. The display
       * contains an Element which can be added to the DOM, causing the
       * display to become visible.
       * 
       * @return {Guacamole.Display} The underlying display of this
       *                             Guacamole.Client.
       */
      this.getDisplay = function() {
          return display;
      };
  
      /**
       * Sends the current size of the screen.
       * 
       * @param {Number} width The width of the screen.
       * @param {Number} height The height of the screen.
       */
      this.sendSize = function(width, height) {
  
          // Do not send requests if not connected
          if (!isConnected())
              return;
  
          tunnel.sendMessage("size", width, height);
  
      };
  
      /**
       * Sends a key event having the given properties as if the user
       * pressed or released a key.
       * 
       * @param {Boolean} pressed Whether the key is pressed (true) or released
       *                          (false).
       * @param {Number} keysym The keysym of the key being pressed or released.
       */
      this.sendKeyEvent = function(pressed, keysym) {
          // Do not send requests if not connected
          if (!isConnected())
              return;
  
          tunnel.sendMessage("key", keysym, pressed);
      };
  
      /**
       * Sends a mouse event having the properties provided by the given mouse
       * state.
       * 
       * @param {Guacamole.Mouse.State} mouseState The state of the mouse to send
       *                                           in the mouse event.
       */
      this.sendMouseState = function(mouseState) {
  
          // Do not send requests if not connected
          if (!isConnected())
              return;
  
          // Update client-side cursor
          display.moveCursor(
              Math.floor(mouseState.x),
              Math.floor(mouseState.y)
          );
  
          // Build mask
          var buttonMask = 0;
          if (mouseState.left)   buttonMask |= 1;
          if (mouseState.middle) buttonMask |= 2;
          if (mouseState.right)  buttonMask |= 4;
          if (mouseState.up)     buttonMask |= 8;
          if (mouseState.down)   buttonMask |= 16;
  
          // Send message
          tunnel.sendMessage("mouse", Math.floor(mouseState.x), Math.floor(mouseState.y), buttonMask);
      };
  
      /**
       * Sets the clipboard of the remote client to the given text data.
       *
       * @deprecated Use createClipboardStream() instead. 
       * @param {String} data The data to send as the clipboard contents.
       */
      this.setClipboard = function(data) {
  
          // Do not send requests if not connected
          if (!isConnected())
              return;
  
          // Open stream
          var stream = guac_client.createClipboardStream("text/plain");
          var writer = new Guacamole.StringWriter(stream);
  
          // Send text chunks
          for (var i=0; i<data.length; i += 4096)
              writer.sendText(data.substring(i, i+4096));
  
          // Close stream
          writer.sendEnd();
  
      };
  
      /**
       * Opens a new file for writing, having the given index, mimetype and
       * filename.
       * 
       * @param {String} mimetype The mimetype of the file being sent.
       * @param {String} filename The filename of the file being sent.
       * @return {Guacamole.OutputStream} The created file stream.
       */
      this.createFileStream = function(mimetype, filename) {
  
          // Allocate index
          var index = stream_indices.next();
  
          // Create new stream
          tunnel.sendMessage("file", index, mimetype, filename);
          var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index);
  
          // Override sendEnd() of stream to automatically free index
          var old_end = stream.sendEnd;
          stream.sendEnd = function() {
              old_end();
              stream_indices.free(index);
              delete output_streams[index];
          };
  
          // Return new, overridden stream
          return stream;
  
      };
  
      /**
       * Opens a new pipe for writing, having the given name and mimetype. 
       * 
       * @param {String} mimetype The mimetype of the data being sent.
       * @param {String} name The name of the pipe.
       * @return {Guacamole.OutputStream} The created file stream.
       */
      this.createPipeStream = function(mimetype, name) {
  
          // Allocate index
          var index = stream_indices.next();
  
          // Create new stream
          tunnel.sendMessage("pipe", index, mimetype, name);
          var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index);
  
          // Override sendEnd() of stream to automatically free index
          var old_end = stream.sendEnd;
          stream.sendEnd = function() {
              old_end();
              stream_indices.free(index);
              delete output_streams[index];
          };
  
          // Return new, overridden stream
          return stream;
  
      };
  
      /**
       * Opens a new clipboard object for writing, having the given mimetype.
       * 
       * @param {String} mimetype The mimetype of the data being sent.
       * @param {String} name The name of the pipe.
       * @return {Guacamole.OutputStream} The created file stream.
       */
      this.createClipboardStream = function(mimetype) {
  
          // Allocate index
          var index = stream_indices.next();
  
          // Create new stream
          tunnel.sendMessage("clipboard", index, mimetype);
          var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index);
  
          // Override sendEnd() of stream to automatically free index
          var old_end = stream.sendEnd;
          stream.sendEnd = function() {
              old_end();
              stream_indices.free(index);
              delete output_streams[index];
          };
  
          // Return new, overridden stream
          return stream;
  
      };
  
      /**
       * Acknowledge receipt of a blob on the stream with the given index.
       * 
       * @param {Number} index The index of the stream associated with the
       *                       received blob.
       * @param {String} message A human-readable message describing the error
       *                         or status.
       * @param {Number} code The error code, if any, or 0 for success.
       */
      this.sendAck = function(index, message, code) {
  
          // Do not send requests if not connected
          if (!isConnected())
              return;
  
          tunnel.sendMessage("ack", index, message, code);
      };
  
      /**
       * Given the index of a file, writes a blob of data to that file.
       * 
       * @param {Number} index The index of the file to write to.
       * @param {String} data Base64-encoded data to write to the file.
       */
      this.sendBlob = function(index, data) {
  
          // Do not send requests if not connected
          if (!isConnected())
              return;
  
          tunnel.sendMessage("blob", index, data);
      };
  
      /**
       * Marks a currently-open stream as complete.
       * 
       * @param {Number} index The index of the stream to end.
       */
      this.endStream = function(index) {
  
          // Do not send requests if not connected
          if (!isConnected())
              return;
  
          tunnel.sendMessage("end", index);
      };
  
      /**
       * Fired whenever the state of this Guacamole.Client changes.
       * 
       * @event
       * @param {Number} state The new state of the client.
       */
      this.onstatechange = null;
  
      /**
       * Fired when the remote client sends a name update.
       * 
       * @event
       * @param {String} name The new name of this client.
       */
      this.onname = null;
  
      /**
       * Fired when an error is reported by the remote client, and the connection
       * is being closed.
       * 
       * @event
       * @param {Guacamole.Status} status A status object which describes the
       *                                  error.
       */
      this.onerror = null;
  
      /**
       * Fired when the clipboard of the remote client is changing.
       * 
       * @event
       * @param {Guacamole.InputStream} stream The stream that will receive
       *                                       clipboard data from the server.
       * @param {String} mimetype The mimetype of the data which will be received.
       */
      this.onclipboard = null;
  
      /**
       * Fired when a file stream is created. The stream provided to this event
       * handler will contain its own event handlers for received data.
       * 
       * @event
       * @param {Guacamole.InputStream} stream The stream that will receive data
       *                                       from the server.
       * @param {String} mimetype The mimetype of the file received.
       * @param {String} filename The name of the file received.
       */
      this.onfile = null;
  
      /**
       * Fired when a pipe stream is created. The stream provided to this event
       * handler will contain its own event handlers for received data;
       * 
       * @event
       * @param {Guacamole.InputStream} stream The stream that will receive data
       *                                       from the server.
       * @param {String} mimetype The mimetype of the data which will be received.
       * @param {String} name The name of the pipe.
       */
      this.onpipe = null;
  
      /**
       * Fired whenever a sync instruction is received from the server, indicating
       * that the server is finished processing any input from the client and
       * has sent any results.
       * 
       * @event
       * @param {Number} timestamp The timestamp associated with the sync
       *                           instruction.
       */
      this.onsync = null;
  	
  	/**
  	 * Fired when a turn instruction is received.
  	 */
  	this.onturn = null;
  	/**
  	 * Fired when a chat message is received.
  	 */
  	this.onchat = null;
  	/**
  	 * Fired in response to a login or rename instruction.
  	 */
  	this.onrename = null;
  	/**
  	 * Fired when one or more users should be added to
  	 * the users list.
  	 */
  	this.onadduser = null;
  	/**
  	 * Fired when one or more users should be removed from
  	 * the users list.
  	 */
  	this.onremuser = null;
  	/**
  	 * Fired in response to a connect instruction.
  	 */
  	this.onconnect = null;
  	/**
  	 * Fired in response to a list instruction.
  	 */
  	this.onlist = null;
  	/**
  	 * Fired in response to a vote instruction.
  	 */
  	this.onvote = null;
  	/**
  	 * Fired in response to an action instruction.
  	 */
  	this.onaction = null;
  	
      /**
       * Returns the layer with the given index, creating it if necessary.
       * Positive indices refer to visible layers, an index of zero refers to
       * the default layer, and negative indices refer to buffers.
       * 
       * @param {Number} index The index of the layer to retrieve.
       * @return {Guacamole.Display.VisibleLayer|Guacamole.Layer} The layer having the given index.
       */
      function getLayer(index) {
  
          // Get layer, create if necessary
          var layer = layers[index];
          if (!layer) {
  
              // Create layer based on index
              if (index === 0)
                  layer = display.getDefaultLayer();
              else if (index > 0)
                  layer = display.createLayer();
              else
                  layer = display.createBuffer();
                  
              // Add new layer
              layers[index] = layer;
  
          }
  
          return layer;
  
      }
  
      function getParser(index) {
  
          var parser = parsers[index];
  
          // If parser not yet created, create it, and tie to the
          // oninstruction handler of the tunnel.
          if (parser == null) {
              parser = parsers[index] = new Guacamole.Parser();
              parser.oninstruction = tunnel.oninstruction;
          }
  
          return parser;
  
      }
  
      function getAudioChannel(index) {
  
          var audio_channel = audio_channels[index];
  
          // If audio channel not yet created, create it
          if (audio_channel == null)
              audio_channel = audio_channels[index] = new Guacamole.AudioChannel();
  
          return audio_channel;
  
      }
  
      /**
       * Handlers for all defined layer properties.
       * @private
       */
      var layerPropertyHandlers = {
  
          "miter-limit": function(layer, value) {
              display.setMiterLimit(layer, parseFloat(value));
          }
  
      };
      
      /**
       * Handlers for all instruction opcodes receivable by a Guacamole protocol
       * client.
       * @private
       */
      var instructionHandlers = {
  
  		"turn": function(parameters) {
  			if (guac_client.onturn)
                  guac_client.onturn(parameters);
  		},
  		"rename": function(parameters) {
  			if (guac_client.onrename)
                  guac_client.onrename(parameters);
  		},
  		"chat": function(parameters) {
  			if (guac_client.onchat)
                  guac_client.onchat(parameters);
  		},
  		"adduser": function(parameters) {
  			if (guac_client.onadduser)
                  guac_client.onadduser(parameters);
  		},
  		"remuser": function(parameters) {
  			if (guac_client.onremuser)
                  guac_client.onremuser(parameters);
  		},
  		"connect": function(parameters) {
  			if (guac_client.onconnect)
                  guac_client.onconnect(parameters);
  		},
  		"list": function(parameters) {
  			if (guac_client.onlist)
                  guac_client.onlist(parameters);
  		},
  		"vote": function(parameters) {
  			if (guac_client.onvote)
                  guac_client.onvote(parameters);
  		},
  		"action": function(parameters) {
  			if (guac_client.onaction)
                  guac_client.onaction(parameters);
  		},
  		"file": function(parameters) {
  			if (guac_client.onfile)
                  guac_client.onfile(parameters);
  		},
  		"nop": function() {
  			// Keep-alive
  			tunnel.sendMessage("nop");
  		},
          "ack": function(parameters) {
  
              var stream_index = parseInt(parameters[0]);
              var reason = parameters[1];
              var code = parseInt(parameters[2]);
  
              // Get stream
              var stream = output_streams[stream_index];
              if (stream) {
  
                  // Signal ack if handler defined
                  if (stream.onack)
                      stream.onack(new Guacamole.Status(code, reason));
  
                  // If code is an error, invalidate stream
                  if (code >= 0x0100) {
                      stream_indices.free(stream_index);
                      delete output_streams[stream_index];
                  }
  
              }
  
          },
  
          "arc": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
              var x = parseInt(parameters[1]);
              var y = parseInt(parameters[2]);
              var radius = parseInt(parameters[3]);
              var startAngle = parseFloat(parameters[4]);
              var endAngle = parseFloat(parameters[5]);
              var negative = parseInt(parameters[6]);
  
              display.arc(layer, x, y, radius, startAngle, endAngle, negative != 0);
  
          },
  
          "audio": function(parameters) {
  
              var stream_index = parseInt(parameters[0]);
              var channel = getAudioChannel(parseInt(parameters[1]));
              var mimetype = parameters[2];
              var duration = parseFloat(parameters[3]);
  
              // Create stream 
              var stream = streams[stream_index] =
                      new Guacamole.InputStream(guac_client, stream_index);
  
              // Assemble entire stream as a blob
              var blob_reader = new Guacamole.BlobReader(stream, mimetype);
  
              // Play blob as audio
              blob_reader.onend = function() {
                  channel.play(mimetype, duration, blob_reader.getBlob());
              };
  
              // Send success response
              guac_client.sendAck(stream_index, "OK", 0x0000);
  
          },
  
          "blob": function(parameters) {
  
              // Get stream 
              var stream_index = parseInt(parameters[0]);
              var data = parameters[1];
              var stream = streams[stream_index];
  
              // Write data
              stream.onblob(data);
  
          },
  
          "cfill": function(parameters) {
  
              var channelMask = parseInt(parameters[0]);
              var layer = getLayer(parseInt(parameters[1]));
              var r = parseInt(parameters[2]);
              var g = parseInt(parameters[3]);
              var b = parseInt(parameters[4]);
              var a = parseInt(parameters[5]);
  
              display.setChannelMask(layer, channelMask);
              display.fillColor(layer, r, g, b, a);
  
          },
  
          "clip": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
  
              display.clip(layer);
  
          },
  
          "clipboard": function(parameters) {
  
              var stream_index = parseInt(parameters[0]);
              var mimetype = parameters[1];
  
              // Create stream 
              if (guac_client.onclipboard) {
                  var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
                  guac_client.onclipboard(stream, mimetype);
              }
  
              // Otherwise, unsupported
              else
                  guac_client.sendAck(stream_index, "Clipboard unsupported", 0x0100);
  
          },
  
          "close": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
  
              display.close(layer);
  
          },
  
          "copy": function(parameters) {
  
              var srcL = getLayer(parseInt(parameters[0]));
              var srcX = parseInt(parameters[1]);
              var srcY = parseInt(parameters[2]);
              var srcWidth = parseInt(parameters[3]);
              var srcHeight = parseInt(parameters[4]);
              var channelMask = parseInt(parameters[5]);
              var dstL = getLayer(parseInt(parameters[6]));
              var dstX = parseInt(parameters[7]);
              var dstY = parseInt(parameters[8]);
  
              display.setChannelMask(dstL, channelMask);
              display.copy(srcL, srcX, srcY, srcWidth, srcHeight, 
                           dstL, dstX, dstY);
  
          },
  
          "cstroke": function(parameters) {
  
              var channelMask = parseInt(parameters[0]);
              var layer = getLayer(parseInt(parameters[1]));
              var cap = lineCap[parseInt(parameters[2])];
              var join = lineJoin[parseInt(parameters[3])];
              var thickness = parseInt(parameters[4]);
              var r = parseInt(parameters[5]);
              var g = parseInt(parameters[6]);
              var b = parseInt(parameters[7]);
              var a = parseInt(parameters[8]);
  
              display.setChannelMask(layer, channelMask);
              display.strokeColor(layer, cap, join, thickness, r, g, b, a);
  
          },
  
          "cursor": function(parameters) {
  
              var cursorHotspotX = parseInt(parameters[0]);
              var cursorHotspotY = parseInt(parameters[1]);
              var srcL = getLayer(parseInt(parameters[2]));
              var srcX = parseInt(parameters[3]);
              var srcY = parseInt(parameters[4]);
              var srcWidth = parseInt(parameters[5]);
              var srcHeight = parseInt(parameters[6]);
  
              display.setCursor(cursorHotspotX, cursorHotspotY,
                                srcL, srcX, srcY, srcWidth, srcHeight);
  
          },
  
          "curve": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
              var cp1x = parseInt(parameters[1]);
              var cp1y = parseInt(parameters[2]);
              var cp2x = parseInt(parameters[3]);
              var cp2y = parseInt(parameters[4]);
              var x = parseInt(parameters[5]);
              var y = parseInt(parameters[6]);
  
              display.curveTo(layer, cp1x, cp1y, cp2x, cp2y, x, y);
  
          },
  
          "dispose": function(parameters) {
              
              var layer_index = parseInt(parameters[0]);
  
              // If visible layer, remove from parent
              if (layer_index > 0) {
  
                  // Remove from parent
                  var layer = getLayer(layer_index);
                  layer.dispose();
  
                  // Delete reference
                  delete layers[layer_index];
  
              }
  
              // If buffer, just delete reference
              else if (layer_index < 0)
                  delete layers[layer_index];
  
              // Attempting to dispose the root layer currently has no effect.
  
          },
  
          "distort": function(parameters) {
  
              var layer_index = parseInt(parameters[0]);
              var a = parseFloat(parameters[1]);
              var b = parseFloat(parameters[2]);
              var c = parseFloat(parameters[3]);
              var d = parseFloat(parameters[4]);
              var e = parseFloat(parameters[5]);
              var f = parseFloat(parameters[6]);
  
              // Only valid for visible layers (not buffers)
              if (layer_index >= 0) {
                  var layer = getLayer(layer_index);
                  layer.distort(a, b, c, d, e, f);
              }
  
          },
   
          "error": function(parameters) {
  
              var reason = parameters[0];
              var code = parseInt(parameters[1]);
  
              // Call handler if defined
              if (guac_client.onerror)
                  guac_client.onerror(new Guacamole.Status(code, reason));
  
              guac_client.disconnect();
  
          },
  
          "end": function(parameters) {
  
              // Get stream
              var stream_index = parseInt(parameters[0]);
              var stream = streams[stream_index];
  
              // Signal end of stream
              if (stream.onend)
                  stream.onend();
  
          },
  
          /*"file": function(parameters) {
  
              var stream_index = parseInt(parameters[0]);
              var mimetype = parameters[1];
              var filename = parameters[2];
  
              // Create stream 
              if (guac_client.onfile) {
                  var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
                  guac_client.onfile(stream, mimetype, filename);
              }
  
              // Otherwise, unsupported
              else
                  guac_client.sendAck(stream_index, "File transfer unsupported", 0x0100);
  
          },*/
  
          "identity": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
  
              display.setTransform(layer, 1, 0, 0, 1, 0, 0);
  
          },
  
          "lfill": function(parameters) {
  
              var channelMask = parseInt(parameters[0]);
              var layer = getLayer(parseInt(parameters[1]));
              var srcLayer = getLayer(parseInt(parameters[2]));
  
              display.setChannelMask(layer, channelMask);
              display.fillLayer(layer, srcLayer);
  
          },
  
          "line": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
              var x = parseInt(parameters[1]);
              var y = parseInt(parameters[2]);
  
              display.lineTo(layer, x, y);
  
          },
  
          "lstroke": function(parameters) {
  
              var channelMask = parseInt(parameters[0]);
              var layer = getLayer(parseInt(parameters[1]));
              var srcLayer = getLayer(parseInt(parameters[2]));
  
              display.setChannelMask(layer, channelMask);
              display.strokeLayer(layer, srcLayer);
  
          },
  
          "move": function(parameters) {
              
              var layer_index = parseInt(parameters[0]);
              var parent_index = parseInt(parameters[1]);
              var x = parseInt(parameters[2]);
              var y = parseInt(parameters[3]);
              var z = parseInt(parameters[4]);
  
              // Only valid for non-default layers
              if (layer_index > 0 && parent_index >= 0) {
                  var layer = getLayer(layer_index);
                  var parent = getLayer(parent_index);
                  layer.move(parent, x, y, z);
              }
  
          },
  
          "name": function(parameters) {
              if (guac_client.onname) guac_client.onname(parameters[0]);
          },
  
          "nest": function(parameters) {
              var parser = getParser(parseInt(parameters[0]));
              parser.receive(parameters[1]);
          },
  
          "pipe": function(parameters) {
  
              var stream_index = parseInt(parameters[0]);
              var mimetype = parameters[1];
              var name = parameters[2];
  
              // Create stream 
              if (guac_client.onpipe) {
                  var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
                  guac_client.onpipe(stream, mimetype, name);
              }
  
              // Otherwise, unsupported
              else
                  guac_client.sendAck(stream_index, "Named pipes unsupported", 0x0100);
  
          },
  
          "png": function(parameters) {
  
              var channelMask = parseInt(parameters[0]);
              var layer = getLayer(parseInt(parameters[1]));
              var x = parseInt(parameters[2]);
              var y = parseInt(parameters[3]);
              var data = parameters[4];
  
              display.setChannelMask(layer, channelMask);
              display.draw(layer, x, y, "data:image/png;base64," + data);
  
          },
  
          "pop": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
  
              display.pop(layer);
  
          },
  
          "push": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
  
              display.push(layer);
  
          },
   
          "rect": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
              var x = parseInt(parameters[1]);
              var y = parseInt(parameters[2]);
              var w = parseInt(parameters[3]);
              var h = parseInt(parameters[4]);
  
              display.rect(layer, x, y, w, h);
  
          },
          
          "reset": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
  
              display.reset(layer);
  
          },
          
          "set": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
              var name = parameters[1];
              var value = parameters[2];
  
              // Call property handler if defined
              var handler = layerPropertyHandlers[name];
              if (handler)
                  handler(layer, value);
  
          },
  
          "shade": function(parameters) {
              
              var layer_index = parseInt(parameters[0]);
              var a = parseInt(parameters[1]);
  
              // Only valid for visible layers (not buffers)
              if (layer_index >= 0) {
                  var layer = getLayer(layer_index);
                  layer.shade(a);
              }
  
          },
  
          "size": function(parameters) {
  
              var layer_index = parseInt(parameters[0]);
              var layer = getLayer(layer_index);
              var width = parseInt(parameters[1]);
              var height = parseInt(parameters[2]);
  
              display.resize(layer, width, height);
  
          },
          
          "start": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
              var x = parseInt(parameters[1]);
              var y = parseInt(parameters[2]);
  
              display.moveTo(layer, x, y);
  
          },
  
          "sync": function(parameters) {
  
              var timestamp = parseInt(parameters[0]);
  
              // Flush display, send sync when done
              display.flush(/*function __send_sync_response() {
                  if (timestamp !== currentTimestamp) {
                      tunnel.sendMessage("sync", timestamp);
                      currentTimestamp = timestamp;
                  }
              }*/ null);
  
              // If received first update, no longer waiting.
              /*if (currentState === STATE_WAITING)
                  setState(STATE_CONNECTED);*/
  
              // Call sync handler if defined
              if (guac_client.onsync)
                  guac_client.onsync(timestamp);
  
          },
  
          "transfer": function(parameters) {
  
              var srcL = getLayer(parseInt(parameters[0]));
              var srcX = parseInt(parameters[1]);
              var srcY = parseInt(parameters[2]);
              var srcWidth = parseInt(parameters[3]);
              var srcHeight = parseInt(parameters[4]);
              var function_index = parseInt(parameters[5]);
              var dstL = getLayer(parseInt(parameters[6]));
              var dstX = parseInt(parameters[7]);
              var dstY = parseInt(parameters[8]);
  
              /* SRC */
              if (function_index === 0x3)
                  display.put(srcL, srcX, srcY, srcWidth, srcHeight, 
                      dstL, dstX, dstY);
  
              /* Anything else that isn't a NO-OP */
              else if (function_index !== 0x5)
                  display.transfer(srcL, srcX, srcY, srcWidth, srcHeight, 
                      dstL, dstX, dstY, Guacamole.Client.DefaultTransferFunction[function_index]);
  
          },
  
          "transform": function(parameters) {
  
              var layer = getLayer(parseInt(parameters[0]));
              var a = parseFloat(parameters[1]);
              var b = parseFloat(parameters[2]);
              var c = parseFloat(parameters[3]);
              var d = parseFloat(parameters[4]);
              var e = parseFloat(parameters[5]);
              var f = parseFloat(parameters[6]);
  
              display.transform(layer, a, b, c, d, e, f);
  
          },
  
          "video": function(parameters) {
  
              var stream_index = parseInt(parameters[0]);
              var layer = getLayer(parseInt(parameters[1]));
              var mimetype = parameters[2];
              var duration = parseFloat(parameters[3]);
  
              // Create stream 
              var stream = streams[stream_index] =
                      new Guacamole.InputStream(guac_client, stream_index);
  
              // Assemble entire stream as a blob
              var blob_reader = new Guacamole.BlobReader(stream, mimetype);
  
              // Play video once finished 
              blob_reader.onend = function() {
  
                  // Read data from blob from stream
                  var reader = new FileReader();
                  reader.onload = function() {
  
                      var binary = "";
                      var bytes = new Uint8Array(reader.result);
  
                      // Produce binary string from bytes in buffer
                      for (var i=0; i<bytes.byteLength; i++)
                          binary += String.fromCharCode(bytes[i]);
  
                      // Play video
                      layer.play(mimetype, duration, "data:" + mimetype + ";base64," + window.btoa(binary));
  
                  };
                  reader.readAsArrayBuffer(blob_reader.getBlob());
  
              };
  
              // Send success response
              tunnel.sendMessage("ack", stream_index, "OK", 0x0000);
  
          }
  
      };
  
      tunnel.oninstruction = function(opcode, parameters) {
  
          var handler = instructionHandlers[opcode];
          if (handler)
              handler(parameters);
  
      };
  
      /**
       * Sends a disconnect instruction to the server and closes the tunnel.
       */
      this.disconnect = function() {
  
          // Only attempt disconnection not disconnected.
          if (currentState != STATE_DISCONNECTED
                  && currentState != STATE_DISCONNECTING) {
  
              setState(STATE_DISCONNECTING);
  
              // Stop ping
              if (pingInterval)
                  window.clearInterval(pingInterval);
  
              // Send disconnect message and disconnect
              tunnel.sendMessage("disconnect");
              tunnel.disconnect();
              setState(STATE_DISCONNECTED);
  
          }
  
      };
      
      /**
       * Connects the underlying tunnel of this Guacamole.Client, passing the
       * given arbitrary data to the tunnel during the connection process.
       *
       * @param data Arbitrary connection data to be sent to the underlying
       *             tunnel during the connection process.
       * @throws {Guacamole.Status} If an error occurs during connection.
       */
      this.connect = function(data) {
  
          setState(STATE_CONNECTING);
  
          try {
              tunnel.connect(data);
          }
          catch (status) {
              setState(STATE_IDLE);
              throw status;
          }
  
          // Ping every 5 seconds (ensure connection alive)
          /*pingInterval = window.setInterval(function() {
              tunnel.sendMessage("sync", currentTimestamp);
          }, 5000);*/
  
          setState(STATE_WAITING);
      };
  
  };
  
  /**
   * Map of all Guacamole binary raster operations to transfer functions.
   * @private
   */
  Guacamole.Client.DefaultTransferFunction = {
  
      /* BLACK */
      0x0: function (src, dst) {
          dst.red = dst.green = dst.blue = 0x00;
      },
  
      /* WHITE */
      0xF: function (src, dst) {
          dst.red = dst.green = dst.blue = 0xFF;
      },
  
      /* SRC */
      0x3: function (src, dst) {
          dst.red   = src.red;
          dst.green = src.green;
          dst.blue  = src.blue;
          dst.alpha = src.alpha;
      },
  
      /* DEST (no-op) */
      0x5: function (src, dst) {
          // Do nothing
      },
  
      /* Invert SRC */
      0xC: function (src, dst) {
          dst.red   = 0xFF & ~src.red;
          dst.green = 0xFF & ~src.green;
          dst.blue  = 0xFF & ~src.blue;
          dst.alpha =  src.alpha;
      },
      
      /* Invert DEST */
      0xA: function (src, dst) {
          dst.red   = 0xFF & ~dst.red;
          dst.green = 0xFF & ~dst.green;
          dst.blue  = 0xFF & ~dst.blue;
      },
  
      /* AND */
      0x1: function (src, dst) {
          dst.red   =  ( src.red   &  dst.red);
          dst.green =  ( src.green &  dst.green);
          dst.blue  =  ( src.blue  &  dst.blue);
      },
  
      /* NAND */
      0xE: function (src, dst) {
          dst.red   = 0xFF & ~( src.red   &  dst.red);
          dst.green = 0xFF & ~( src.green &  dst.green);
          dst.blue  = 0xFF & ~( src.blue  &  dst.blue);
      },
  
      /* OR */
      0x7: function (src, dst) {
          dst.red   =  ( src.red   |  dst.red);
          dst.green =  ( src.green |  dst.green);
          dst.blue  =  ( src.blue  |  dst.blue);
      },
  
      /* NOR */
      0x8: function (src, dst) {
          dst.red   = 0xFF & ~( src.red   |  dst.red);
          dst.green = 0xFF & ~( src.green |  dst.green);
          dst.blue  = 0xFF & ~( src.blue  |  dst.blue);
      },
  
      /* XOR */
      0x6: function (src, dst) {
          dst.red   =  ( src.red   ^  dst.red);
          dst.green =  ( src.green ^  dst.green);
          dst.blue  =  ( src.blue  ^  dst.blue);
      },
  
      /* XNOR */
      0x9: function (src, dst) {
          dst.red   = 0xFF & ~( src.red   ^  dst.red);
          dst.green = 0xFF & ~( src.green ^  dst.green);
          dst.blue  = 0xFF & ~( src.blue  ^  dst.blue);
      },
  
      /* AND inverted source */
      0x4: function (src, dst) {
          dst.red   =  0xFF & (~src.red   &  dst.red);
          dst.green =  0xFF & (~src.green &  dst.green);
          dst.blue  =  0xFF & (~src.blue  &  dst.blue);
      },
  
      /* OR inverted source */
      0xD: function (src, dst) {
          dst.red   =  0xFF & (~src.red   |  dst.red);
          dst.green =  0xFF & (~src.green |  dst.green);
          dst.blue  =  0xFF & (~src.blue  |  dst.blue);
      },
  
      /* AND inverted destination */
      0x2: function (src, dst) {
          dst.red   =  0xFF & ( src.red   & ~dst.red);
          dst.green =  0xFF & ( src.green & ~dst.green);
          dst.blue  =  0xFF & ( src.blue  & ~dst.blue);
      },
  
      /* OR inverted destination */
      0xB: function (src, dst) {
          dst.red   =  0xFF & ( src.red   | ~dst.red);
          dst.green =  0xFF & ( src.green | ~dst.green);
          dst.blue  =  0xFF & ( src.blue  | ~dst.blue);
      }
  
  };
  