UNPKG

@huangqiansan/guacamole-common-js

Version:
15,987 lines 499 kB
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A reader which automatically handles the given input stream, returning
 * strictly received packets as array buffers. Note that this object will
 * overwrite any installed event handlers on the given Guacamole.InputStream.
 * 
 * @constructor
 * @param {!Guacamole.InputStream} stream
 *     The stream that data will be read from.
 */
Guacamole.ArrayBufferReader = function(stream) {

    /**
     * Reference to this Guacamole.InputStream.
     * @private
     */
    var guac_reader = this;

    // Receive blobs as array buffers
    stream.onblob = function(data) {

        // Convert to ArrayBuffer
        var binary = window.atob(data);
        var arrayBuffer = new ArrayBuffer(binary.length);
        var bufferView = new Uint8Array(arrayBuffer);

        for (var i=0; i<binary.length; i++)
            bufferView[i] = binary.charCodeAt(i);

        // Call handler, if present
        if (guac_reader.ondata)
            guac_reader.ondata(arrayBuffer);

    };

    // Simply call onend when end received
    stream.onend = function() {
        if (guac_reader.onend)
            guac_reader.onend();
    };

    /**
     * Fired once for every blob of data received.
     * 
     * @event
     * @param {!ArrayBuffer} buffer
     *     The data packet received.
     */
    this.ondata = null;

    /**
     * Fired once this stream is finished and no further data will be written.
     * @event
     */
    this.onend = null;

};
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A writer which automatically writes to the given output stream with arbitrary
 * binary data, supplied as ArrayBuffers.
 * 
 * @constructor
 * @param {!Guacamole.OutputStream} stream
 *     The stream that data will be written to.
 */
Guacamole.ArrayBufferWriter = function(stream) {

    /**
     * Reference to this Guacamole.StringWriter.
     *
     * @private
     * @type {!Guacamole.ArrayBufferWriter}
     */
    var guac_writer = this;

    // Simply call onack for acknowledgements
    stream.onack = function(status) {
        if (guac_writer.onack)
            guac_writer.onack(status);
    };

    /**
     * Encodes the given data as base64, sending it as a blob. The data must
     * be small enough to fit into a single blob instruction.
     * 
     * @private
     * @param {!Uint8Array} bytes
     *     The data to send.
     */
    function __send_blob(bytes) {

        var binary = "";

        // Produce binary string from bytes in buffer
        for (var i=0; i<bytes.byteLength; i++)
            binary += String.fromCharCode(bytes[i]);

        // Send as base64
        stream.sendBlob(window.btoa(binary));

    }

    /**
     * The maximum length of any blob sent by this Guacamole.ArrayBufferWriter,
     * in bytes. Data sent via
     * [sendData()]{@link Guacamole.ArrayBufferWriter#sendData} which exceeds
     * this length will be split into multiple blobs. As the Guacamole protocol
     * limits the maximum size of any instruction or instruction element to
     * 8192 bytes, and the contents of blobs will be base64-encoded, this value
     * should only be increased with extreme caution.
     *
     * @type {!number}
     * @default {@link Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH}
     */
    this.blobLength = Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH;

    /**
     * Sends the given data.
     * 
     * @param {!(ArrayBuffer|TypedArray)} data
     *     The data to send.
     */
    this.sendData = function(data) {

        var bytes = new Uint8Array(data);

        // If small enough to fit into single instruction, send as-is
        if (bytes.length <= guac_writer.blobLength)
            __send_blob(bytes);

        // Otherwise, send as multiple instructions
        else {
            for (var offset=0; offset<bytes.length; offset += guac_writer.blobLength)
                __send_blob(bytes.subarray(offset, offset + guac_writer.blobLength));
        }

    };

    /**
     * Signals that no further text will be sent, effectively closing the
     * stream.
     */
    this.sendEnd = function() {
        stream.sendEnd();
    };

    /**
     * Fired for received data, if acknowledged by the server.
     * @event
     * @param {!Guacamole.Status} status
     *     The status of the operation.
     */
    this.onack = null;

};

/**
 * The default maximum blob length for new Guacamole.ArrayBufferWriter
 * instances.
 *
 * @constant
 * @type {!number}
 */
Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH = 6048;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Maintains a singleton instance of the Web Audio API AudioContext class,
 * instantiating the AudioContext only in response to the first call to
 * getAudioContext(), and only if no existing AudioContext instance has been
 * provided via the singleton property. Subsequent calls to getAudioContext()
 * will return the same instance.
 *
 * @namespace
 */
Guacamole.AudioContextFactory = {

    /**
     * A singleton instance of a Web Audio API AudioContext object, or null if
     * no instance has yes been created. This property may be manually set if
     * you wish to supply your own AudioContext instance, but care must be
     * taken to do so as early as possible. Assignments to this property will
     * not retroactively affect the value returned by previous calls to
     * getAudioContext().
     *
     * @type {AudioContext}
     */
    'singleton' : null,

    /**
     * Returns a singleton instance of a Web Audio API AudioContext object.
     *
     * @return {AudioContext}
     *     A singleton instance of a Web Audio API AudioContext object, or null
     *     if the Web Audio API is not supported.
     */
    'getAudioContext' : function getAudioContext() {

        // Fallback to Webkit-specific AudioContext implementation
        var AudioContext = window.AudioContext || window.webkitAudioContext;

        // Get new AudioContext instance if Web Audio API is supported
        if (AudioContext) {
            try {

                // Create new instance if none yet exists
                if (!Guacamole.AudioContextFactory.singleton)
                    Guacamole.AudioContextFactory.singleton = new AudioContext();

                // Return singleton instance
                return Guacamole.AudioContextFactory.singleton;

            }
            catch (e) {
                // Do not use Web Audio API if not allowed by browser
            }
        }

        // Web Audio API not supported
        return null;

    }

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Abstract audio player which accepts, queues and plays back arbitrary audio
 * data. It is up to implementations of this class to provide some means of
 * handling a provided Guacamole.InputStream. Data received along the provided
 * stream is to be played back immediately.
 *
 * @constructor
 */
Guacamole.AudioPlayer = function AudioPlayer() {

    /**
     * Notifies this Guacamole.AudioPlayer that all audio up to the current
     * point in time has been given via the underlying stream, and that any
     * difference in time between queued audio data and the current time can be
     * considered latency.
     */
    this.sync = function sync() {
        // Default implementation - do nothing
    };

};

/**
 * Determines whether the given mimetype is supported by any built-in
 * implementation of Guacamole.AudioPlayer, and thus will be properly handled
 * by Guacamole.AudioPlayer.getInstance().
 *
 * @param {!string} mimetype
 *     The mimetype to check.
 *
 * @returns {!boolean}
 *     true if the given mimetype is supported by any built-in
 *     Guacamole.AudioPlayer, false otherwise.
 */
Guacamole.AudioPlayer.isSupportedType = function isSupportedType(mimetype) {

    return Guacamole.RawAudioPlayer.isSupportedType(mimetype);

};

/**
 * Returns a list of all mimetypes supported by any built-in
 * Guacamole.AudioPlayer, in rough order of priority. Beware that only the core
 * mimetypes themselves will be listed. Any mimetype parameters, even required
 * ones, will not be included in the list. For example, "audio/L8" is a
 * supported raw audio mimetype that is supported, but it is invalid without
 * additional parameters. Something like "audio/L8;rate=44100" would be valid,
 * however (see https://tools.ietf.org/html/rfc4856).
 *
 * @returns {!string[]}
 *     A list of all mimetypes supported by any built-in Guacamole.AudioPlayer,
 *     excluding any parameters.
 */
Guacamole.AudioPlayer.getSupportedTypes = function getSupportedTypes() {

    return Guacamole.RawAudioPlayer.getSupportedTypes();

};

/**
 * Returns an instance of Guacamole.AudioPlayer providing support for the given
 * audio format. If support for the given audio format is not available, null
 * is returned.
 *
 * @param {!Guacamole.InputStream} stream
 *     The Guacamole.InputStream to read audio data from.
 *
 * @param {!string} mimetype
 *     The mimetype of the audio data in the provided stream.
 *
 * @return {Guacamole.AudioPlayer}
 *     A Guacamole.AudioPlayer instance supporting the given mimetype and
 *     reading from the given stream, or null if support for the given mimetype
 *     is absent.
 */
Guacamole.AudioPlayer.getInstance = function getInstance(stream, mimetype) {

    // Use raw audio player if possible
    if (Guacamole.RawAudioPlayer.isSupportedType(mimetype))
        return new Guacamole.RawAudioPlayer(stream, mimetype);

    // No support for given mimetype
    return null;

};

/**
 * Implementation of Guacamole.AudioPlayer providing support for raw PCM format
 * audio. This player relies only on the Web Audio API and does not require any
 * browser-level support for its audio formats.
 *
 * @constructor
 * @augments Guacamole.AudioPlayer
 * @param {!Guacamole.InputStream} stream
 *     The Guacamole.InputStream to read audio data from.
 *
 * @param {!string} mimetype
 *     The mimetype of the audio data in the provided stream, which must be a
 *     "audio/L8" or "audio/L16" mimetype with necessary parameters, such as:
 *     "audio/L16;rate=44100,channels=2".
 */
Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) {

    /**
     * The format of audio this player will decode.
     *
     * @private
     * @type {Guacamole.RawAudioFormat}
     */
    var format = Guacamole.RawAudioFormat.parse(mimetype);

    /**
     * An instance of a Web Audio API AudioContext object, or null if the
     * Web Audio API is not supported.
     *
     * @private
     * @type {AudioContext}
     */
    var context = Guacamole.AudioContextFactory.getAudioContext();

    /**
     * The earliest possible time that the next packet could play without
     * overlapping an already-playing packet, in seconds. Note that while this
     * value is in seconds, it is not an integer value and has microsecond
     * resolution.
     *
     * @private
     * @type {!number}
     */
    var nextPacketTime = context.currentTime;

    /**
     * Guacamole.ArrayBufferReader wrapped around the audio input stream
     * provided with this Guacamole.RawAudioPlayer was created.
     *
     * @private
     * @type {!Guacamole.ArrayBufferReader}
     */
    var reader = new Guacamole.ArrayBufferReader(stream);

    /**
     * The minimum size of an audio packet split by splitAudioPacket(), in
     * seconds. Audio packets smaller than this will not be split, nor will the
     * split result of a larger packet ever be smaller in size than this
     * minimum.
     *
     * @private
     * @constant
     * @type {!number}
     */
    var MIN_SPLIT_SIZE = 0.02;

    /**
     * The maximum amount of latency to allow between the buffered data stream
     * and the playback position, in seconds. Initially, this is set to
     * roughly one third of a second.
     *
     * @private
     * @type {!number}
     */
    var maxLatency = 0.3;

    /**
     * The type of typed array that will be used to represent each audio packet
     * internally. This will be either Int8Array or Int16Array, depending on
     * whether the raw audio format is 8-bit or 16-bit.
     *
     * @private
     * @constructor
     */
    var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array;

    /**
     * The maximum absolute value of any sample within a raw audio packet
     * received by this audio player. This depends only on the size of each
     * sample, and will be 128 for 8-bit audio and 32768 for 16-bit audio.
     *
     * @private
     * @type {!number}
     */
    var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768;

    /**
     * The queue of all pending audio packets, as an array of sample arrays.
     * Audio packets which are pending playback will be added to this queue for
     * further manipulation prior to scheduling via the Web Audio API. Once an
     * audio packet leaves this queue and is scheduled via the Web Audio API,
     * no further modifications can be made to that packet.
     *
     * @private
     * @type {!SampleArray[]}
     */
    var packetQueue = [];

    /**
     * Given an array of audio packets, returns a single audio packet
     * containing the concatenation of those packets.
     *
     * @private
     * @param {!SampleArray[]} packets
     *     The array of audio packets to concatenate.
     *
     * @returns {SampleArray}
     *     A single audio packet containing the concatenation of all given
     *     audio packets. If no packets are provided, this will be undefined.
     */
    var joinAudioPackets = function joinAudioPackets(packets) {

        // Do not bother joining if one or fewer packets are in the queue
        if (packets.length <= 1)
            return packets[0];

        // Determine total sample length of the entire queue
        var totalLength = 0;
        packets.forEach(function addPacketLengths(packet) {
            totalLength += packet.length;
        });

        // Append each packet within queue
        var offset = 0;
        var joined = new SampleArray(totalLength);
        packets.forEach(function appendPacket(packet) {
            joined.set(packet, offset);
            offset += packet.length;
        });

        return joined;

    };

    /**
     * Given a single packet of audio data, splits off an arbitrary length of
     * audio data from the beginning of that packet, returning the split result
     * as an array of two packets. The split location is determined through an
     * algorithm intended to minimize the liklihood of audible clicking between
     * packets. If no such split location is possible, an array containing only
     * the originally-provided audio packet is returned.
     *
     * @private
     * @param {!SampleArray} data
     *     The audio packet to split.
     *
     * @returns {!SampleArray[]}
     *     An array of audio packets containing the result of splitting the
     *     provided audio packet. If splitting is possible, this array will
     *     contain two packets. If splitting is not possible, this array will
     *     contain only the originally-provided packet.
     */
    var splitAudioPacket = function splitAudioPacket(data) {

        var minValue = Number.MAX_VALUE;
        var optimalSplitLength = data.length;

        // Calculate number of whole samples in the provided audio packet AND
        // in the minimum possible split packet
        var samples = Math.floor(data.length / format.channels);
        var minSplitSamples = Math.floor(format.rate * MIN_SPLIT_SIZE);

        // Calculate the beginning of the "end" of the audio packet
        var start = Math.max(
            format.channels * minSplitSamples,
            format.channels * (samples - minSplitSamples)
        );

        // For all samples at the end of the given packet, find a point where
        // the perceptible volume across all channels is lowest (and thus is
        // the optimal point to split)
        for (var offset = start; offset < data.length; offset += format.channels) {

            // Calculate the sum of all values across all channels (the result
            // will be proportional to the average volume of a sample)
            var totalValue = 0;
            for (var channel = 0; channel < format.channels; channel++) {
                totalValue += Math.abs(data[offset + channel]);
            }

            // If this is the smallest average value thus far, set the split
            // length such that the first packet ends with the current sample
            if (totalValue <= minValue) {
                optimalSplitLength = offset + format.channels;
                minValue = totalValue;
            }

        }

        // If packet is not split, return the supplied packet untouched
        if (optimalSplitLength === data.length)
            return [data];

        // Otherwise, split the packet into two new packets according to the
        // calculated optimal split length
        return [
            new SampleArray(data.buffer.slice(0, optimalSplitLength * format.bytesPerSample)),
            new SampleArray(data.buffer.slice(optimalSplitLength * format.bytesPerSample))
        ];

    };

    /**
     * Pushes the given packet of audio data onto the playback queue. Unlike
     * other private functions within Guacamole.RawAudioPlayer, the type of the
     * ArrayBuffer packet of audio data here need not be specific to the type
     * of audio (as with SampleArray). The ArrayBuffer type provided by a
     * Guacamole.ArrayBufferReader, for example, is sufficient. Any necessary
     * conversions will be performed automatically internally.
     *
     * @private
     * @param {!ArrayBuffer} data
     *     A raw packet of audio data that should be pushed onto the audio
     *     playback queue.
     */
    var pushAudioPacket = function pushAudioPacket(data) {
        packetQueue.push(new SampleArray(data));
    };

    /**
     * Shifts off and returns a packet of audio data from the beginning of the
     * playback queue. The length of this audio packet is determined
     * dynamically according to the click-reduction algorithm implemented by
     * splitAudioPacket().
     *
     * @private
     * @returns {SampleArray}
     *     A packet of audio data pulled from the beginning of the playback
     *     queue. If there is no audio currently in the playback queue, this
     *     will be null.
     */
    var shiftAudioPacket = function shiftAudioPacket() {

        // Flatten data in packet queue
        var data = joinAudioPackets(packetQueue);
        if (!data)
            return null;

        // Pull an appropriate amount of data from the front of the queue
        packetQueue = splitAudioPacket(data);
        data = packetQueue.shift();

        return data;

    };

    /**
     * Converts the given audio packet into an AudioBuffer, ready for playback
     * by the Web Audio API. Unlike the raw audio packets received by this
     * audio player, AudioBuffers require floating point samples and are split
     * into isolated planes of channel-specific data.
     *
     * @private
     * @param {!SampleArray} data
     *     The raw audio packet that should be converted into a Web Audio API
     *     AudioBuffer.
     *
     * @returns {!AudioBuffer}
     *     A new Web Audio API AudioBuffer containing the provided audio data,
     *     converted to the format used by the Web Audio API.
     */
    var toAudioBuffer = function toAudioBuffer(data) {

        // Calculate total number of samples
        var samples = data.length / format.channels;

        // Determine exactly when packet CAN play
        var packetTime = context.currentTime;
        if (nextPacketTime < packetTime)
            nextPacketTime = packetTime;

        // Get audio buffer for specified format
        var audioBuffer = context.createBuffer(format.channels, samples, format.rate);

        // Convert each channel
        for (var channel = 0; channel < format.channels; channel++) {

            var audioData = audioBuffer.getChannelData(channel);

            // Fill audio buffer with data for channel
            var offset = channel;
            for (var i = 0; i < samples; i++) {
                audioData[i] = data[offset] / maxSampleValue;
                offset += format.channels;
            }

        }

        return audioBuffer;

    };

    // Defer playback of received audio packets slightly
    reader.ondata = function playReceivedAudio(data) {

        // Push received samples onto queue
        pushAudioPacket(new SampleArray(data));

        // Shift off an arbitrary packet of audio data from the queue (this may
        // be different in size from the packet just pushed)
        var packet = shiftAudioPacket();
        if (!packet)
            return;

        // Determine exactly when packet CAN play
        var packetTime = context.currentTime;
        if (nextPacketTime < packetTime)
            nextPacketTime = packetTime;

        // Set up buffer source
        var source = context.createBufferSource();
        source.connect(context.destination);

        // Use noteOn() instead of start() if necessary
        if (!source.start)
            source.start = source.noteOn;

        // Schedule packet
        source.buffer = toAudioBuffer(packet);
        source.start(nextPacketTime);

        // Update timeline by duration of scheduled packet
        nextPacketTime += packet.length / format.channels / format.rate;

    };

    /** @override */
    this.sync = function sync() {

        // Calculate elapsed time since last sync
        var now = context.currentTime;

        // Reschedule future playback time such that playback latency is
        // bounded within a reasonable latency threshold
        nextPacketTime = Math.min(nextPacketTime, now + maxLatency);

    };

};

Guacamole.RawAudioPlayer.prototype = new Guacamole.AudioPlayer();

/**
 * Determines whether the given mimetype is supported by
 * Guacamole.RawAudioPlayer.
 *
 * @param {!string} mimetype
 *     The mimetype to check.
 *
 * @returns {!boolean}
 *     true if the given mimetype is supported by Guacamole.RawAudioPlayer,
 *     false otherwise.
 */
Guacamole.RawAudioPlayer.isSupportedType = function isSupportedType(mimetype) {

    // No supported types if no Web Audio API
    if (!Guacamole.AudioContextFactory.getAudioContext())
        return false;

    return Guacamole.RawAudioFormat.parse(mimetype) !== null;

};

/**
 * Returns a list of all mimetypes supported by Guacamole.RawAudioPlayer. Only
 * the core mimetypes themselves will be listed. Any mimetype parameters, even
 * required ones, will not be included in the list. For example, "audio/L8" is
 * a raw audio mimetype that may be supported, but it is invalid without
 * additional parameters. Something like "audio/L8;rate=44100" would be valid,
 * however (see https://tools.ietf.org/html/rfc4856).
 *
 * @returns {!string[]}
 *     A list of all mimetypes supported by Guacamole.RawAudioPlayer, excluding
 *     any parameters. If the necessary JavaScript APIs for playing raw audio
 *     are absent, this list will be empty.
 */
Guacamole.RawAudioPlayer.getSupportedTypes = function getSupportedTypes() {

    // No supported types if no Web Audio API
    if (!Guacamole.AudioContextFactory.getAudioContext())
        return [];

    // We support 8-bit and 16-bit raw PCM
    return [
        'audio/L8',
        'audio/L16'
    ];

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Abstract audio recorder which streams arbitrary audio data to an underlying
 * Guacamole.OutputStream. It is up to implementations of this class to provide
 * some means of handling this Guacamole.OutputStream. Data produced by the
 * recorder is to be sent along the provided stream immediately.
 *
 * @constructor
 */
Guacamole.AudioRecorder = function AudioRecorder() {

    /**
     * Callback which is invoked when the audio recording process has stopped
     * and the underlying Guacamole stream has been closed normally. Audio will
     * only resume recording if a new Guacamole.AudioRecorder is started. This
     * Guacamole.AudioRecorder instance MAY NOT be reused.
     *
     * @event
     */
    this.onclose = null;

    /**
     * Callback which is invoked when the audio recording process cannot
     * continue due to an error, if it has started at all. The underlying
     * Guacamole stream is automatically closed. Future attempts to record
     * audio should not be made, and this Guacamole.AudioRecorder instance
     * MAY NOT be reused.
     *
     * @event
     */
    this.onerror = null;

};

/**
 * Determines whether the given mimetype is supported by any built-in
 * implementation of Guacamole.AudioRecorder, and thus will be properly handled
 * by Guacamole.AudioRecorder.getInstance().
 *
 * @param {!string} mimetype
 *     The mimetype to check.
 *
 * @returns {!boolean}
 *     true if the given mimetype is supported by any built-in
 *     Guacamole.AudioRecorder, false otherwise.
 */
Guacamole.AudioRecorder.isSupportedType = function isSupportedType(mimetype) {

    return Guacamole.RawAudioRecorder.isSupportedType(mimetype);

};

/**
 * Returns a list of all mimetypes supported by any built-in
 * Guacamole.AudioRecorder, in rough order of priority. Beware that only the
 * core mimetypes themselves will be listed. Any mimetype parameters, even
 * required ones, will not be included in the list. For example, "audio/L8" is
 * a supported raw audio mimetype that is supported, but it is invalid without
 * additional parameters. Something like "audio/L8;rate=44100" would be valid,
 * however (see https://tools.ietf.org/html/rfc4856).
 *
 * @returns {!string[]}
 *     A list of all mimetypes supported by any built-in
 *     Guacamole.AudioRecorder, excluding any parameters.
 */
Guacamole.AudioRecorder.getSupportedTypes = function getSupportedTypes() {

    return Guacamole.RawAudioRecorder.getSupportedTypes();

};

/**
 * Returns an instance of Guacamole.AudioRecorder providing support for the
 * given audio format. If support for the given audio format is not available,
 * null is returned.
 *
 * @param {!Guacamole.OutputStream} stream
 *     The Guacamole.OutputStream to send audio data through.
 *
 * @param {!string} mimetype
 *     The mimetype of the audio data to be sent along the provided stream.
 *
 * @return {Guacamole.AudioRecorder}
 *     A Guacamole.AudioRecorder instance supporting the given mimetype and
 *     writing to the given stream, or null if support for the given mimetype
 *     is absent.
 */
Guacamole.AudioRecorder.getInstance = function getInstance(stream, mimetype) {

    // Use raw audio recorder if possible
    if (Guacamole.RawAudioRecorder.isSupportedType(mimetype))
        return new Guacamole.RawAudioRecorder(stream, mimetype);

    // No support for given mimetype
    return null;

};

/**
 * Implementation of Guacamole.AudioRecorder providing support for raw PCM
 * format audio. This recorder relies only on the Web Audio API and does not
 * require any browser-level support for its audio formats.
 *
 * @constructor
 * @augments Guacamole.AudioRecorder
 * @param {!Guacamole.OutputStream} stream
 *     The Guacamole.OutputStream to write audio data to.
 *
 * @param {!string} mimetype
 *     The mimetype of the audio data to send along the provided stream, which
 *     must be a "audio/L8" or "audio/L16" mimetype with necessary parameters,
 *     such as: "audio/L16;rate=44100,channels=2".
 */
Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) {

    /**
     * Reference to this RawAudioRecorder.
     *
     * @private
     * @type {!Guacamole.RawAudioRecorder}
     */
    var recorder = this;

    /**
     * The size of audio buffer to request from the Web Audio API when
     * recording or processing audio, in sample-frames. This must be a power of
     * two between 256 and 16384 inclusive, as required by
     * AudioContext.createScriptProcessor().
     *
     * @private
     * @constant
     * @type {!number}
     */
    var BUFFER_SIZE = 2048;

    /**
     * The window size to use when applying Lanczos interpolation, commonly
     * denoted by the variable "a".
     * See: https://en.wikipedia.org/wiki/Lanczos_resampling
     *
     * @private
     * @contant
     * @type {!number}
     */
    var LANCZOS_WINDOW_SIZE = 3;

    /**
     * The format of audio this recorder will encode.
     *
     * @private
     * @type {Guacamole.RawAudioFormat}
     */
    var format = Guacamole.RawAudioFormat.parse(mimetype);

    /**
     * An instance of a Web Audio API AudioContext object, or null if the
     * Web Audio API is not supported.
     *
     * @private
     * @type {AudioContext}
     */
    var context = Guacamole.AudioContextFactory.getAudioContext();

    // Some browsers do not implement navigator.mediaDevices - this
    // shims in this functionality to ensure code compatibility.
    if (!navigator.mediaDevices)
        navigator.mediaDevices = {};

    // Browsers that either do not implement navigator.mediaDevices
    // at all or do not implement it completely need the getUserMedia
    // method defined.  This shims in this function by detecting
    // one of the supported legacy methods.
    if (!navigator.mediaDevices.getUserMedia)
        navigator.mediaDevices.getUserMedia = (navigator.getUserMedia
                || navigator.webkitGetUserMedia
                || navigator.mozGetUserMedia
                || navigator.msGetUserMedia).bind(navigator);

    /**
     * Guacamole.ArrayBufferWriter wrapped around the audio output stream
     * provided when this Guacamole.RawAudioRecorder was created.
     *
     * @private
     * @type {!Guacamole.ArrayBufferWriter}
     */
    var writer = new Guacamole.ArrayBufferWriter(stream);

    /**
     * The type of typed array that will be used to represent each audio packet
     * internally. This will be either Int8Array or Int16Array, depending on
     * whether the raw audio format is 8-bit or 16-bit.
     *
     * @private
     * @constructor
     */
    var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array;

    /**
     * The maximum absolute value of any sample within a raw audio packet sent
     * by this audio recorder. This depends only on the size of each sample,
     * and will be 128 for 8-bit audio and 32768 for 16-bit audio.
     *
     * @private
     * @type {!number}
     */
    var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768;

    /**
     * The total number of audio samples read from the local audio input device
     * over the life of this audio recorder.
     *
     * @private
     * @type {!number}
     */
    var readSamples = 0;

    /**
     * The total number of audio samples written to the underlying Guacamole
     * connection over the life of this audio recorder.
     *
     * @private
     * @type {!number}
     */
    var writtenSamples = 0;

    /**
     * The audio stream provided by the browser, if allowed. If no stream has
     * yet been received, this will be null.
     *
     * @private
     * @type {MediaStream}
     */
    var mediaStream = null;

    /**
     * The source node providing access to the local audio input device.
     *
     * @private
     * @type {MediaStreamAudioSourceNode}
     */
    var source = null;

    /**
     * The script processing node which receives audio input from the media
     * stream source node as individual audio buffers.
     *
     * @private
     * @type {ScriptProcessorNode}
     */
    var processor = null;

    /**
     * The normalized sinc function. The normalized sinc function is defined as
     * 1 for x=0 and sin(PI * x) / (PI * x) for all other values of x.
     *
     * See: https://en.wikipedia.org/wiki/Sinc_function
     *
     * @private
     * @param {!number} x
     *     The point at which the normalized sinc function should be computed.
     *
     * @returns {!number}
     *     The value of the normalized sinc function at x.
     */
    var sinc = function sinc(x) {

        // The value of sinc(0) is defined as 1
        if (x === 0)
            return 1;

        // Otherwise, normlized sinc(x) is sin(PI * x) / (PI * x)
        var piX = Math.PI * x;
        return Math.sin(piX) / piX;

    };

    /**
     * Calculates the value of the Lanczos kernal at point x for a given window
     * size. See: https://en.wikipedia.org/wiki/Lanczos_resampling
     *
     * @private
     * @param {!number} x
     *     The point at which the value of the Lanczos kernel should be
     *     computed.
     *
     * @param {!number} a
     *     The window size to use for the Lanczos kernel.
     *
     * @returns {!number}
     *     The value of the Lanczos kernel at the given point for the given
     *     window size.
     */
    var lanczos = function lanczos(x, a) {

        // Lanczos is sinc(x) * sinc(x / a) for -a < x < a ...
        if (-a < x && x < a)
            return sinc(x) * sinc(x / a);

        // ... and 0 otherwise
        return 0;

    };

    /**
     * Determines the value of the waveform represented by the audio data at
     * the given location. If the value cannot be determined exactly as it does
     * not correspond to an exact sample within the audio data, the value will
     * be derived through interpolating nearby samples.
     *
     * @private
     * @param {!Float32Array} audioData
     *     An array of audio data, as returned by AudioBuffer.getChannelData().
     *
     * @param {!number} t
     *     The relative location within the waveform from which the value
     *     should be retrieved, represented as a floating point number between
     *     0 and 1 inclusive, where 0 represents the earliest point in time and
     *     1 represents the latest.
     *
     * @returns {!number}
     *     The value of the waveform at the given location.
     */
    var interpolateSample = function getValueAt(audioData, t) {

        // Convert [0, 1] range to [0, audioData.length - 1]
        var index = (audioData.length - 1) * t;

        // Determine the start and end points for the summation used by the
        // Lanczos interpolation algorithm (see: https://en.wikipedia.org/wiki/Lanczos_resampling)
        var start = Math.floor(index) - LANCZOS_WINDOW_SIZE + 1;
        var end = Math.floor(index) + LANCZOS_WINDOW_SIZE;

        // Calculate the value of the Lanczos interpolation function for the
        // required range
        var sum = 0;
        for (var i = start; i <= end; i++) {
            sum += (audioData[i] || 0) * lanczos(index - i, LANCZOS_WINDOW_SIZE);
        }

        return sum;

    };

    /**
     * Converts the given AudioBuffer into an audio packet, ready for streaming
     * along the underlying output stream. Unlike the raw audio packets used by
     * this audio recorder, AudioBuffers require floating point samples and are
     * split into isolated planes of channel-specific data.
     *
     * @private
     * @param {!AudioBuffer} audioBuffer
     *     The Web Audio API AudioBuffer that should be converted to a raw
     *     audio packet.
     *
     * @returns {!SampleArray}
     *     A new raw audio packet containing the audio data from the provided
     *     AudioBuffer.
     */
    var toSampleArray = function toSampleArray(audioBuffer) {

        // Track overall amount of data read
        var inSamples = audioBuffer.length;
        readSamples += inSamples;

        // Calculate the total number of samples that should be written as of
        // the audio data just received and adjust the size of the output
        // packet accordingly
        var expectedWrittenSamples = Math.round(readSamples * format.rate / audioBuffer.sampleRate);
        var outSamples = expectedWrittenSamples - writtenSamples;

        // Update number of samples written
        writtenSamples += outSamples;

        // Get array for raw PCM storage
        var data = new SampleArray(outSamples * format.channels);

        // Convert each channel
        for (var channel = 0; channel < format.channels; channel++) {

            var audioData = audioBuffer.getChannelData(channel);

            // Fill array with data from audio buffer channel
            var offset = channel;
            for (var i = 0; i < outSamples; i++) {
                data[offset] = interpolateSample(audioData, i / (outSamples - 1)) * maxSampleValue;
                offset += format.channels;
            }

        }

        return data;

    };

    /**
     * getUserMedia() callback which handles successful retrieval of an
     * audio stream (successful start of recording).
     *
     * @private
     * @param {!MediaStream} stream
     *     A MediaStream which provides access to audio data read from the
     *     user's local audio input device.
     */
    var streamReceived = function streamReceived(stream) {

        // Create processing node which receives appropriately-sized audio buffers
        processor = context.createScriptProcessor(BUFFER_SIZE, format.channels, format.channels);
        processor.connect(context.destination);

        // Send blobs when audio buffers are received
        processor.onaudioprocess = function processAudio(e) {
            writer.sendData(toSampleArray(e.inputBuffer).buffer);
        };

        // Connect processing node to user's audio input source
        source = context.createMediaStreamSource(stream);
        source.connect(processor);

        // Attempt to explicitly resume AudioContext, as it may be paused
        // by default
        if (context.state === 'suspended')
            context.resume();

        // Save stream for later cleanup
        mediaStream = stream;

    };

    /**
     * getUserMedia() callback which handles audio recording denial. The
     * underlying Guacamole output stream is closed, and the failure to
     * record is noted using onerror.
     *
     * @private
     */
    var streamDenied = function streamDenied() {

        // Simply end stream if audio access is not allowed
        writer.sendEnd();

        // Notify of closure
        if (recorder.onerror)
            recorder.onerror();

    };

    /**
     * Requests access to the user's microphone and begins capturing audio. All
     * received audio data is resampled as necessary and forwarded to the
     * Guacamole stream underlying this Guacamole.RawAudioRecorder. This
     * function must be invoked ONLY ONCE per instance of
     * Guacamole.RawAudioRecorder.
     *
     * @private
     */
    var beginAudioCapture = function beginAudioCapture() {

        // Attempt to retrieve an audio input stream from the browser
        var promise = navigator.mediaDevices.getUserMedia({
            'audio' : true
        }, streamReceived, streamDenied);

        // Handle stream creation/rejection via Promise for newer versions of
        // getUserMedia()
        if (promise && promise.then)
            promise.then(streamReceived, streamDenied);

    };

    /**
     * Stops capturing audio, if the capture has started, freeing all associated
     * resources. If the capture has not started, this function simply ends the
     * underlying Guacamole stream.
     *
     * @private
     */
    var stopAudioCapture = function stopAudioCapture() {

        // Disconnect media source node from script processor
        if (source)
            source.disconnect();

        // Disconnect associated script processor node
        if (processor)
            processor.disconnect();

        // Stop capture
        if (mediaStream) {
            var tracks = mediaStream.getTracks();
            for (var i = 0; i < tracks.length; i++)
                tracks[i].stop();
        }

        // Remove references to now-unneeded components
        processor = null;
        source = null;
        mediaStream = null;

        // End stream
        writer.sendEnd();

    };

    // Once audio stream is successfully open, request and begin reading audio
    writer.onack = function audioStreamAcknowledged(status) {

        // Begin capture if successful response and not yet started
        if (status.code === Guacamole.Status.Code.SUCCESS && !mediaStream)
            beginAudioCapture();

        // Otherwise stop capture and cease handling any further acks
        else {

            // Stop capturing audio
            stopAudioCapture();
            writer.onack = null;

            // Notify if stream has closed normally
            if (status.code === Guacamole.Status.Code.RESOURCE_CLOSED) {
                if (recorder.onclose)
                    recorder.onclose();
            }

            // Otherwise notify of closure due to error
            else {
                if (recorder.onerror)
                    recorder.onerror();
            }

        }

    };

};

Guacamole.RawAudioRecorder.prototype = new Guacamole.AudioRecorder();

/**
 * Determines whether the given mimetype is supported by
 * Guacamole.RawAudioRecorder.
 *
 * @param {!string} mimetype
 *     The mimetype to check.
 *
 * @returns {!boolean}
 *     true if the given mimetype is supported by Guacamole.RawAudioRecorder,
 *     false otherwise.
 */
Guacamole.RawAudioRecorder.isSupportedType = function isSupportedType(mimetype) {

    // No supported types if no Web Audio API
    if (!Guacamole.AudioContextFactory.getAudioContext())
        return false;

    return Guacamole.RawAudioFormat.parse(mimetype) !== null;

};

/**
 * Returns a list of all mimetypes supported by Guacamole.RawAudioRecorder. Only
 * the core mimetypes themselves will be listed. Any mimetype parameters, even
 * required ones, will not be included in the list. For example, "audio/L8" is
 * a raw audio mimetype that may be supported, but it is invalid without
 * additional parameters. Something like "audio/L8;rate=44100" would be valid,
 * however (see https://tools.ietf.org/html/rfc4856).
 *
 * @returns {!string[]}
 *     A list of all mimetypes supported by Guacamole.RawAudioRecorder,
 *     excluding any parameters. If the necessary JavaScript APIs for recording
 *     raw audio are absent, this list will be empty.
 */
Guacamole.RawAudioRecorder.getSupportedTypes = function getSupportedTypes() {

    // No supported types if no Web Audio API
    if (!Guacamole.AudioContextFactory.getAudioContext())
        return [];

    // We support 8-bit and 16-bit raw PCM
    return [
        'audio/L8',
        'audio/L16'
    ];

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A reader which automatically handles the given input stream, assembling all
 * received blobs into a single blob by appending them to each other in order.
 * Note that this object will overwrite any installed event handlers on the
 * given Guacamole.InputStream.
 * 
 * @constructor
 * @param {!Guacamole.InputStream} stream
 *     The stream that data will be read from.
 *
 * @param {!string} mimetype
 *     The mimetype of the blob being built.
 */
Guacamole.BlobReader = function(stream, mimetype) {

    /**
     * Reference to this Guacamole.InputStream.
     *
     * @private
     * @type {!Guacamole.BlobReader}
     */
    var guac_reader = this;

    /**
     * The length of this Guacamole.InputStream in bytes.
     *
     * @private
     * @type {!number}
     */
    var length = 0;

    // Get blob builder
    var blob_builder;
    if      (window.BlobBuilder)       blob_builder = new BlobBuilder();
    else if (window.WebKitBlobBuilder) blob_builder = new WebKitBlobBuilder();
    else if (window.MozBlobBuilder)    blob_builder = new MozBlobBuilder();
    else
        blob_builder = new (function() {

            var blobs = [];

            /** @ignore */
            this.append = function(data) {
                blobs.push(new Blob([data], {"type": mimetype}));
            };

            /** @ignore */
            this.getBlob = function() {
                return new Blob(blobs, {"type": mimetype});
            };

        })();

    // Append received blobs
    stream.onblob = function(data) {

        // Convert to ArrayBuffer
        var binary = window.atob(data);
        var arrayBuffer = new ArrayBuffer(binary.length);
        var bufferView = new Uint8Array(arrayBuffer);

        for (var i=0; i<binary.length; i++)
            bufferView[i] = binary.charCodeAt(i);

        blob_builder.append(arrayBuffer);
        length += arrayBuffer.byteLength;

        // Call handler, if present
        if (guac_reader.onprogress)
            guac_reader.onprogress(arrayBuffer.byteLength);

        // Send success response
        stream.sendAck("OK", 0x0000);

    };

    // Simply call onend when end received
    stream.onend = function() {
        if (guac_reader.onend)
            guac_reader.onend();
    };

    /**
     * Returns the current length of this Guacamole.InputStream, in bytes.
     *
     * @return {!number}
     *     The current length of this Guacamole.InputStream.
     */
    this.getLength = function() {
        return length;
    };

    /**
     * Returns the contents of this Guacamole.BlobReader as a Blob.
     *
     * @return {!Blob}
     *     The contents of this Guacamole.BlobReader.
     */
    this.getBlob = function() {
        return blob_builder.getBlob();
    };

    /**
     * Fired once for every blob of data received.
     * 
     * @event
     * @param {!number} length
     *     The number of bytes received.
     */
    this.onprogress = null;

    /**
     * Fired once this stream is finished and no further data will be written.
     * @event
     */
    this.onend = null;

};
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A writer which automatically writes to the given output stream with the
 * contents of provided Blob objects.
 *
 * @constructor
 * @param {!Guacamole.OutputStream} stream
 *     The stream that data will be written to.
 */
Guacamole.BlobWriter = function BlobWriter(stream) {

    /**
     * Reference to this Guacamole.BlobWriter.
     *
     * @private
     * @type {!Guacamole.BlobWriter}
     */
    var guacWriter = this;

    /**
     * Wrapped Guacamole.ArrayBufferWriter which will be used to send any
     * provided file data.
     *
     * @private
     * @type {!Guacamole.ArrayBufferWriter}
     */
    var arrayBufferWriter = new Guacamole.ArrayBufferWriter(stream);

    // Initially, simply call onack for acknowledgements
    arrayBufferWriter.onack = function(status) {
        if (guacWriter.onack)
            guacWriter.onack(status);
    };

    /**
     * Browser-independent implementation of Blob.slice() which uses an end
     * offset to determine the span of the resulting slice, rather than a
     * length.
     *
     * @private
     * @param {!Blob} blob
     *     The Blob to slice.
     *
     * @param {!number} start
     *     The starting offset of the slice, in bytes, inclusive.
     *
     * @param {!number} end
     *     The ending offset of the slice, in bytes, exclusive.
     *
     * @returns {!Blob}
     *     A Blob containing the data within the given Blob starting at
     *     <code>start</code> and ending at <code>end - 1</code>.
     */
    var slice = function slice(blob, start, end) {

        // Use prefixed implementations if necessary
        var sliceImplementation = (
                blob.slice
             || blob.webkitSlice
             || blob.mozSlice
        ).bind(blob);

        var length = end - start;

        // The old Blob.slice() was length-based (not end-based). Try the
        // length version first, if the two calls are not equivalent.
        if (length !== end) {

            // If the result of the slice() call matches the expected length,
            // trust that result. It must be correct.
            var sliceResult = sliceImplementation(start, length);
            if (sliceResult.size === length)
                return sliceResult;

        }

        // Otherwise, use the most-recent standard: end-based slice()
        return sliceImplementation(start, end);

    };

    /**
     * Sends the contents of the given blob over the underlying stream.
     *
     * @param {!Blob} blob
     *     The blob to send.
     */
    this.sendBlob = function sendBlob(blob) {

        var offset = 0;
        var reader = new FileReader();

        /**
         * Reads the next chunk of the blob provided to
         * [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}. The chunk itself
         * is read asynchronously, and will not be available until
         * reader.onload fires.
         *
         * @private
         */
        var readNextChunk = function readNextChunk() {

            // If no further chunks remain, inform of completion and stop
            if (offset >= blob.size) {

                // Fire completion event for completed blob
                if (guacWriter.oncomplete)
                    guacWriter.oncomplete(blob);

                // No further chunks to read
                return;

            }

            // Obtain reference to next chunk as a new blob
            var chunk = slice(blob, offset, offset + arrayBufferWriter.blobLength);
            offset += arrayBufferWriter.blobLength;

            // Attempt to read the blob contents represented by the blob into
            // a new array buffer
            reader.readAsArrayBuffer(chunk);

        };

        // Send each chunk over the stream, continue reading the next chunk
        reader.onload = function chunkLoadComplete() {

            // Send the successfully-read chunk
            arrayBufferWriter.sendData(reader.result);

            // Continue sending more chunks after the latest chunk is
            // acknowledged
            arrayBufferWriter.onack = function sendMoreChunks(status) {

                if (guacWriter.onack)
                    guacWriter.onack(status);

                // Abort transfer if an error occurs
                if (status.isError())
                    return;

                // Inform of blob upload progress via progress events
                if (guacWriter.onprogress)
                    guacWriter.onprogress(blob, offset - arrayBufferWriter.blobLength);

                // Queue the next chunk for reading
                readNextChunk();

            };

        };

        // If an error prevents further reading, inform of error and stop
        reader.onerror = function chunkLoadFailed() {

            // Fire error event, including the context of the error
            if (guacWriter.onerror)
                guacWriter.onerror(blob, offset, reader.error);

        };

        // Begin reading the first chunk
        readNextChunk();

    };

    /**
     * Signals that no further text will be sent, effectively closing the
     * stream.
     */
    this.sendEnd = function sendEnd() {
        arrayBufferWriter.sendEnd();
    };

    /**
     * Fired for received data, if acknowledged by the server.
     *
     * @event
     * @param {!Guacamole.Status} status
     *     The status of the operation.
     */
    this.onack = null;

    /**
     * Fired when an error occurs reading a blob passed to
     * [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}. The transfer for the
     * the given blob will cease, but the stream will remain open.
     *
     * @event
     * @param {!Blob} blob
     *     The blob that was being read when the error occurred.
     *
     * @param {!number} offset
     *     The offset of the failed read attempt within the blob, in bytes.
     *
     * @param {!DOMError} error
     *     The error that occurred.
     */
    this.onerror = null;

    /**
     * Fired for each successfully-read chunk of data as a blob is being sent
     * via [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}.
     *
     * @event
     * @param {!Blob} blob
     *     The blob that is being read.
     *
     * @param {!number} offset
     *     The offset of the read that just succeeded.
     */
    this.onprogress = null;

    /**
     * Fired when a blob passed to
     * [sendBlob()]{@link Guacamole.BlobWriter#sendBlob} has finished being
     * sent.
     *
     * @event
     * @param {!Blob} blob
     *     The blob that was sent.
     */
    this.oncomplete = null;

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

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;

    /**
     * The rough number of milliseconds to wait between sending keep-alive
     * pings. This may vary depending on how frequently the browser allows
     * timers to run, as well as how frequently the client receives messages
     * from the server.
     *
     * @private
     * @constant
     * @type {!number}
     */
    var KEEP_ALIVE_FREQUENCY = 5000;

    /**
     * The current keep-alive ping timeout ID, if any. This will only be set
     * upon connecting.
     *
     * @private
     * @type {number}
     */
    var keepAliveTimeout = null;

    /**
     * The timestamp of the point in time that the last keep-live ping was
     * sent, in milliseconds elapsed since midnight of January 1, 1970 UTC.
     *
     * @private
     * @type {!number}
     */
    var lastSentKeepAlive = 0;

    /**
     * Translation from Guacamole protocol line caps to Layer line caps.
     *
     * @private
     * @type {!Object.<number, string>}
     */
    var lineCap = {
        0: "butt",
        1: "round",
        2: "square"
    };

    /**
     * Translation from Guacamole protocol line caps to Layer line caps.
     *
     * @private
     * @type {!Object.<number, string>}
     */
    var lineJoin = {
        0: "bevel",
        1: "miter",
        2: "round"
    };

    /**
     * The underlying Guacamole display.
     *
     * @private
     * @type {!Guacamole.Display}
     */
    var display = new Guacamole.Display();

    /**
     * All available layers and buffers
     *
     * @private
     * @type {!Object.<number, (Guacamole.Display.VisibleLayer|Guacamole.Layer)>}
     */
    var layers = {};
    
    /**
     * All audio players currently in use by the client. Initially, this will
     * be empty, but audio players may be allocated by the server upon request.
     *
     * @private
     * @type {!Object.<number, Guacamole.AudioPlayer>}
     */
    var audioPlayers = {};

    /**
     * All video players currently in use by the client. Initially, this will
     * be empty, but video players may be allocated by the server upon request.
     *
     * @private
     * @type {!Object.<number, Guacamole.VideoPlayer>}
     */
    var videoPlayers = {};

    // No initial parsers
    var parsers = [];

    // No initial streams 
    var streams = [];

    /**
     * All current objects. The index of each object is dictated by the
     * Guacamole server.
     *
     * @private
     * @type {!Guacamole.Object[]}
     */
    var objects = [];

    // 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;
    }

    /**
     * Produces an opaque representation of Guacamole.Client state which can be
     * later imported through a call to importState(). This object is
     * effectively an independent, compressed snapshot of protocol and display
     * state. Invoking this function implicitly flushes the display.
     *
     * @param {!function} callback
     *     Callback which should be invoked once the state object is ready. The
     *     state object will be passed to the callback as the sole parameter.
     *     This callback may be invoked immediately, or later as the display
     *     finishes rendering and becomes ready.
     */
    this.exportState = function exportState(callback) {

        // Start with empty state
        var state = {
            'currentState' : currentState,
            'currentTimestamp' : currentTimestamp,
            'layers' : {}
        };

        var layersSnapshot = {};

        // Make a copy of all current layers (protocol state)
        for (var key in layers) {
            layersSnapshot[key] = layers[key];
        }

        // Populate layers once data is available (display state, requires flush)
        display.flush(function populateLayers() {

            // Export each defined layer/buffer
            for (var key in layersSnapshot) {

                var index = parseInt(key);
                var layer = layersSnapshot[key];
                var canvas = layer.toCanvas();

                // Store layer/buffer dimensions
                var exportLayer = {
                    'width'  : layer.width,
                    'height' : layer.height
                };

                // Store layer/buffer image data, if it can be generated
                if (layer.width && layer.height)
                    exportLayer.url = canvas.toDataURL('image/png');

                // Add layer properties if not a buffer nor the default layer
                if (index > 0) {
                    exportLayer.x = layer.x;
                    exportLayer.y = layer.y;
                    exportLayer.z = layer.z;
                    exportLayer.alpha = layer.alpha;
                    exportLayer.matrix = layer.matrix;
                    exportLayer.parent = getLayerIndex(layer.parent);
                }

                // Store exported layer
                state.layers[key] = exportLayer;

            }

            // Invoke callback now that the state is ready
            callback(state);

        });

    };

    /**
     * Restores Guacamole.Client protocol and display state based on an opaque
     * object from a prior call to exportState(). The Guacamole.Client instance
     * used to export that state need not be the same as this instance.
     *
     * @param {!object} state
     *     An opaque representation of Guacamole.Client state from a prior call
     *     to exportState().
     *
     * @param {function} [callback]
     *     The function to invoke when state has finished being imported. This
     *     may happen immediately, or later as images within the provided state
     *     object are loaded.
     */
    this.importState = function importState(state, callback) {

        var key;
        var index;

        currentState = state.currentState;
        currentTimestamp = state.currentTimestamp;

        // Cancel any pending display operations/frames
        display.cancel();

        // Dispose of all layers
        for (key in layers) {
            index = parseInt(key);
            if (index > 0)
                layers[key].dispose();
        }

        layers = {};

        // Import state of each layer/buffer
        for (key in state.layers) {

            index = parseInt(key);

            var importLayer = state.layers[key];
            var layer = getLayer(index);

            // Reset layer size
            display.resize(layer, importLayer.width, importLayer.height);

            // Initialize new layer if it has associated data
            if (importLayer.url) {
                display.setChannelMask(layer, Guacamole.Layer.SRC);
                display.draw(layer, 0, 0, importLayer.url);
            }

            // Set layer-specific properties if not a buffer nor the default layer
            if (index > 0 && importLayer.parent >= 0) {

                // Apply layer position and set parent
                var parent = getLayer(importLayer.parent);
                display.move(layer, parent, importLayer.x, importLayer.y, importLayer.z);

                // Set layer transparency
                display.shade(layer, importLayer.alpha);

                // Apply matrix transform
                var matrix = importLayer.matrix;
                display.distort(layer,
                    matrix[0], matrix[1], matrix[2],
                    matrix[3], matrix[4], matrix[5]);

            }

        }

        // Flush changes to display
        display.flush(callback);

    };

    /**
     * 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.
     *
     * @param {boolean} [applyDisplayScale=false]
     *     Whether the provided mouse state uses local display units, rather
     *     than remote display units, and should be scaled to match the
     *     {@link Guacamole.Display}.
     */
    this.sendMouseState = function sendMouseState(mouseState, applyDisplayScale) {

        // Do not send requests if not connected
        if (!isConnected())
            return;

        var x = mouseState.x;
        var y = mouseState.y;

        // Translate for display units if requested
        if (applyDisplayScale) {
            x /= display.getScale();
            y /= display.getScale();
        }

        // Update client-side cursor
        display.moveCursor(
            Math.floor(x),
            Math.floor(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(x), Math.floor(y), buttonMask);
    };

    /**
     * Sends a touch event having the properties provided by the given touch
     * state.
     *
     * @param {!Guacamole.Touch.State} touchState
     *     The state of the touch contact to send in the touch event.
     *
     * @param {boolean} [applyDisplayScale=false]
     *     Whether the provided touch state uses local display units, rather
     *     than remote display units, and should be scaled to match the
     *     {@link Guacamole.Display}.
     */
    this.sendTouchState = function sendTouchState(touchState, applyDisplayScale) {

        // Do not send requests if not connected
        if (!isConnected())
            return;

        var x = touchState.x;
        var y = touchState.y;

        // Translate for display units if requested
        if (applyDisplayScale) {
            x /= display.getScale();
            y /= display.getScale();
        }

        tunnel.sendMessage('touch', touchState.id, Math.floor(x), Math.floor(y),
            Math.floor(touchState.radiusX), Math.floor(touchState.radiusY),
            touchState.angle, touchState.force);

    };

    /**
     * Allocates an available stream index and creates a new
     * Guacamole.OutputStream using that index, associating the resulting
     * stream with this Guacamole.Client. Note that this stream will not yet
     * exist as far as the other end of the Guacamole connection is concerned.
     * Streams exist within the Guacamole protocol only when referenced by an
     * instruction which creates the stream, such as a "clipboard", "file", or
     * "pipe" instruction.
     *
     * @returns {!Guacamole.OutputStream}
     *     A new Guacamole.OutputStream with a newly-allocated index and
     *     associated with this Guacamole.Client.
     */
    this.createOutputStream = function createOutputStream() {

        // Allocate index
        var index = stream_indices.next();

        // Return new stream
        var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index);
        return stream;

    };

    /**
     * Opens a new audio stream for writing, where audio data having the give
     * mimetype will be sent along the returned stream. The instruction
     * necessary to create this stream will automatically be sent.
     *
     * @param {!string} mimetype
     *     The mimetype of the audio data that will be sent along the returned
     *     stream.
     *
     * @return {!Guacamole.OutputStream}
     *     The created audio stream.
     */
    this.createAudioStream = function(mimetype) {

        // Allocate and associate stream with audio metadata
        var stream = guac_client.createOutputStream();
        tunnel.sendMessage("audio", stream.index, mimetype);
        return stream;

    };

    /**
     * Opens a new file for writing, having the given index, mimetype and
     * filename. The instruction necessary to create this stream will
     * automatically be sent.
     *
     * @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 and associate stream with file metadata
        var stream = guac_client.createOutputStream();
        tunnel.sendMessage("file", stream.index, mimetype, filename);
        return stream;

    };

    /**
     * Opens a new pipe for writing, having the given name and mimetype. The
     * instruction necessary to create this stream will automatically be sent.
     *
     * @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 and associate stream with pipe metadata
        var stream = guac_client.createOutputStream();
        tunnel.sendMessage("pipe", stream.index, mimetype, name);
        return stream;

    };

    /**
     * Opens a new clipboard object for writing, having the given mimetype. The
     * instruction necessary to create this stream will automatically be sent.
     *
     * @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 and associate stream with clipboard metadata
        var stream = guac_client.createOutputStream();
        tunnel.sendMessage("clipboard", stream.index, mimetype);
        return stream;

    };

    /**
     * Opens a new argument value stream for writing, having the given
     * parameter name and mimetype, requesting that the connection parameter
     * with the given name be updated to the value described by the contents
     * of the following stream. The instruction necessary to create this stream
     * will automatically be sent.
     *
     * @param {!string} mimetype
     *     The mimetype of the data being sent.
     *
     * @param {!string} name
     *     The name of the connection parameter to attempt to update.
     *
     * @return {!Guacamole.OutputStream}
     *     The created argument value stream.
     */
    this.createArgumentValueStream = function createArgumentValueStream(mimetype, name) {

        // Allocate and associate stream with argument value metadata
        var stream = guac_client.createOutputStream();
        tunnel.sendMessage("argv", stream.index, mimetype, name);
        return stream;

    };

    /**
     * Creates a new output stream associated with the given object and having
     * the given mimetype and name. The legality of a mimetype and name is
     * dictated by the object itself. The instruction necessary to create this
     * stream will automatically be sent.
     *
     * @param {!number} index
     *     The index of the object for which the output stream is being
     *     created.
     *
     * @param {!string} mimetype
     *     The mimetype of the data which will be sent to the output stream.
     *
     * @param {!string} name
     *     The defined name of an output stream within the given object.
     *
     * @returns {!Guacamole.OutputStream}
     *     An output stream which will write blobs to the named output stream
     *     of the given object.
     */
    this.createObjectOutputStream = function createObjectOutputStream(index, mimetype, name) {

        // Allocate and associate stream with object metadata
        var stream = guac_client.createOutputStream();
        tunnel.sendMessage("put", index, stream.index, mimetype, name);
        return stream;

    };

    /**
     * Requests read access to the input stream having the given name. If
     * successful, a new input stream will be created.
     *
     * @param {!number} index
     *     The index of the object from which the input stream is being
     *     requested.
     *
     * @param {!string} name
     *     The name of the input stream to request.
     */
    this.requestObjectInputStream = function requestObjectInputStream(index, name) {

        // Do not send requests if not connected
        if (!isConnected())
            return;

        tunnel.sendMessage("get", index, name);
    };

    /**
     * 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. The other end of the
     * Guacamole connection will be notified via an "end" instruction that the
     * stream is closed, and the index will be made available for reuse in
     * future streams.
     * 
     * @param {!number} index
     *     The index of the stream to end.
     */
    this.endStream = function(index) {

        // Do not send requests if not connected
        if (!isConnected())
            return;

        // Explicitly close stream by sending "end" instruction
        tunnel.sendMessage("end", index);

        // Free associated index and stream if they exist
        if (output_streams[index]) {
            stream_indices.free(index);
            delete output_streams[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 an arbitrary message is received from the tunnel that should
     * be processed by the client. By default, additional message-specific
     * events such as "onjoin" and "onleave" will fire for the received message
     * after this event has been processed. An event handler for "onmsg" need
     * not be supplied if "onjoin" and/or "onleave" will be used.
     * 
     * @event
     * @param {!number} msgcode
     *     A status code sent by the remote server that indicates the nature of
     *     the message that is being sent to the client.
     *     
     * @param {string[]} args
     *     An array of arguments to be processed with the message sent to the
     *     client.
     *
     * @return {boolean}
     *     true if message-specific events such as "onjoin" and
     *     "onleave" should be fired for this message, false otherwise. If
     *     no value is returned, message-specific events will be allowed to
     *     fire.
     */
    this.onmsg = null;

    /**
     * Fired when a user joins a shared connection.
     *
     * @event
     * @param {!string} userID
     *     A unique value representing this specific user's connection to the
     *     shared connection. This value is generated by the server and is
     *     guaranteed to be unique relative to other users of the connection.
     *
     * @param {!string} name
     *     A human-readable name representing the user that joined, such as
     *     their username. This value is provided by the web application during
     *     the connection handshake and is not necessarily unique relative to
     *     other users of the connection.
     */
    this.onjoin = null;

    /**
     * Fired when a user leaves a shared connection.
     *
     * @event
     * @param {!string} userID
     *     A unique value representing this specific user's connection to the
     *     shared connection. This value is generated by the server and is
     *     guaranteed to be unique relative to other users of the connection.
     *
     * @param {!string} name
     *     A human-readable name representing the user that left, such as their
     *     username. This value is provided by the web application during the
     *     connection handshake and is not necessarily unique relative to other
     *     users of the connection.
     */
    this.onleave = null;

    /**
     * Fired when a audio 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 audio data from the server.
     *
     * @param {!string} mimetype
     *     The mimetype of the audio data which will be received.
     *
     * @return {Guacamole.AudioPlayer}
     *     An object which implements the Guacamole.AudioPlayer interface and
     *     has been initialized to play the data in the provided stream, or null
     *     if the built-in audio players of the Guacamole client should be
     *     used.
     */
    this.onaudio = null;

    /**
     * Fired when a video 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 video data from the server.
     *
     * @param {!Guacamole.Display.VisibleLayer} layer
     *     The destination layer on which the received video data should be
     *     played. It is the responsibility of the Guacamole.VideoPlayer
     *     implementation to play the received data within this layer.
     *
     * @param {!string} mimetype
     *     The mimetype of the video data which will be received.
     *
     * @return {Guacamole.VideoPlayer}
     *     An object which implements the Guacamole.VideoPlayer interface and
     *     has been initialized to play the data in the provided stream, or null
     *     if the built-in video players of the Guacamole client should be
     *     used.
     */
    this.onvideo = null;

    /**
     * Fired when the remote client is explicitly declaring the level of
     * multi-touch support provided by a particular display layer.
     *
     * @event
     * @param {!Guacamole.Display.VisibleLayer} layer
     *     The layer whose multi-touch support level is being declared.
     *
     * @param {!number} touches
     *     The maximum number of simultaneous touches supported by the given
     *     layer, where 0 indicates that touch events are not supported at all.
     */
    this.onmultitouch = null;

    /**
     * Fired when the current value of a connection parameter is being exposed
     * by the server.
     *
     * @event
     * @param {!Guacamole.InputStream} stream
     *     The stream that will receive connection parameter data from the
     *     server.
     *
     * @param {!string} mimetype
     *     The mimetype of the data which will be received.
     *
     * @param {!string} name
     *     The name of the connection parameter whose value is being exposed.
     */
    this.onargv = 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 filesystem object is created. The object provided to this
     * event handler will contain its own event handlers and functions for
     * requesting and handling data.
     *
     * @event
     * @param {!Guacamole.Object} object
     *     The created filesystem object.
     *
     * @param {!string} name
     *     The name of the filesystem.
     */
    this.onfilesystem = 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 when a "required" instruction is received. A required instruction
     * indicates that additional parameters are required for the connection to
     * continue, such as user credentials.
     * 
     * @event
     * @param {!string[]} parameters
     *      The names of the connection parameters that are required to be
     *      provided for the connection to continue.
     */
    this.onrequired = 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;

    /**
     * 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.
     *
     * @private
     * @param {!number} index
     *     The index of the layer to retrieve.
     *
     * @return {!(Guacamole.Display.VisibleLayer|Guacamole.Layer)}
     *     The layer having the given index.
     */
    var getLayer = 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;

    };

    /**
     * Returns the index passed to getLayer() when the given layer was created.
     * Positive indices refer to visible layers, an index of zero refers to the
     * default layer, and negative indices refer to buffers.
     *
     * @param {!(Guacamole.Display.VisibleLayer|Guacamole.Layer)} layer
     *     The layer whose index should be determined.
     *
     * @returns {number}
     *     The index of the given layer, or null if no such layer is associated
     *     with this client.
     */
    var getLayerIndex = function getLayerIndex(layer) {

        // Avoid searching if there clearly is no such layer
        if (!layer)
            return null;

        // Search through each layer, returning the index of the given layer
        // once found
        for (var key in layers) {
            if (layer === layers[key])
                return parseInt(key);
        }

        // Otherwise, no such index
        return null;

    };

    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;

    }

    /**
     * Handlers for all defined layer properties.
     *
     * @private
     * @type {!Object.<string, function>}
     */
    var layerPropertyHandlers = {

        "miter-limit": function(layer, value) {
            display.setMiterLimit(layer, parseFloat(value));
        },

        "multi-touch" : function layerSupportsMultiTouch(layer, value) {

            // Process "multi-touch" property only for true visible layers (not off-screen buffers)
            if (guac_client.onmultitouch && layer instanceof Guacamole.Display.VisibleLayer)
                guac_client.onmultitouch(layer, parseInt(value));

        }

    };
    
    /**
     * Handlers for all instruction opcodes receivable by a Guacamole protocol
     * client.
     *
     * @private
     * @type {!Object.<string, function>}
     */
    var instructionHandlers = {

        "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 not already
                // invalidated by onack handler
                if (code >= 0x0100 && output_streams[stream_index] === stream) {
                    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);

        },

        "argv": function(parameters) {

            var stream_index = parseInt(parameters[0]);
            var mimetype = parameters[1];
            var name = parameters[2];

            // Create stream
            if (guac_client.onargv) {
                var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
                guac_client.onargv(stream, mimetype, name);
            }

            // Otherwise, unsupported
            else
                guac_client.sendAck(stream_index, "Receiving argument values unsupported", 0x0100);

        },

        "audio": function(parameters) {

            var stream_index = parseInt(parameters[0]);
            var mimetype = parameters[1];

            // Create stream 
            var stream = streams[stream_index] =
                    new Guacamole.InputStream(guac_client, stream_index);

            // Get player instance via callback
            var audioPlayer = null;
            if (guac_client.onaudio)
                audioPlayer = guac_client.onaudio(stream, mimetype);

            // If unsuccessful, try to use a default implementation
            if (!audioPlayer)
                audioPlayer = Guacamole.AudioPlayer.getInstance(stream, mimetype);

            // If we have successfully retrieved an audio player, send success response
            if (audioPlayer) {
                audioPlayers[stream_index] = audioPlayer;
                guac_client.sendAck(stream_index, "OK", 0x0000);
            }

            // Otherwise, mimetype must be unsupported
            else
                guac_client.sendAck(stream_index, "BAD TYPE", 0x030F);

        },

        "blob": function(parameters) {

            // Get stream 
            var stream_index = parseInt(parameters[0]);
            var data = parameters[1];
            var stream = streams[stream_index];

            // Write data
            if (stream && stream.onblob)
                stream.onblob(data);

        },

        "body" : function handleBody(parameters) {

            // Get object
            var objectIndex = parseInt(parameters[0]);
            var object = objects[objectIndex];

            var streamIndex = parseInt(parameters[1]);
            var mimetype = parameters[2];
            var name = parameters[3];

            // Create stream if handler defined
            if (object && object.onbody) {
                var stream = streams[streamIndex] = new Guacamole.InputStream(guac_client, streamIndex);
                object.onbody(stream, mimetype, name);
            }

            // Otherwise, unsupported
            else
                guac_client.sendAck(streamIndex, "Receipt of body unsupported", 0x0100);

        },

        "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);

        },

        "disconnect" : function handleDisconnect(parameters) {

            // Explicitly tear down connection
            guac_client.disconnect();

        },

        "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);
                display.dispose(layer);

                // 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);
                display.distort(layer, 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) {

            var stream_index = parseInt(parameters[0]);

            // Get stream
            var stream = streams[stream_index];
            if (stream) {

                // Signal end of stream if handler defined
                if (stream.onend)
                    stream.onend();

                // Invalidate stream
                delete streams[stream_index];

            }

        },

        "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);

        },

        "filesystem" : function handleFilesystem(parameters) {

            var objectIndex = parseInt(parameters[0]);
            var name = parameters[1];

            // Create object, if supported
            if (guac_client.onfilesystem) {
                var object = objects[objectIndex] = new Guacamole.Object(guac_client, objectIndex);
                guac_client.onfilesystem(object, name);
            }

            // If unsupported, simply ignore the availability of the filesystem

        },

        "identity": function(parameters) {

            var layer = getLayer(parseInt(parameters[0]));

            display.setTransform(layer, 1, 0, 0, 1, 0, 0);

        },

        "img": function(parameters) {

            var stream_index = parseInt(parameters[0]);
            var channelMask = parseInt(parameters[1]);
            var layer = getLayer(parseInt(parameters[2]));
            var mimetype = parameters[3];
            var x = parseInt(parameters[4]);
            var y = parseInt(parameters[5]);

            // Create stream
            var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);

            // Draw received contents once decoded
            display.setChannelMask(layer, channelMask);
            display.drawStream(layer, x, y, stream, mimetype);

        },

        "jpeg": 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/jpeg;base64," + data);

        },

        "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);

        },

        "mouse" : function handleMouse(parameters) {

            var x = parseInt(parameters[0]);
            var y = parseInt(parameters[1]);

            // Display and move software cursor to received coordinates
            display.showCursor(true);
            display.moveCursor(x, y);

        },

        "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);
                display.move(layer, parent, x, y, z);
            }

        },
        
        "msg" : function(parameters) {

            var userID;
            var username;

            // Fire general message handling event first
            var allowDefault = true;
            var msgid = parseInt(parameters[0]);
            if (guac_client.onmsg) {
                allowDefault = guac_client.onmsg(msgid, parameters.slice(1));
                if (allowDefault === undefined)
                    allowDefault = true;
            }

            // Fire message-specific convenience events if not prevented by the
            // "onmsg" handler
            if (allowDefault) {
                switch (msgid) {

                    case Guacamole.Client.Message.USER_JOINED:
                        userID = parameters[1];
                        username = parameters[2];
                        if (guac_client.onjoin)
                            guac_client.onjoin(userID, username);
                        break;

                    case Guacamole.Client.Message.USER_LEFT:
                        userID = parameters[1];
                        username = parameters[2];
                        if (guac_client.onleave)
                            guac_client.onleave(userID, username);
                        break;

                }
            }
            
        },

        "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);

        },
                
        "required": function required(parameters) {
            if (guac_client.onrequired) guac_client.onrequired(parameters);
        },
        
        "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);
                display.shade(layer, 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 displaySyncComplete() {

                // Synchronize all audio players
                for (var index in audioPlayers) {
                    var audioPlayer = audioPlayers[index];
                    if (audioPlayer)
                        audioPlayer.sync();
                }

                // Send sync response to server
                if (timestamp !== currentTimestamp) {
                    tunnel.sendMessage("sync", timestamp);
                    currentTimestamp = timestamp;
                }

            });

            // 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);

        },

        "undefine" : function handleUndefine(parameters) {

            // Get object
            var objectIndex = parseInt(parameters[0]);
            var object = objects[objectIndex];

            // Signal end of object definition
            if (object && object.onundefine)
                object.onundefine();

        },

        "video": function(parameters) {

            var stream_index = parseInt(parameters[0]);
            var layer = getLayer(parseInt(parameters[1]));
            var mimetype = parameters[2];

            // Create stream
            var stream = streams[stream_index] =
                    new Guacamole.InputStream(guac_client, stream_index);

            // Get player instance via callback
            var videoPlayer = null;
            if (guac_client.onvideo)
                videoPlayer = guac_client.onvideo(stream, layer, mimetype);

            // If unsuccessful, try to use a default implementation
            if (!videoPlayer)
                videoPlayer = Guacamole.VideoPlayer.getInstance(stream, layer, mimetype);

            // If we have successfully retrieved an video player, send success response
            if (videoPlayer) {
                videoPlayers[stream_index] = videoPlayer;
                guac_client.sendAck(stream_index, "OK", 0x0000);
            }

            // Otherwise, mimetype must be unsupported
            else
                guac_client.sendAck(stream_index, "BAD TYPE", 0x030F);

        }

    };

    /**
     * Sends a keep-alive ping to the Guacamole server, advising the server
     * that the client is still connected and responding. The lastSentKeepAlive
     * timestamp is automatically updated as a result of calling this function.
     *
     * @private
     */
    var sendKeepAlive = function sendKeepAlive() {
        tunnel.sendMessage('nop');
        lastSentKeepAlive = new Date().getTime();
    };

    /**
     * Schedules the next keep-alive ping based on the KEEP_ALIVE_FREQUENCY and
     * the time that the last ping was sent, if ever. If enough time has
     * elapsed that a ping should have already been sent, calling this function
     * will send that ping immediately.
     *
     * @private
     */
    var scheduleKeepAlive = function scheduleKeepAlive() {

        window.clearTimeout(keepAliveTimeout);

        var currentTime = new Date().getTime();
        var keepAliveDelay = Math.max(lastSentKeepAlive + KEEP_ALIVE_FREQUENCY - currentTime, 0);

        // Ping server regularly to keep connection alive, but send the ping
        // immediately if enough time has elapsed that it should have already
        // been sent
        if (keepAliveDelay > 0)
            keepAliveTimeout = window.setTimeout(sendKeepAlive, keepAliveDelay);
        else
            sendKeepAlive();

    };

    /**
     * Stops sending any further keep-alive pings. If a keep-alive ping was
     * scheduled to be sent, that ping is cancelled.
     *
     * @private
     */
    var stopKeepAlive = function stopKeepAlive() {
        window.clearTimeout(keepAliveTimeout);
    };

    tunnel.oninstruction = function(opcode, parameters) {

        var handler = instructionHandlers[opcode];
        if (handler)
            handler(parameters);

        // Leverage network activity to ensure the next keep-alive ping is
        // sent, even if the browser is currently throttling timers
        scheduleKeepAlive();

    };

    /**
     * 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 sending keep-alive messages
            stopKeepAlive();

            // 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 {string} 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;
        }

        // Regularly send keep-alive ping to ensure the server knows we're
        // still here, even if not active
        scheduleKeepAlive();

        setState(STATE_WAITING);
    };

};

/**
 * Map of all Guacamole binary raster operations to transfer functions.
 *
 * @private
 * @type {!Object.<number, function>}
 */
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);
    }

};

/**
 * A list of possible messages that can be sent by the server for processing
 * by the client.
 * 
 * @type {!Object.<string, number>}
 */
Guacamole.Client.Message = {
    
    /**
     * A client message that indicates that a user has joined an existing
     * connection. This message expects a single additional argument - the
     * name of the user who has joined the connection.
     * 
     * @type {!number}
     */
    "USER_JOINED": 0x0001,
    
    /**
     * A client message that indicates that a user has left an existing
     * connection. This message expects a single additional argument - the
     * name of the user who has left the connection.
     * 
     * @type {!number}
     */
    "USER_LEFT": 0x0002
    
};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A reader which automatically handles the given input stream, returning
 * received blobs as a single data URI built over the course of the stream.
 * Note that this object will overwrite any installed event handlers on the
 * given Guacamole.InputStream.
 * 
 * @constructor
 * @param {!Guacamole.InputStream} stream
 *     The stream that data will be read from.
 *
 * @param {!string} mimetype
 *     The mimetype of the data being received.
 */
Guacamole.DataURIReader = function(stream, mimetype) {

    /**
     * Reference to this Guacamole.DataURIReader.
     *
     * @private
     * @type {!Guacamole.DataURIReader}
     */
    var guac_reader = this;

    /**
     * Current data URI.
     *
     * @private
     * @type {!string}
     */
    var uri = 'data:' + mimetype + ';base64,';

    // Receive blobs as array buffers
    stream.onblob = function dataURIReaderBlob(data) {

        // Currently assuming data will ALWAYS be safe to simply append. This
        // will not be true if the received base64 data encodes a number of
        // bytes that isn't a multiple of three (as base64 expands in a ratio
        // of exactly 3:4).
        uri += data;

    };

    // Simply call onend when end received
    stream.onend = function dataURIReaderEnd() {
        if (guac_reader.onend)
            guac_reader.onend();
    };

    /**
     * Returns the data URI of all data received through the underlying stream
     * thus far.
     *
     * @returns {!string}
     *     The data URI of all data received through the underlying stream thus
     *     far.
     */
    this.getURI = function getURI() {
        return uri;
    };

    /**
     * Fired once this stream is finished and no further data will be written.
     *
     * @event
     */
    this.onend = null;

};
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * The Guacamole display. The display does not deal with the Guacamole
 * protocol, and instead implements a set of graphical operations which
 * embody the set of operations present in the protocol. The order operations
 * are executed is guaranteed to be in the same order as their corresponding
 * functions are called.
 * 
 * @constructor
 */
Guacamole.Display = function() {

    /**
     * Reference to this Guacamole.Display.
     * @private
     */
    var guac_display = this;

    var displayWidth = 0;
    var displayHeight = 0;
    var displayScale = 1;

    // Create display
    var display = document.createElement("div");
    display.style.position = "relative";
    display.style.width = displayWidth + "px";
    display.style.height = displayHeight + "px";

    // Ensure transformations on display originate at 0,0
    display.style.transformOrigin =
    display.style.webkitTransformOrigin =
    display.style.MozTransformOrigin =
    display.style.OTransformOrigin =
    display.style.msTransformOrigin =
        "0 0";

    // Create default layer
    var default_layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight);

    // Create cursor layer
    var cursor = new Guacamole.Display.VisibleLayer(0, 0);
    cursor.setChannelMask(Guacamole.Layer.SRC);

    // Add default layer and cursor to display
    display.appendChild(default_layer.getElement());
    display.appendChild(cursor.getElement());

    // Create bounding div 
    var bounds = document.createElement("div");
    bounds.style.position = "relative";
    bounds.style.width = (displayWidth*displayScale) + "px";
    bounds.style.height = (displayHeight*displayScale) + "px";

    // Add display to bounds
    bounds.appendChild(display);

    /**
     * The X coordinate of the hotspot of the mouse cursor. The hotspot is
     * the relative location within the image of the mouse cursor at which
     * each click occurs.
     * 
     * @type {!number}
     */
    this.cursorHotspotX = 0;

    /**
     * The Y coordinate of the hotspot of the mouse cursor. The hotspot is
     * the relative location within the image of the mouse cursor at which
     * each click occurs.
     * 
     * @type {!number}
     */
    this.cursorHotspotY = 0;

    /**
     * The current X coordinate of the local mouse cursor. This is not
     * necessarily the location of the actual mouse - it refers only to
     * the location of the cursor image within the Guacamole display, as
     * last set by moveCursor().
     * 
     * @type {!number}
     */
    this.cursorX = 0;

    /**
     * The current X coordinate of the local mouse cursor. This is not
     * necessarily the location of the actual mouse - it refers only to
     * the location of the cursor image within the Guacamole display, as
     * last set by moveCursor().
     * 
     * @type {!number}
     */
    this.cursorY = 0;

    /**
     * Fired when the default layer (and thus the entire Guacamole display)
     * is resized.
     * 
     * @event
     * @param {!number} width
     *     The new width of the Guacamole display.
     *
     * @param {!number} height
     *     The new height of the Guacamole display.
     */
    this.onresize = null;

    /**
     * Fired whenever the local cursor image is changed. This can be used to
     * implement special handling of the client-side cursor, or to override
     * the default use of a software cursor layer.
     * 
     * @event
     * @param {!HTMLCanvasElement} canvas
     *     The cursor image.
     *
     * @param {!number} x
     *     The X-coordinate of the cursor hotspot.
     *
     * @param {!number} y
     *     The Y-coordinate of the cursor hotspot.
     */
    this.oncursor = null;

    /**
     * The queue of all pending Tasks. Tasks will be run in order, with new
     * tasks added at the end of the queue and old tasks removed from the
     * front of the queue (FIFO). These tasks will eventually be grouped
     * into a Frame.
     *
     * @private
     * @type {!Task[]}
     */
    var tasks = [];

    /**
     * The queue of all frames. Each frame is a pairing of an array of tasks
     * and a callback which must be called when the frame is rendered.
     *
     * @private
     * @type {!Frame[]}
     */
    var frames = [];

    /**
     * Flushes all pending frames.
     * @private
     */
    function __flush_frames() {

        var rendered_frames = 0;

        // Draw all pending frames, if ready
        while (rendered_frames < frames.length) {

            var frame = frames[rendered_frames];
            if (!frame.isReady())
                break;

            frame.flush();
            rendered_frames++;

        } 

        // Remove rendered frames from array
        frames.splice(0, rendered_frames);

    }

    /**
     * An ordered list of tasks which must be executed atomically. Once
     * executed, an associated (and optional) callback will be called.
     *
     * @private
     * @constructor
     * @param {function} [callback]
     *     The function to call when this frame is rendered.
     *
     * @param {!Task[]} tasks
     *     The set of tasks which must be executed to render this frame.
     */
    function Frame(callback, tasks) {

        /**
         * Cancels rendering of this frame and all associated tasks. The
         * callback provided at construction time, if any, is not invoked.
         */
        this.cancel = function cancel() {

            callback = null;

            tasks.forEach(function cancelTask(task) {
                task.cancel();
            });

            tasks = [];

        };

        /**
         * Returns whether this frame is ready to be rendered. This function
         * returns true if and only if ALL underlying tasks are unblocked.
         * 
         * @returns {!boolean}
         *     true if all underlying tasks are unblocked, false otherwise.
         */
        this.isReady = function() {

            // Search for blocked tasks
            for (var i=0; i < tasks.length; i++) {
                if (tasks[i].blocked)
                    return false;
            }

            // If no blocked tasks, the frame is ready
            return true;

        };

        /**
         * Renders this frame, calling the associated callback, if any, after
         * the frame is complete. This function MUST only be called when no
         * blocked tasks exist. Calling this function with blocked tasks
         * will result in undefined behavior.
         */
        this.flush = function() {

            // Draw all pending tasks.
            for (var i=0; i < tasks.length; i++)
                tasks[i].execute();

            // Call callback
            if (callback) callback();

        };

    }

    /**
     * A container for an task handler. Each operation which must be ordered
     * is associated with a Task that goes into a task queue. Tasks in this
     * queue are executed in order once their handlers are set, while Tasks 
     * without handlers block themselves and any following Tasks from running.
     *
     * @constructor
     * @private
     * @param {function} [taskHandler]
     *     The function to call when this task runs, if any.
     *
     * @param {boolean} [blocked]
     *     Whether this task should start blocked.
     */
    function Task(taskHandler, blocked) {

        /**
         * Reference to this Task.
         *
         * @private
         * @type {!Guacamole.Display.Task}
         */
        var task = this;
       
        /**
         * Whether this Task is blocked.
         * 
         * @type {boolean}
         */
        this.blocked = blocked;

        /**
         * Cancels this task such that it will not run. The task handler
         * provided at construction time, if any, is not invoked. Calling
         * execute() after calling this function has no effect.
         */
        this.cancel = function cancel() {
            task.blocked = false;
            taskHandler = null;
        };

        /**
         * Unblocks this Task, allowing it to run.
         */
        this.unblock = function() {
            if (task.blocked) {
                task.blocked = false;
                __flush_frames();
            }
        };

        /**
         * Calls the handler associated with this task IMMEDIATELY. This
         * function does not track whether this task is marked as blocked.
         * Enforcing the blocked status of tasks is up to the caller.
         */
        this.execute = function() {
            if (taskHandler) taskHandler();
        };

    }

    /**
     * Schedules a task for future execution. The given handler will execute
     * immediately after all previous tasks upon frame flush, unless this
     * task is blocked. If any tasks is blocked, the entire frame will not
     * render (and no tasks within will execute) until all tasks are unblocked.
     * 
     * @private
     * @param {function} [handler]
     *     The function to call when possible, if any.
     *
     * @param {boolean} [blocked]
     *     Whether the task should start blocked.
     *
     * @returns {!Task}
     *     The Task created and added to the queue for future running.
     */
    function scheduleTask(handler, blocked) {
        var task = new Task(handler, blocked);
        tasks.push(task);
        return task;
    }

    /**
     * Returns the element which contains the Guacamole display.
     * 
     * @return {!Element}
     *     The element containing the Guacamole display.
     */
    this.getElement = function() {
        return bounds;
    };

    /**
     * Returns the width of this display.
     * 
     * @return {!number}
     *     The width of this display;
     */
    this.getWidth = function() {
        return displayWidth;
    };

    /**
     * Returns the height of this display.
     * 
     * @return {!number}
     *     The height of this display;
     */
    this.getHeight = function() {
        return displayHeight;
    };

    /**
     * Returns the default layer of this display. Each Guacamole display always
     * has at least one layer. Other layers can optionally be created within
     * this layer, but the default layer cannot be removed and is the absolute
     * ancestor of all other layers.
     * 
     * @return {!Guacamole.Display.VisibleLayer}
     *     The default layer.
     */
    this.getDefaultLayer = function() {
        return default_layer;
    };

    /**
     * Returns the cursor layer of this display. Each Guacamole display contains
     * a layer for the image of the mouse cursor. This layer is a special case
     * and exists above all other layers, similar to the hardware mouse cursor.
     * 
     * @return {!Guacamole.Display.VisibleLayer}
     *     The cursor layer.
     */
    this.getCursorLayer = function() {
        return cursor;
    };

    /**
     * Creates a new layer. The new layer will be a direct child of the default
     * layer, but can be moved to be a child of any other layer. Layers returned
     * by this function are visible.
     * 
     * @return {!Guacamole.Display.VisibleLayer}
     *     The newly-created layer.
     */
    this.createLayer = function() {
        var layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight);
        layer.move(default_layer, 0, 0, 0);
        return layer;
    };

    /**
     * Creates a new buffer. Buffers are invisible, off-screen surfaces. They
     * are implemented in the same manner as layers, but do not provide the
     * same nesting semantics.
     * 
     * @return {!Guacamole.Layer}
     *     The newly-created buffer.
     */
    this.createBuffer = function() {
        var buffer = new Guacamole.Layer(0, 0);
        buffer.autosize = 1;
        return buffer;
    };

    /**
     * Flush all pending draw tasks, if possible, as a new frame. If the entire
     * frame is not ready, the flush will wait until all required tasks are
     * unblocked.
     * 
     * @param {function} [callback]
     *     The function to call when this frame is flushed. This may happen
     *     immediately, or later when blocked tasks become unblocked.
     */
    this.flush = function(callback) {

        // Add frame, reset tasks
        frames.push(new Frame(callback, tasks));
        tasks = [];

        // Attempt flush
        __flush_frames();

    };

    /**
     * Cancels rendering of all pending frames and associated rendering
     * operations. The callbacks provided to outstanding past calls to flush(),
     * if any, are not invoked.
     */
    this.cancel = function cancel() {

        frames.forEach(function cancelFrame(frame) {
            frame.cancel();
        });

        frames = [];

        tasks.forEach(function cancelTask(task) {
            task.cancel();
        });

        tasks = [];

    };

    /**
     * Sets the hotspot and image of the mouse cursor displayed within the
     * Guacamole display.
     * 
     * @param {!number} hotspotX
     *     The X coordinate of the cursor hotspot.
     *
     * @param {!number} hotspotY
     *     The Y coordinate of the cursor hotspot.
     *
     * @param {!Guacamole.Layer} layer
     *     The source layer containing the data which should be used as the
     *     mouse cursor image.
     *
     * @param {!number} srcx
     *     The X coordinate of the upper-left corner of the rectangle within
     *     the source layer's coordinate space to copy data from.
     *
     * @param {!number} srcy
     *     The Y coordinate of the upper-left corner of the rectangle within
     *     the source layer's coordinate space to copy data from.
     *
     * @param {!number} srcw
     *     The width of the rectangle within the source layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} srch
     *     The height of the rectangle within the source layer's coordinate
     *     space to copy data from.
     */
    this.setCursor = function(hotspotX, hotspotY, layer, srcx, srcy, srcw, srch) {
        scheduleTask(function __display_set_cursor() {

            // Set hotspot
            guac_display.cursorHotspotX = hotspotX;
            guac_display.cursorHotspotY = hotspotY;

            // Reset cursor size
            cursor.resize(srcw, srch);

            // Draw cursor to cursor layer
            cursor.copy(layer, srcx, srcy, srcw, srch, 0, 0);
            guac_display.moveCursor(guac_display.cursorX, guac_display.cursorY);

            // Fire cursor change event
            if (guac_display.oncursor)
                guac_display.oncursor(cursor.toCanvas(), hotspotX, hotspotY);

        });
    };

    /**
     * Sets whether the software-rendered cursor is shown. This cursor differs
     * from the hardware cursor in that it is built into the Guacamole.Display,
     * and relies on its own Guacamole layer to render.
     *
     * @param {boolean} [shown=true]
     *     Whether to show the software cursor.
     */
    this.showCursor = function(shown) {

        var element = cursor.getElement();
        var parent = element.parentNode;

        // Remove from DOM if hidden
        if (shown === false) {
            if (parent)
                parent.removeChild(element);
        }

        // Otherwise, ensure cursor is child of display
        else if (parent !== display)
            display.appendChild(element);

    };

    /**
     * Sets the location of the local cursor to the given coordinates. For the
     * sake of responsiveness, this function performs its action immediately.
     * Cursor motion is not maintained within atomic frames.
     * 
     * @param {!number} x
     *     The X coordinate to move the cursor to.
     *
     * @param {!number} y
     *     The Y coordinate to move the cursor to.
     */
    this.moveCursor = function(x, y) {

        // Move cursor layer
        cursor.translate(x - guac_display.cursorHotspotX,
                         y - guac_display.cursorHotspotY);

        // Update stored position
        guac_display.cursorX = x;
        guac_display.cursorY = y;

    };

    /**
     * Changes the size of the given Layer to the given width and height.
     * Resizing is only attempted if the new size provided is actually different
     * from the current size.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to resize.
     *
     * @param {!number} width
     *     The new width.
     *
     * @param {!number} height
     *     The new height.
     */
    this.resize = function(layer, width, height) {
        scheduleTask(function __display_resize() {

            layer.resize(width, height);

            // Resize display if default layer is resized
            if (layer === default_layer) {

                // Update (set) display size
                displayWidth = width;
                displayHeight = height;
                display.style.width = displayWidth + "px";
                display.style.height = displayHeight + "px";

                // Update bounds size
                bounds.style.width = (displayWidth*displayScale) + "px";
                bounds.style.height = (displayHeight*displayScale) + "px";

                // Notify of resize
                if (guac_display.onresize)
                    guac_display.onresize(width, height);

            }

        });
    };

    /**
     * Draws the specified image at the given coordinates. The image specified
     * must already be loaded.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y 
     *     The destination Y coordinate.
     *
     * @param {!CanvasImageSource} image
     *     The image to draw. Note that this not a URL.
     */
    this.drawImage = function(layer, x, y, image) {
        scheduleTask(function __display_drawImage() {
            layer.drawImage(x, y, image);
        });
    };

    /**
     * Draws the image contained within the specified Blob at the given
     * coordinates. The Blob specified must already be populated with image
     * data.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     *
     * @param {!Blob} blob
     *     The Blob containing the image data to draw.
     */
    this.drawBlob = function(layer, x, y, blob) {

        var task;

        // Prefer createImageBitmap() over blob URLs if available
        if (window.createImageBitmap) {

            var bitmap;

            // Draw image once loaded
            task = scheduleTask(function drawImageBitmap() {
                layer.drawImage(x, y, bitmap);
            }, true);

            // Load image from provided blob
            window.createImageBitmap(blob).then(function bitmapLoaded(decoded) {
                bitmap = decoded;
                task.unblock();
            });

        }

        // Use blob URLs and the Image object if createImageBitmap() is
        // unavailable
        else {

            // Create URL for blob
            var url = URL.createObjectURL(blob);

            // Draw and free blob URL when ready
            task = scheduleTask(function __display_drawBlob() {

                // Draw the image only if it loaded without errors
                if (image.width && image.height)
                    layer.drawImage(x, y, image);

                // Blob URL no longer needed
                URL.revokeObjectURL(url);

            }, true);

            // Load image from URL
            var image = new Image();
            image.onload = task.unblock;
            image.onerror = task.unblock;
            image.src = url;

        }

    };

    /**
     * Draws the image within the given stream at the given coordinates. The
     * image will be loaded automatically, and this and any future operations
     * will wait for the image to finish loading. This function will
     * automatically choose an appropriate method for reading and decoding the
     * given image stream, and should be preferred for received streams except
     * where manual decoding of the stream is unavoidable.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     *
     * @param {!Guacamole.InputStream} stream
     *     The stream along which image data will be received.
     *
     * @param {!string} mimetype
     *     The mimetype of the image within the stream.
     */
    this.drawStream = function drawStream(layer, x, y, stream, mimetype) {

        // If createImageBitmap() is available, load the image as a blob so
        // that function can be used
        if (window.createImageBitmap) {
            var reader = new Guacamole.BlobReader(stream, mimetype);
            reader.onend = function drawImageBlob() {
                guac_display.drawBlob(layer, x, y, reader.getBlob());
            };
        }

        // Lacking createImageBitmap(), fall back to data URIs and the Image
        // object
        else {
            var reader = new Guacamole.DataURIReader(stream, mimetype);
            reader.onend = function drawImageDataURI() {
                guac_display.draw(layer, x, y, reader.getURI());
            };
        }

    };

    /**
     * Draws the image at the specified URL at the given coordinates. The image
     * will be loaded automatically, and this and any future operations will
     * wait for the image to finish loading.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     *
     * @param {!string} url
     *     The URL of the image to draw.
     */
    this.draw = function(layer, x, y, url) {

        var task = scheduleTask(function __display_draw() {

            // Draw the image only if it loaded without errors
            if (image.width && image.height)
                layer.drawImage(x, y, image);

        }, true);

        var image = new Image();
        image.onload = task.unblock;
        image.onerror = task.unblock;
        image.src = url;

    };

    /**
     * Plays the video at the specified URL within this layer. The video
     * will be loaded automatically, and this and any future operations will
     * wait for the video to finish loading. Future operations will not be
     * executed until the video finishes playing.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!string} mimetype
     *     The mimetype of the video to play.
     *
     * @param {!number} duration
     *     The duration of the video in milliseconds.
     *
     * @param {!string} url
     *     The URL of the video to play.
     */
    this.play = function(layer, mimetype, duration, url) {

        // Start loading the video
        var video = document.createElement("video");
        video.type = mimetype;
        video.src = url;

        // Start copying frames when playing
        video.addEventListener("play", function() {
            
            function render_callback() {
                layer.drawImage(0, 0, video);
                if (!video.ended)
                    window.setTimeout(render_callback, 20);
            }
            
            render_callback();
            
        }, false);

        scheduleTask(video.play);

    };

    /**
     * Transfer a rectangle of image data from one Layer to this Layer using the
     * specified transfer function.
     * 
     * @param {!Guacamole.Layer} srcLayer
     *     The Layer to copy image data from.
     *
     * @param {!number} srcx
     *     The X coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcy
     *     The Y coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcw
     *     The width of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} srch
     *     The height of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!Guacamole.Layer} dstLayer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     *
     * @param {!function} transferFunction
     *     The transfer function to use to transfer data from source to
     *     destination.
     */
    this.transfer = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y, transferFunction) {
        scheduleTask(function __display_transfer() {
            dstLayer.transfer(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction);
        });
    };

    /**
     * Put a rectangle of image data from one Layer to this Layer directly
     * without performing any alpha blending. Simply copy the data.
     * 
     * @param {!Guacamole.Layer} srcLayer
     *     The Layer to copy image data from.
     *
     * @param {!number} srcx
     *     The X coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcy
     *     The Y coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcw
     *     The width of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} srch
     *     The height of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!Guacamole.Layer} dstLayer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     */
    this.put = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) {
        scheduleTask(function __display_put() {
            dstLayer.put(srcLayer, srcx, srcy, srcw, srch, x, y);
        });
    };

    /**
     * Copy a rectangle of image data from one Layer to this Layer. This
     * operation will copy exactly the image data that will be drawn once all
     * operations of the source Layer that were pending at the time this
     * function was called are complete. This operation will not alter the
     * size of the source Layer even if its autosize property is set to true.
     * 
     * @param {!Guacamole.Layer} srcLayer
     *     The Layer to copy image data from.
     *
     * @param {!number} srcx
     *     The X coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcy
     *     The Y coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcw
     *     The width of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} srch
     *     The height of the rectangle within the source Layer's coordinate space to copy data from.
     *
     * @param {!Guacamole.Layer} dstLayer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     */
    this.copy = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) {
        scheduleTask(function __display_copy() {
            dstLayer.copy(srcLayer, srcx, srcy, srcw, srch, x, y);
        });
    };

    /**
     * Starts a new path at the specified point.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The X coordinate of the point to draw.
     *
     * @param {!number} y
     *     The Y coordinate of the point to draw.
     */
    this.moveTo = function(layer, x, y) {
        scheduleTask(function __display_moveTo() {
            layer.moveTo(x, y);
        });
    };

    /**
     * Add the specified line to the current path.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The X coordinate of the endpoint of the line to draw.
     *
     * @param {!number} y
     *     The Y coordinate of the endpoint of the line to draw.
     */
    this.lineTo = function(layer, x, y) {
        scheduleTask(function __display_lineTo() {
            layer.lineTo(x, y);
        });
    };

    /**
     * Add the specified arc to the current path.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The X coordinate of the center of the circle which will contain the
     *     arc.
     *
     * @param {!number} y
     *     The Y coordinate of the center of the circle which will contain the
     *     arc.
     *
     * @param {!number} radius
     *     The radius of the circle.
     *
     * @param {!number} startAngle
     *     The starting angle of the arc, in radians.
     *
     * @param {!number} endAngle
     *     The ending angle of the arc, in radians.
     *
     * @param {!boolean} negative
     *     Whether the arc should be drawn in order of decreasing angle.
     */
    this.arc = function(layer, x, y, radius, startAngle, endAngle, negative) {
        scheduleTask(function __display_arc() {
            layer.arc(x, y, radius, startAngle, endAngle, negative);
        });
    };

    /**
     * Starts a new path at the specified point.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} cp1x
     *     The X coordinate of the first control point.
     *
     * @param {!number} cp1y
     *     The Y coordinate of the first control point.
     *
     * @param {!number} cp2x
     *     The X coordinate of the second control point.
     *
     * @param {!number} cp2y
     *     The Y coordinate of the second control point.
     *
     * @param {!number} x
     *     The X coordinate of the endpoint of the curve.
     *
     * @param {!number} y
     *     The Y coordinate of the endpoint of the curve.
     */
    this.curveTo = function(layer, cp1x, cp1y, cp2x, cp2y, x, y) {
        scheduleTask(function __display_curveTo() {
            layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y);
        });
    };

    /**
     * Closes the current path by connecting the end point with the start
     * point (if any) with a straight line.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     */
    this.close = function(layer) {
        scheduleTask(function __display_close() {
            layer.close();
        });
    };

    /**
     * Add the specified rectangle to the current path.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The X coordinate of the upper-left corner of the rectangle to draw.
     *
     * @param {!number} y
     *     The Y coordinate of the upper-left corner of the rectangle to draw.
     *
     * @param {!number} w
     *     The width of the rectangle to draw.
     *
     * @param {!number} h
     *     The height of the rectangle to draw.
     */
    this.rect = function(layer, x, y, w, h) {
        scheduleTask(function __display_rect() {
            layer.rect(x, y, w, h);
        });
    };

    /**
     * Clip all future drawing operations by the current path. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as fillColor()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to affect.
     */
    this.clip = function(layer) {
        scheduleTask(function __display_clip() {
            layer.clip();
        });
    };

    /**
     * Stroke the current path with the specified color. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!string} cap
     *     The line cap style. Can be "round", "square", or "butt".
     *
     * @param {!string} join
     *     The line join style. Can be "round", "bevel", or "miter".
     *
     * @param {!number} thickness
     *     The line thickness in pixels.
     *
     * @param {!number} r
     *     The red component of the color to fill.
     *
     * @param {!number} g
     *     The green component of the color to fill.
     *
     * @param {!number} b
     *     The blue component of the color to fill.
     *
     * @param {!number} a
     *     The alpha component of the color to fill.
     */
    this.strokeColor = function(layer, cap, join, thickness, r, g, b, a) {
        scheduleTask(function __display_strokeColor() {
            layer.strokeColor(cap, join, thickness, r, g, b, a);
        });
    };

    /**
     * Fills the current path with the specified color. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} r
     *     The red component of the color to fill.
     *
     * @param {!number} g
     *     The green component of the color to fill.
     *
     * @param {!number} b
     *     The blue component of the color to fill.
     *
     * @param {!number} a
     *     The alpha component of the color to fill.
     */
    this.fillColor = function(layer, r, g, b, a) {
        scheduleTask(function __display_fillColor() {
            layer.fillColor(r, g, b, a);
        });
    };

    /**
     * Stroke the current path with the image within the specified layer. The
     * image data will be tiled infinitely within the stroke. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!string} cap
     *     The line cap style. Can be "round", "square", or "butt".
     *
     * @param {!string} join
     *     The line join style. Can be "round", "bevel", or "miter".
     *
     * @param {!number} thickness
     *     The line thickness in pixels.
     *
     * @param {!Guacamole.Layer} srcLayer
     *     The layer to use as a repeating pattern within the stroke.
     */
    this.strokeLayer = function(layer, cap, join, thickness, srcLayer) {
        scheduleTask(function __display_strokeLayer() {
            layer.strokeLayer(cap, join, thickness, srcLayer);
        });
    };

    /**
     * Fills the current path with the image within the specified layer. The
     * image data will be tiled infinitely within the stroke. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!Guacamole.Layer} srcLayer
     *     The layer to use as a repeating pattern within the fill.
     */
    this.fillLayer = function(layer, srcLayer) {
        scheduleTask(function __display_fillLayer() {
            layer.fillLayer(srcLayer);
        });
    };

    /**
     * Push current layer state onto stack.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     */
    this.push = function(layer) {
        scheduleTask(function __display_push() {
            layer.push();
        });
    };

    /**
     * Pop layer state off stack.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     */
    this.pop = function(layer) {
        scheduleTask(function __display_pop() {
            layer.pop();
        });
    };

    /**
     * Reset the layer, clearing the stack, the current path, and any transform
     * matrix.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     */
    this.reset = function(layer) {
        scheduleTask(function __display_reset() {
            layer.reset();
        });
    };

    /**
     * Sets the given affine transform (defined with six values from the
     * transform's matrix).
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to modify.
     *
     * @param {!number} a
     *     The first value in the affine transform's matrix.
     *
     * @param {!number} b
     *     The second value in the affine transform's matrix.
     *
     * @param {!number} c
     *     The third value in the affine transform's matrix.
     *
     * @param {!number} d
     *     The fourth value in the affine transform's matrix.
     *
     * @param {!number} e
     *     The fifth value in the affine transform's matrix.
     *
     * @param {!number} f
     *     The sixth value in the affine transform's matrix.
     */
    this.setTransform = function(layer, a, b, c, d, e, f) {
        scheduleTask(function __display_setTransform() {
            layer.setTransform(a, b, c, d, e, f);
        });
    };

    /**
     * Applies the given affine transform (defined with six values from the
     * transform's matrix).
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to modify.
     *
     * @param {!number} a
     *     The first value in the affine transform's matrix.
     *
     * @param {!number} b
     *     The second value in the affine transform's matrix.
     *
     * @param {!number} c
     *     The third value in the affine transform's matrix.
     *
     * @param {!number} d
     *     The fourth value in the affine transform's matrix.
     *
     * @param {!number} e
     *     The fifth value in the affine transform's matrix.
     *
     * @param {!number} f
     *     The sixth value in the affine transform's matrix.
     *
     */
    this.transform = function(layer, a, b, c, d, e, f) {
        scheduleTask(function __display_transform() {
            layer.transform(a, b, c, d, e, f);
        });
    };

    /**
     * Sets the channel mask for future operations on this Layer.
     * 
     * The channel mask is a Guacamole-specific compositing operation identifier
     * with a single bit representing each of four channels (in order): source
     * image where destination transparent, source where destination opaque,
     * destination where source transparent, and destination where source
     * opaque.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to modify.
     *
     * @param {!number} mask
     *     The channel mask for future operations on this Layer.
     */
    this.setChannelMask = function(layer, mask) {
        scheduleTask(function __display_setChannelMask() {
            layer.setChannelMask(mask);
        });
    };

    /**
     * Sets the miter limit for stroke operations using the miter join. This
     * limit is the maximum ratio of the size of the miter join to the stroke
     * width. If this ratio is exceeded, the miter will not be drawn for that
     * joint of the path.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to modify.
     *
     * @param {!number} limit
     *     The miter limit for stroke operations using the miter join.
     */
    this.setMiterLimit = function(layer, limit) {
        scheduleTask(function __display_setMiterLimit() {
            layer.setMiterLimit(limit);
        });
    };

    /**
     * Removes the given layer container entirely, such that it is no longer
     * contained within its parent layer, if any.
     *
     * @param {!Guacamole.Display.VisibleLayer} layer
     *     The layer being removed from its parent.
     */
    this.dispose = function dispose(layer) {
        scheduleTask(function disposeLayer() {
            layer.dispose();
        });
    };

    /**
     * Applies the given affine transform (defined with six values from the
     * transform's matrix) to the given layer.
     *
     * @param {!Guacamole.Display.VisibleLayer} layer
     *     The layer being distorted.
     *
     * @param {!number} a
     *     The first value in the affine transform's matrix.
     *
     * @param {!number} b
     *     The second value in the affine transform's matrix.
     *
     * @param {!number} c
     *     The third value in the affine transform's matrix.
     *
     * @param {!number} d
     *     The fourth value in the affine transform's matrix.
     *
     * @param {!number} e
     *     The fifth value in the affine transform's matrix.
     *
     * @param {!number} f
     *     The sixth value in the affine transform's matrix.
     */
    this.distort = function distort(layer, a, b, c, d, e, f) {
        scheduleTask(function distortLayer() {
            layer.distort(a, b, c, d, e, f);
        });
    };

    /**
     * Moves the upper-left corner of the given layer to the given X and Y
     * coordinate, sets the Z stacking order, and reparents the layer
     * to the given parent layer.
     *
     * @param {!Guacamole.Display.VisibleLayer} layer
     *     The layer being moved.
     *
     * @param {!Guacamole.Display.VisibleLayer} parent
     *     The parent to set.
     *
     * @param {!number} x
     *     The X coordinate to move to.
     *
     * @param {!number} y
     *     The Y coordinate to move to.
     *
     * @param {!number} z
     *     The Z coordinate to move to.
     */
    this.move = function move(layer, parent, x, y, z) {
        scheduleTask(function moveLayer() {
            layer.move(parent, x, y, z);
        });
    };

    /**
     * Sets the opacity of the given layer to the given value, where 255 is
     * fully opaque and 0 is fully transparent.
     *
     * @param {!Guacamole.Display.VisibleLayer} layer
     *     The layer whose opacity should be set.
     *
     * @param {!number} alpha
     *     The opacity to set.
     */
    this.shade = function shade(layer, alpha) {
        scheduleTask(function shadeLayer() {
            layer.shade(alpha);
        });
    };

    /**
     * Sets the scale of the client display element such that it renders at
     * a relatively smaller or larger size, without affecting the true
     * resolution of the display.
     *
     * @param {!number} scale
     *     The scale to resize to, where 1.0 is normal size (1:1 scale).
     */
    this.scale = function(scale) {

        display.style.transform =
        display.style.WebkitTransform =
        display.style.MozTransform =
        display.style.OTransform =
        display.style.msTransform =

            "scale(" + scale + "," + scale + ")";

        displayScale = scale;

        // Update bounds size
        bounds.style.width = (displayWidth*displayScale) + "px";
        bounds.style.height = (displayHeight*displayScale) + "px";

    };

    /**
     * Returns the scale of the display.
     *
     * @return {!number}
     *     The scale of the display.
     */
    this.getScale = function() {
        return displayScale;
    };

    /**
     * Returns a canvas element containing the entire display, with all child
     * layers composited within.
     *
     * @return {!HTMLCanvasElement}
     *     A new canvas element containing a copy of the display.
     */
    this.flatten = function() {
       
        // Get destination canvas
        var canvas = document.createElement("canvas");
        canvas.width = default_layer.width;
        canvas.height = default_layer.height;

        var context = canvas.getContext("2d");

        // Returns sorted array of children
        function get_children(layer) {

            // Build array of children
            var children = [];
            for (var index in layer.children)
                children.push(layer.children[index]);

            // Sort
            children.sort(function children_comparator(a, b) {

                // Compare based on Z order
                var diff = a.z - b.z;
                if (diff !== 0)
                    return diff;

                // If Z order identical, use document order
                var a_element = a.getElement();
                var b_element = b.getElement();
                var position = b_element.compareDocumentPosition(a_element);

                if (position & Node.DOCUMENT_POSITION_PRECEDING) return -1;
                if (position & Node.DOCUMENT_POSITION_FOLLOWING) return  1;

                // Otherwise, assume same
                return 0;

            });

            // Done
            return children;

        }

        // Draws the contents of the given layer at the given coordinates
        function draw_layer(layer, x, y) {

            // Draw layer
            if (layer.width > 0 && layer.height > 0) {

                // Save and update alpha
                var initial_alpha = context.globalAlpha;
                context.globalAlpha *= layer.alpha / 255.0;

                // Copy data
                context.drawImage(layer.getCanvas(), x, y);

                // Draw all children
                var children = get_children(layer);
                for (var i=0; i<children.length; i++) {
                    var child = children[i];
                    draw_layer(child, x + child.x, y + child.y);
                }

                // Restore alpha
                context.globalAlpha = initial_alpha;

            }

        }

        // Draw default layer and all children
        draw_layer(default_layer, 0, 0);

        // Return new canvas copy
        return canvas;
        
    };

};

/**
 * Simple container for Guacamole.Layer, allowing layers to be easily
 * repositioned and nested. This allows certain operations to be accelerated
 * through DOM manipulation, rather than raster operations.
 * 
 * @constructor
 * @augments Guacamole.Layer
 * @param {!number} width
 *     The width of the Layer, in pixels. The canvas element backing this Layer
 *     will be given this width.
 *
 * @param {!number} height
 *     The height of the Layer, in pixels. The canvas element backing this
 *     Layer will be given this height.
 */
Guacamole.Display.VisibleLayer = function(width, height) {

    Guacamole.Layer.apply(this, [width, height]);

    /**
     * Reference to this layer.
     *
     * @private
     * @type {!Guacamole.Display.Layer}
     */
    var layer = this;

    /**
     * Identifier which uniquely identifies this layer. This is COMPLETELY
     * UNRELATED to the index of the underlying layer, which is specific
     * to the Guacamole protocol, and not relevant at this level.
     * 
     * @private
     * @type {!number}
     */
    this.__unique_id = Guacamole.Display.VisibleLayer.__next_id++;

    /**
     * The opacity of the layer container, where 255 is fully opaque and 0 is
     * fully transparent.
     *
     * @type {!number}
     */
    this.alpha = 0xFF;

    /**
     * X coordinate of the upper-left corner of this layer container within
     * its parent, in pixels.
     *
     * @type {!number}
     */
    this.x = 0;

    /**
     * Y coordinate of the upper-left corner of this layer container within
     * its parent, in pixels.
     *
     * @type {!number}
     */
    this.y = 0;

    /**
     * Z stacking order of this layer relative to other sibling layers.
     *
     * @type {!number}
     */
    this.z = 0;

    /**
     * The affine transformation applied to this layer container. Each element
     * corresponds to a value from the transformation matrix, with the first
     * three values being the first row, and the last three values being the
     * second row. There are six values total.
     * 
     * @type {!number[]}
     */
    this.matrix = [1, 0, 0, 1, 0, 0];

    /**
     * The parent layer container of this layer, if any.
     * @type {Guacamole.Display.VisibleLayer}
     */
    this.parent = null;

    /**
     * Set of all children of this layer, indexed by layer index. This object
     * will have one property per child.
     *
     * @type {!Object.<number, Guacamole.Display.VisibleLayer>}
     */
    this.children = {};

    // Set layer position
    var canvas = layer.getCanvas();
    canvas.style.position = "absolute";
    canvas.style.left = "0px";
    canvas.style.top = "0px";

    // Create div with given size
    var div = document.createElement("div");
    div.appendChild(canvas);
    div.style.width = width + "px";
    div.style.height = height + "px";
    div.style.position = "absolute";
    div.style.left = "0px";
    div.style.top = "0px";
    div.style.overflow = "hidden";

    /**
     * Superclass resize() function.
     * @private
     */
    var __super_resize = this.resize;

    this.resize = function(width, height) {

        // Resize containing div
        div.style.width = width + "px";
        div.style.height = height + "px";

        __super_resize(width, height);

    };
  
    /**
     * Returns the element containing the canvas and any other elements
     * associated with this layer.
     *
     * @returns {!Element}
     *     The element containing this layer's canvas.
     */
    this.getElement = function() {
        return div;
    };

    /**
     * The translation component of this layer's transform.
     *
     * @private
     * @type {!string}
     */
    var translate = "translate(0px, 0px)"; // (0, 0)

    /**
     * The arbitrary matrix component of this layer's transform.
     *
     * @private
     * @type {!string}
     */
    var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity

    /**
     * Moves the upper-left corner of this layer to the given X and Y
     * coordinate.
     * 
     * @param {!number} x
     *     The X coordinate to move to.
     *
     * @param {!number} y
     *     The Y coordinate to move to.
     */
    this.translate = function(x, y) {

        layer.x = x;
        layer.y = y;

        // Generate translation
        translate = "translate("
                        + x + "px,"
                        + y + "px)";

        // Set layer transform 
        div.style.transform =
        div.style.WebkitTransform =
        div.style.MozTransform =
        div.style.OTransform =
        div.style.msTransform =

            translate + " " + matrix;

    };

    /**
     * Moves the upper-left corner of this VisibleLayer to the given X and Y
     * coordinate, sets the Z stacking order, and reparents this VisibleLayer
     * to the given VisibleLayer.
     * 
     * @param {!Guacamole.Display.VisibleLayer} parent
     *     The parent to set.
     *
     * @param {!number} x
     *     The X coordinate to move to.
     *
     * @param {!number} y
     *     The Y coordinate to move to.
     *
     * @param {!number} z
     *     The Z coordinate to move to.
     */
    this.move = function(parent, x, y, z) {

        // Set parent if necessary
        if (layer.parent !== parent) {

            // Maintain relationship
            if (layer.parent)
                delete layer.parent.children[layer.__unique_id];
            layer.parent = parent;
            parent.children[layer.__unique_id] = layer;

            // Reparent element
            var parent_element = parent.getElement();
            parent_element.appendChild(div);

        }

        // Set location
        layer.translate(x, y);
        layer.z = z;
        div.style.zIndex = z;

    };

    /**
     * Sets the opacity of this layer to the given value, where 255 is fully
     * opaque and 0 is fully transparent.
     * 
     * @param {!number} a
     *     The opacity to set.
     */
    this.shade = function(a) {
        layer.alpha = a;
        div.style.opacity = a/255.0;
    };

    /**
     * Removes this layer container entirely, such that it is no longer
     * contained within its parent layer, if any.
     */
    this.dispose = function() {

        // Remove from parent container
        if (layer.parent) {
            delete layer.parent.children[layer.__unique_id];
            layer.parent = null;
        }

        // Remove from parent element
        if (div.parentNode)
            div.parentNode.removeChild(div);
        
    };

    /**
     * Applies the given affine transform (defined with six values from the
     * transform's matrix).
     *
     * @param {!number} a
     *     The first value in the affine transform's matrix.
     *
     * @param {!number} b
     *     The second value in the affine transform's matrix.
     *
     * @param {!number} c
     *     The third value in the affine transform's matrix.
     *
     * @param {!number} d
     *     The fourth value in the affine transform's matrix.
     *
     * @param {!number} e
     *     The fifth value in the affine transform's matrix.
     *
     * @param {!number} f
     *     The sixth value in the affine transform's matrix.
     */
    this.distort = function(a, b, c, d, e, f) {

        // Store matrix
        layer.matrix = [a, b, c, d, e, f];

        // Generate matrix transformation
        matrix =

            /* a c e
             * b d f
             * 0 0 1
             */
    
            "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")";

        // Set layer transform 
        div.style.transform =
        div.style.WebkitTransform =
        div.style.MozTransform =
        div.style.OTransform =
        div.style.msTransform =

            translate + " " + matrix;

    };

};

/**
 * The next identifier to be assigned to the layer container. This identifier
 * uniquely identifies each VisibleLayer, but is unrelated to the index of
 * the layer, which exists at the protocol/client level only.
 * 
 * @private
 * @type {!number}
 */
Guacamole.Display.VisibleLayer.__next_id = 0;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * An arbitrary event, emitted by a {@link Guacamole.Event.Target}. This object
 * should normally serve as the base class for a different object that is more
 * specific to the event type.
 *
 * @constructor
 * @param {!string} type
 *     The unique name of this event type.
 */
Guacamole.Event = function Event(type) {

    /**
     * The unique name of this event type.
     *
     * @type {!string}
     */
    this.type = type;

    /**
     * An arbitrary timestamp in milliseconds, indicating this event's
     * position in time relative to other events.
     *
     * @type {!number}
     */
    this.timestamp = new Date().getTime();

    /**
     * Returns the number of milliseconds elapsed since this event was created.
     *
     * @return {!number}
     *     The number of milliseconds elapsed since this event was created.
     */
    this.getAge = function getAge() {
        return new Date().getTime() - this.timestamp;
    };

    /**
     * Requests that the legacy event handler associated with this event be
     * invoked on the given event target. This function will be invoked
     * automatically by implementations of {@link Guacamole.Event.Target}
     * whenever {@link Guacamole.Event.Target#emit emit()} is invoked.
     * <p>
     * Older versions of Guacamole relied on single event handlers with the
     * prefix "on", such as "onmousedown" or "onkeyup". If a Guacamole.Event
     * implementation is replacing the event previously represented by one of
     * these handlers, this function gives the implementation the opportunity
     * to provide backward compatibility with the old handler.
     * <p>
     * Unless overridden, this function does nothing.
     *
     * @param {!Guacamole.Event.Target} eventTarget
     *     The {@link Guacamole.Event.Target} that emitted this event.
     */
    this.invokeLegacyHandler = function invokeLegacyHandler(eventTarget) {
        // Do nothing
    };

};

/**
 * A {@link Guacamole.Event} that may relate to one or more DOM events.
 * Continued propagation and default behavior of the related DOM events may be
 * prevented with {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()}
 * and {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()}
 * respectively.
 *
 * @constructor
 * @augments Guacamole.Event
 *
 * @param {!string} type
 *     The unique name of this event type.
 *
 * @param {Event|Event[]} [events=[]]
 *     The DOM events that are related to this event, if any. Future calls to
 *     {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()} and
 *     {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()} will
 *     affect these events.
 */
Guacamole.Event.DOMEvent = function DOMEvent(type, events) {

    Guacamole.Event.call(this, type);

    // Default to empty array
    events = events || [];

    // Automatically wrap non-array single Event in an array
    if (!Array.isArray(events))
        events = [ events ];

    /**
     * Requests that the default behavior of related DOM events be prevented.
     * Whether this request will be honored by the browser depends on the
     * nature of those events and the timing of the request.
     */
    this.preventDefault = function preventDefault() {
        events.forEach(function applyPreventDefault(event) {
            if (event.preventDefault) event.preventDefault();
            event.returnValue = false;
        });
    };

    /**
     * Stops further propagation of related events through the DOM. Only events
     * that are directly related to this event will be stopped.
     */
    this.stopPropagation = function stopPropagation() {
        events.forEach(function applyStopPropagation(event) {
            event.stopPropagation();
        });
    };

};

/**
 * Convenience function for cancelling all further processing of a given DOM
 * event. Invoking this function prevents the default behavior of the event and
 * stops any further propagation.
 *
 * @param {!Event} event
 *     The DOM event to cancel.
 */
Guacamole.Event.DOMEvent.cancelEvent = function cancelEvent(event) {
    event.stopPropagation();
    if (event.preventDefault) event.preventDefault();
    event.returnValue = false;
};

/**
 * An object which can dispatch {@link Guacamole.Event} objects. Listeners
 * registered with {@link Guacamole.Event.Target#on on()} will automatically
 * be invoked based on the type of {@link Guacamole.Event} passed to
 * {@link Guacamole.Event.Target#dispatch dispatch()}. It is normally
 * subclasses of Guacamole.Event.Target that will dispatch events, and usages
 * of those subclasses that will catch dispatched events with on().
 *
 * @constructor
 */
Guacamole.Event.Target = function Target() {

    /**
     * A callback function which handles an event dispatched by an event
     * target.
     *
     * @callback Guacamole.Event.Target~listener
     * @param {!Guacamole.Event} event
     *     The event that was dispatched.
     *
     * @param {!Guacamole.Event.Target} target
     *     The object that dispatched the event.
     */

    /**
     * All listeners (callback functions) registered for each event type passed
     * to {@link Guacamole.Event.Targer#on on()}.
     *
     * @private
     * @type {!Object.<string, Guacamole.Event.Target~listener[]>}
     */
    var listeners = {};

    /**
     * Registers a listener for events having the given type, as dictated by
     * the {@link Guacamole.Event#type type} property of {@link Guacamole.Event}
     * provided to {@link Guacamole.Event.Target#dispatch dispatch()}.
     *
     * @param {!string} type
     *     The unique name of this event type.
     *
     * @param {!Guacamole.Event.Target~listener} listener
     *     The function to invoke when an event having the given type is
     *     dispatched. The {@link Guacamole.Event} object provided to
     *     {@link Guacamole.Event.Target#dispatch dispatch()} will be passed to
     *     this function, along with the dispatching Guacamole.Event.Target.
     */
    this.on = function on(type, listener) {

        var relevantListeners = listeners[type];
        if (!relevantListeners)
            listeners[type] = relevantListeners = [];

        relevantListeners.push(listener);

    };

    /**
     * Registers a listener for events having the given types, as dictated by
     * the {@link Guacamole.Event#type type} property of {@link Guacamole.Event}
     * provided to {@link Guacamole.Event.Target#dispatch dispatch()}.
     * <p>
     * Invoking this function is equivalent to manually invoking
     * {@link Guacamole.Event.Target#on on()} for each of the provided types.
     *
     * @param {!string[]} types
     *     The unique names of the event types to associate with the given
     *     listener.
     *
     * @param {!Guacamole.Event.Target~listener} listener
     *     The function to invoke when an event having any of the given types
     *     is dispatched. The {@link Guacamole.Event} object provided to
     *     {@link Guacamole.Event.Target#dispatch dispatch()} will be passed to
     *     this function, along with the dispatching Guacamole.Event.Target.
     */
    this.onEach = function onEach(types, listener) {
        types.forEach(function addListener(type) {
            this.on(type, listener);
        }, this);
    };

    /**
     * Dispatches the given event, invoking all event handlers registered with
     * this Guacamole.Event.Target for that event's
     * {@link Guacamole.Event#type type}.
     *
     * @param {!Guacamole.Event} event
     *     The event to dispatch.
     */
    this.dispatch = function dispatch(event) {

        // Invoke any relevant legacy handler for the event
        event.invokeLegacyHandler(this);

        // Invoke all registered listeners
        var relevantListeners = listeners[event.type];
        if (relevantListeners) {
            for (var i = 0; i < relevantListeners.length; i++) {
                relevantListeners[i](event, this);
            }
        }

    };

    /**
     * Unregisters a listener that was previously registered with
     * {@link Guacamole.Event.Target#on on()} or
     * {@link Guacamole.Event.Target#onEach onEach()}. If no such listener was
     * registered, this function has no effect. If multiple copies of the same
     * listener were registered, the first listener still registered will be
     * removed.
     *
     * @param {!string} type
     *     The unique name of the event type handled by the listener being
     *     removed.
     *
     * @param {!Guacamole.Event.Target~listener} listener
     *     The listener function previously provided to
     *     {@link Guacamole.Event.Target#on on()}or
     *     {@link Guacamole.Event.Target#onEach onEach()}.
     *
     * @returns {!boolean}
     *     true if the specified listener was removed, false otherwise.
     */
    this.off = function off(type, listener) {

        var relevantListeners = listeners[type];
        if (!relevantListeners)
            return false;

        for (var i = 0; i < relevantListeners.length; i++) {
            if (relevantListeners[i] === listener) {
                relevantListeners.splice(i, 1);
                return true;
            }
        }

        return false;

    };

    /**
     * Unregisters listeners that were previously registered with
     * {@link Guacamole.Event.Target#on on()} or
     * {@link Guacamole.Event.Target#onEach onEach()}. If no such listeners
     * were registered, this function has no effect. If multiple copies of the
     * same listener were registered for the same event type, the first
     * listener still registered will be removed.
     * <p>
     * Invoking this function is equivalent to manually invoking
     * {@link Guacamole.Event.Target#off off()} for each of the provided types.
     *
     * @param {!string[]} types
     *     The unique names of the event types handled by the listeners being
     *     removed.
     *
     * @param {!Guacamole.Event.Target~listener} listener
     *     The listener function previously provided to
     *     {@link Guacamole.Event.Target#on on()} or
     *     {@link Guacamole.Event.Target#onEach onEach()}.
     *
     * @returns {!boolean}
     *     true if any of the specified listeners were removed, false
     *     otherwise.
     */
    this.offEach = function offEach(types, listener) {

        var changed = false;

        types.forEach(function removeListener(type) {
            changed |= this.off(type, listener);
        }, this);

        return changed;

    };

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A hidden input field which attempts to keep itself focused at all times,
 * except when another input field has been intentionally focused, whether
 * programatically or by the user. The actual underlying input field, returned
 * by getElement(), may be used as a reliable source of keyboard-related events,
 * particularly composition and input events which may require a focused input
 * field to be dispatched at all.
 *
 * @constructor
 */
Guacamole.InputSink = function InputSink() {

    /**
     * Reference to this instance of Guacamole.InputSink.
     *
     * @private
     * @type {!Guacamole.InputSink}
     */
    var sink = this;

    /**
     * The underlying input field, styled to be invisible.
     *
     * @private
     * @type {!Element}
     */
    var field = document.createElement('textarea');
    field.style.position   = 'fixed';
    field.style.outline    = 'none';
    field.style.border     = 'none';
    field.style.margin     = '0';
    field.style.padding    = '0';
    field.style.height     = '0';
    field.style.width      = '0';
    field.style.left       = '0';
    field.style.bottom     = '0';
    field.style.resize     = 'none';
    field.style.background = 'transparent';
    field.style.color      = 'transparent';

    // Keep field clear when modified via normal keypresses
    field.addEventListener("keypress", function clearKeypress(e) {
        field.value = '';
    }, false);

    // Keep field clear when modofied via composition events
    field.addEventListener("compositionend", function clearCompletedComposition(e) {
        if (e.data)
            field.value = '';
    }, false);

    // Keep field clear when modofied via input events
    field.addEventListener("input", function clearCompletedInput(e) {
        if (e.data && !e.isComposing)
            field.value = '';
    }, false);

    // Whenever focus is gained, automatically click to ensure cursor is
    // actually placed within the field (the field may simply be highlighted or
    // outlined otherwise)
    field.addEventListener("focus", function focusReceived() {
        window.setTimeout(function deferRefocus() {
            field.click();
            field.select();
        }, 0);
    }, true);

    /**
     * Attempts to focus the underlying input field. The focus attempt occurs
     * asynchronously, and may silently fail depending on browser restrictions.
     */
    this.focus = function focus() {
        window.setTimeout(function deferRefocus() {
            field.focus(); // Focus must be deferred to work reliably across browsers
        }, 0);
    };

    /**
     * Returns the underlying input field. This input field MUST be manually
     * added to the DOM for the Guacamole.InputSink to have any effect.
     *
     * @returns {!Element}
     *     The underlying input field.
     */
    this.getElement = function getElement() {
        return field;
    };

    // Automatically refocus input sink if part of DOM
    document.addEventListener("keydown", function refocusSink(e) {

        // Do not refocus if focus is on an input field
        var focused = document.activeElement;
        if (focused && focused !== document.body) {

            // Only consider focused input fields which are actually visible
            var rect = focused.getBoundingClientRect();
            if (rect.left + rect.width > 0 && rect.top + rect.height > 0)
                return;

        }

        // Refocus input sink instead of handling click
        sink.focus();

    }, true);

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * An input stream abstraction used by the Guacamole client to facilitate
 * transfer of files or other binary data.
 * 
 * @constructor
 * @param {!Guacamole.Client} client
 *     The client owning this stream.
 *
 * @param {!number} index
 *     The index of this stream.
 */
Guacamole.InputStream = function(client, index) {

    /**
     * Reference to this stream.
     *
     * @private
     * @type {!Guacamole.InputStream}
     */
    var guac_stream = this;

    /**
     * The index of this stream.
     *
     * @type {!number}
     */
    this.index = index;

    /**
     * Called when a blob of data is received.
     * 
     * @event
     * @param {!string} data
     *     The received base64 data.
     */
    this.onblob = null;

    /**
     * Called when this stream is closed.
     * 
     * @event
     */
    this.onend = null;

    /**
     * Acknowledges the receipt of a 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(message, code) {
        client.sendAck(guac_stream.index, message, code);
    };

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Integer pool which returns consistently increasing integers while integers
 * are in use, and previously-used integers when possible.
 * @constructor 
 */
Guacamole.IntegerPool = function() {

    /**
     * Reference to this integer pool.
     *
     * @private
     */
    var guac_pool = this;

    /**
     * Array of available integers.
     *
     * @private
     * @type {!number[]}
     */
    var pool = [];

    /**
     * The next integer to return if no more integers remain.
     *
     * @type {!number}
     */
    this.next_int = 0;

    /**
     * Returns the next available integer in the pool. If possible, a previously
     * used integer will be returned.
     * 
     * @return {!number}
     *     The next available integer.
     */
    this.next = function() {

        // If free'd integers exist, return one of those
        if (pool.length > 0)
            return pool.shift();

        // Otherwise, return a new integer
        return guac_pool.next_int++;

    };

    /**
     * Frees the given integer, allowing it to be reused.
     * 
     * @param {!number} integer
     *     The integer to free.
     */
    this.free = function(integer) {
        pool.push(integer);
    };

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A reader which automatically handles the given input stream, assembling all
 * received blobs into a JavaScript object by appending them to each other, in
 * order, and decoding the result as JSON. Note that this object will overwrite
 * any installed event handlers on the given Guacamole.InputStream.
 * 
 * @constructor
 * @param {Guacamole.InputStream} stream
 *     The stream that JSON will be read from.
 */
Guacamole.JSONReader = function guacamoleJSONReader(stream) {

    /**
     * Reference to this Guacamole.JSONReader.
     *
     * @private
     * @type {!Guacamole.JSONReader}
     */
    var guacReader = this;

    /**
     * Wrapped Guacamole.StringReader.
     *
     * @private
     * @type {!Guacamole.StringReader}
     */
    var stringReader = new Guacamole.StringReader(stream);

    /**
     * All JSON read thus far.
     *
     * @private
     * @type {!string}
     */
    var json = '';

    /**
     * Returns the current length of this Guacamole.JSONReader, in characters.
     *
     * @return {!number}
     *     The current length of this Guacamole.JSONReader.
     */
    this.getLength = function getLength() {
        return json.length;
    };

    /**
     * Returns the contents of this Guacamole.JSONReader as a JavaScript
     * object.
     *
     * @return {object}
     *     The contents of this Guacamole.JSONReader, as parsed from the JSON
     *     contents of the input stream.
     */
    this.getJSON = function getJSON() {
        return JSON.parse(json);
    };

    // Append all received text
    stringReader.ontext = function ontext(text) {

        // Append received text
        json += text;

        // Call handler, if present
        if (guacReader.onprogress)
            guacReader.onprogress(text.length);

    };

    // Simply call onend when end received
    stringReader.onend = function onend() {
        if (guacReader.onend)
            guacReader.onend();
    };

    /**
     * Fired once for every blob of data received.
     * 
     * @event
     * @param {!number} length
     *     The number of characters received.
     */
    this.onprogress = null;

    /**
     * Fired once this stream is finished and no further data will be written.
     *
     * @event
     */
    this.onend = null;

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Provides cross-browser and cross-keyboard keyboard for a specific element.
 * Browser and keyboard layout variation is abstracted away, providing events
 * which represent keys as their corresponding X11 keysym.
 * 
 * @constructor
 * @param {Element|Document} [element]
 *    The Element to use to provide keyboard events. If omitted, at least one
 *    Element must be manually provided through the listenTo() function for
 *    the Guacamole.Keyboard instance to have any effect.
 */
Guacamole.Keyboard = function Keyboard(element) {

    /**
     * Reference to this Guacamole.Keyboard.
     *
     * @private
     * @type {!Guacamole.Keyboard}
     */
    var guac_keyboard = this;

    /**
     * An integer value which uniquely identifies this Guacamole.Keyboard
     * instance with respect to other Guacamole.Keyboard instances.
     *
     * @private
     * @type {!number}
     */
    var guacKeyboardID = Guacamole.Keyboard._nextID++;

    /**
     * The name of the property which is added to event objects via markEvent()
     * to note that they have already been handled by this Guacamole.Keyboard.
     *
     * @private
     * @constant
     * @type {!string}
     */
    var EVENT_MARKER = '_GUAC_KEYBOARD_HANDLED_BY_' + guacKeyboardID;

    /**
     * Fired whenever the user presses a key with the element associated
     * with this Guacamole.Keyboard in focus.
     * 
     * @event
     * @param {!number} keysym
     *     The keysym of the key being pressed.
     *
     * @return {!boolean}
     *     true if the key event should be allowed through to the browser,
     *     false otherwise.
     */
    this.onkeydown = null;

    /**
     * Fired whenever the user releases a key with the element associated
     * with this Guacamole.Keyboard in focus.
     * 
     * @event
     * @param {!number} keysym
     *     The keysym of the key being released.
     */
    this.onkeyup = null;

    /**
     * Set of known platform-specific or browser-specific quirks which must be
     * accounted for to properly interpret key events, even if the only way to
     * reliably detect that quirk is to platform/browser-sniff.
     *
     * @private
     * @type {!Object.<string, boolean>}
     */
    var quirks = {

        /**
         * Whether keyup events are universally unreliable.
         *
         * @type {!boolean}
         */
        keyupUnreliable: false,

        /**
         * Whether the Alt key is actually a modifier for typable keys and is
         * thus never used for keyboard shortcuts.
         *
         * @type {!boolean}
         */
        altIsTypableOnly: false,

        /**
         * Whether we can rely on receiving a keyup event for the Caps Lock
         * key.
         *
         * @type {!boolean}
         */
        capsLockKeyupUnreliable: false

    };

    // Set quirk flags depending on platform/browser, if such information is
    // available
    if (navigator && navigator.platform) {

        // All keyup events are unreliable on iOS (sadly)
        if (navigator.platform.match(/ipad|iphone|ipod/i))
            quirks.keyupUnreliable = true;

        // The Alt key on Mac is never used for keyboard shortcuts, and the
        // Caps Lock key never dispatches keyup events
        else if (navigator.platform.match(/^mac/i)) {
            quirks.altIsTypableOnly = true;
            quirks.capsLockKeyupUnreliable = true;
        }

    }

    /**
     * A key event having a corresponding timestamp. This event is non-specific.
     * Its subclasses should be used instead when recording specific key
     * events.
     *
     * @private
     * @constructor
     * @param {KeyboardEvent} [orig]
     *     The relevant DOM keyboard event.
     */
    var KeyEvent = function KeyEvent(orig) {

        /**
         * Reference to this key event.
         *
         * @private
         * @type {!KeyEvent}
         */
        var key_event = this;

        /**
         * The JavaScript key code of the key pressed. For most events (keydown
         * and keyup), this is a scancode-like value related to the position of
         * the key on the US English "Qwerty" keyboard. For keypress events, 
         * this is the Unicode codepoint of the character that would be typed
         * by the key pressed.
         *
         * @type {!number}
         */
        this.keyCode = orig ? (orig.which || orig.keyCode) : 0;

        /**
         * The legacy DOM3 "keyIdentifier" of the key pressed, as defined at:
         * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
         *
         * @type {!string}
         */
        this.keyIdentifier = orig && orig.keyIdentifier;

        /**
         * The standard name of the key pressed, as defined at:
         * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
         * 
         * @type {!string}
         */
        this.key = orig && orig.key;

        /**
         * The location on the keyboard corresponding to the key pressed, as
         * defined at:
         * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
         * 
         * @type {!number}
         */
        this.location = orig ? getEventLocation(orig) : 0;

        /**
         * The state of all local keyboard modifiers at the time this event was
         * received.
         *
         * @type {!Guacamole.Keyboard.ModifierState}
         */
        this.modifiers = orig ? Guacamole.Keyboard.ModifierState.fromKeyboardEvent(orig) : new Guacamole.Keyboard.ModifierState();

        /**
         * An arbitrary timestamp in milliseconds, indicating this event's
         * position in time relative to other events.
         *
         * @type {!number}
         */
        this.timestamp = new Date().getTime();

        /**
         * Whether the default action of this key event should be prevented.
         *
         * @type {!boolean}
         */
        this.defaultPrevented = false;

        /**
         * The keysym of the key associated with this key event, as determined
         * by a best-effort guess using available event properties and keyboard
         * state.
         *
         * @type {number}
         */
        this.keysym = null;

        /**
         * Whether the keysym value of this key event is known to be reliable.
         * If false, the keysym may still be valid, but it's only a best guess,
         * and future key events may be a better source of information.
         *
         * @type {!boolean}
         */
        this.reliable = false;

        /**
         * Returns the number of milliseconds elapsed since this event was
         * received.
         *
         * @return {!number}
         *     The number of milliseconds elapsed since this event was
         *     received.
         */
        this.getAge = function() {
            return new Date().getTime() - key_event.timestamp;
        };

    };

    /**
     * Information related to the pressing of a key, which need not be a key
     * associated with a printable character. The presence or absence of any
     * information within this object is browser-dependent.
     *
     * @private
     * @constructor
     * @augments Guacamole.Keyboard.KeyEvent
     * @param {!KeyboardEvent} orig
     *     The relevant DOM "keydown" event.
     */
    var KeydownEvent = function KeydownEvent(orig) {

        // We extend KeyEvent
        KeyEvent.call(this, orig);

        // If key is known from keyCode or DOM3 alone, use that
        this.keysym =  keysym_from_key_identifier(this.key, this.location)
                    || keysym_from_keycode(this.keyCode, this.location);

        /**
         * Whether the keyup following this keydown event is known to be
         * reliable. If false, we cannot rely on the keyup event to occur.
         *
         * @type {!boolean}
         */
        this.keyupReliable = !quirks.keyupUnreliable;

        // DOM3 and keyCode are reliable sources if the corresponding key is
        // not a printable key
        if (this.keysym && !isPrintable(this.keysym))
            this.reliable = true;

        // Use legacy keyIdentifier as a last resort, if it looks sane
        if (!this.keysym && key_identifier_sane(this.keyCode, this.keyIdentifier))
            this.keysym = keysym_from_key_identifier(this.keyIdentifier, this.location, this.modifiers.shift);

        // If a key is pressed while meta is held down, the keyup will
        // never be sent in Chrome (bug #108404)
        if (this.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8)
            this.keyupReliable = false;

        // We cannot rely on receiving keyup for Caps Lock on certain platforms
        else if (this.keysym === 0xFFE5 && quirks.capsLockKeyupUnreliable)
            this.keyupReliable = false;

        // Determine whether default action for Alt+combinations must be prevented
        var prevent_alt = !this.modifiers.ctrl && !quirks.altIsTypableOnly;

        // Determine whether default action for Ctrl+combinations must be prevented
        var prevent_ctrl = !this.modifiers.alt;

        // We must rely on the (potentially buggy) keyIdentifier if preventing
        // the default action is important
        if ((prevent_ctrl && this.modifiers.ctrl)
         || (prevent_alt  && this.modifiers.alt)
         || this.modifiers.meta
         || this.modifiers.hyper)
            this.reliable = true;

        // Record most recently known keysym by associated key code
        recentKeysym[this.keyCode] = this.keysym;

    };

    KeydownEvent.prototype = new KeyEvent();

    /**
     * Information related to the pressing of a key, which MUST be
     * associated with a printable character. The presence or absence of any
     * information within this object is browser-dependent.
     *
     * @private
     * @constructor
     * @augments Guacamole.Keyboard.KeyEvent
     * @param {!KeyboardEvent} orig
     *     The relevant DOM "keypress" event.
     */
    var KeypressEvent = function KeypressEvent(orig) {

        // We extend KeyEvent
        KeyEvent.call(this, orig);

        // Pull keysym from char code
        this.keysym = keysym_from_charcode(this.keyCode);

        // Keypress is always reliable
        this.reliable = true;

    };

    KeypressEvent.prototype = new KeyEvent();

    /**
     * Information related to the releasing of a key, which need not be a key
     * associated with a printable character. The presence or absence of any
     * information within this object is browser-dependent.
     *
     * @private
     * @constructor
     * @augments Guacamole.Keyboard.KeyEvent
     * @param {!KeyboardEvent} orig
     *     The relevant DOM "keyup" event.
     */
    var KeyupEvent = function KeyupEvent(orig) {

        // We extend KeyEvent
        KeyEvent.call(this, orig);

        // If key is known from keyCode or DOM3 alone, use that (keyCode is
        // still more reliable for keyup when dead keys are in use)
        this.keysym =  keysym_from_keycode(this.keyCode, this.location)
                    || keysym_from_key_identifier(this.key, this.location);

        // Fall back to the most recently pressed keysym associated with the
        // keyCode if the inferred key doesn't seem to actually be pressed
        if (!guac_keyboard.pressed[this.keysym])
            this.keysym = recentKeysym[this.keyCode] || this.keysym;

        // Keyup is as reliable as it will ever be
        this.reliable = true;

    };

    KeyupEvent.prototype = new KeyEvent();

    /**
     * An array of recorded events, which can be instances of the private
     * KeydownEvent, KeypressEvent, and KeyupEvent classes.
     *
     * @private
     * @type {!KeyEvent[]}
     */
    var eventLog = [];

    /**
     * Map of known JavaScript keycodes which do not map to typable characters
     * to their X11 keysym equivalents.
     *
     * @private
     * @type {!Object.<number, number[]>}
     */
    var keycodeKeysyms = {
        8:   [0xFF08], // backspace
        9:   [0xFF09], // tab
        12:  [0xFF0B, 0xFF0B, 0xFF0B, 0xFFB5], // clear       / KP 5
        13:  [0xFF0D], // enter
        16:  [0xFFE1, 0xFFE1, 0xFFE2], // shift
        17:  [0xFFE3, 0xFFE3, 0xFFE4], // ctrl
        18:  [0xFFE9, 0xFFE9, 0xFE03], // alt
        19:  [0xFF13], // pause/break
        20:  [0xFFE5], // caps lock
        27:  [0xFF1B], // escape
        32:  [0x0020], // space
        33:  [0xFF55, 0xFF55, 0xFF55, 0xFFB9], // page up     / KP 9
        34:  [0xFF56, 0xFF56, 0xFF56, 0xFFB3], // page down   / KP 3
        35:  [0xFF57, 0xFF57, 0xFF57, 0xFFB1], // end         / KP 1
        36:  [0xFF50, 0xFF50, 0xFF50, 0xFFB7], // home        / KP 7
        37:  [0xFF51, 0xFF51, 0xFF51, 0xFFB4], // left arrow  / KP 4
        38:  [0xFF52, 0xFF52, 0xFF52, 0xFFB8], // up arrow    / KP 8
        39:  [0xFF53, 0xFF53, 0xFF53, 0xFFB6], // right arrow / KP 6
        40:  [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow  / KP 2
        45:  [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert      / KP 0
        46:  [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete      / KP decimal
        91:  [0xFFE7], // left windows/command key (meta_l)
        92:  [0xFFE8], // right window/command key (meta_r)
        93:  [0xFF67], // menu key
        96:  [0xFFB0], // KP 0
        97:  [0xFFB1], // KP 1
        98:  [0xFFB2], // KP 2
        99:  [0xFFB3], // KP 3
        100: [0xFFB4], // KP 4
        101: [0xFFB5], // KP 5
        102: [0xFFB6], // KP 6
        103: [0xFFB7], // KP 7
        104: [0xFFB8], // KP 8
        105: [0xFFB9], // KP 9
        106: [0xFFAA], // KP multiply
        107: [0xFFAB], // KP add
        109: [0xFFAD], // KP subtract
        110: [0xFFAE], // KP decimal
        111: [0xFFAF], // KP divide
        112: [0xFFBE], // f1
        113: [0xFFBF], // f2
        114: [0xFFC0], // f3
        115: [0xFFC1], // f4
        116: [0xFFC2], // f5
        117: [0xFFC3], // f6
        118: [0xFFC4], // f7
        119: [0xFFC5], // f8
        120: [0xFFC6], // f9
        121: [0xFFC7], // f10
        122: [0xFFC8], // f11
        123: [0xFFC9], // f12
        144: [0xFF7F], // num lock
        145: [0xFF14], // scroll lock
        225: [0xFE03]  // altgraph (iso_level3_shift)
    };

    /**
     * Map of known JavaScript keyidentifiers which do not map to typable
     * characters to their unshifted X11 keysym equivalents.
     *
     * @private
     * @type {!Object.<string, number[]>}
     */
    var keyidentifier_keysym = {
        "Again": [0xFF66],
        "AllCandidates": [0xFF3D],
        "Alphanumeric": [0xFF30],
        "Alt": [0xFFE9, 0xFFE9, 0xFE03],
        "Attn": [0xFD0E],
        "AltGraph": [0xFE03],
        "ArrowDown": [0xFF54],
        "ArrowLeft": [0xFF51],
        "ArrowRight": [0xFF53],
        "ArrowUp": [0xFF52],
        "Backspace": [0xFF08],
        "CapsLock": [0xFFE5],
        "Cancel": [0xFF69],
        "Clear": [0xFF0B],
        "Convert": [0xFF21],
        "Copy": [0xFD15],
        "Crsel": [0xFD1C],
        "CrSel": [0xFD1C],
        "CodeInput": [0xFF37],
        "Compose": [0xFF20],
        "Control": [0xFFE3, 0xFFE3, 0xFFE4],
        "ContextMenu": [0xFF67],
        "Delete": [0xFFFF],
        "Down": [0xFF54],
        "End": [0xFF57],
        "Enter": [0xFF0D],
        "EraseEof": [0xFD06],
        "Escape": [0xFF1B],
        "Execute": [0xFF62],
        "Exsel": [0xFD1D],
        "ExSel": [0xFD1D],
        "F1": [0xFFBE],
        "F2": [0xFFBF],
        "F3": [0xFFC0],
        "F4": [0xFFC1],
        "F5": [0xFFC2],
        "F6": [0xFFC3],
        "F7": [0xFFC4],
        "F8": [0xFFC5],
        "F9": [0xFFC6],
        "F10": [0xFFC7],
        "F11": [0xFFC8],
        "F12": [0xFFC9],
        "F13": [0xFFCA],
        "F14": [0xFFCB],
        "F15": [0xFFCC],
        "F16": [0xFFCD],
        "F17": [0xFFCE],
        "F18": [0xFFCF],
        "F19": [0xFFD0],
        "F20": [0xFFD1],
        "F21": [0xFFD2],
        "F22": [0xFFD3],
        "F23": [0xFFD4],
        "F24": [0xFFD5],
        "Find": [0xFF68],
        "GroupFirst": [0xFE0C],
        "GroupLast": [0xFE0E],
        "GroupNext": [0xFE08],
        "GroupPrevious": [0xFE0A],
        "FullWidth": null,
        "HalfWidth": null,
        "HangulMode": [0xFF31],
        "Hankaku": [0xFF29],
        "HanjaMode": [0xFF34],
        "Help": [0xFF6A],
        "Hiragana": [0xFF25],
        "HiraganaKatakana": [0xFF27],
        "Home": [0xFF50],
        "Hyper": [0xFFED, 0xFFED, 0xFFEE],
        "Insert": [0xFF63],
        "JapaneseHiragana": [0xFF25],
        "JapaneseKatakana": [0xFF26],
        "JapaneseRomaji": [0xFF24],
        "JunjaMode": [0xFF38],
        "KanaMode": [0xFF2D],
        "KanjiMode": [0xFF21],
        "Katakana": [0xFF26],
        "Left": [0xFF51],
        "Meta": [0xFFE7, 0xFFE7, 0xFFE8],
        "ModeChange": [0xFF7E],
        "NumLock": [0xFF7F],
        "PageDown": [0xFF56],
        "PageUp": [0xFF55],
        "Pause": [0xFF13],
        "Play": [0xFD16],
        "PreviousCandidate": [0xFF3E],
        "PrintScreen": [0xFF61],
        "Redo": [0xFF66],
        "Right": [0xFF53],
        "RomanCharacters": null,
        "Scroll": [0xFF14],
        "Select": [0xFF60],
        "Separator": [0xFFAC],
        "Shift": [0xFFE1, 0xFFE1, 0xFFE2],
        "SingleCandidate": [0xFF3C],
        "Super": [0xFFEB, 0xFFEB, 0xFFEC],
        "Tab": [0xFF09],
        "UIKeyInputDownArrow": [0xFF54],
        "UIKeyInputEscape": [0xFF1B],
        "UIKeyInputLeftArrow": [0xFF51],
        "UIKeyInputRightArrow": [0xFF53],
        "UIKeyInputUpArrow": [0xFF52],
        "Up": [0xFF52],
        "Undo": [0xFF65],
        "Win": [0xFFE7, 0xFFE7, 0xFFE8],
        "Zenkaku": [0xFF28],
        "ZenkakuHankaku": [0xFF2A]
    };

    /**
     * All keysyms which should not repeat when held down.
     *
     * @private
     * @type {!Object.<number, boolean>}
     */
    var no_repeat = {
        0xFE03: true, // ISO Level 3 Shift (AltGr)
        0xFFE1: true, // Left shift
        0xFFE2: true, // Right shift
        0xFFE3: true, // Left ctrl 
        0xFFE4: true, // Right ctrl 
        0xFFE5: true, // Caps Lock
        0xFFE7: true, // Left meta 
        0xFFE8: true, // Right meta 
        0xFFE9: true, // Left alt
        0xFFEA: true, // Right alt
        0xFFEB: true, // Left super/hyper
        0xFFEC: true  // Right super/hyper
    };

    /**
     * All modifiers and their states.
     *
     * @type {!Guacamole.Keyboard.ModifierState}
     */
    this.modifiers = new Guacamole.Keyboard.ModifierState();
        
    /**
     * The state of every key, indexed by keysym. If a particular key is
     * pressed, the value of pressed for that keysym will be true. If a key
     * is not currently pressed, it will not be defined. 
     *
     * @type {!Object.<number, boolean>}
     */
    this.pressed = {};

    /**
     * The state of every key, indexed by keysym, for strictly those keys whose
     * status has been indirectly determined thorugh observation of other key
     * events. If a particular key is implicitly pressed, the value of
     * implicitlyPressed for that keysym will be true. If a key
     * is not currently implicitly pressed (the key is not pressed OR the state
     * of the key is explicitly known), it will not be defined.
     *
     * @private
     * @type {!Object.<number, boolean>}
     */
    var implicitlyPressed = {};

    /**
     * The last result of calling the onkeydown handler for each key, indexed
     * by keysym. This is used to prevent/allow default actions for key events,
     * even when the onkeydown handler cannot be called again because the key
     * is (theoretically) still pressed.
     *
     * @private
     * @type {!Object.<number, boolean>}
     */
    var last_keydown_result = {};

    /**
     * The keysym most recently associated with a given keycode when keydown
     * fired. This object maps keycodes to keysyms.
     *
     * @private
     * @type {!Object.<number, number>}
     */
    var recentKeysym = {};

    /**
     * Timeout before key repeat starts.
     *
     * @private
     * @type {number}
     */
    var key_repeat_timeout = null;

    /**
     * Interval which presses and releases the last key pressed while that
     * key is still being held down.
     *
     * @private
     * @type {number}
     */
    var key_repeat_interval = null;

    /**
     * Given an array of keysyms indexed by location, returns the keysym
     * for the given location, or the keysym for the standard location if
     * undefined.
     * 
     * @private
     * @param {number[]} keysyms
     *     An array of keysyms, where the index of the keysym in the array is
     *     the location value.
     *
     * @param {!number} location
     *     The location on the keyboard corresponding to the key pressed, as
     *     defined at: http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
     */
    var get_keysym = function get_keysym(keysyms, location) {

        if (!keysyms)
            return null;

        return keysyms[location] || keysyms[0];
    };

    /**
     * Returns true if the given keysym corresponds to a printable character,
     * false otherwise.
     *
     * @param {!number} keysym
     *     The keysym to check.
     *
     * @returns {!boolean}
     *     true if the given keysym corresponds to a printable character,
     *     false otherwise.
     */
    var isPrintable = function isPrintable(keysym) {

        // Keysyms with Unicode equivalents are printable
        return (keysym >= 0x00 && keysym <= 0xFF)
            || (keysym & 0xFFFF0000) === 0x01000000;

    };

    function keysym_from_key_identifier(identifier, location, shifted) {

        if (!identifier)
            return null;

        var typedCharacter;

        // If identifier is U+xxxx, decode Unicode character 
        var unicodePrefixLocation = identifier.indexOf("U+");
        if (unicodePrefixLocation >= 0) {
            var hex = identifier.substring(unicodePrefixLocation+2);
            typedCharacter = String.fromCharCode(parseInt(hex, 16));
        }

        // If single character and not keypad, use that as typed character
        else if (identifier.length === 1 && location !== 3)
            typedCharacter = identifier;

        // Otherwise, look up corresponding keysym
        else
            return get_keysym(keyidentifier_keysym[identifier], location);

        // Alter case if necessary
        if (shifted === true)
            typedCharacter = typedCharacter.toUpperCase();
        else if (shifted === false)
            typedCharacter = typedCharacter.toLowerCase();

        // Get codepoint
        var codepoint = typedCharacter.charCodeAt(0);
        return keysym_from_charcode(codepoint);

    }

    function isControlCharacter(codepoint) {
        return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F);
    }

    function keysym_from_charcode(codepoint) {

        // Keysyms for control characters
        if (isControlCharacter(codepoint)) return 0xFF00 | codepoint;

        // Keysyms for ASCII chars
        if (codepoint >= 0x0000 && codepoint <= 0x00FF)
            return codepoint;

        // Keysyms for Unicode
        if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
            return 0x01000000 | codepoint;

        return null;

    }

    function keysym_from_keycode(keyCode, location) {
        return get_keysym(keycodeKeysyms[keyCode], location);
    }

    /**
     * Heuristically detects if the legacy keyIdentifier property of
     * a keydown/keyup event looks incorrectly derived. Chrome, and
     * presumably others, will produce the keyIdentifier by assuming
     * the keyCode is the Unicode codepoint for that key. This is not
     * correct in all cases.
     *
     * @private
     * @param {!number} keyCode
     *     The keyCode from a browser keydown/keyup event.
     *
     * @param {string} keyIdentifier
     *     The legacy keyIdentifier from a browser keydown/keyup event.
     *
     * @returns {!boolean}
     *     true if the keyIdentifier looks sane, false if the keyIdentifier
     *     appears incorrectly derived or is missing entirely.
     */
    var key_identifier_sane = function key_identifier_sane(keyCode, keyIdentifier) {

        // Missing identifier is not sane
        if (!keyIdentifier)
            return false;

        // Assume non-Unicode keyIdentifier values are sane
        var unicodePrefixLocation = keyIdentifier.indexOf("U+");
        if (unicodePrefixLocation === -1)
            return true;

        // If the Unicode codepoint isn't identical to the keyCode,
        // then the identifier is likely correct
        var codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation+2), 16);
        if (keyCode !== codepoint)
            return true;

        // The keyCodes for A-Z and 0-9 are actually identical to their
        // Unicode codepoints
        if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57))
            return true;

        // The keyIdentifier does NOT appear sane
        return false;

    };

    /**
     * Marks a key as pressed, firing the keydown event if registered. Key
     * repeat for the pressed key will start after a delay if that key is
     * not a modifier. The return value of this function depends on the
     * return value of the keydown event handler, if any.
     * 
     * @param {number} keysym
     *     The keysym of the key to press.
     *
     * @return {boolean}
     *     true if event should NOT be canceled, false otherwise.
     */
    this.press = function(keysym) {

        // Don't bother with pressing the key if the key is unknown
        if (keysym === null) return;

        // Only press if released
        if (!guac_keyboard.pressed[keysym]) {

            // Mark key as pressed
            guac_keyboard.pressed[keysym] = true;

            // Send key event
            if (guac_keyboard.onkeydown) {
                var result = guac_keyboard.onkeydown(keysym);
                last_keydown_result[keysym] = result;

                // Stop any current repeat
                window.clearTimeout(key_repeat_timeout);
                window.clearInterval(key_repeat_interval);

                // Repeat after a delay as long as pressed
                if (!no_repeat[keysym])
                    key_repeat_timeout = window.setTimeout(function() {
                        key_repeat_interval = window.setInterval(function() {
                            guac_keyboard.onkeyup(keysym);
                            guac_keyboard.onkeydown(keysym);
                        }, 50);
                    }, 500);

                return result;
            }
        }

        // Return the last keydown result by default, resort to false if unknown
        return last_keydown_result[keysym] || false;

    };

    /**
     * Marks a key as released, firing the keyup event if registered.
     * 
     * @param {number} keysym
     *     The keysym of the key to release.
     */
    this.release = function(keysym) {

        // Only release if pressed
        if (guac_keyboard.pressed[keysym]) {
            
            // Mark key as released
            delete guac_keyboard.pressed[keysym];
            delete implicitlyPressed[keysym];

            // Stop repeat
            window.clearTimeout(key_repeat_timeout);
            window.clearInterval(key_repeat_interval);

            // Send key event
            if (keysym !== null && guac_keyboard.onkeyup)
                guac_keyboard.onkeyup(keysym);

        }

    };

    /**
     * Presses and releases the keys necessary to type the given string of
     * text.
     *
     * @param {!string} str
     *     The string to type.
     */
    this.type = function type(str) {

        // Press/release the key corresponding to each character in the string
        for (var i = 0; i < str.length; i++) {

            // Determine keysym of current character
            var codepoint = str.codePointAt ? str.codePointAt(i) : str.charCodeAt(i);
            var keysym = keysym_from_charcode(codepoint);

            // Press and release key for current character
            guac_keyboard.press(keysym);
            guac_keyboard.release(keysym);

        }

    };

    /**
     * Resets the state of this keyboard, releasing all keys, and firing keyup
     * events for each released key.
     */
    this.reset = function() {

        // Release all pressed keys
        for (var keysym in guac_keyboard.pressed)
            guac_keyboard.release(parseInt(keysym));

        // Clear event log
        eventLog = [];

    };

    /**
     * Resynchronizes the remote state of the given modifier with its
     * corresponding local modifier state, as dictated by
     * {@link KeyEvent#modifiers} within the given key event, by pressing or
     * releasing keysyms.
     *
     * @private
     * @param {!string} modifier
     *     The name of the {@link Guacamole.Keyboard.ModifierState} property
     *     being updated.
     *
     * @param {!number[]} keysyms
     *     The keysyms which represent the modifier being updated.
     *
     * @param {!KeyEvent} keyEvent
     *     Guacamole's current best interpretation of the key event being
     *     processed.
     */
    var updateModifierState = function updateModifierState(modifier,
        keysyms, keyEvent) {

        var localState = keyEvent.modifiers[modifier];
        var remoteState = guac_keyboard.modifiers[modifier];

        var i;

        // Do not trust changes in modifier state for events directly involving
        // that modifier: (1) the flag may erroneously be cleared despite
        // another version of the same key still being held and (2) the change
        // in flag may be due to the current event being processed, thus
        // updating things here is at best redundant and at worst incorrect
        if (keysyms.indexOf(keyEvent.keysym) !== -1)
            return;

        // Release all related keys if modifier is implicitly released
        if (remoteState && localState === false) {
            for (i = 0; i < keysyms.length; i++) {
                guac_keyboard.release(keysyms[i]);
            }
        }

        // Press if modifier is implicitly pressed
        else if (!remoteState && localState) {

            // Verify that modifier flag isn't already pressed or already set
            // due to another version of the same key being held down
            for (i = 0; i < keysyms.length; i++) {
                if (guac_keyboard.pressed[keysyms[i]])
                    return;
            }

            // Mark as implicitly pressed only if there is other information
            // within the key event relating to a different key. Some
            // platforms, such as iOS, will send essentially empty key events
            // for modifier keys, using only the modifier flags to signal the
            // identity of the key.
            var keysym = keysyms[0];
            if (keyEvent.keysym)
                implicitlyPressed[keysym] = true;

            guac_keyboard.press(keysym);

        }

    };

    /**
     * Given a keyboard event, updates the remote key state to match the local
     * modifier state and remote based on the modifier flags within the event.
     * This function pays no attention to keycodes.
     *
     * @private
     * @param {!KeyEvent} keyEvent
     *     Guacamole's current best interpretation of the key event being
     *     processed.
     */
    var syncModifierStates = function syncModifierStates(keyEvent) {

        // Resync state of alt
        updateModifierState('alt', [
            0xFFE9, // Left alt
            0xFFEA, // Right alt
            0xFE03  // AltGr
        ], keyEvent);

        // Resync state of shift
        updateModifierState('shift', [
            0xFFE1, // Left shift
            0xFFE2  // Right shift
        ], keyEvent);

        // Resync state of ctrl
        updateModifierState('ctrl', [
            0xFFE3, // Left ctrl
            0xFFE4  // Right ctrl
        ], keyEvent);

        // Resync state of meta
        updateModifierState('meta', [
            0xFFE7, // Left meta
            0xFFE8  // Right meta
        ], keyEvent);

        // Resync state of hyper
        updateModifierState('hyper', [
            0xFFEB, // Left super/hyper
            0xFFEC  // Right super/hyper
        ], keyEvent);

        // Update state
        guac_keyboard.modifiers = keyEvent.modifiers;

    };

    /**
     * Returns whether all currently pressed keys were implicitly pressed. A
     * key is implicitly pressed if its status was inferred indirectly from
     * inspection of other key events.
     *
     * @private
     * @returns {!boolean}
     *     true if all currently pressed keys were implicitly pressed, false
     *     otherwise.
     */
    var isStateImplicit = function isStateImplicit() {

        for (var keysym in guac_keyboard.pressed) {
            if (!implicitlyPressed[keysym])
                return false;
        }

        return true;

    };

    /**
     * Reads through the event log, removing events from the head of the log
     * when the corresponding true key presses are known (or as known as they
     * can be).
     * 
     * @private
     * @return {boolean}
     *     Whether the default action of the latest event should be prevented.
     */
    function interpret_events() {

        // Do not prevent default if no event could be interpreted
        var handled_event = interpret_event();
        if (!handled_event)
            return false;

        // Interpret as much as possible
        var last_event;
        do {
            last_event = handled_event;
            handled_event = interpret_event();
        } while (handled_event !== null);

        // Reset keyboard state if we cannot expect to receive any further
        // keyup events
        if (isStateImplicit())
            guac_keyboard.reset();

        return last_event.defaultPrevented;

    }

    /**
     * Releases Ctrl+Alt, if both are currently pressed and the given keysym
     * looks like a key that may require AltGr.
     *
     * @private
     * @param {!number} keysym
     *     The key that was just pressed.
     */
    var release_simulated_altgr = function release_simulated_altgr(keysym) {

        // Both Ctrl+Alt must be pressed if simulated AltGr is in use
        if (!guac_keyboard.modifiers.ctrl || !guac_keyboard.modifiers.alt)
            return;

        // Assume [A-Z] never require AltGr
        if (keysym >= 0x0041 && keysym <= 0x005A)
            return;

        // Assume [a-z] never require AltGr
        if (keysym >= 0x0061 && keysym <= 0x007A)
            return;

        // Release Ctrl+Alt if the keysym is printable
        if (keysym <= 0xFF || (keysym & 0xFF000000) === 0x01000000) {
            guac_keyboard.release(0xFFE3); // Left ctrl 
            guac_keyboard.release(0xFFE4); // Right ctrl 
            guac_keyboard.release(0xFFE9); // Left alt
            guac_keyboard.release(0xFFEA); // Right alt
        }

    };

    /**
     * Reads through the event log, interpreting the first event, if possible,
     * and returning that event. If no events can be interpreted, due to a
     * total lack of events or the need for more events, null is returned. Any
     * interpreted events are automatically removed from the log.
     * 
     * @private
     * @return {KeyEvent}
     *     The first key event in the log, if it can be interpreted, or null
     *     otherwise.
     */
    var interpret_event = function interpret_event() {

        // Peek at first event in log
        var first = eventLog[0];
        if (!first)
            return null;

        // Keydown event
        if (first instanceof KeydownEvent) {

            var keysym = null;
            var accepted_events = [];

            // Defer handling of Meta until it is known to be functioning as a
            // modifier (it may otherwise actually be an alternative method for
            // pressing a single key, such as Meta+Left for Home on ChromeOS)
            if (first.keysym === 0xFFE7 || first.keysym === 0xFFE8) {

                // Defer handling until further events exist to provide context
                if (eventLog.length === 1)
                    return null;

                // Drop keydown if it turns out Meta does not actually apply
                if (eventLog[1].keysym !== first.keysym) {
                    if (!eventLog[1].modifiers.meta)
                        return eventLog.shift();
                }

                // Drop duplicate keydown events while waiting to determine
                // whether to acknowledge Meta (browser may repeat keydown
                // while the key is held)
                else if (eventLog[1] instanceof KeydownEvent)
                    return eventLog.shift();

            }

            // If event itself is reliable, no need to wait for other events
            if (first.reliable) {
                keysym = first.keysym;
                accepted_events = eventLog.splice(0, 1);
            }

            // If keydown is immediately followed by a keypress, use the indicated character
            else if (eventLog[1] instanceof KeypressEvent) {
                keysym = eventLog[1].keysym;
                accepted_events = eventLog.splice(0, 2);
            }

            // If keydown is immediately followed by anything else, then no
            // keypress can possibly occur to clarify this event, and we must
            // handle it now
            else if (eventLog[1]) {
                keysym = first.keysym;
                accepted_events = eventLog.splice(0, 1);
            }

            // Fire a key press if valid events were found
            if (accepted_events.length > 0) {

                syncModifierStates(first);

                if (keysym) {

                    // Fire event
                    release_simulated_altgr(keysym);
                    var defaultPrevented = !guac_keyboard.press(keysym);
                    recentKeysym[first.keyCode] = keysym;

                    // Release the key now if we cannot rely on the associated
                    // keyup event
                    if (!first.keyupReliable)
                        guac_keyboard.release(keysym);

                    // Record whether default was prevented
                    for (var i=0; i<accepted_events.length; i++)
                        accepted_events[i].defaultPrevented = defaultPrevented;

                }

                return first;

            }

        } // end if keydown

        // Keyup event
        else if (first instanceof KeyupEvent && !quirks.keyupUnreliable) {

            // Release specific key if known
            var keysym = first.keysym;
            if (keysym) {
                guac_keyboard.release(keysym);
                delete recentKeysym[first.keyCode];
                first.defaultPrevented = true;
            }

            // Otherwise, fall back to releasing all keys
            else {
                guac_keyboard.reset();
                return first;
            }

            syncModifierStates(first);
            return eventLog.shift();

        } // end if keyup

        // Ignore any other type of event (keypress by itself is invalid, and
        // unreliable keyup events should simply be dumped)
        else
            return eventLog.shift();

        // No event interpreted
        return null;

    };

    /**
     * Returns the keyboard location of the key associated with the given
     * keyboard event. The location differentiates key events which otherwise
     * have the same keycode, such as left shift vs. right shift.
     *
     * @private
     * @param {!KeyboardEvent} e
     *     A JavaScript keyboard event, as received through the DOM via a
     *     "keydown", "keyup", or "keypress" handler.
     *
     * @returns {!number}
     *     The location of the key event on the keyboard, as defined at:
     *     http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
     */
    var getEventLocation = function getEventLocation(e) {

        // Use standard location, if possible
        if ('location' in e)
            return e.location;

        // Failing that, attempt to use deprecated keyLocation
        if ('keyLocation' in e)
            return e.keyLocation;

        // If no location is available, assume left side
        return 0;

    };

    /**
     * Attempts to mark the given Event as having been handled by this
     * Guacamole.Keyboard. If the Event has already been marked as handled,
     * false is returned.
     *
     * @param {!Event} e
     *     The Event to mark.
     *
     * @returns {!boolean}
     *     true if the given Event was successfully marked, false if the given
     *     Event was already marked.
     */
    var markEvent = function markEvent(e) {

        // Fail if event is already marked
        if (e[EVENT_MARKER])
            return false;

        // Mark event otherwise
        e[EVENT_MARKER] = true;
        return true;

    };

    /**
     * Attaches event listeners to the given Element, automatically translating
     * received key, input, and composition events into simple keydown/keyup
     * events signalled through this Guacamole.Keyboard's onkeydown and
     * onkeyup handlers.
     *
     * @param {!(Element|Document)} element
     *     The Element to attach event listeners to for the sake of handling
     *     key or input events.
     */
    this.listenTo = function listenTo(element) {

        // When key pressed
        element.addEventListener("keydown", function(e) {

            // Only intercept if handler set
            if (!guac_keyboard.onkeydown) return;

            // Ignore events which have already been handled
            if (!markEvent(e)) return;

            var keydownEvent = new KeydownEvent(e);

            // Ignore (but do not prevent) the "composition" keycode sent by some
            // browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html)
            if (keydownEvent.keyCode === 229)
                return;

            // Log event
            eventLog.push(keydownEvent);

            // Interpret as many events as possible, prevent default if indicated
            if (interpret_events())
                e.preventDefault();

        }, true);

        // When key pressed
        element.addEventListener("keypress", function(e) {

            // Only intercept if handler set
            if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;

            // Ignore events which have already been handled
            if (!markEvent(e)) return;

            // Log event
            eventLog.push(new KeypressEvent(e));

            // Interpret as many events as possible, prevent default if indicated
            if (interpret_events())
                e.preventDefault();

        }, true);

        // When key released
        element.addEventListener("keyup", function(e) {

            // Only intercept if handler set
            if (!guac_keyboard.onkeyup) return;

            // Ignore events which have already been handled
            if (!markEvent(e)) return;

            e.preventDefault();

            // Log event, call for interpretation
            eventLog.push(new KeyupEvent(e));
            interpret_events();

        }, true);

        /**
         * Handles the given "input" event, typing the data within the input text.
         * If the event is complete (text is provided), handling of "compositionend"
         * events is suspended, as such events may conflict with input events.
         *
         * @private
         * @param {!InputEvent} e
         *     The "input" event to handle.
         */
        var handleInput = function handleInput(e) {

            // Only intercept if handler set
            if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;

            // Ignore events which have already been handled
            if (!markEvent(e)) return;

            // Type all content written
            if (e.data && !e.isComposing) {
                element.removeEventListener("compositionend", handleComposition, false);
                guac_keyboard.type(e.data);
            }

        };

        /**
         * Handles the given "compositionend" event, typing the data within the
         * composed text. If the event is complete (composed text is provided),
         * handling of "input" events is suspended, as such events may conflict
         * with composition events.
         *
         * @private
         * @param {!CompositionEvent} e
         *     The "compositionend" event to handle.
         */
        var handleComposition = function handleComposition(e) {

            // Only intercept if handler set
            if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;

            // Ignore events which have already been handled
            if (!markEvent(e)) return;

            // Type all content written
            if (e.data) {
                element.removeEventListener("input", handleInput, false);
                guac_keyboard.type(e.data);
            }

        };

        // Automatically type text entered into the wrapped field
        element.addEventListener("input", handleInput, false);
        element.addEventListener("compositionend", handleComposition, false);

    };

    // Listen to given element, if any
    if (element)
        guac_keyboard.listenTo(element);

};

/**
 * The unique numerical identifier to assign to the next Guacamole.Keyboard
 * instance.
 *
 * @private
 * @type {!number}
 */
Guacamole.Keyboard._nextID = 0;

/**
 * The state of all supported keyboard modifiers.
 * @constructor
 */
Guacamole.Keyboard.ModifierState = function() {
    
    /**
     * Whether shift is currently pressed.
     *
     * @type {!boolean}
     */
    this.shift = false;
    
    /**
     * Whether ctrl is currently pressed.
     *
     * @type {!boolean}
     */
    this.ctrl = false;
    
    /**
     * Whether alt is currently pressed.
     *
     * @type {!boolean}
     */
    this.alt = false;
    
    /**
     * Whether meta (apple key) is currently pressed.
     *
     * @type {!boolean}
     */
    this.meta = false;

    /**
     * Whether hyper (windows key) is currently pressed.
     *
     * @type {!boolean}
     */
    this.hyper = false;

};

/**
 * Returns the modifier state applicable to the keyboard event given.
 * 
 * @param {!KeyboardEvent} e
 *     The keyboard event to read.
 *
 * @returns {!Guacamole.Keyboard.ModifierState}
 *     The current state of keyboard modifiers.
 */
Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) {
    
    var state = new Guacamole.Keyboard.ModifierState();

    // Assign states from old flags
    state.shift = e.shiftKey;
    state.ctrl  = e.ctrlKey;
    state.alt   = e.altKey;
    state.meta  = e.metaKey;

    // Use DOM3 getModifierState() for others
    if (e.getModifierState) {
        state.hyper = e.getModifierState("OS")
                   || e.getModifierState("Super")
                   || e.getModifierState("Hyper")
                   || e.getModifierState("Win");
    }

    return state;
    
};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Abstract ordered drawing surface. Each Layer contains a canvas element and
 * provides simple drawing instructions for drawing to that canvas element,
 * however unlike the canvas element itself, drawing operations on a Layer are
 * guaranteed to run in order, even if such an operation must wait for an image
 * to load before completing.
 * 
 * @constructor
 * 
 * @param {!number} width
 *     The width of the Layer, in pixels. The canvas element backing this Layer
 *     will be given this width.
 *                       
 * @param {!number} height
 *     The height of the Layer, in pixels. The canvas element backing this
 *     Layer will be given this height.
 */
Guacamole.Layer = function(width, height) {

    /**
     * Reference to this Layer.
     *
     * @private
     * @type {!Guacamole.Layer}
     */
    var layer = this;

    /**
     * The number of pixels the width or height of a layer must change before
     * the underlying canvas is resized. The underlying canvas will be kept at
     * dimensions which are integer multiples of this factor.
     *
     * @private
     * @constant
     * @type {!number}
     */
    var CANVAS_SIZE_FACTOR = 64;

    /**
     * The canvas element backing this Layer.
     *
     * @private
     * @type {!HTMLCanvasElement}
     */
    var canvas = document.createElement("canvas");

    /**
     * The 2D display context of the canvas element backing this Layer.
     *
     * @private
     * @type {!CanvasRenderingContext2D}
     */
    var context = canvas.getContext("2d");
    context.save();

    /**
     * Whether the layer has not yet been drawn to. Once any draw operation
     * which affects the underlying canvas is invoked, this flag will be set to
     * false.
     *
     * @private
     * @type {!boolean}
     */
    var empty = true;

    /**
     * Whether a new path should be started with the next path drawing
     * operations.
     *
     * @private
     * @type {!boolean}
     */
    var pathClosed = true;

    /**
     * The number of states on the state stack.
     * 
     * Note that there will ALWAYS be one element on the stack, but that
     * element is not exposed. It is only used to reset the layer to its
     * initial state.
     * 
     * @private
     * @type {!number}
     */
    var stackSize = 0;

    /**
     * Map of all Guacamole channel masks to HTML5 canvas composite operation
     * names. Not all channel mask combinations are currently implemented.
     *
     * @private
     * @type {!Object.<number, string>}
     */
    var compositeOperation = {
     /* 0x0 NOT IMPLEMENTED */
        0x1: "destination-in",
        0x2: "destination-out",
     /* 0x3 NOT IMPLEMENTED */
        0x4: "source-in",
     /* 0x5 NOT IMPLEMENTED */
        0x6: "source-atop",
     /* 0x7 NOT IMPLEMENTED */
        0x8: "source-out",
        0x9: "destination-atop",
        0xA: "xor",
        0xB: "destination-over",
        0xC: "copy",
     /* 0xD NOT IMPLEMENTED */
        0xE: "source-over",
        0xF: "lighter"
    };

    /**
     * Resizes the canvas element backing this Layer. This function should only
     * be used internally.
     * 
     * @private
     * @param {number} [newWidth=0]
     *     The new width to assign to this Layer.
     *
     * @param {number} [newHeight=0]
     *     The new height to assign to this Layer.
     */
    var resize = function resize(newWidth, newHeight) {

        // Default size to zero
        newWidth = newWidth || 0;
        newHeight = newHeight || 0;

        // Calculate new dimensions of internal canvas
        var canvasWidth  = Math.ceil(newWidth  / CANVAS_SIZE_FACTOR) * CANVAS_SIZE_FACTOR;
        var canvasHeight = Math.ceil(newHeight / CANVAS_SIZE_FACTOR) * CANVAS_SIZE_FACTOR;

        // Resize only if canvas dimensions are actually changing
        if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {

            // Copy old data only if relevant and non-empty
            var oldData = null;
            if (!empty && canvas.width !== 0 && canvas.height !== 0) {

                // Create canvas and context for holding old data
                oldData = document.createElement("canvas");
                oldData.width = Math.min(layer.width, newWidth);
                oldData.height = Math.min(layer.height, newHeight);

                var oldDataContext = oldData.getContext("2d");

                // Copy image data from current
                oldDataContext.drawImage(canvas,
                        0, 0, oldData.width, oldData.height,
                        0, 0, oldData.width, oldData.height);

            }

            // Preserve composite operation
            var oldCompositeOperation = context.globalCompositeOperation;

            // Resize canvas
            canvas.width = canvasWidth;
            canvas.height = canvasHeight;

            // Redraw old data, if any
            if (oldData)
                context.drawImage(oldData,
                    0, 0, oldData.width, oldData.height,
                    0, 0, oldData.width, oldData.height);

            // Restore composite operation
            context.globalCompositeOperation = oldCompositeOperation;

            // Acknowledge reset of stack (happens on resize of canvas)
            stackSize = 0;
            context.save();

        }

        // If the canvas size is not changing, manually force state reset
        else
            layer.reset();

        // Assign new layer dimensions
        layer.width = newWidth;
        layer.height = newHeight;

    };

    /**
     * Given the X and Y coordinates of the upper-left corner of a rectangle
     * and the rectangle's width and height, resize the backing canvas element
     * as necessary to ensure that the rectangle fits within the canvas
     * element's coordinate space. This function will only make the canvas
     * larger. If the rectangle already fits within the canvas element's
     * coordinate space, the canvas is left unchanged.
     * 
     * @private
     * @param {!number} x
     *     The X coordinate of the upper-left corner of the rectangle to fit.
     *
     * @param {!number} y
     *     The Y coordinate of the upper-left corner of the rectangle to fit.
     *
     * @param {!number} w
     *     The width of the rectangle to fit.
     *
     * @param {!number} h
     *     The height of the rectangle to fit.
     */
    function fitRect(x, y, w, h) {
        
        // Calculate bounds
        var opBoundX = w + x;
        var opBoundY = h + y;
        
        // Determine max width
        var resizeWidth;
        if (opBoundX > layer.width)
            resizeWidth = opBoundX;
        else
            resizeWidth = layer.width;

        // Determine max height
        var resizeHeight;
        if (opBoundY > layer.height)
            resizeHeight = opBoundY;
        else
            resizeHeight = layer.height;

        // Resize if necessary
        layer.resize(resizeWidth, resizeHeight);

    }

    /**
     * Set to true if this Layer should resize itself to accommodate the
     * dimensions of any drawing operation, and false (the default) otherwise.
     * 
     * Note that setting this property takes effect immediately, and thus may
     * take effect on operations that were started in the past but have not
     * yet completed. If you wish the setting of this flag to only modify
     * future operations, you will need to make the setting of this flag an
     * operation with sync().
     * 
     * @example
     * // Set autosize to true for all future operations
     * layer.sync(function() {
     *     layer.autosize = true;
     * });
     * 
     * @type {!boolean}
     * @default false
     */
    this.autosize = false;

    /**
     * The current width of this layer.
     *
     * @type {!number}
     */
    this.width = width;

    /**
     * The current height of this layer.
     *
     * @type {!number}
     */
    this.height = height;

    /**
     * Returns the canvas element backing this Layer. Note that the dimensions
     * of the canvas may not exactly match those of the Layer, as resizing a
     * canvas while maintaining its state is an expensive operation.
     *
     * @returns {!HTMLCanvasElement}
     *     The canvas element backing this Layer.
     */
    this.getCanvas = function getCanvas() {
        return canvas;
    };

    /**
     * Returns a new canvas element containing the same image as this Layer.
     * Unlike getCanvas(), the canvas element returned is guaranteed to have
     * the exact same dimensions as the Layer.
     *
     * @returns {!HTMLCanvasElement}
     *     A new canvas element containing a copy of the image content this
     *     Layer.
     */
    this.toCanvas = function toCanvas() {

        // Create new canvas having same dimensions
        var canvas = document.createElement('canvas');
        canvas.width = layer.width;
        canvas.height = layer.height;

        // Copy image contents to new canvas
        var context = canvas.getContext('2d');
        context.drawImage(layer.getCanvas(), 0, 0);

        return canvas;

    };

    /**
     * Changes the size of this Layer to the given width and height. Resizing
     * is only attempted if the new size provided is actually different from
     * the current size.
     * 
     * @param {!number} newWidth
     *     The new width to assign to this Layer.
     *
     * @param {!number} newHeight
     *     The new height to assign to this Layer.
     */
    this.resize = function(newWidth, newHeight) {
        if (newWidth !== layer.width || newHeight !== layer.height)
            resize(newWidth, newHeight);
    };

    /**
     * Draws the specified image at the given coordinates. The image specified
     * must already be loaded.
     * 
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     *
     * @param {!CanvasImageSource} image
     *     The image to draw. Note that this is not a URL.
     */
    this.drawImage = function(x, y, image) {
        if (layer.autosize) fitRect(x, y, image.width, image.height);
        context.drawImage(image, x, y);
        empty = false;
    };

    /**
     * Transfer a rectangle of image data from one Layer to this Layer using the
     * specified transfer function.
     * 
     * @param {!Guacamole.Layer} srcLayer
     *     The Layer to copy image data from.
     *
     * @param {!number} srcx
     *     The X coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcy
     *     The Y coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcw
     *     The width of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} srch
     *     The height of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     *
     * @param {!function} transferFunction
     *     The transfer function to use to transfer data from source to
     *     destination.
     */
    this.transfer = function(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction) {

        var srcCanvas = srcLayer.getCanvas();

        // If entire rectangle outside source canvas, stop
        if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return;

        // Otherwise, clip rectangle to area
        if (srcx + srcw > srcCanvas.width)
            srcw = srcCanvas.width - srcx;

        if (srcy + srch > srcCanvas.height)
            srch = srcCanvas.height - srcy;

        // Stop if nothing to draw.
        if (srcw === 0 || srch === 0) return;

        if (layer.autosize) fitRect(x, y, srcw, srch);

        // Get image data from src and dst
        var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch);
        var dst = context.getImageData(x , y, srcw, srch);

        // Apply transfer for each pixel
        for (var i=0; i<srcw*srch*4; i+=4) {

            // Get source pixel environment
            var src_pixel = new Guacamole.Layer.Pixel(
                src.data[i],
                src.data[i+1],
                src.data[i+2],
                src.data[i+3]
            );
                
            // Get destination pixel environment
            var dst_pixel = new Guacamole.Layer.Pixel(
                dst.data[i],
                dst.data[i+1],
                dst.data[i+2],
                dst.data[i+3]
            );

            // Apply transfer function
            transferFunction(src_pixel, dst_pixel);

            // Save pixel data
            dst.data[i  ] = dst_pixel.red;
            dst.data[i+1] = dst_pixel.green;
            dst.data[i+2] = dst_pixel.blue;
            dst.data[i+3] = dst_pixel.alpha;

        }

        // Draw image data
        context.putImageData(dst, x, y);
        empty = false;

    };

    /**
     * Put a rectangle of image data from one Layer to this Layer directly
     * without performing any alpha blending. Simply copy the data.
     * 
     * @param {!Guacamole.Layer} srcLayer
     *     The Layer to copy image data from.
     *
     * @param {!number} srcx
     *     The X coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcy
     *     The Y coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcw
     *     The width of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} srch
     *     The height of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     */
    this.put = function(srcLayer, srcx, srcy, srcw, srch, x, y) {

        var srcCanvas = srcLayer.getCanvas();

        // If entire rectangle outside source canvas, stop
        if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return;

        // Otherwise, clip rectangle to area
        if (srcx + srcw > srcCanvas.width)
            srcw = srcCanvas.width - srcx;

        if (srcy + srch > srcCanvas.height)
            srch = srcCanvas.height - srcy;

        // Stop if nothing to draw.
        if (srcw === 0 || srch === 0) return;

        if (layer.autosize) fitRect(x, y, srcw, srch);

        // Get image data from src and dst
        var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch);
        context.putImageData(src, x, y);
        empty = false;

    };

    /**
     * Copy a rectangle of image data from one Layer to this Layer. This
     * operation will copy exactly the image data that will be drawn once all
     * operations of the source Layer that were pending at the time this
     * function was called are complete. This operation will not alter the
     * size of the source Layer even if its autosize property is set to true.
     * 
     * @param {!Guacamole.Layer} srcLayer
     *     The Layer to copy image data from.
     *
     * @param {!number} srcx
     *     The X coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcy
     *     The Y coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcw
     *     The width of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} srch
     *     The height of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     */
    this.copy = function(srcLayer, srcx, srcy, srcw, srch, x, y) {

        var srcCanvas = srcLayer.getCanvas();

        // If entire rectangle outside source canvas, stop
        if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return;

        // Otherwise, clip rectangle to area
        if (srcx + srcw > srcCanvas.width)
            srcw = srcCanvas.width - srcx;

        if (srcy + srch > srcCanvas.height)
            srch = srcCanvas.height - srcy;

        // Stop if nothing to draw.
        if (srcw === 0 || srch === 0) return;

        if (layer.autosize) fitRect(x, y, srcw, srch);
        context.drawImage(srcCanvas, srcx, srcy, srcw, srch, x, y, srcw, srch);
        empty = false;

    };

    /**
     * Starts a new path at the specified point.
     * 
     * @param {!number} x
     *     The X coordinate of the point to draw.
     *
     * @param {!number} y
     *     The Y coordinate of the point to draw.
     */
    this.moveTo = function(x, y) {
        
        // Start a new path if current path is closed
        if (pathClosed) {
            context.beginPath();
            pathClosed = false;
        }
        
        if (layer.autosize) fitRect(x, y, 0, 0);
        context.moveTo(x, y);

    };

    /**
     * Add the specified line to the current path.
     * 
     * @param {!number} x
     *     The X coordinate of the endpoint of the line to draw.
     *
     * @param {!number} y
     *     The Y coordinate of the endpoint of the line to draw.
     */
    this.lineTo = function(x, y) {
        
        // Start a new path if current path is closed
        if (pathClosed) {
            context.beginPath();
            pathClosed = false;
        }
        
        if (layer.autosize) fitRect(x, y, 0, 0);
        context.lineTo(x, y);
        
    };

    /**
     * Add the specified arc to the current path.
     * 
     * @param {!number} x
     *     The X coordinate of the center of the circle which will contain the
     *     arc.
     *
     * @param {!number} y
     *     The Y coordinate of the center of the circle which will contain the
     *     arc.
     *
     * @param {!number} radius
     *     The radius of the circle.
     *
     * @param {!number} startAngle
     *     The starting angle of the arc, in radians.
     *
     * @param {!number} endAngle
     *     The ending angle of the arc, in radians.
     *
     * @param {!boolean} negative
     *     Whether the arc should be drawn in order of decreasing angle.
     */
    this.arc = function(x, y, radius, startAngle, endAngle, negative) {
        
        // Start a new path if current path is closed
        if (pathClosed) {
            context.beginPath();
            pathClosed = false;
        }
        
        if (layer.autosize) fitRect(x, y, 0, 0);
        context.arc(x, y, radius, startAngle, endAngle, negative);
        
    };

    /**
     * Starts a new path at the specified point.
     * 
     * @param {!number} cp1x
     *     The X coordinate of the first control point.
     *
     * @param {!number} cp1y
     *     The Y coordinate of the first control point.
     *
     * @param {!number} cp2x
     *     The X coordinate of the second control point.
     *
     * @param {!number} cp2y
     *     The Y coordinate of the second control point.
     *
     * @param {!number} x
     *     The X coordinate of the endpoint of the curve.
     *
     * @param {!number} y
     *     The Y coordinate of the endpoint of the curve.
     */
    this.curveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
        
        // Start a new path if current path is closed
        if (pathClosed) {
            context.beginPath();
            pathClosed = false;
        }
        
        if (layer.autosize) fitRect(x, y, 0, 0);
        context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
        
    };

    /**
     * Closes the current path by connecting the end point with the start
     * point (if any) with a straight line.
     */
    this.close = function() {
        context.closePath();
        pathClosed = true;
    };

    /**
     * Add the specified rectangle to the current path.
     * 
     * @param {!number} x
     *     The X coordinate of the upper-left corner of the rectangle to draw.
     *
     * @param {!number} y
     *     The Y coordinate of the upper-left corner of the rectangle to draw.
     *
     * @param {!number} w
     *     The width of the rectangle to draw.
     *
     * @param {!number} h
     *     The height of the rectangle to draw.
     */
    this.rect = function(x, y, w, h) {
            
        // Start a new path if current path is closed
        if (pathClosed) {
            context.beginPath();
            pathClosed = false;
        }
        
        if (layer.autosize) fitRect(x, y, w, h);
        context.rect(x, y, w, h);
        
    };

    /**
     * Clip all future drawing operations by the current path. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as fillColor()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     */
    this.clip = function() {

        // Set new clipping region
        context.clip();

        // Path now implicitly closed
        pathClosed = true;

    };

    /**
     * Stroke the current path with the specified color. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!string} cap
     *     The line cap style. Can be "round", "square", or "butt".
     *
     * @param {!string} join
     *     The line join style. Can be "round", "bevel", or "miter".
     *
     * @param {!number} thickness
     *     The line thickness in pixels.
     *
     * @param {!number} r
     *     The red component of the color to fill.
     *
     * @param {!number} g
     *     The green component of the color to fill.
     *
     * @param {!number} b
     *     The blue component of the color to fill.
     *
     * @param {!number} a
     *     The alpha component of the color to fill.
     */
    this.strokeColor = function(cap, join, thickness, r, g, b, a) {

        // Stroke with color
        context.lineCap = cap;
        context.lineJoin = join;
        context.lineWidth = thickness;
        context.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")";
        context.stroke();
        empty = false;

        // Path now implicitly closed
        pathClosed = true;

    };

    /**
     * Fills the current path with the specified color. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!number} r
     *     The red component of the color to fill.
     *
     * @param {!number} g
     *     The green component of the color to fill.
     *
     * @param {!number} b
     *     The blue component of the color to fill.
     *
     * @param {!number} a
     *     The alpha component of the color to fill.
     */
    this.fillColor = function(r, g, b, a) {

        // Fill with color
        context.fillStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")";
        context.fill();
        empty = false;

        // Path now implicitly closed
        pathClosed = true;

    };

    /**
     * Stroke the current path with the image within the specified layer. The
     * image data will be tiled infinitely within the stroke. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!string} cap
     *     The line cap style. Can be "round", "square", or "butt".
     *
     * @param {!string} join
     *     The line join style. Can be "round", "bevel", or "miter".
     *
     * @param {!number} thickness
     *     The line thickness in pixels.
     *
     * @param {!Guacamole.Layer} srcLayer
     *     The layer to use as a repeating pattern within the stroke.
     */
    this.strokeLayer = function(cap, join, thickness, srcLayer) {

        // Stroke with image data
        context.lineCap = cap;
        context.lineJoin = join;
        context.lineWidth = thickness;
        context.strokeStyle = context.createPattern(
            srcLayer.getCanvas(),
            "repeat"
        );
        context.stroke();
        empty = false;

        // Path now implicitly closed
        pathClosed = true;

    };

    /**
     * Fills the current path with the image within the specified layer. The
     * image data will be tiled infinitely within the stroke. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!Guacamole.Layer} srcLayer
     *     The layer to use as a repeating pattern within the fill.
     */
    this.fillLayer = function(srcLayer) {

        // Fill with image data 
        context.fillStyle = context.createPattern(
            srcLayer.getCanvas(),
            "repeat"
        );
        context.fill();
        empty = false;

        // Path now implicitly closed
        pathClosed = true;

    };

    /**
     * Push current layer state onto stack.
     */
    this.push = function() {

        // Save current state onto stack
        context.save();
        stackSize++;

    };

    /**
     * Pop layer state off stack.
     */
    this.pop = function() {

        // Restore current state from stack
        if (stackSize > 0) {
            context.restore();
            stackSize--;
        }

    };

    /**
     * Reset the layer, clearing the stack, the current path, and any transform
     * matrix.
     */
    this.reset = function() {

        // Clear stack
        while (stackSize > 0) {
            context.restore();
            stackSize--;
        }

        // Restore to initial state
        context.restore();
        context.save();

        // Clear path
        context.beginPath();
        pathClosed = false;

    };

    /**
     * Sets the given affine transform (defined with six values from the
     * transform's matrix).
     * 
     * @param {!number} a
     *     The first value in the affine transform's matrix.
     *
     * @param {!number} b
     *     The second value in the affine transform's matrix.
     *
     * @param {!number} c
     *     The third value in the affine transform's matrix.
     *
     * @param {!number} d
     *     The fourth value in the affine transform's matrix.
     *
     * @param {!number} e
     *     The fifth value in the affine transform's matrix.
     *
     * @param {!number} f
     *     The sixth value in the affine transform's matrix.
     */
    this.setTransform = function(a, b, c, d, e, f) {
        context.setTransform(
            a, b, c,
            d, e, f
          /*0, 0, 1*/
        );
    };

    /**
     * Applies the given affine transform (defined with six values from the
     * transform's matrix).
     *
     * @param {!number} a
     *     The first value in the affine transform's matrix.
     *
     * @param {!number} b
     *     The second value in the affine transform's matrix.
     *
     * @param {!number} c
     *     The third value in the affine transform's matrix.
     *
     * @param {!number} d
     *     The fourth value in the affine transform's matrix.
     *
     * @param {!number} e
     *     The fifth value in the affine transform's matrix.
     *
     * @param {!number} f
     *     The sixth value in the affine transform's matrix.
     */
    this.transform = function(a, b, c, d, e, f) {
        context.transform(
            a, b, c,
            d, e, f
          /*0, 0, 1*/
        );
    };

    /**
     * Sets the channel mask for future operations on this Layer.
     * 
     * The channel mask is a Guacamole-specific compositing operation identifier
     * with a single bit representing each of four channels (in order): source
     * image where destination transparent, source where destination opaque,
     * destination where source transparent, and destination where source
     * opaque.
     * 
     * @param {!number} mask
     *     The channel mask for future operations on this Layer.
     */
    this.setChannelMask = function(mask) {
        context.globalCompositeOperation = compositeOperation[mask];
    };

    /**
     * Sets the miter limit for stroke operations using the miter join. This
     * limit is the maximum ratio of the size of the miter join to the stroke
     * width. If this ratio is exceeded, the miter will not be drawn for that
     * joint of the path.
     * 
     * @param {!number} limit
     *     The miter limit for stroke operations using the miter join.
     */
    this.setMiterLimit = function(limit) {
        context.miterLimit = limit;
    };

    // Initialize canvas dimensions
    resize(width, height);

    // Explicitly render canvas below other elements in the layer (such as
    // child layers). Chrome and others may fail to render layers properly
    // without this.
    canvas.style.zIndex = -1;

};

/**
 * Channel mask for the composite operation "rout".
 *
 * @type {!number}
 */
Guacamole.Layer.ROUT  = 0x2;

/**
 * Channel mask for the composite operation "atop".
 *
 * @type {!number}
 */
Guacamole.Layer.ATOP  = 0x6;

/**
 * Channel mask for the composite operation "xor".
 *
 * @type {!number}
 */
Guacamole.Layer.XOR   = 0xA;

/**
 * Channel mask for the composite operation "rover".
 *
 * @type {!number}
 */
Guacamole.Layer.ROVER = 0xB;

/**
 * Channel mask for the composite operation "over".
 *
 * @type {!number}
 */
Guacamole.Layer.OVER  = 0xE;

/**
 * Channel mask for the composite operation "plus".
 *
 * @type {!number}
 */
Guacamole.Layer.PLUS  = 0xF;

/**
 * Channel mask for the composite operation "rin".
 * Beware that WebKit-based browsers may leave the contents of the destination
 * layer where the source layer is transparent, despite the definition of this
 * operation.
 *
 * @type {!number}
 */
Guacamole.Layer.RIN   = 0x1;

/**
 * Channel mask for the composite operation "in".
 * Beware that WebKit-based browsers may leave the contents of the destination
 * layer where the source layer is transparent, despite the definition of this
 * operation.
 *
 * @type {!number}
 */
Guacamole.Layer.IN    = 0x4;

/**
 * Channel mask for the composite operation "out".
 * Beware that WebKit-based browsers may leave the contents of the destination
 * layer where the source layer is transparent, despite the definition of this
 * operation.
 *
 * @type {!number}
 */
Guacamole.Layer.OUT   = 0x8;

/**
 * Channel mask for the composite operation "ratop".
 * Beware that WebKit-based browsers may leave the contents of the destination
 * layer where the source layer is transparent, despite the definition of this
 * operation.
 *
 * @type {!number}
 */
Guacamole.Layer.RATOP = 0x9;

/**
 * Channel mask for the composite operation "src".
 * Beware that WebKit-based browsers may leave the contents of the destination
 * layer where the source layer is transparent, despite the definition of this
 * operation.
 *
 * @type {!number}
 */
Guacamole.Layer.SRC   = 0xC;

/**
 * Represents a single pixel of image data. All components have a minimum value
 * of 0 and a maximum value of 255.
 * 
 * @constructor
 * 
 * @param {!number} r
 *     The red component of this pixel.
 *
 * @param {!number} g
 *     The green component of this pixel.
 *
 * @param {!number} b
 *     The blue component of this pixel.
 *
 * @param {!number} a
 *     The alpha component of this pixel.
 */
Guacamole.Layer.Pixel = function(r, g, b, a) {

    /**
     * The red component of this pixel, where 0 is the minimum value,
     * and 255 is the maximum.
     *
     * @type {!number}
     */
    this.red   = r;

    /**
     * The green component of this pixel, where 0 is the minimum value,
     * and 255 is the maximum.
     *
     * @type {!number}
     */
    this.green = g;

    /**
     * The blue component of this pixel, where 0 is the minimum value,
     * and 255 is the maximum.
     *
     * @type {!number}
     */
    this.blue  = b;

    /**
     * The alpha component of this pixel, where 0 is the minimum value,
     * and 255 is the maximum.
     *
     * @type {!number}
     */
    this.alpha = a;

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Provides cross-browser mouse events for a given element. The events of
 * the given element are automatically populated with handlers that translate
 * mouse events into a non-browser-specific event provided by the
 * Guacamole.Mouse instance.
 *
 * @example
 * var mouse = new Guacamole.Mouse(client.getDisplay().getElement());
 *
 * // Forward all mouse interaction over Guacamole connection
 * mouse.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) {
 *     client.sendMouseState(e.state, true);
 * });
 *
 * @example
 * // Hide software cursor when mouse leaves display
 * mouse.on('mouseout', function hideCursor() {
 *     client.getDisplay().showCursor(false);
 * });
 *
 * @constructor
 * @augments Guacamole.Mouse.Event.Target
 * @param {!Element} element
 *     The Element to use to provide mouse events.
 */
Guacamole.Mouse = function Mouse(element) {

    Guacamole.Mouse.Event.Target.call(this);

    /**
     * Reference to this Guacamole.Mouse.
     *
     * @private
     * @type {!Guacamole.Mouse}
     */
    var guac_mouse = this;

    /**
     * The number of mousemove events to require before re-enabling mouse
     * event handling after receiving a touch event.
     *
     * @type {!number}
     */
    this.touchMouseThreshold = 3;

    /**
     * The minimum amount of pixels scrolled required for a single scroll button
     * click.
     *
     * @type {!number}
     */
    this.scrollThreshold = 53;

    /**
     * The number of pixels to scroll per line.
     *
     * @type {!number}
     */
    this.PIXELS_PER_LINE = 18;

    /**
     * The number of pixels to scroll per page.
     *
     * @type {!number}
     */
    this.PIXELS_PER_PAGE = this.PIXELS_PER_LINE * 16;

    /**
     * Array of {@link Guacamole.Mouse.State} button names corresponding to the
     * mouse button indices used by DOM mouse events.
     *
     * @private
     * @type {!string[]}
     */
    var MOUSE_BUTTONS = [
        Guacamole.Mouse.State.Buttons.LEFT,
        Guacamole.Mouse.State.Buttons.MIDDLE,
        Guacamole.Mouse.State.Buttons.RIGHT
    ];

    /**
     * Counter of mouse events to ignore. This decremented by mousemove, and
     * while non-zero, mouse events will have no effect.
     *
     * @private
     * @type {!number}
     */
    var ignore_mouse = 0;

    /**
     * Cumulative scroll delta amount. This value is accumulated through scroll
     * events and results in scroll button clicks if it exceeds a certain
     * threshold.
     *
     * @private
     * @type {!number}
     */
    var scroll_delta = 0;

    // Block context menu so right-click gets sent properly
    element.addEventListener("contextmenu", function(e) {
        Guacamole.Event.DOMEvent.cancelEvent(e);
    }, false);

    element.addEventListener("mousemove", function(e) {

        // If ignoring events, decrement counter
        if (ignore_mouse) {
            Guacamole.Event.DOMEvent.cancelEvent(e);
            ignore_mouse--;
            return;
        }

        guac_mouse.move(Guacamole.Position.fromClientPosition(element, e.clientX, e.clientY), e);

    }, false);

    element.addEventListener("mousedown", function(e) {

        // Do not handle if ignoring events
        if (ignore_mouse) {
            Guacamole.Event.DOMEvent.cancelEvent(e);
            return;
        }

        var button = MOUSE_BUTTONS[e.button];
        if (button)
            guac_mouse.press(button, e);

    }, false);

    element.addEventListener("mouseup", function(e) {

        // Do not handle if ignoring events
        if (ignore_mouse) {
            Guacamole.Event.DOMEvent.cancelEvent(e);
            return;
        }

        var button = MOUSE_BUTTONS[e.button];
        if (button)
            guac_mouse.release(button, e);

    }, false);

    element.addEventListener("mouseout", function(e) {

        // Get parent of the element the mouse pointer is leaving
       	if (!e) e = window.event;

        // Check that mouseout is due to actually LEAVING the element
        var target = e.relatedTarget || e.toElement;
        while (target) {
            if (target === element)
                return;
            target = target.parentNode;
        }

        // Release all buttons and fire mouseout
        guac_mouse.reset(e);
        guac_mouse.out(e);

    }, false);

    // Override selection on mouse event element.
    element.addEventListener("selectstart", function(e) {
        Guacamole.Event.DOMEvent.cancelEvent(e);
    }, false);

    // Ignore all pending mouse events when touch events are the apparent source
    function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; }

    element.addEventListener("touchmove",  ignorePendingMouseEvents, false);
    element.addEventListener("touchstart", ignorePendingMouseEvents, false);
    element.addEventListener("touchend",   ignorePendingMouseEvents, false);

    // Scroll wheel support
    function mousewheel_handler(e) {

        // Determine approximate scroll amount (in pixels)
        var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta;

        // If successfully retrieved scroll amount, convert to pixels if not
        // already in pixels
        if (delta) {

            // Convert to pixels if delta was lines
            if (e.deltaMode === 1)
                delta = e.deltaY * guac_mouse.PIXELS_PER_LINE;

            // Convert to pixels if delta was pages
            else if (e.deltaMode === 2)
                delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE;

        }

        // Otherwise, assume legacy mousewheel event and line scrolling
        else
            delta = e.detail * guac_mouse.PIXELS_PER_LINE;
        
        // Update overall delta
        scroll_delta += delta;

        // Up
        if (scroll_delta <= -guac_mouse.scrollThreshold) {

            // Repeatedly click the up button until insufficient delta remains
            do {
                guac_mouse.click(Guacamole.Mouse.State.Buttons.UP);
                scroll_delta += guac_mouse.scrollThreshold;
            } while (scroll_delta <= -guac_mouse.scrollThreshold);

            // Reset delta
            scroll_delta = 0;

        }

        // Down
        if (scroll_delta >= guac_mouse.scrollThreshold) {

            // Repeatedly click the down button until insufficient delta remains
            do {
                guac_mouse.click(Guacamole.Mouse.State.Buttons.DOWN);
                scroll_delta -= guac_mouse.scrollThreshold;
            } while (scroll_delta >= guac_mouse.scrollThreshold);

            // Reset delta
            scroll_delta = 0;

        }

        // All scroll/wheel events must currently be cancelled regardless of
        // whether the dispatched event is cancelled, as there is no Guacamole
        // scroll event and thus no way to cancel scroll events that are
        // smaller than required to produce an up/down click
        Guacamole.Event.DOMEvent.cancelEvent(e);

    }

    element.addEventListener('DOMMouseScroll', mousewheel_handler, false);
    element.addEventListener('mousewheel',     mousewheel_handler, false);
    element.addEventListener('wheel',          mousewheel_handler, false);

    /**
     * Whether the browser supports CSS3 cursor styling, including hotspot
     * coordinates.
     *
     * @private
     * @type {!boolean}
     */
    var CSS3_CURSOR_SUPPORTED = (function() {

        var div = document.createElement("div");

        // If no cursor property at all, then no support
        if (!("cursor" in div.style))
            return false;

        try {
            // Apply simple 1x1 PNG
            div.style.cursor = "url(data:image/png;base64,"
                             + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
                             + "AQMAAAAl21bKAAAAA1BMVEX///+nxBvI"
                             + "AAAACklEQVQI12NgAAAAAgAB4iG8MwAA"
                             + "AABJRU5ErkJggg==) 0 0, auto";
        }
        catch (e) {
            return false;
        }

        // Verify cursor property is set to URL with hotspot
        return /\burl\([^()]*\)\s+0\s+0\b/.test(div.style.cursor || "");

    })();

    /**
     * Changes the local mouse cursor to the given canvas, having the given
     * hotspot coordinates. This affects styling of the element backing this
     * Guacamole.Mouse only, and may fail depending on browser support for
     * setting the mouse cursor.
     * 
     * If setting the local cursor is desired, it is up to the implementation
     * to do something else, such as use the software cursor built into
     * Guacamole.Display, if the local cursor cannot be set.
     *
     * @param {!HTMLCanvasElement} canvas
     *     The cursor image.
     *
     * @param {!number} x
     *     The X-coordinate of the cursor hotspot.
     *
     * @param {!number} y
     *     The Y-coordinate of the cursor hotspot.
     *
     * @return {!boolean}
     *     true if the cursor was successfully set, false if the cursor could
     *     not be set for any reason.
     */
    this.setCursor = function(canvas, x, y) {

        // Attempt to set via CSS3 cursor styling
        if (CSS3_CURSOR_SUPPORTED) {
            var dataURL = canvas.toDataURL('image/png');
            element.style.cursor = "url(" + dataURL + ") " + x + " " + y + ", auto";
            return true;
        }

        // Otherwise, setting cursor failed
        return false;

    };

};

/**
 * The current state of a mouse, including position and buttons.
 *
 * @constructor
 * @augments Guacamole.Position
 * @param {Guacamole.Mouse.State|object} [template={}]
 *     The object whose properties should be copied within the new
 *     Guacamole.Mouse.State.
 */
Guacamole.Mouse.State = function State(template) {

    /**
     * Returns the template object that would be provided to the
     * Guacamole.Mouse.State constructor to produce a new Guacamole.Mouse.State
     * object with the properties specified. The order and type of arguments
     * used by this function are identical to those accepted by the
     * Guacamole.Mouse.State constructor of Apache Guacamole 1.3.0 and older.
     *
     * @private
     * @param {!number} x
     *     The X position of the mouse pointer in pixels.
     *
     * @param {!number} y
     *     The Y position of the mouse pointer in pixels.
     *
     * @param {!boolean} left
     *     Whether the left mouse button is pressed.
     *
     * @param {!boolean} middle
     *     Whether the middle mouse button is pressed.
     *
     * @param {!boolean} right
     *     Whether the right mouse button is pressed.
     *
     * @param {!boolean} up
     *     Whether the up mouse button is pressed (the fourth button, usually
     *     part of a scroll wheel).
     *
     * @param {!boolean} down
     *     Whether the down mouse button is pressed (the fifth button, usually
     *     part of a scroll wheel).
     *
     * @return {!object}
     *     The equivalent template object that would be passed to the new
     *     Guacamole.Mouse.State constructor.
     */
    var legacyConstructor = function legacyConstructor(x, y, left, middle, right, up, down) {
        return {
            x      : x,
            y      : y,
            left   : left,
            middle : middle,
            right  : right,
            up     : up,
            down   : down
        };
    };

    // Accept old-style constructor, as well
    if (arguments.length > 1)
        template = legacyConstructor.apply(this, arguments);
    else
        template = template || {};

    Guacamole.Position.call(this, template);

    /**
     * Whether the left mouse button is currently pressed.
     *
     * @type {!boolean}
     * @default false
     */
    this.left = template.left || false;

    /**
     * Whether the middle mouse button is currently pressed.
     *
     * @type {!boolean}
     * @default false
     */
    this.middle = template.middle || false;

    /**
     * Whether the right mouse button is currently pressed.
     *
     * @type {!boolean}
     * @default false
     */
    this.right = template.right || false;

    /**
     * Whether the up mouse button is currently pressed. This is the fourth
     * mouse button, associated with upward scrolling of the mouse scroll
     * wheel.
     *
     * @type {!boolean}
     * @default false
     */
    this.up = template.up || false;

    /**
     * Whether the down mouse button is currently pressed. This is the fifth 
     * mouse button, associated with downward scrolling of the mouse scroll
     * wheel.
     *
     * @type {!boolean}
     * @default false
     */
    this.down = template.down || false;

};

/**
 * All mouse buttons that may be represented by a
 * {@link Guacamole.Mouse.State}. 
 *
 * @readonly
 * @enum
 */
Guacamole.Mouse.State.Buttons = {

    /**
     * The name of the {@link Guacamole.Mouse.State} property representing the
     * left mouse button.
     *
     * @constant
     * @type {!string}
     */
    LEFT : 'left',

    /**
     * The name of the {@link Guacamole.Mouse.State} property representing the
     * middle mouse button.
     *
     * @constant
     * @type {!string}
     */
    MIDDLE : 'middle',

    /**
     * The name of the {@link Guacamole.Mouse.State} property representing the
     * right mouse button.
     *
     * @constant
     * @type {!string}
     */
    RIGHT : 'right',

    /**
     * The name of the {@link Guacamole.Mouse.State} property representing the
     * up mouse button (the fourth mouse button, clicked when the mouse scroll
     * wheel is scrolled up).
     *
     * @constant
     * @type {!string}
     */
    UP : 'up',

    /**
     * The name of the {@link Guacamole.Mouse.State} property representing the
     * down mouse button (the fifth mouse button, clicked when the mouse scroll
     * wheel is scrolled up).
     *
     * @constant
     * @type {!string}
     */
    DOWN : 'down'

};

/**
 * Base event type for all mouse events. The mouse producing the event may be
 * the user's local mouse (as with {@link Guacamole.Mouse}) or an emulated
 * mouse (as with {@link Guacamole.Mouse.Touchpad}).
 *
 * @constructor
 * @augments Guacamole.Event.DOMEvent
 * @param {!string} type
 *     The type name of the event ("mousedown", "mouseup", etc.)
 *
 * @param {!Guacamole.Mouse.State} state
 *     The current mouse state.
 *     
 * @param {Event|Event[]} [events=[]]
 *     The DOM events that are related to this event, if any.
 */
Guacamole.Mouse.Event = function MouseEvent(type, state, events) {

    Guacamole.Event.DOMEvent.call(this, type, events);

    /**
     * The name of the event handler used by the Guacamole JavaScript API for
     * this event prior to the migration to Guacamole.Event.Target.
     *
     * @private
     * @constant
     * @type {!string}
     */
    var legacyHandlerName = 'on' + this.type;

    /**
     * The current mouse state at the time this event was fired.
     *
     * @type {!Guacamole.Mouse.State}
     */
    this.state = state;

    /**
     * @inheritdoc
     */
    this.invokeLegacyHandler = function invokeLegacyHandler(target) {
        if (target[legacyHandlerName]) {

            this.preventDefault();
            this.stopPropagation();

            target[legacyHandlerName](this.state);

        }
    };

};

/**
 * An object which can dispatch {@link Guacamole.Mouse.Event} objects
 * representing mouse events. These mouse events may be produced from an actual
 * mouse device (as with {@link Guacamole.Mouse}), from an emulated mouse
 * device (as with {@link Guacamole.Mouse.Touchpad}, or may be programmatically
 * generated (using functions like [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
 * [press()]{@link Guacamole.Mouse.Event.Target#press}, and
 * [release()]{@link Guacamole.Mouse.Event.Target#release}).
 * 
 * @constructor
 * @augments Guacamole.Event.Target
 */
Guacamole.Mouse.Event.Target = function MouseEventTarget() {

    Guacamole.Event.Target.call(this);

    /**
     * The current mouse state. The properties of this state are updated when
     * mouse events fire. This state object is also passed in as a parameter to
     * the handler of any mouse events.
     *
     * @type {!Guacamole.Mouse.State}
     */
    this.currentState = new Guacamole.Mouse.State();

    /**
     * Fired whenever a mouse button is effectively pressed. Depending on the
     * object dispatching the event, this can be due to a true mouse button
     * press ({@link Guacamole.Mouse}), an emulated mouse button press from a
     * touch gesture ({@link Guacamole.Mouse.Touchpad} and
     * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically
     * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
     * [press()]{@link Guacamole.Mouse.Event.Target#press}, or
     * [click()]{@link Guacamole.Mouse.Event.Target#click}.
     *
     * @event Guacamole.Mouse.Event.Target#mousedown
     * @param {!Guacamole.Mouse.Event} event
     *     The mousedown event that was fired.
     */

    /**
     * Fired whenever a mouse button is effectively released. Depending on the
     * object dispatching the event, this can be due to a true mouse button
     * release ({@link Guacamole.Mouse}), an emulated mouse button release from
     * a touch gesture ({@link Guacamole.Mouse.Touchpad} and
     * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically
     * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
     * [release()]{@link Guacamole.Mouse.Event.Target#release}, or
     * [click()]{@link Guacamole.Mouse.Event.Target#click}.
     *
     * @event Guacamole.Mouse.Event.Target#mouseup
     * @param {!Guacamole.Mouse.Event} event
     *     The mouseup event that was fired.
     */

    /**
     * Fired whenever the mouse pointer is effectively moved. Depending on the
     * object dispatching the event, this can be due to true mouse movement
     * ({@link Guacamole.Mouse}), emulated mouse movement from
     * a touch gesture ({@link Guacamole.Mouse.Touchpad} and
     * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically
     * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
     * or [move()]{@link Guacamole.Mouse.Event.Target#move}.
     *
     * @event Guacamole.Mouse.Event.Target#mousemove
     * @param {!Guacamole.Mouse.Event} event
     *     The mousemove event that was fired.
     */

    /**
     * Fired whenever the mouse pointer leaves the boundaries of the element
     * being monitored for interaction. This will only ever be automatically
     * fired due to movement of an actual mouse device via
     * {@link Guacamole.Mouse} unless programmatically generated through
     * [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
     * or [out()]{@link Guacamole.Mouse.Event.Target#out}.
     *
     * @event Guacamole.Mouse.Event.Target#mouseout
     * @param {!Guacamole.Mouse.Event} event
     *     The mouseout event that was fired.
     */

    /**
     * Presses the given mouse button, if it isn't already pressed. Valid
     * button names are defined by {@link Guacamole.Mouse.State.Buttons} and
     * correspond to the button-related properties of
     * {@link Guacamole.Mouse.State}.
     *
     * @fires Guacamole.Mouse.Event.Target#mousedown
     *
     * @param {!string} button
     *     The name of the mouse button to press, as defined by
     *     {@link Guacamole.Mouse.State.Buttons}.
     *
     * @param {Event|Event[]} [events=[]]
     *     The DOM events that are related to the mouse button press, if any.
     */
    this.press = function press(button, events) {
        if (!this.currentState[button]) {
            this.currentState[button] = true;
            this.dispatch(new Guacamole.Mouse.Event('mousedown', this.currentState, events));
        }
    };

    /**
     * Releases the given mouse button, if it isn't already released. Valid
     * button names are defined by {@link Guacamole.Mouse.State.Buttons} and
     * correspond to the button-related properties of
     * {@link Guacamole.Mouse.State}.
     *
     * @fires Guacamole.Mouse.Event.Target#mouseup
     *
     * @param {!string} button
     *     The name of the mouse button to release, as defined by
     *     {@link Guacamole.Mouse.State.Buttons}.
     *
     * @param {Event|Event[]} [events=[]]
     *     The DOM events related to the mouse button release, if any.
     */
    this.release = function release(button, events) {
        if (this.currentState[button]) {
            this.currentState[button] = false;
            this.dispatch(new Guacamole.Mouse.Event('mouseup', this.currentState, events));
        }
    };

    /**
     * Clicks (presses and releases) the given mouse button. Valid button
     * names are defined by {@link Guacamole.Mouse.State.Buttons} and
     * correspond to the button-related properties of
     * {@link Guacamole.Mouse.State}.
     *
     * @fires Guacamole.Mouse.Event.Target#mousedown
     * @fires Guacamole.Mouse.Event.Target#mouseup
     *
     * @param {!string} button
     *     The name of the mouse button to click, as defined by
     *     {@link Guacamole.Mouse.State.Buttons}.
     *
     * @param {Event|Event[]} [events=[]]
     *     The DOM events related to the click, if any.
     */
    this.click = function click(button, events) {
        this.press(button, events);
        this.release(button, events);
    };

    /**
     * Moves the mouse to the given coordinates.
     *
     * @fires Guacamole.Mouse.Event.Target#mousemove
     *
     * @param {!(Guacamole.Position|object)} position
     *     The new coordinates of the mouse pointer. This object may be a
     *     {@link Guacamole.Position} or any object with "x" and "y"
     *     properties.
     *
     * @param {Event|Event[]} [events=[]]
     *     The DOM events related to the mouse movement, if any.
     */
    this.move = function move(position, events) {

        if (this.currentState.x !== position.x || this.currentState.y !== position.y) {
            this.currentState.x = position.x;
            this.currentState.y = position.y;
            this.dispatch(new Guacamole.Mouse.Event('mousemove', this.currentState, events));
        }

    };

    /**
     * Notifies event listeners that the mouse pointer has left the boundaries
     * of the area being monitored for mouse events.
     *
     * @fires Guacamole.Mouse.Event.Target#mouseout
     *
     * @param {Event|Event[]} [events=[]]
     *     The DOM events related to the mouse leaving the boundaries of the
     *     monitored object, if any.
     */
    this.out = function out(events) {
        this.dispatch(new Guacamole.Mouse.Event('mouseout', this.currentState, events));
    };

    /**
     * Releases all mouse buttons that are currently pressed. If all mouse
     * buttons have already been released, this function has no effect.
     *
     * @fires Guacamole.Mouse.Event.Target#mouseup
     *
     * @param {Event|Event[]} [events=[]]
     *     The DOM event related to all mouse buttons being released, if any.
     */
    this.reset = function reset(events) {
        for (var button in Guacamole.Mouse.State.Buttons) {
            this.release(Guacamole.Mouse.State.Buttons[button], events);
        }
    };

};

/**
 * Provides cross-browser relative touch event translation for a given element.
 * 
 * Touch events are translated into mouse events as if the touches occurred
 * on a touchpad (drag to push the mouse pointer, tap to click).
 * 
 * @example
 * var touchpad = new Guacamole.Mouse.Touchpad(client.getDisplay().getElement());
 *
 * // Emulate a mouse using touchpad-style gestures, forwarding all mouse
 * // interaction over Guacamole connection
 * touchpad.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) {
 *
 *     // Re-show software mouse cursor if possibly hidden by a prior call to
 *     // showCursor(), such as a "mouseout" event handler that hides the
 *     // cursor
 *     client.getDisplay().showCursor(true);
 *
 *     client.sendMouseState(e.state, true);
 *
 * });
 *
 * @constructor
 * @augments Guacamole.Mouse.Event.Target
 * @param {!Element} element
 *     The Element to use to provide touch events.
 */
Guacamole.Mouse.Touchpad = function Touchpad(element) {

    Guacamole.Mouse.Event.Target.call(this);

    /**
     * The "mouseout" event will never be fired by Guacamole.Mouse.Touchpad.
     *
     * @ignore
     * @event Guacamole.Mouse.Touchpad#mouseout
     */

    /**
     * Reference to this Guacamole.Mouse.Touchpad.
     *
     * @private
     * @type {!Guacamole.Mouse.Touchpad}
     */
    var guac_touchpad = this;

    /**
     * The distance a two-finger touch must move per scrollwheel event, in
     * pixels.
     *
     * @type {!number}
     */
    this.scrollThreshold = 20 * (window.devicePixelRatio || 1);

    /**
     * The maximum number of milliseconds to wait for a touch to end for the
     * gesture to be considered a click.
     *
     * @type {!number}
     */
    this.clickTimingThreshold = 250;

    /**
     * The maximum number of pixels to allow a touch to move for the gesture to
     * be considered a click.
     *
     * @type {!number}
     */
    this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1);

    /**
     * The current mouse state. The properties of this state are updated when
     * mouse events fire. This state object is also passed in as a parameter to
     * the handler of any mouse events.
     * 
     * @type {!Guacamole.Mouse.State}
     */
    this.currentState = new Guacamole.Mouse.State();

    var touch_count = 0;
    var last_touch_x = 0;
    var last_touch_y = 0;
    var last_touch_time = 0;
    var pixels_moved = 0;

    var touch_buttons = {
        1: "left",
        2: "right",
        3: "middle"
    };

    var gesture_in_progress = false;
    var click_release_timeout = null;

    element.addEventListener("touchend", function(e) {
        
        e.preventDefault();
            
        // If we're handling a gesture AND this is the last touch
        if (gesture_in_progress && e.touches.length === 0) {
            
            var time = new Date().getTime();

            // Get corresponding mouse button
            var button = touch_buttons[touch_count];

            // If mouse already down, release anad clear timeout
            if (guac_touchpad.currentState[button]) {

                // Fire button up event
                guac_touchpad.release(button, e);

                // Clear timeout, if set
                if (click_release_timeout) {
                    window.clearTimeout(click_release_timeout);
                    click_release_timeout = null;
                }

            }

            // If single tap detected (based on time and distance)
            if (time - last_touch_time <= guac_touchpad.clickTimingThreshold
                    && pixels_moved < guac_touchpad.clickMoveThreshold) {

                // Fire button down event
                guac_touchpad.press(button, e);

                // Delay mouse up - mouse up should be canceled if
                // touchstart within timeout.
                click_release_timeout = window.setTimeout(function() {
                    
                    // Fire button up event
                    guac_touchpad.release(button, e);

                    // Gesture now over
                    gesture_in_progress = false;

                }, guac_touchpad.clickTimingThreshold);

            }

            // If we're not waiting to see if this is a click, stop gesture
            if (!click_release_timeout)
                gesture_in_progress = false;

        }

    }, false);

    element.addEventListener("touchstart", function(e) {

        e.preventDefault();

        // Track number of touches, but no more than three
        touch_count = Math.min(e.touches.length, 3);

        // Clear timeout, if set
        if (click_release_timeout) {
            window.clearTimeout(click_release_timeout);
            click_release_timeout = null;
        }

        // Record initial touch location and time for touch movement
        // and tap gestures
        if (!gesture_in_progress) {

            // Stop mouse events while touching
            gesture_in_progress = true;

            // Record touch location and time
            var starting_touch = e.touches[0];
            last_touch_x = starting_touch.clientX;
            last_touch_y = starting_touch.clientY;
            last_touch_time = new Date().getTime();
            pixels_moved = 0;

        }

    }, false);

    element.addEventListener("touchmove", function(e) {

        e.preventDefault();

        // Get change in touch location
        var touch = e.touches[0];
        var delta_x = touch.clientX - last_touch_x;
        var delta_y = touch.clientY - last_touch_y;

        // Track pixels moved
        pixels_moved += Math.abs(delta_x) + Math.abs(delta_y);

        // If only one touch involved, this is mouse move
        if (touch_count === 1) {

            // Calculate average velocity in Manhatten pixels per millisecond
            var velocity = pixels_moved / (new Date().getTime() - last_touch_time);

            // Scale mouse movement relative to velocity
            var scale = 1 + velocity;

            // Update mouse location
            var position = new Guacamole.Position(guac_touchpad.currentState);
            position.x += delta_x*scale;
            position.y += delta_y*scale;

            // Prevent mouse from leaving screen
            position.x = Math.min(Math.max(0, position.x), element.offsetWidth - 1);
            position.y = Math.min(Math.max(0, position.y), element.offsetHeight - 1);

            // Fire movement event, if defined
            guac_touchpad.move(position, e);

            // Update touch location
            last_touch_x = touch.clientX;
            last_touch_y = touch.clientY;

        }

        // Interpret two-finger swipe as scrollwheel
        else if (touch_count === 2) {

            // If change in location passes threshold for scroll
            if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) {

                // Decide button based on Y movement direction
                var button;
                if (delta_y > 0) button = "down";
                else             button = "up";

                guac_touchpad.click(button, e);

                // Only update touch location after a scroll has been
                // detected
                last_touch_x = touch.clientX;
                last_touch_y = touch.clientY;

            }

        }

    }, false);

};

/**
 * Provides cross-browser absolute touch event translation for a given element.
 *
 * Touch events are translated into mouse events as if the touches occurred
 * on a touchscreen (tapping anywhere on the screen clicks at that point,
 * long-press to right-click).
 *
 * @example
 * var touchscreen = new Guacamole.Mouse.Touchscreen(client.getDisplay().getElement());
 *
 * // Emulate a mouse using touchscreen-style gestures, forwarding all mouse
 * // interaction over Guacamole connection
 * touchscreen.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) {
 *
 *     // Re-show software mouse cursor if possibly hidden by a prior call to
 *     // showCursor(), such as a "mouseout" event handler that hides the
 *     // cursor
 *     client.getDisplay().showCursor(true);
 *
 *     client.sendMouseState(e.state, true);
 *
 * });
 *
 * @constructor
 * @augments Guacamole.Mouse.Event.Target
 * @param {!Element} element
 *     The Element to use to provide touch events.
 */
Guacamole.Mouse.Touchscreen = function Touchscreen(element) {

    Guacamole.Mouse.Event.Target.call(this);

    /**
     * The "mouseout" event will never be fired by Guacamole.Mouse.Touchscreen.
     *
     * @ignore
     * @event Guacamole.Mouse.Touchscreen#mouseout
     */

    /**
     * Reference to this Guacamole.Mouse.Touchscreen.
     *
     * @private
     * @type {!Guacamole.Mouse.Touchscreen}
     */
    var guac_touchscreen = this;

    /**
     * Whether a gesture is known to be in progress. If false, touch events
     * will be ignored.
     *
     * @private
     * @type {!boolean}
     */
    var gesture_in_progress = false;

    /**
     * The start X location of a gesture.
     *
     * @private
     * @type {number}
     */
    var gesture_start_x = null;

    /**
     * The start Y location of a gesture.
     *
     * @private
     * @type {number}
     */
    var gesture_start_y = null;

    /**
     * The timeout associated with the delayed, cancellable click release.
     *
     * @private
     * @type {number}
     */
    var click_release_timeout = null;

    /**
     * The timeout associated with long-press for right click.
     *
     * @private
     * @type {number}
     */
    var long_press_timeout = null;

    /**
     * The distance a two-finger touch must move per scrollwheel event, in
     * pixels.
     *
     * @type {!number}
     */
    this.scrollThreshold = 20 * (window.devicePixelRatio || 1);

    /**
     * The maximum number of milliseconds to wait for a touch to end for the
     * gesture to be considered a click.
     *
     * @type {!number}
     */
    this.clickTimingThreshold = 250;

    /**
     * The maximum number of pixels to allow a touch to move for the gesture to
     * be considered a click.
     *
     * @type {!number}
     */
    this.clickMoveThreshold = 16 * (window.devicePixelRatio || 1);

    /**
     * The amount of time a press must be held for long press to be
     * detected.
     */
    this.longPressThreshold = 500;

    /**
     * Returns whether the given touch event exceeds the movement threshold for
     * clicking, based on where the touch gesture began.
     *
     * @private
     * @param {!TouchEvent} e
     *     The touch event to check.
     *
     * @return {!boolean}
     *     true if the movement threshold is exceeded, false otherwise.
     */
    function finger_moved(e) {
        var touch = e.touches[0] || e.changedTouches[0];
        var delta_x = touch.clientX - gesture_start_x;
        var delta_y = touch.clientY - gesture_start_y;
        return Math.sqrt(delta_x*delta_x + delta_y*delta_y) >= guac_touchscreen.clickMoveThreshold;
    }

    /**
     * Begins a new gesture at the location of the first touch in the given
     * touch event.
     * 
     * @private
     * @param {!TouchEvent} e
     *     The touch event beginning this new gesture.
     */
    function begin_gesture(e) {
        var touch = e.touches[0];
        gesture_in_progress = true;
        gesture_start_x = touch.clientX;
        gesture_start_y = touch.clientY;
    }

    /**
     * End the current gesture entirely. Wait for all touches to be done before
     * resuming gesture detection.
     * 
     * @private
     */
    function end_gesture() {
        window.clearTimeout(click_release_timeout);
        window.clearTimeout(long_press_timeout);
        gesture_in_progress = false;
    }

    element.addEventListener("touchend", function(e) {

        // Do not handle if no gesture
        if (!gesture_in_progress)
            return;

        // Ignore if more than one touch
        if (e.touches.length !== 0 || e.changedTouches.length !== 1) {
            end_gesture();
            return;
        }

        // Long-press, if any, is over
        window.clearTimeout(long_press_timeout);

        // Always release mouse button if pressed
        guac_touchscreen.release(Guacamole.Mouse.State.Buttons.LEFT, e);

        // If finger hasn't moved enough to cancel the click
        if (!finger_moved(e)) {

            e.preventDefault();

            // If not yet pressed, press and start delay release
            if (!guac_touchscreen.currentState.left) {

                var touch = e.changedTouches[0];
                guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY));
                guac_touchscreen.press(Guacamole.Mouse.State.Buttons.LEFT, e);

                // Release button after a delay, if not canceled
                click_release_timeout = window.setTimeout(function() {
                    guac_touchscreen.release(Guacamole.Mouse.State.Buttons.LEFT, e);
                    end_gesture();
                }, guac_touchscreen.clickTimingThreshold);

            }

        } // end if finger not moved

    }, false);

    element.addEventListener("touchstart", function(e) {

        // Ignore if more than one touch
        if (e.touches.length !== 1) {
            end_gesture();
            return;
        }

        e.preventDefault();

        // New touch begins a new gesture
        begin_gesture(e);

        // Keep button pressed if tap after left click
        window.clearTimeout(click_release_timeout);

        // Click right button if this turns into a long-press
        long_press_timeout = window.setTimeout(function() {
            var touch = e.touches[0];
            guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY));
            guac_touchscreen.click(Guacamole.Mouse.State.Buttons.RIGHT, e);
            end_gesture();
        }, guac_touchscreen.longPressThreshold);

    }, false);

    element.addEventListener("touchmove", function(e) {

        // Do not handle if no gesture
        if (!gesture_in_progress)
            return;

        // Cancel long press if finger moved
        if (finger_moved(e))
            window.clearTimeout(long_press_timeout);

        // Ignore if more than one touch
        if (e.touches.length !== 1) {
            end_gesture();
            return;
        }

        // Update mouse position if dragging
        if (guac_touchscreen.currentState.left) {

            e.preventDefault();

            // Update state
            var touch = e.touches[0];
            guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY), e);

        }

    }, false);

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/**
 * The namespace used by the Guacamole JavaScript API. Absolutely all classes
 * defined by the Guacamole JavaScript API will be within this namespace.
 *
 * @namespace
 */
var Guacamole = Guacamole || {};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * An object used by the Guacamole client to house arbitrarily-many named
 * input and output streams.
 * 
 * @constructor
 * @param {!Guacamole.Client} client
 *     The client owning this object.
 *
 * @param {!number} index
 *     The index of this object.
 */
Guacamole.Object = function guacamoleObject(client, index) {

    /**
     * Reference to this Guacamole.Object.
     *
     * @private
     * @type {!Guacamole.Object}
     */
    var guacObject = this;

    /**
     * Map of stream name to corresponding queue of callbacks. The queue of
     * callbacks is guaranteed to be in order of request.
     *
     * @private
     * @type {!Object.<string, function[]>}
     */
    var bodyCallbacks = {};

    /**
     * Removes and returns the callback at the head of the callback queue for
     * the stream having the given name. If no such callbacks exist, null is
     * returned.
     *
     * @private
     * @param {!string} name
     *     The name of the stream to retrieve a callback for.
     *
     * @returns {function}
     *     The next callback associated with the stream having the given name,
     *     or null if no such callback exists.
     */
    var dequeueBodyCallback = function dequeueBodyCallback(name) {

        // If no callbacks defined, simply return null
        var callbacks = bodyCallbacks[name];
        if (!callbacks)
            return null;

        // Otherwise, pull off first callback, deleting the queue if empty
        var callback = callbacks.shift();
        if (callbacks.length === 0)
            delete bodyCallbacks[name];

        // Return found callback
        return callback;

    };

    /**
     * Adds the given callback to the tail of the callback queue for the stream
     * having the given name.
     *
     * @private
     * @param {!string} name
     *     The name of the stream to associate with the given callback.
     *
     * @param {!function} callback
     *     The callback to add to the queue of the stream with the given name.
     */
    var enqueueBodyCallback = function enqueueBodyCallback(name, callback) {

        // Get callback queue by name, creating first if necessary
        var callbacks = bodyCallbacks[name];
        if (!callbacks) {
            callbacks = [];
            bodyCallbacks[name] = callbacks;
        }

        // Add callback to end of queue
        callbacks.push(callback);

    };

    /**
     * The index of this object.
     *
     * @type {!number}
     */
    this.index = index;

    /**
     * Called when this object receives the body of a requested input stream.
     * By default, all objects will invoke the callbacks provided to their
     * requestInputStream() functions based on the name of the stream
     * requested. This behavior can be overridden by specifying a different
     * handler here.
     *
     * @event
     * @param {!Guacamole.InputStream} inputStream
     *     The input stream of the received body.
     *
     * @param {!string} mimetype
     *     The mimetype of the data being received.
     *
     * @param {!string} name
     *     The name of the stream whose body has been received.
     */
    this.onbody = function defaultBodyHandler(inputStream, mimetype, name) {

        // Call queued callback for the received body, if any
        var callback = dequeueBodyCallback(name);
        if (callback)
            callback(inputStream, mimetype);

    };

    /**
     * Called when this object is being undefined. Once undefined, no further
     * communication involving this object may occur.
     * 
     * @event
     */
    this.onundefine = null;

    /**
     * Requests read access to the input stream having the given name. If
     * successful, a new input stream will be created.
     *
     * @param {!string} name
     *     The name of the input stream to request.
     *
     * @param {function} [bodyCallback]
     *     The callback to invoke when the body of the requested input stream
     *     is received. This callback will be provided a Guacamole.InputStream
     *     and its mimetype as its two only arguments. If the onbody handler of
     *     this object is overridden, this callback will not be invoked.
     */
    this.requestInputStream = function requestInputStream(name, bodyCallback) {

        // Queue body callback if provided
        if (bodyCallback)
            enqueueBodyCallback(name, bodyCallback);

        // Send request for input stream
        client.requestObjectInputStream(guacObject.index, name);

    };

    /**
     * Creates a new output stream associated with this object and having the
     * given mimetype and name. The legality of a mimetype and name is dictated
     * by the object itself.
     *
     * @param {!string} mimetype
     *     The mimetype of the data which will be sent to the output stream.
     *
     * @param {!string} name
     *     The defined name of an output stream within this object.
     *
     * @returns {!Guacamole.OutputStream}
     *     An output stream which will write blobs to the named output stream
     *     of this object.
     */
    this.createOutputStream = function createOutputStream(mimetype, name) {
        return client.createObjectOutputStream(guacObject.index, mimetype, name);
    };

};

/**
 * The reserved name denoting the root stream of any object. The contents of
 * the root stream MUST be a JSON map of stream name to mimetype.
 *
 * @constant
 * @type {!string}
 */
Guacamole.Object.ROOT_STREAM = '/';

/**
 * The mimetype of a stream containing JSON which maps available stream names
 * to their corresponding mimetype. The root stream of a Guacamole.Object MUST
 * have this mimetype.
 *
 * @constant
 * @type {!string}
 */
Guacamole.Object.STREAM_INDEX_MIMETYPE = 'application/vnd.glyptodon.guacamole.stream-index+json';

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Dynamic on-screen keyboard. Given the layout object for an on-screen
 * keyboard, this object will construct a clickable on-screen keyboard with its
 * own key events.
 *
 * @constructor
 * @param {!Guacamole.OnScreenKeyboard.Layout} layout
 *     The layout of the on-screen keyboard to display.
 */
Guacamole.OnScreenKeyboard = function(layout) {

    /**
     * Reference to this Guacamole.OnScreenKeyboard.
     *
     * @private
     * @type {!Guacamole.OnScreenKeyboard}
     */
    var osk = this;

    /**
     * Map of currently-set modifiers to the keysym associated with their
     * original press. When the modifier is cleared, this keysym must be
     * released.
     *
     * @private
     * @type {!Object.<String, Number>}
     */
    var modifierKeysyms = {};

    /**
     * Map of all key names to their current pressed states. If a key is not
     * pressed, it may not be in this map at all, but all pressed keys will
     * have a corresponding mapping to true.
     *
     * @private
     * @type {!Object.<String, Boolean>}
     */
    var pressed = {};

    /**
     * All scalable elements which are part of the on-screen keyboard. Each
     * scalable element is carefully controlled to ensure the interface layout
     * and sizing remains constant, even on browsers that would otherwise
     * experience rounding error due to unit conversions.
     *
     * @private
     * @type {!ScaledElement[]}
     */
    var scaledElements = [];

    /**
     * Adds a CSS class to an element.
     * 
     * @private
     * @function
     * @param {!Element} element
     *     The element to add a class to.
     *
     * @param {!string} classname
     *     The name of the class to add.
     */
    var addClass = function addClass(element, classname) {

        // If classList supported, use that
        if (element.classList)
            element.classList.add(classname);

        // Otherwise, simply append the class
        else
            element.className += " " + classname;

    };

    /**
     * Removes a CSS class from an element.
     * 
     * @private
     * @function
     * @param {!Element} element
     *     The element to remove a class from.
     *
     * @param {!string} classname
     *     The name of the class to remove.
     */
    var removeClass = function removeClass(element, classname) {

        // If classList supported, use that
        if (element.classList)
            element.classList.remove(classname);

        // Otherwise, manually filter out classes with given name
        else {
            element.className = element.className.replace(/([^ ]+)[ ]*/g,
                function removeMatchingClasses(match, testClassname) {

                    // If same class, remove
                    if (testClassname === classname)
                        return "";

                    // Otherwise, allow
                    return match;
                    
                }
            );
        }

    };

    /**
     * Counter of mouse events to ignore. This decremented by mousemove, and
     * while non-zero, mouse events will have no effect.
     *
     * @private
     * @type {!number}
     */
    var ignoreMouse = 0;

    /**
     * Ignores all pending mouse events when touch events are the apparent
     * source. Mouse events are ignored until at least touchMouseThreshold
     * mouse events occur without corresponding touch events.
     *
     * @private
     */
    var ignorePendingMouseEvents = function ignorePendingMouseEvents() {
        ignoreMouse = osk.touchMouseThreshold;
    };

    /**
     * An element whose dimensions are maintained according to an arbitrary
     * scale. The conversion factor for these arbitrary units to pixels is
     * provided later via a call to scale().
     *
     * @private
     * @constructor
     * @param {!Element} element
     *     The element whose scale should be maintained.
     *
     * @param {!number} width
     *     The width of the element, in arbitrary units, relative to other
     *     ScaledElements.
     *
     * @param {!number} height
     *     The height of the element, in arbitrary units, relative to other
     *     ScaledElements.
     *     
     * @param {boolean} [scaleFont=false]
     *     Whether the line height and font size should be scaled as well.
     */
    var ScaledElement = function ScaledElement(element, width, height, scaleFont) {

        /**
         * The width of this ScaledElement, in arbitrary units, relative to
         * other ScaledElements.
         *
         * @type {!number}
         */
         this.width = width;

        /**
         * The height of this ScaledElement, in arbitrary units, relative to
         * other ScaledElements.
         *
         * @type {!number}
         */
         this.height = height;
 
        /**
         * Resizes the associated element, updating its dimensions according to
         * the given pixels per unit.
         *
         * @param {!number} pixels
         *     The number of pixels to assign per arbitrary unit.
         */
        this.scale = function(pixels) {

            // Scale element width/height
            element.style.width  = (width  * pixels) + "px";
            element.style.height = (height * pixels) + "px";

            // Scale font, if requested
            if (scaleFont) {
                element.style.lineHeight = (height * pixels) + "px";
                element.style.fontSize   = pixels + "px";
            }

        };

    };

    /**
     * Returns whether all modifiers having the given names are currently
     * active.
     *
     * @private
     * @param {!string[]} names
     *     The names of all modifiers to test.
     *
     * @returns {!boolean}
     *     true if all specified modifiers are pressed, false otherwise.
     */
    var modifiersPressed = function modifiersPressed(names) {

        // If any required modifiers are not pressed, return false
        for (var i=0; i < names.length; i++) {

            // Test whether current modifier is pressed
            var name = names[i];
            if (!(name in modifierKeysyms))
                return false;

        }

        // Otherwise, all required modifiers are pressed
        return true;

    };

    /**
     * Returns the single matching Key object associated with the key of the
     * given name, where that Key object's requirements (such as pressed
     * modifiers) are all currently satisfied.
     *
     * @private
     * @param {!string} keyName
     *     The name of the key to retrieve.
     *
     * @returns {Guacamole.OnScreenKeyboard.Key}
     *     The Key object associated with the given name, where that object's
     *     requirements are all currently satisfied, or null if no such Key
     *     can be found.
     */
    var getActiveKey = function getActiveKey(keyName) {

        // Get key array for given name
        var keys = osk.keys[keyName];
        if (!keys)
            return null;

        // Find last matching key
        for (var i = keys.length - 1; i >= 0; i--) {

            // Get candidate key
            var candidate = keys[i];

            // If all required modifiers are pressed, use that key
            if (modifiersPressed(candidate.requires))
                return candidate;

        }

        // No valid key
        return null;

    };

    /**
     * Presses the key having the given name, updating the associated key
     * element with the "guac-keyboard-pressed" CSS class. If the key is
     * already pressed, this function has no effect.
     *
     * @private
     * @param {!string} keyName
     *     The name of the key to press.
     *
     * @param {!string} keyElement
     *     The element associated with the given key.
     */
    var press = function press(keyName, keyElement) {

        // Press key if not yet pressed
        if (!pressed[keyName]) {

            addClass(keyElement, "guac-keyboard-pressed");

            // Get current key based on modifier state
            var key = getActiveKey(keyName);

            // Update modifier state
            if (key.modifier) {

                // Construct classname for modifier
                var modifierClass = "guac-keyboard-modifier-" + getCSSName(key.modifier);

                // Retrieve originally-pressed keysym, if modifier was already pressed
                var originalKeysym = modifierKeysyms[key.modifier];

                // Activate modifier if not pressed
                if (originalKeysym === undefined) {
                    
                    addClass(keyboard, modifierClass);
                    modifierKeysyms[key.modifier] = key.keysym;
                    
                    // Send key event only if keysym is meaningful
                    if (key.keysym && osk.onkeydown)
                        osk.onkeydown(key.keysym);

                }

                // Deactivate if not pressed
                else {

                    removeClass(keyboard, modifierClass);
                    delete modifierKeysyms[key.modifier];
                    
                    // Send key event only if original keysym is meaningful
                    if (originalKeysym && osk.onkeyup)
                        osk.onkeyup(originalKeysym);

                }

            }

            // If not modifier, send key event now
            else if (osk.onkeydown)
                osk.onkeydown(key.keysym);

            // Mark key as pressed
            pressed[keyName] = true;

        }

    };

    /**
     * Releases the key having the given name, removing the
     * "guac-keyboard-pressed" CSS class from the associated element. If the
     * key is already released, this function has no effect.
     *
     * @private
     * @param {!string} keyName
     *     The name of the key to release.
     *
     * @param {!string} keyElement
     *     The element associated with the given key.
     */
    var release = function release(keyName, keyElement) {

        // Release key if currently pressed
        if (pressed[keyName]) {

            removeClass(keyElement, "guac-keyboard-pressed");

            // Get current key based on modifier state
            var key = getActiveKey(keyName);

            // Send key event if not a modifier key
            if (!key.modifier && osk.onkeyup)
                osk.onkeyup(key.keysym);

            // Mark key as released
            pressed[keyName] = false;

        }

    };

    // Create keyboard
    var keyboard = document.createElement("div");
    keyboard.className = "guac-keyboard";

    // Do not allow selection or mouse movement to propagate/register.
    keyboard.onselectstart =
    keyboard.onmousemove   =
    keyboard.onmouseup     =
    keyboard.onmousedown   = function handleMouseEvents(e) {

        // If ignoring events, decrement counter
        if (ignoreMouse)
            ignoreMouse--;

        e.stopPropagation();
        return false;

    };

    /**
     * The number of mousemove events to require before re-enabling mouse
     * event handling after receiving a touch event.
     *
     * @type {!number}
     */
    this.touchMouseThreshold = 3;

    /**
     * Fired whenever the user presses a key on this Guacamole.OnScreenKeyboard.
     * 
     * @event
     * @param {!number} keysym
     *     The keysym of the key being pressed.
     */
    this.onkeydown = null;

    /**
     * Fired whenever the user releases a key on this Guacamole.OnScreenKeyboard.
     * 
     * @event
     * @param {!number} keysym
     *     The keysym of the key being released.
     */
    this.onkeyup = null;

    /**
     * The keyboard layout provided at time of construction.
     *
     * @type {!Guacamole.OnScreenKeyboard.Layout}
     */
    this.layout = new Guacamole.OnScreenKeyboard.Layout(layout);

    /**
     * Returns the element containing the entire on-screen keyboard.
     *
     * @returns {!Element}
     *     The element containing the entire on-screen keyboard.
     */
    this.getElement = function() {
        return keyboard;
    };

    /**
     * Resizes all elements within this Guacamole.OnScreenKeyboard such that
     * the width is close to but does not exceed the specified width. The
     * height of the keyboard is determined based on the width.
     * 
     * @param {!number} width
     *     The width to resize this Guacamole.OnScreenKeyboard to, in pixels.
     */
    this.resize = function(width) {

        // Get pixel size of a unit
        var unit = Math.floor(width * 10 / osk.layout.width) / 10;

        // Resize all scaled elements
        for (var i=0; i<scaledElements.length; i++) {
            var scaledElement = scaledElements[i];
            scaledElement.scale(unit);
        }

    };

    /**
     * Given the name of a key and its corresponding definition, which may be
     * an array of keys objects, a number (keysym), a string (key title), or a
     * single key object, returns an array of key objects, deriving any missing
     * properties as needed, and ensuring the key name is defined.
     *
     * @private
     * @param {!string} name
     *     The name of the key being coerced into an array of Key objects.
     *
     * @param {!(number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[])} object
     *     The object defining the behavior of the key having the given name,
     *     which may be the title of the key (a string), the keysym (a number),
     *     a single Key object, or an array of Key objects.
     *     
     * @returns {!Guacamole.OnScreenKeyboard.Key[]}
     *     An array of all keys associated with the given name.
     */
    var asKeyArray = function asKeyArray(name, object) {

        // If already an array, just coerce into a true Key[] 
        if (object instanceof Array) {
            var keys = [];
            for (var i=0; i < object.length; i++) {
                keys.push(new Guacamole.OnScreenKeyboard.Key(object[i], name));
            }
            return keys;
        }

        // Derive key object from keysym if that's all we have
        if (typeof object === 'number') {
            return [new Guacamole.OnScreenKeyboard.Key({
                name   : name,
                keysym : object
            })];
        }

        // Derive key object from title if that's all we have
        if (typeof object === 'string') {
            return [new Guacamole.OnScreenKeyboard.Key({
                name  : name,
                title : object
            })];
        }

        // Otherwise, assume it's already a key object, just not an array
        return [new Guacamole.OnScreenKeyboard.Key(object, name)];

    };

    /**
     * Converts the rather forgiving key mapping allowed by
     * Guacamole.OnScreenKeyboard.Layout into a rigorous mapping of key name
     * to key definition, where the key definition is always an array of Key
     * objects.
     *
     * @private
     * @param {!Object.<string, number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[]>} keys
     *     A mapping of key name to key definition, where the key definition is
     *     the title of the key (a string), the keysym (a number), a single
     *     Key object, or an array of Key objects.
     *
     * @returns {!Object.<string, Guacamole.OnScreenKeyboard.Key[]>}
     *     A more-predictable mapping of key name to key definition, where the
     *     key definition is always simply an array of Key objects.
     */
    var getKeys = function getKeys(keys) {

        var keyArrays = {};

        // Coerce all keys into individual key arrays
        for (var name in layout.keys) {
            keyArrays[name] = asKeyArray(name, keys[name]);
        }

        return keyArrays;

    };

    /**
     * Map of all key names to their corresponding set of keys. Each key name
     * may correspond to multiple keys due to the effect of modifiers.
     *
     * @type {!Object.<string, Guacamole.OnScreenKeyboard.Key[]>}
     */
    this.keys = getKeys(layout.keys);

    /**
     * Given an arbitrary string representing the name of some component of the
     * on-screen keyboard, returns a string formatted for use as a CSS class
     * name. The result will be lowercase. Word boundaries previously denoted
     * by CamelCase will be replaced by individual hyphens, as will all
     * contiguous non-alphanumeric characters.
     *
     * @private
     * @param {!string} name
     *     An arbitrary string representing the name of some component of the
     *     on-screen keyboard.
     *
     * @returns {!string}
     *     A string formatted for use as a CSS class name.
     */
    var getCSSName = function getCSSName(name) {

        // Convert name from possibly-CamelCase to hyphenated lowercase
        var cssName = name
               .replace(/([a-z])([A-Z])/g, '$1-$2')
               .replace(/[^A-Za-z0-9]+/g, '-')
               .toLowerCase();

        return cssName;

    };

    /**
     * Appends DOM elements to the given element as dictated by the layout
     * structure object provided. If a name is provided, an additional CSS
     * class, prepended with "guac-keyboard-", will be added to the top-level
     * element.
     * 
     * If the layout structure object is an array, all elements within that
     * array will be recursively appended as children of a group, and the
     * top-level element will be given the CSS class "guac-keyboard-group".
     *
     * If the layout structure object is an object, all properties within that
     * object will be recursively appended as children of a group, and the
     * top-level element will be given the CSS class "guac-keyboard-group". The
     * name of each property will be applied as the name of each child object
     * for the sake of CSS. Each property will be added in sorted order.
     *
     * If the layout structure object is a string, the key having that name
     * will be appended. The key will be given the CSS class
     * "guac-keyboard-key" and "guac-keyboard-key-NAME", where NAME is the name
     * of the key. If the name of the key is a single character, this will
     * first be transformed into the C-style hexadecimal literal for the
     * Unicode codepoint of that character. For example, the key "A" would
     * become "guac-keyboard-key-0x41".
     * 
     * If the layout structure object is a number, a gap of that size will be
     * inserted. The gap will be given the CSS class "guac-keyboard-gap", and
     * will be scaled according to the same size units as each key.
     *
     * @private
     * @param {!Element} element
     *     The element to append elements to.
     *
     * @param {!(Array|object|string|number)} object
     *     The layout structure object to use when constructing the elements to
     *     append.
     *
     * @param {string} [name]
     *     The name of the top-level element being appended, if any.
     */
    var appendElements = function appendElements(element, object, name) {

        var i;

        // Create div which will become the group or key
        var div = document.createElement('div');

        // Add class based on name, if name given
        if (name)
            addClass(div, 'guac-keyboard-' + getCSSName(name));

        // If an array, append each element
        if (object instanceof Array) {

            // Add group class
            addClass(div, 'guac-keyboard-group');

            // Append all elements of array
            for (i=0; i < object.length; i++)
                appendElements(div, object[i]);

        }

        // If an object, append each property value
        else if (object instanceof Object) {

            // Add group class
            addClass(div, 'guac-keyboard-group');

            // Append all children, sorted by name
            var names = Object.keys(object).sort();
            for (i=0; i < names.length; i++) {
                var name = names[i];
                appendElements(div, object[name], name);
            }

        }

        // If a number, create as a gap 
        else if (typeof object === 'number') {

            // Add gap class
            addClass(div, 'guac-keyboard-gap');

            // Maintain scale
            scaledElements.push(new ScaledElement(div, object, object));

        }

        // If a string, create as a key
        else if (typeof object === 'string') {

            // If key name is only one character, use codepoint for name
            var keyName = object;
            if (keyName.length === 1)
                keyName = '0x' + keyName.charCodeAt(0).toString(16);

            // Add key container class
            addClass(div, 'guac-keyboard-key-container');

            // Create key element which will contain all possible caps
            var keyElement = document.createElement('div');
            keyElement.className = 'guac-keyboard-key '
                                 + 'guac-keyboard-key-' + getCSSName(keyName);

            // Add all associated keys as caps within DOM
            var keys = osk.keys[object];
            if (keys) {
                for (i=0; i < keys.length; i++) {

                    // Get current key
                    var key = keys[i];

                    // Create cap element for key
                    var capElement = document.createElement('div');
                    capElement.className   = 'guac-keyboard-cap';
                    capElement.textContent = key.title;

                    // Add classes for any requirements
                    for (var j=0; j < key.requires.length; j++) {
                        var requirement = key.requires[j];
                        addClass(capElement, 'guac-keyboard-requires-' + getCSSName(requirement));
                        addClass(keyElement, 'guac-keyboard-uses-'     + getCSSName(requirement));
                    }

                    // Add cap to key within DOM
                    keyElement.appendChild(capElement);

                }
            }

            // Add key to DOM, maintain scale
            div.appendChild(keyElement);
            scaledElements.push(new ScaledElement(div, osk.layout.keyWidths[object] || 1, 1, true));

            /**
             * Handles a touch event which results in the pressing of an OSK
             * key. Touch events will result in mouse events being ignored for
             * touchMouseThreshold events.
             *
             * @private
             * @param {!TouchEvent} e
             *     The touch event being handled.
             */
            var touchPress = function touchPress(e) {
                e.preventDefault();
                ignoreMouse = osk.touchMouseThreshold;
                press(object, keyElement);
            };

            /**
             * Handles a touch event which results in the release of an OSK
             * key. Touch events will result in mouse events being ignored for
             * touchMouseThreshold events.
             *
             * @private
             * @param {!TouchEvent} e
             *     The touch event being handled.
             */
            var touchRelease = function touchRelease(e) {
                e.preventDefault();
                ignoreMouse = osk.touchMouseThreshold;
                release(object, keyElement);
            };

            /**
             * Handles a mouse event which results in the pressing of an OSK
             * key. If mouse events are currently being ignored, this handler
             * does nothing.
             *
             * @private
             * @param {!MouseEvent} e
             *     The touch event being handled.
             */
            var mousePress = function mousePress(e) {
                e.preventDefault();
                if (ignoreMouse === 0)
                    press(object, keyElement);
            };

            /**
             * Handles a mouse event which results in the release of an OSK
             * key. If mouse events are currently being ignored, this handler
             * does nothing.
             *
             * @private
             * @param {!MouseEvent} e
             *     The touch event being handled.
             */
            var mouseRelease = function mouseRelease(e) {
                e.preventDefault();
                if (ignoreMouse === 0)
                    release(object, keyElement);
            };

            // Handle touch events on key
            keyElement.addEventListener("touchstart", touchPress,   true);
            keyElement.addEventListener("touchend",   touchRelease, true);

            // Handle mouse events on key
            keyElement.addEventListener("mousedown", mousePress,   true);
            keyElement.addEventListener("mouseup",   mouseRelease, true);
            keyElement.addEventListener("mouseout",  mouseRelease, true);

        } // end if object is key name

        // Add newly-created group/key
        element.appendChild(div);

    };

    // Create keyboard layout in DOM
    appendElements(keyboard, layout.layout);

};

/**
 * Represents an entire on-screen keyboard layout, including all available
 * keys, their behaviors, and their relative position and sizing.
 *
 * @constructor
 * @param {!(Guacamole.OnScreenKeyboard.Layout|object)} template
 *     The object whose identically-named properties will be used to initialize
 *     the properties of this layout.
 */
Guacamole.OnScreenKeyboard.Layout = function(template) {

    /**
     * The language of keyboard layout, such as "en_US". This property is for
     * informational purposes only, but it is recommend to conform to the
     * [language code]_[country code] format.
     *
     * @type {!string}
     */
    this.language = template.language;

    /**
     * The type of keyboard layout, such as "qwerty". This property is for
     * informational purposes only, and does not conform to any standard.
     *
     * @type {!string}
     */
    this.type = template.type;

    /**
     * Map of key name to corresponding keysym, title, or key object. If only
     * the keysym or title is provided, the key object will be created
     * implicitly. In all cases, the name property of the key object will be
     * taken from the name given in the mapping.
     *
     * @type {!Object.<string, number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[]>}
     */
    this.keys = template.keys;

    /**
     * Arbitrarily nested, arbitrarily grouped key names. The contents of the
     * layout will be traversed to produce an identically-nested grouping of
     * keys in the DOM tree. All strings will be transformed into their
     * corresponding sets of keys, while all objects and arrays will be
     * transformed into named groups and anonymous groups respectively. Any
     * numbers present will be transformed into gaps of that size, scaled
     * according to the same units as each key.
     *
     * @type {!object}
     */
    this.layout = template.layout;

    /**
     * The width of the entire keyboard, in arbitrary units. The width of each
     * key is relative to this width, as both width values are assumed to be in
     * the same units. The conversion factor between these units and pixels is
     * derived later via a call to resize() on the Guacamole.OnScreenKeyboard.
     *
     * @type {!number}
     */
    this.width = template.width;

    /**
     * The width of each key, in arbitrary units, relative to other keys in
     * this layout. The true pixel size of each key will be determined by the
     * overall size of the keyboard. If not defined here, the width of each
     * key will default to 1.
     *
     * @type {!Object.<string, number>}
     */
    this.keyWidths = template.keyWidths || {};

};

/**
 * Represents a single key, or a single possible behavior of a key. Each key
 * on the on-screen keyboard must have at least one associated
 * Guacamole.OnScreenKeyboard.Key, whether that key is explicitly defined or
 * implied, and may have multiple Guacamole.OnScreenKeyboard.Key if behavior
 * depends on modifier states.
 *
 * @constructor
 * @param {!(Guacamole.OnScreenKeyboard.Key|object)} template
 *     The object whose identically-named properties will be used to initialize
 *     the properties of this key.
 *     
 * @param {string} [name]
 *     The name to use instead of any name provided within the template, if
 *     any. If omitted, the name within the template will be used, assuming the
 *     template contains a name.
 */
Guacamole.OnScreenKeyboard.Key = function(template, name) {

    /**
     * The unique name identifying this key within the keyboard layout.
     *
     * @type {!string}
     */
    this.name = name || template.name;

    /**
     * The human-readable title that will be displayed to the user within the
     * key. If not provided, this will be derived from the key name.
     *
     * @type {!string}
     */
    this.title = template.title || this.name;

    /**
     * The keysym to be pressed/released when this key is pressed/released. If
     * not provided, this will be derived from the title if the title is a
     * single character.
     *
     * @type {number}
     */
    this.keysym = template.keysym || (function deriveKeysym(title) {

        // Do not derive keysym if title is not exactly one character
        if (!title || title.length !== 1)
            return null;

        // For characters between U+0000 and U+00FF, the keysym is the codepoint
        var charCode = title.charCodeAt(0);
        if (charCode >= 0x0000 && charCode <= 0x00FF)
            return charCode;

        // For characters between U+0100 and U+10FFFF, the keysym is the codepoint or'd with 0x01000000
        if (charCode >= 0x0100 && charCode <= 0x10FFFF)
            return 0x01000000 | charCode;

        // Unable to derive keysym
        return null;

    })(this.title);

    /**
     * The name of the modifier set when the key is pressed and cleared when
     * this key is released, if any. The names of modifiers are distinct from
     * the names of keys; both the "RightShift" and "LeftShift" keys may set
     * the "shift" modifier, for example. By default, the key will affect no
     * modifiers.
     * 
     * @type {string}
     */
    this.modifier = template.modifier;

    /**
     * An array containing the names of each modifier required for this key to
     * have an effect. For example, a lowercase letter may require nothing,
     * while an uppercase letter would require "shift", assuming the Shift key
     * is named "shift" within the layout. By default, the key will require
     * no modifiers.
     *
     * @type {!string[]}
     */
    this.requires = template.requires || [];

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Abstract stream which can receive data.
 * 
 * @constructor
 * @param {!Guacamole.Client} client
 *     The client owning this stream.
 *
 * @param {!number} index
 *     The index of this stream.
 */
Guacamole.OutputStream = function(client, index) {

    /**
     * Reference to this stream.
     *
     * @private
     * @type {!Guacamole.OutputStream}
     */
    var guac_stream = this;

    /**
     * The index of this stream.
     * @type {!number}
     */
    this.index = index;

    /**
     * Fired whenever an acknowledgement is received from the server, indicating
     * that a stream operation has completed, or an error has occurred.
     * 
     * @event
     * @param {!Guacamole.Status} status
     *     The status of the operation.
     */
    this.onack = null;

    /**
     * Writes the given base64-encoded data to this stream as a blob.
     * 
     * @param {!string} data
     *     The base64-encoded data to send.
     */
    this.sendBlob = function(data) {
        client.sendBlob(guac_stream.index, data);
    };

    /**
     * Closes this stream.
     */
    this.sendEnd = function() {
        client.endStream(guac_stream.index);
    };

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Simple Guacamole protocol parser that invokes an oninstruction event when
 * full instructions are available from data received via receive().
 *
 * @constructor
 */
Guacamole.Parser = function Parser() {

    /**
     * Reference to this parser.
     *
     * @private
     * @type {!Guacamole.Parser}
     */
    var parser = this;

    /**
     * Current buffer of received data. This buffer grows until a full
     * element is available. After a full element is available, that element
     * is flushed into the element buffer.
     *
     * @private
     * @type {!string}
     */
    var buffer = '';

    /**
     * Buffer of all received, complete elements. After an entire instruction
     * is read, this buffer is flushed, and a new instruction begins.
     *
     * @private
     * @type {!string[]}
     */
    var elementBuffer = [];

    /**
     * The character offset within the buffer of the current or most recently
     * parsed element's terminator. If sufficient characters have not yet been
     * read via calls to receive(), this may point to an offset well beyond the
     * end of the buffer. If no characters for an element have yet been read,
     * this will be -1.
     *
     * @private
     * @type {!number}
     */
    var elementEnd = -1;

    /**
     * The character offset within the buffer of the location that the parser
     * should start looking for the next element length search or next element
     * value.
     *
     * @private
     * @type {!number}
     */
    var startIndex = 0;

    /**
     * The declared length of the current element being parsed, in Unicode
     * codepoints.
     *
     * @private
     * @type {!number}
     */
    var elementCodepoints = 0;

    /**
     * The number of parsed characters that must accumulate in the begining of
     * the parse buffer before processing time is expended to truncate that
     * buffer and conserve memory.
     *
     * @private
     * @constant
     * @type {!number}
     */
    var BUFFER_TRUNCATION_THRESHOLD = 4096;

    /**
     * The lowest Unicode codepoint to require a surrogate pair when encoded
     * with UTF-16. In UTF-16, characters with codepoints at or above this
     * value are represented with a surrogate pair, while characters with
     * codepoints below this value are represented with a single character.
     *
     * @private
     * @constant
     * @type {!number}
     */
    var MIN_CODEPOINT_REQUIRES_SURROGATE = 0x10000;

    /**
     * Appends the given instruction data packet to the internal buffer of
     * this Guacamole.Parser, executing all completed instructions at
     * the beginning of this buffer, if any.
     *
     * @param {!string} packet
     *     The instruction data to receive.
     *
     * @param {!boolean} [isBuffer=false]
     *     Whether the provided data should be treated as an instruction buffer
     *     that grows continuously. If true, the data provided to receive()
     *     MUST always start with the data provided to the previous call. If
     *     false (the default), only the new data should be provided to
     *     receive(), and previously-received data will automatically be
     *     buffered by the parser as needed.
     */
    this.receive = function receive(packet, isBuffer) {

        if (isBuffer)
            buffer = packet;

        else {

            // Truncate buffer as necessary
            if (startIndex > BUFFER_TRUNCATION_THRESHOLD && elementEnd >= startIndex) {

                buffer = buffer.substring(startIndex);

                // Reset parse relative to truncation
                elementEnd -= startIndex;
                startIndex = 0;

            }

            // Append data to buffer ONLY if there is outstanding data present. It
            // is otherwise much faster to simply parse the received buffer as-is,
            // and tunnel implementations can take advantage of this by preferring
            // to send only complete instructions. Both the HTTP and WebSocket
            // tunnel implementations included with Guacamole already do this.
            if (buffer.length)
                buffer += packet;
            else
                buffer = packet;

        }

        // While search is within currently received data
        while (elementEnd < buffer.length) {

            // If we are waiting for element data
            if (elementEnd >= startIndex) {

                // If we have enough data in the buffer to fill the element
                // value, but the number of codepoints in the expected substring
                // containing the element value value is less that its declared
                // length, that can only be because the element contains
                // characters split between high and low surrogates, and the
                // actual end of the element value is further out. The minimum
                // number of additional characters that must be read to satisfy
                // the declared length is simply the difference between the
                // number of codepoints actually present vs. the expected
                // length.
                var codepoints = Guacamole.Parser.codePointCount(buffer, startIndex, elementEnd);
                if (codepoints < elementCodepoints) {
                    elementEnd += elementCodepoints - codepoints;
                    continue;
                }

                // If the current element ends with a character involving both
                // a high and low surrogate, elementEnd points to the low
                // surrogate and NOT the element terminator. We must shift the
                // end and reevaluate.
                else if (elementCodepoints && buffer.codePointAt(elementEnd - 1) >= MIN_CODEPOINT_REQUIRES_SURROGATE) {
                    elementEnd++;
                    continue;
                }

                // We now have enough data for the element. Parse.
                var element = buffer.substring(startIndex, elementEnd);
                var terminator = buffer.substring(elementEnd, elementEnd + 1);

                // Add element to array
                elementBuffer.push(element);

                // If last element, handle instruction
                if (terminator === ';') {

                    // Get opcode
                    var opcode = elementBuffer.shift();

                    // Call instruction handler.
                    if (parser.oninstruction !== null)
                        parser.oninstruction(opcode, elementBuffer);

                    // Clear elements
                    elementBuffer = [];

                    // Immediately truncate buffer if its contents have been
                    // completely parsed, so that the next call to receive()
                    // need not append to the buffer unnecessarily
                    if (elementEnd + 1 === buffer.length) {
                        elementEnd = -1;
                        buffer = '';
                    }

                }
                else if (terminator !== ',')
                    throw new Error('Element terminator of instruction was not ";" nor ",".');

                // Start searching for length at character after
                // element terminator
                startIndex = elementEnd + 1;

            }

            // Search for end of length
            var lengthEnd = buffer.indexOf('.', startIndex);
            if (lengthEnd !== -1) {

                // Parse length
                elementCodepoints = parseInt(buffer.substring(elementEnd + 1, lengthEnd));
                if (isNaN(elementCodepoints))
                    throw new Error('Non-numeric character in element length.');

                // Calculate start of element
                startIndex = lengthEnd + 1;

                // Calculate location of element terminator
                elementEnd = startIndex + elementCodepoints;

            }

            // If no period yet, continue search when more data
            // is received
            else {
                startIndex = buffer.length;
                break;
            }

        } // end parse loop

    };

    /**
     * Fired once for every complete Guacamole instruction received, in order.
     *
     * @event
     * @param {!string} opcode
     *     The Guacamole instruction opcode.
     *
     * @param {!string[]} parameters
     *     The parameters provided for the instruction, if any.
     */
    this.oninstruction = null;

};

/**
 * Returns the number of Unicode codepoints (not code units) within the given
 * string. If character offsets are provided, only codepoints between those
 * offsets are counted. Unlike the length property of a string, this function
 * counts proper surrogate pairs as a single codepoint. High and low surrogate
 * characters that are not part of a proper surrogate pair are counted
 * separately as individual codepoints.
 *
 * @param {!string} str
 *     The string whose contents should be inspected.
 *
 * @param {number} [start=0]
 *     The index of the location in the given string where codepoint counting
 *     should start. If omitted, counting will begin at the start of the
 *     string.
 *
 * @param {number} [end]
 *     The index of the first location in the given string after where counting
 *     should stop (the character after the last character being counted). If
 *     omitted, all characters after the start location will be counted.
 *
 * @returns {!number}
 *     The number of Unicode codepoints within the requested portion of the
 *     given string.
 */
Guacamole.Parser.codePointCount = function codePointCount(str, start, end) {

    // Count only characters within the specified region
    str = str.substring(start || 0, end);

    // Locate each proper Unicode surrogate pair (one high surrogate followed
    // by one low surrogate)
    var surrogatePairs = str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);

    // Each surrogate pair represents a single codepoint but is represented by
    // two characters in a JavaScript string, and thus is counted twice toward
    // string length. Subtracting the number of surrogate pairs adjusts that
    // length value such that it gives us the number of codepoints.
    return str.length - (surrogatePairs ? surrogatePairs.length : 0);

};

/**
 * Converts each of the values within the given array to strings, formatting
 * those strings as length-prefixed elements of a complete Guacamole
 * instruction.
 *
 * @param {!Array.<*>} elements
 *     The values that should be encoded as the elements of a Guacamole
 *     instruction. Order of these elements is preserved. This array MUST have
 *     at least one element.
 *
 * @returns {!string}
 *     A complete Guacamole instruction consisting of each of the provided
 *     element values, in order.
 */
Guacamole.Parser.toInstruction = function toInstruction(elements) {

    /**
     * Converts the given value to a length/string pair for use as an
     * element in a Guacamole instruction.
     *
     * @private
     * @param {*} value
     *     The value to convert.
     *
     * @return {!string}
     *     The converted value.
     */
    var toElement = function toElement(value) {
        var str = '' + value;
        return Guacamole.Parser.codePointCount(str) + "." + str;
    };

    var instr = toElement(elements[0]);
    for (var i = 1; i < elements.length; i++)
        instr += ',' + toElement(elements[i]);

    return instr + ';';

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A position in 2-D space.
 *
 * @constructor
 * @param {Guacamole.Position|object} [template={}]
 *     The object whose properties should be copied within the new
 *     Guacamole.Position.
 */
Guacamole.Position = function Position(template) {

    template = template || {};

    /**
     * The current X position, in pixels.
     *
     * @type {!number}
     * @default 0
     */
    this.x = template.x || 0;

    /**
     * The current Y position, in pixels.
     *
     * @type {!number}
     * @default 0
     */
    this.y = template.y || 0;

    /**
     * Assigns the position represented by the given element and
     * clientX/clientY coordinates. The clientX and clientY coordinates are
     * relative to the browser viewport and are commonly available within
     * JavaScript event objects. The final position is translated to
     * coordinates that are relative the given element.
     *
     * @param {!Element} element
     *     The element the coordinates should be relative to.
     *
     * @param {!number} clientX
     *     The viewport-relative X coordinate to translate.
     *
     * @param {!number} clientY
     *     The viewport-relative Y coordinate to translate.
     */
    this.fromClientPosition = function fromClientPosition(element, clientX, clientY) {

        this.x = clientX - element.offsetLeft;
        this.y = clientY - element.offsetTop;

        // This is all JUST so we can get the position within the element
        var parent = element.offsetParent;
        while (parent && !(parent === document.body)) {
            this.x -= parent.offsetLeft - parent.scrollLeft;
            this.y -= parent.offsetTop  - parent.scrollTop;

            parent = parent.offsetParent;
        }

        // Element ultimately depends on positioning within document body,
        // take document scroll into account.
        if (parent) {
            var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
            var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop;

            this.x -= parent.offsetLeft - documentScrollLeft;
            this.y -= parent.offsetTop  - documentScrollTop;
        }

    };

};

/**
 * Returns a new {@link Guacamole.Position} representing the relative position
 * of the given clientX/clientY coordinates within the given element. The
 * clientX and clientY coordinates are relative to the browser viewport and are
 * commonly available within JavaScript event objects. The final position is
 * translated to  coordinates that are relative the given element.
 *
 * @param {!Element} element
 *     The element the coordinates should be relative to.
 *
 * @param {!number} clientX
 *     The viewport-relative X coordinate to translate.
 *
 * @param {!number} clientY
 *     The viewport-relative Y coordinate to translate.
 *
 * @returns {!Guacamole.Position}
 *     A new Guacamole.Position representing the relative position of the given
 *     client coordinates.
 */
Guacamole.Position.fromClientPosition = function fromClientPosition(element, clientX, clientY) {
    var position = new Guacamole.Position();
    position.fromClientPosition(element, clientX, clientY);
    return position;
};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A description of the format of raw PCM audio, such as that used by
 * Guacamole.RawAudioPlayer and Guacamole.RawAudioRecorder. This object
 * describes the number of bytes per sample, the number of channels, and the
 * overall sample rate.
 *
 * @constructor
 * @param {!(Guacamole.RawAudioFormat|object)} template
 *     The object whose properties should be copied into the corresponding
 *     properties of the new Guacamole.RawAudioFormat.
 */
Guacamole.RawAudioFormat = function RawAudioFormat(template) {

    /**
     * The number of bytes in each sample of audio data. This value is
     * independent of the number of channels.
     *
     * @type {!number}
     */
    this.bytesPerSample = template.bytesPerSample;

    /**
     * The number of audio channels (ie: 1 for mono, 2 for stereo).
     *
     * @type {!number}
     */
    this.channels = template.channels;

    /**
     * The number of samples per second, per channel.
     *
     * @type {!number}
     */
    this.rate = template.rate;

};

/**
 * Parses the given mimetype, returning a new Guacamole.RawAudioFormat
 * which describes the type of raw audio data represented by that mimetype. If
 * the mimetype is not a supported raw audio data mimetype, null is returned.
 *
 * @param {!string} mimetype
 *     The audio mimetype to parse.
 *
 * @returns {Guacamole.RawAudioFormat}
 *     A new Guacamole.RawAudioFormat which describes the type of raw
 *     audio data represented by the given mimetype, or null if the given
 *     mimetype is not supported.
 */
Guacamole.RawAudioFormat.parse = function parseFormat(mimetype) {

    var bytesPerSample;

    // Rate is absolutely required - if null is still present later, the
    // mimetype must not be supported
    var rate = null;

    // Default for both "audio/L8" and "audio/L16" is one channel
    var channels = 1;

    // "audio/L8" has one byte per sample
    if (mimetype.substring(0, 9) === 'audio/L8;') {
        mimetype = mimetype.substring(9);
        bytesPerSample = 1;
    }

    // "audio/L16" has two bytes per sample
    else if (mimetype.substring(0, 10) === 'audio/L16;') {
        mimetype = mimetype.substring(10);
        bytesPerSample = 2;
    }

    // All other types are unsupported
    else
        return null;

    // Parse all parameters
    var parameters = mimetype.split(',');
    for (var i = 0; i < parameters.length; i++) {

        var parameter = parameters[i];

        // All parameters must have an equals sign separating name from value
        var equals = parameter.indexOf('=');
        if (equals === -1)
            return null;

        // Parse name and value from parameter string
        var name  = parameter.substring(0, equals);
        var value = parameter.substring(equals+1);

        // Handle each supported parameter
        switch (name) {

            // Number of audio channels
            case 'channels':
                channels = parseInt(value);
                break;

            // Sample rate
            case 'rate':
                rate = parseInt(value);
                break;

            // All other parameters are unsupported
            default:
                return null;

        }

    };

    // The rate parameter is required
    if (rate === null)
        return null;

    // Return parsed format details
    return new Guacamole.RawAudioFormat({
        bytesPerSample : bytesPerSample,
        channels       : channels,
        rate           : rate
    });

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A recording of a Guacamole session. Given a {@link Guacamole.Tunnel} or Blob,
 * the Guacamole.SessionRecording automatically parses Guacamole instructions
 * within the recording source as it plays back the recording. Playback of the
 * recording may be controlled through function calls to the
 * Guacamole.SessionRecording, even while the recording has not yet finished
 * being created or downloaded. Parsing of the contents of the recording will
 * begin immediately and automatically after this constructor is invoked.
 *
 * @constructor
 * @param {!Blob|Guacamole.Tunnel} source
 *     The Blob from which the instructions of the recording should
 *     be read.
 */
Guacamole.SessionRecording = function SessionRecording(source) {

    /**
     * Reference to this Guacamole.SessionRecording.
     *
     * @private
     * @type {!Guacamole.SessionRecording}
     */
    var recording = this;

    /**
     * The Blob from which the instructions of the recording should be read.
     * Note that this value is initialized far below.
     *
     * @private
     * @type {!Blob}
     */
    var recordingBlob;

    /**
     * The tunnel from which the recording should be read, if the recording is
     * being read from a tunnel. If the recording was supplied as a Blob, this
     * will be null.
     *
     * @private
     * @type {Guacamole.Tunnel}
     */
    var tunnel = null;

    /**
     * The number of bytes that this Guacamole.SessionRecording should attempt
     * to read from the given blob in each read operation. Larger blocks will
     * generally read the blob more quickly, but may result in excessive
     * time being spent within the parser, making the page unresponsive
     * while the recording is loading.
     *
     * @private
     * @constant
     * @type {Number}
     */
    var BLOCK_SIZE = 262144;

    /**
     * The minimum number of characters which must have been read between
     * keyframes.
     *
     * @private
     * @constant
     * @type {Number}
     */
    var KEYFRAME_CHAR_INTERVAL = 16384;

    /**
     * The minimum number of milliseconds which must elapse between keyframes.
     *
     * @private
     * @constant
     * @type {Number}
     */
    var KEYFRAME_TIME_INTERVAL = 5000;

    /**
     * All frames parsed from the provided blob.
     *
     * @private
     * @type {!Guacamole.SessionRecording._Frame[]}
     */
    var frames = [];

    /**
     * The timestamp of the last frame which was flagged for use as a keyframe.
     * If no timestamp has yet been flagged, this will be 0.
     *
     * @private
     * @type {!number}
     */
    var lastKeyframe = 0;

    /**
     * Tunnel which feeds arbitrary instructions to the client used by this
     * Guacamole.SessionRecording for playback of the session recording.
     *
     * @private
     * @type {!Guacamole.SessionRecording._PlaybackTunnel}
     */
    var playbackTunnel = new Guacamole.SessionRecording._PlaybackTunnel();

    /**
     * Guacamole.Client instance used for visible playback of the session
     * recording.
     *
     * @private
     * @type {!Guacamole.Client}
     */
    var playbackClient = new Guacamole.Client(playbackTunnel);

    /**
     * The current frame rendered within the playback client. If no frame is
     * yet rendered, this will be -1.
     *
     * @private
     * @type {!number}
     */
    var currentFrame = -1;

    /**
     * The timestamp of the frame when playback began, in milliseconds. If
     * playback is not in progress, this will be null.
     *
     * @private
     * @type {number}
     */
    var startVideoTimestamp = null;

    /**
     * The real-world timestamp when playback began, in milliseconds. If
     * playback is not in progress, this will be null.
     *
     * @private
     * @type {number}
     */
    var startRealTimestamp = null;

    /**
     * An object containing a single "aborted" property which is set to
     * true if the in-progress seek operation should be aborted. If no seek
     * operation is in progress, this will be null.
     *
     * @private
     * @type {object}
     */
    var activeSeek = null;

    /**
     * The byte offset within the recording blob of the first character of
     * the first instruction of the current frame. Here, "current frame"
     * refers to the frame currently being parsed when the provided
     * recording is initially loading. If the recording is not being
     * loaded, this value has no meaning.
     *
     * @private
     * @type {!number}
     */
    var frameStart = 0;

    /**
     * The byte offset within the recording blob of the character which
     * follows the last character of the most recently parsed instruction
     * of the current frame. Here, "current frame" refers to the frame
     * currently being parsed when the provided recording is initially
     * loading. If the recording is not being loaded, this value has no
     * meaning.
     *
     * @private
     * @type {!number}
     */
    var frameEnd = 0;

    /**
     * Whether the initial loading process has been aborted. If the loading
     * process has been aborted, no further blocks of data should be read
     * from the recording.
     *
     * @private
     * @type {!boolean}
     */
    var aborted = false;

    /**
     * The function to invoke when the seek operation initiated by a call
     * to seek() is cancelled or successfully completed. If no seek
     * operation is in progress, this will be null.
     *
     * @private
     * @type {function}
     */
    var seekCallback = null;

    /**
     * Parses all Guacamole instructions within the given blob, invoking
     * the provided instruction callback for each such instruction. Once
     * the end of the blob has been reached (no instructions remain to be
     * parsed), the provided completion callback is invoked. If a parse
     * error prevents reading instructions from the blob, the onerror
     * callback of the Guacamole.SessionRecording is invoked, and no further
     * data is handled within the blob.
     *
     * @private
     * @param {!Blob} blob
     *     The blob to parse Guacamole instructions from.
     *
     * @param {function} [instructionCallback]
     *     The callback to invoke for each Guacamole instruction read from
     *     the given blob. This function must accept the same arguments
     *     as the oninstruction handler of Guacamole.Parser.
     *
     * @param {function} [completionCallback]
     *     The callback to invoke once all instructions have been read from
     *     the given blob.
     */
    var parseBlob = function parseBlob(blob, instructionCallback, completionCallback) {

        // Do not read any further blocks if loading has been aborted
        if (aborted && blob === recordingBlob)
            return;

        // Prepare a parser to handle all instruction data within the blob,
        // automatically invoking the provided instruction callback for all
        // parsed instructions
        var parser = new Guacamole.Parser();
        parser.oninstruction = instructionCallback;

        var offset = 0;
        var reader = new FileReader();

        /**
         * Reads the block of data at offset bytes within the blob. If no
         * such block exists, then the completion callback provided to
         * parseBlob() is invoked as all data has been read.
         *
         * @private
         */
        var readNextBlock = function readNextBlock() {

            // Do not read any further blocks if loading has been aborted
            if (aborted && blob === recordingBlob)
                return;

            // Parse all instructions within the block, invoking the
            // onerror handler if a parse error occurs
            if (reader.readyState === 2 /* DONE */) {
                try {
                    parser.receive(reader.result);
                }
                catch (parseError) {
                    if (recording.onerror) {
                        recording.onerror(parseError.message);
                    }
                    return;
                }
            }

            // If no data remains, the read operation is complete and no
            // further blocks need to be read
            if (offset >= blob.size) {
                if (completionCallback)
                    completionCallback();
            }

            // Otherwise, read the next block
            else {
                var block = blob.slice(offset, offset + BLOCK_SIZE);
                offset += block.size;
                reader.readAsText(block);
            }

        };

        // Read blocks until the end of the given blob is reached
        reader.onload = readNextBlock;
        readNextBlock();

    };

    /**
     * Calculates the size of the given Guacamole instruction element, in
     * Unicode characters. The size returned includes the characters which
     * make up the length, the "." separator between the length and the
     * element itself, and the "," or ";" terminator which follows the
     * element.
     *
     * @private
     * @param {!string} value
     *     The value of the element which has already been parsed (lacks
     *     the initial length, "." separator, and "," or ";" terminator).
     *
     * @returns {!number}
     *     The number of Unicode characters which would make up the given
     *     element within a Guacamole instruction.
     */
    var getElementSize = function getElementSize(value) {

        var valueLength = value.length;

        // Calculate base size, assuming at least one digit, the "."
        // separator, and the "," or ";" terminator
        var protocolSize = valueLength + 3;

        // Add one character for each additional digit that would occur
        // in the element length prefix
        while (valueLength >= 10) {
            protocolSize++;
            valueLength = Math.floor(valueLength / 10);
        }

        return protocolSize;

    };

    // Start playback client connected
    playbackClient.connect();

    // Hide cursor unless mouse position is received
    playbackClient.getDisplay().showCursor(false);

    /**
     * Handles a newly-received instruction, whether from the main Blob or a
     * tunnel, adding new frames and keyframes as necessary. Load progress is
     * reported via onprogress automatically.
     *
     * @private
     * @param {!string} opcode
     *     The opcode of the instruction to handle.
     *
     * @param {!string[]} args
     *     The arguments of the received instruction, if any.
     */
    var loadInstruction = function loadInstruction(opcode, args) {

        // Advance end of frame by overall length of parsed instruction
        frameEnd += getElementSize(opcode);
        for (var i = 0; i < args.length; i++)
            frameEnd += getElementSize(args[i]);

        // Once a sync is received, store all instructions since the last
        // frame as a new frame
        if (opcode === 'sync') {

            // Parse frame timestamp from sync instruction
            var timestamp = parseInt(args[0]);

            // Add a new frame containing the instructions read since last frame
            var frame = new Guacamole.SessionRecording._Frame(timestamp, frameStart, frameEnd);
            frames.push(frame);
            frameStart = frameEnd;

            // This frame should eventually become a keyframe if enough data
            // has been processed and enough recording time has elapsed, or if
            // this is the absolute first frame
            if (frames.length === 1 || (frameEnd - frames[lastKeyframe].start >= KEYFRAME_CHAR_INTERVAL
                    && timestamp - frames[lastKeyframe].timestamp >= KEYFRAME_TIME_INTERVAL)) {
                frame.keyframe = true;
                lastKeyframe = frames.length - 1;
            }

            // Notify that additional content is available
            if (recording.onprogress)
                recording.onprogress(recording.getDuration(), frameEnd);

        }

    };

    /**
     * Notifies that the session recording has been fully loaded. If the onload
     * handler has not been defined, this function has no effect.
     *
     * @private
     */
    var notifyLoaded = function notifyLoaded() {
        if (recording.onload)
            recording.onload();
    };

    // Read instructions from provided blob, extracting each frame
    if (source instanceof Blob) {
        recordingBlob = source;
        parseBlob(recordingBlob, loadInstruction, notifyLoaded);
    }

    // If tunnel provided instead of Blob, extract frames, etc. as instructions
    // are received, buffering things into a Blob for future seeks
    else {

        tunnel = source;
        recordingBlob = new Blob();

        var errorEncountered = false;
        var instructionBuffer = '';

        // Read instructions from provided tunnel, extracting each frame
        tunnel.oninstruction = function handleInstruction(opcode, args) {

            // Reconstitute received instruction
            instructionBuffer += opcode.length + '.' + opcode;
            args.forEach(function appendArg(arg) {
                instructionBuffer += ',' + arg.length + '.' + arg;
            });
            instructionBuffer += ';';

            // Append to Blob (creating a new Blob in the process)
            if (instructionBuffer.length >= BLOCK_SIZE) {
                recordingBlob = new Blob([recordingBlob, instructionBuffer]);
                instructionBuffer = '';
            }

            // Load parsed instruction into recording
            loadInstruction(opcode, args);

        };

        // Report any errors encountered
        tunnel.onerror = function tunnelError(status) {
            errorEncountered = true;
            if (recording.onerror)
                recording.onerror(status.message);
        };

        tunnel.onstatechange = function tunnelStateChanged(state) {
            if (state === Guacamole.Tunnel.State.CLOSED) {

                // Append any remaining instructions
                if (instructionBuffer.length) {
                    recordingBlob = new Blob([recordingBlob, instructionBuffer]);
                    instructionBuffer = '';
                }

                // Consider recording loaded if tunnel has closed without errors
                if (!errorEncountered)
                    notifyLoaded();
            }
        };

    }

    /**
     * Converts the given absolute timestamp to a timestamp which is relative
     * to the first frame in the recording.
     *
     * @private
     * @param {!number} timestamp
     *     The timestamp to convert to a relative timestamp.
     *
     * @returns {!number}
     *     The difference in milliseconds between the given timestamp and the
     *     first frame of the recording, or zero if no frames yet exist.
     */
    var toRelativeTimestamp = function toRelativeTimestamp(timestamp) {

        // If no frames yet exist, all timestamps are zero
        if (frames.length === 0)
            return 0;

        // Calculate timestamp relative to first frame
        return timestamp - frames[0].timestamp;

    };

    /**
     * Searches through the given region of frames for the frame having a
     * relative timestamp closest to the timestamp given.
     *
     * @private
     * @param {!number} minIndex
     *     The index of the first frame in the region (the frame having the
     *     smallest timestamp).
     *
     * @param {!number} maxIndex
     *     The index of the last frame in the region (the frame having the
     *     largest timestamp).
     *
     * @param {!number} timestamp
     *     The relative timestamp to search for, where zero denotes the first
     *     frame in the recording.
     *
     * @returns {!number}
     *     The index of the frame having a relative timestamp closest to the
     *     given value.
     */
    var findFrame = function findFrame(minIndex, maxIndex, timestamp) {

        // Do not search if the region contains only one element
        if (minIndex === maxIndex)
            return minIndex;

        // Split search region into two halves
        var midIndex = Math.floor((minIndex + maxIndex) / 2);
        var midTimestamp = toRelativeTimestamp(frames[midIndex].timestamp);

        // If timestamp is within lesser half, search again within that half
        if (timestamp < midTimestamp && midIndex > minIndex)
            return findFrame(minIndex, midIndex - 1, timestamp);

        // If timestamp is within greater half, search again within that half
        if (timestamp > midTimestamp && midIndex < maxIndex)
            return findFrame(midIndex + 1, maxIndex, timestamp);

        // Otherwise, we lucked out and found a frame with exactly the
        // desired timestamp
        return midIndex;

    };

    /**
     * Replays the instructions associated with the given frame, sending those
     * instructions to the playback client.
     *
     * @private
     * @param {!number} index
     *     The index of the frame within the frames array which should be
     *     replayed.
     *
     * @param {function} callback
     *     The callback to invoke once replay of the frame has completed.
     */
    var replayFrame = function replayFrame(index, callback) {

        var frame = frames[index];

        // Replay all instructions within the retrieved frame
        parseBlob(recordingBlob.slice(frame.start, frame.end), function handleInstruction(opcode, args) {
            playbackTunnel.receiveInstruction(opcode, args);
        }, function replayCompleted() {

            // Store client state if frame is flagged as a keyframe
            if (frame.keyframe && !frame.clientState) {
                playbackClient.exportState(function storeClientState(state) {
                    frame.clientState = new Blob([JSON.stringify(state)]);
                });
            }

            // Update state to correctly represent the current frame
            currentFrame = index;

            if (callback)
                callback();

        });

    };

    /**
     * Moves the playback position to the given frame, resetting the state of
     * the playback client and replaying frames as necessary. The seek
     * operation will proceed asynchronously. If a seek operation is already in
     * progress, that seek is first aborted. The progress of the seek operation
     * can be observed through the onseek handler and the provided callback.
     *
     * @private
     * @param {!number} index
     *     The index of the frame which should become the new playback
     *     position.
     *
     * @param {function} callback
     *     The callback to invoke once the seek operation has completed.
     *
     * @param {number} [nextRealTimestamp]
     *     The timestamp of the point in time that the given frame should be
     *     displayed, as would be returned by new Date().getTime(). If omitted,
     *     the frame will be displayed as soon as possible.
     */
    var seekToFrame = function seekToFrame(index, callback, nextRealTimestamp) {

        // Abort any in-progress seek
        abortSeek();

        // Note that a new seek operation is in progress
        var thisSeek = activeSeek = {
            aborted : false
        };

        var startIndex = index;

        // Replay any applicable incremental frames
        var continueReplay = function continueReplay() {

            // Notify of changes in position
            if (recording.onseek && currentFrame > startIndex) {
                recording.onseek(toRelativeTimestamp(frames[currentFrame].timestamp),
                    currentFrame - startIndex, index - startIndex);
            }

            // Cancel seek if aborted
            if (thisSeek.aborted)
                return;

            // If frames remain, replay the next frame
            if (currentFrame < index)
                replayFrame(currentFrame + 1, continueReplay);

            // Otherwise, the seek operation is completed
            else
                callback();

        };

        // Continue replay after requested delay has elapsed, or
        // immediately if no delay was requested
        var continueAfterRequiredDelay = function continueAfterRequiredDelay() {
            var delay = nextRealTimestamp ? Math.max(nextRealTimestamp - new Date().getTime(), 0) : 0;
            if (delay)
                window.setTimeout(continueReplay, delay);
            else
                continueReplay();
        };

        // Back up until startIndex represents current state
        for (; startIndex >= 0; startIndex--) {

            var frame = frames[startIndex];

            // If we've reached the current frame, startIndex represents
            // current state by definition
            if (startIndex === currentFrame)
                break;

            // If frame has associated absolute state, make that frame the
            // current state
            if (frame.clientState) {
                frame.clientState.text().then(function textReady(text) {
                    playbackClient.importState(JSON.parse(text));
                    currentFrame = startIndex;
                    continueAfterRequiredDelay();
                });
                return;
            }

        }

        continueAfterRequiredDelay();
        
    };

    /**
     * Aborts the seek operation currently in progress, if any. If no seek
     * operation is in progress, this function has no effect.
     *
     * @private
     */
    var abortSeek = function abortSeek() {
        if (activeSeek) {
            activeSeek.aborted = true;
            activeSeek = null;
        }
    };

    /**
     * Advances playback to the next frame in the frames array and schedules
     * playback of the frame following that frame based on their associated
     * timestamps. If no frames exist after the next frame, playback is paused.
     *
     * @private
     */
    var continuePlayback = function continuePlayback() {

        // If frames remain after advancing, schedule next frame
        if (currentFrame + 1 < frames.length) {

            // Pull the upcoming frame
            var next = frames[currentFrame + 1];

            // Calculate the real timestamp corresponding to when the next
            // frame begins
            var nextRealTimestamp = next.timestamp - startVideoTimestamp + startRealTimestamp;

            // Advance to next frame after enough time has elapsed
            seekToFrame(currentFrame + 1, function frameDelayElapsed() {
                continuePlayback();
            }, nextRealTimestamp);

        }

        // Otherwise stop playback
        else
            recording.pause();

    };

    /**
     * Fired when loading of this recording has completed and all frames
     * are available.
     *
     * @event
     */
    this.onload = null;

    /**
     * Fired when an error occurs which prevents the recording from being
     * played back.
     *
     * @event
     * @param {!string} message
     *     A human-readable message describing the error that occurred.
     */
    this.onerror = null;

    /**
     * Fired when further loading of this recording has been explicitly
     * aborted through a call to abort().
     *
     * @event
     */
    this.onabort = null;

    /**
     * Fired when new frames have become available while the recording is
     * being downloaded.
     *
     * @event
     * @param {!number} duration
     *     The new duration of the recording, in milliseconds.
     *
     * @param {!number} parsedSize
     *     The number of bytes that have been loaded/parsed.
     */
    this.onprogress = null;

    /**
     * Fired whenever playback of the recording has started.
     *
     * @event
     */
    this.onplay = null;

    /**
     * Fired whenever playback of the recording has been paused. This may
     * happen when playback is explicitly paused with a call to pause(), or
     * when playback is implicitly paused due to reaching the end of the
     * recording.
     *
     * @event
     */
    this.onpause = null;

    /**
     * Fired whenever the playback position within the recording changes.
     *
     * @event
     * @param {!number} position
     *     The new position within the recording, in milliseconds.
     *
     * @param {!number} current
     *     The number of frames that have been seeked through. If not
     *     seeking through multiple frames due to a call to seek(), this
     *     will be 1.
     *
     * @param {!number} total
     *     The number of frames that are being seeked through in the
     *     current seek operation. If not seeking through multiple frames
     *     due to a call to seek(), this will be 1.
     */
    this.onseek = null;

    /**
     * Connects the underlying tunnel, beginning download of the Guacamole
     * session. Playback of the Guacamole session cannot occur until at least
     * one frame worth of instructions has been downloaded. If the underlying
     * recording source is a Blob, this function has no effect.
     *
     * @param {string} [data]
     *     The data to send to the tunnel when connecting.
     */
    this.connect = function connect(data) {
        if (tunnel)
            tunnel.connect(data);
    };

    /**
     * Disconnects the underlying tunnel, stopping further download of the
     * Guacamole session. If the underlying recording source is a Blob, this
     * function has no effect.
     */
    this.disconnect = function disconnect() {
        if (tunnel)
            tunnel.disconnect();
    };

    /**
     * Aborts the loading process, stopping further processing of the
     * provided data. If the underlying recording source is a Guacamole tunnel,
     * it will be disconnected.
     */
    this.abort = function abort() {
        if (!aborted) {

            aborted = true;
            if (recording.onabort)
                recording.onabort();

            if (tunnel)
                tunnel.disconnect();

        }
    };

    /**
     * Returns the underlying display of the Guacamole.Client used by this
     * Guacamole.SessionRecording for playback. The display contains an Element
     * which can be added to the DOM, causing the display (and thus playback of
     * the recording) to become visible.
     *
     * @return {!Guacamole.Display}
     *     The underlying display of the Guacamole.Client used by this
     *     Guacamole.SessionRecording for playback.
     */
    this.getDisplay = function getDisplay() {
        return playbackClient.getDisplay();
    };

    /**
     * Returns whether playback is currently in progress.
     *
     * @returns {!boolean}
     *     true if playback is currently in progress, false otherwise.
     */
    this.isPlaying = function isPlaying() {
        return !!startVideoTimestamp;
    };

    /**
     * Returns the current playback position within the recording, in
     * milliseconds, where zero is the start of the recording.
     *
     * @returns {!number}
     *     The current playback position within the recording, in milliseconds.
     */
    this.getPosition = function getPosition() {

        // Position is simply zero if playback has not started at all
        if (currentFrame === -1)
            return 0;

        // Return current position as a millisecond timestamp relative to the
        // start of the recording
        return toRelativeTimestamp(frames[currentFrame].timestamp);

    };

    /**
     * Returns the duration of this recording, in milliseconds. If the
     * recording is still being downloaded, this value will gradually increase.
     *
     * @returns {!number}
     *     The duration of this recording, in milliseconds.
     */
    this.getDuration = function getDuration() {

        // If no frames yet exist, duration is zero
        if (frames.length === 0)
            return 0;

        // Recording duration is simply the timestamp of the last frame
        return toRelativeTimestamp(frames[frames.length - 1].timestamp);

    };

    /**
     * Begins continuous playback of the recording downloaded thus far.
     * Playback of the recording will continue until pause() is invoked or
     * until no further frames exist. Playback is initially paused when a
     * Guacamole.SessionRecording is created, and must be explicitly started
     * through a call to this function. If playback is already in progress,
     * this function has no effect. If a seek operation is in progress,
     * playback resumes at the current position, and the seek is aborted as if
     * completed.
     */
    this.play = function play() {

        // If playback is not already in progress and frames remain,
        // begin playback
        if (!recording.isPlaying() && currentFrame + 1 < frames.length) {

            // Notify that playback is starting
            if (recording.onplay)
                recording.onplay();

            // Store timestamp of playback start for relative scheduling of
            // future frames
            var next = frames[currentFrame + 1];
            startVideoTimestamp = next.timestamp;
            startRealTimestamp = new Date().getTime();

            // Begin playback of video
            continuePlayback();

        }

    };

    /**
     * Seeks to the given position within the recording. If the recording is
     * currently being played back, playback will continue after the seek is
     * performed. If the recording is currently paused, playback will be
     * paused after the seek is performed. If a seek operation is already in
     * progress, that seek is first aborted. The seek operation will proceed
     * asynchronously.
     *
     * @param {!number} position
     *     The position within the recording to seek to, in milliseconds.
     *
     * @param {function} [callback]
     *     The callback to invoke once the seek operation has completed.
     */
    this.seek = function seek(position, callback) {

        // Do not seek if no frames exist
        if (frames.length === 0)
            return;

        // Abort active seek operation, if any
        recording.cancel();

        // Pause playback, preserving playback state
        var originallyPlaying = recording.isPlaying();
        recording.pause();

        // Restore playback when seek is completed or cancelled
        seekCallback = function restorePlaybackState() {

            // Seek is no longer in progress
            seekCallback = null;

            // Restore playback state
            if (originallyPlaying) {
                recording.play();
                originallyPlaying = null;
            }

            // Notify that seek has completed
            if (callback)
                callback();

        };

        // Perform seek
        seekToFrame(findFrame(0, frames.length - 1, position), seekCallback);

    };

    /**
     * Cancels the current seek operation, setting the current frame of the
     * recording to wherever the seek operation was able to reach prior to
     * being cancelled. If a callback was provided to seek(), that callback
     * is invoked. If a seek operation is not currently underway, this
     * function has no effect.
     */
    this.cancel = function cancel() {
        if (seekCallback) {
            abortSeek();
            seekCallback();
        }
    };

    /**
     * Pauses playback of the recording, if playback is currently in progress.
     * If playback is not in progress, this function has no effect. If a seek
     * operation is in progress, the seek is aborted. Playback is initially
     * paused when a Guacamole.SessionRecording is created, and must be
     * explicitly started through a call to play().
     */
    this.pause = function pause() {

        // Abort any in-progress seek / playback
        abortSeek();

        // Stop playback only if playback is in progress
        if (recording.isPlaying()) {

            // Notify that playback is stopping
            if (recording.onpause)
                recording.onpause();

            // Playback is stopped
            startVideoTimestamp = null;
            startRealTimestamp = null;

        }

    };

};

/**
 * A single frame of Guacamole session data. Each frame is made up of the set
 * of instructions used to generate that frame, and the timestamp as dictated
 * by the "sync" instruction terminating the frame. Optionally, a frame may
 * also be associated with a snapshot of Guacamole client state, such that the
 * frame can be rendered without replaying all previous frames.
 *
 * @private
 * @constructor
 * @param {!number} timestamp
 *     The timestamp of this frame, as dictated by the "sync" instruction which
 *     terminates the frame.
 *
 * @param {!number} start
 *     The byte offset within the blob of the first character of the first
 *     instruction of this frame.
 *
 * @param {!number} end
 *     The byte offset within the blob of character which follows the last
 *     character of the last instruction of this frame.
 */
Guacamole.SessionRecording._Frame = function _Frame(timestamp, start, end) {

    /**
     * Whether this frame should be used as a keyframe if possible. This value
     * is purely advisory. The stored clientState must eventually be manually
     * set for the frame to be used as a keyframe. By default, frames are not
     * keyframes.
     *
     * @type {!boolean}
     * @default false
     */
    this.keyframe = false;

    /**
     * The timestamp of this frame, as dictated by the "sync" instruction which
     * terminates the frame.
     *
     * @type {!number}
     */
    this.timestamp = timestamp;

    /**
     * The byte offset within the blob of the first character of the first
     * instruction of this frame.
     *
     * @type {!number}
     */
    this.start = start;

    /**
     * The byte offset within the blob of character which follows the last
     * character of the last instruction of this frame.
     *
     * @type {!number}
     */
    this.end = end;

    /**
     * A snapshot of client state after this frame was rendered, as returned by
     * a call to exportState(), serialized as JSON, and stored within a Blob.
     * Use of Blobs here is required to ensure the browser can make use of
     * larger disk-backed storage if the size of the recording is large. If no
     * such snapshot has been taken, this will be null.
     *
     * @type {Blob}
     * @default null
     */
    this.clientState = null;

};

/**
 * A read-only Guacamole.Tunnel implementation which streams instructions
 * received through explicit calls to its receiveInstruction() function.
 *
 * @private
 * @constructor
 * @augments {Guacamole.Tunnel}
 */
Guacamole.SessionRecording._PlaybackTunnel = function _PlaybackTunnel() {

    /**
     * Reference to this Guacamole.SessionRecording._PlaybackTunnel.
     *
     * @private
     * @type {!Guacamole.SessionRecording._PlaybackTunnel}
     */
    var tunnel = this;

    this.connect = function connect(data) {
        // Do nothing
    };

    this.sendMessage = function sendMessage(elements) {
        // Do nothing
    };

    this.disconnect = function disconnect() {
        // Do nothing
    };

    /**
     * Invokes this tunnel's oninstruction handler, notifying users of this
     * tunnel (such as a Guacamole.Client instance) that an instruction has
     * been received. If the oninstruction handler has not been set, this
     * function has no effect.
     *
     * @param {!string} opcode
     *     The opcode of the Guacamole instruction.
     *
     * @param {!string[]} args
     *     All arguments associated with this Guacamole instruction.
     */
    this.receiveInstruction = function receiveInstruction(opcode, args) {
        if (tunnel.oninstruction)
            tunnel.oninstruction(opcode, args);
    };

};
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A Guacamole status. Each Guacamole status consists of a status code, defined
 * by the protocol, and an optional human-readable message, usually only
 * included for debugging convenience.
 *
 * @constructor
 * @param {!number} code
 *     The Guacamole status code, as defined by Guacamole.Status.Code.
 *
 * @param {string} [message]
 *     An optional human-readable message.
 */
Guacamole.Status = function(code, message) {

    /**
     * Reference to this Guacamole.Status.
     *
     * @private
     * @type {!Guacamole.Status}
     */
    var guac_status = this;

    /**
     * The Guacamole status code.
     *
     * @see Guacamole.Status.Code
     * @type {!number}
     */
    this.code = code;

    /**
     * An arbitrary human-readable message associated with this status, if any.
     * The human-readable message is not required, and is generally provided
     * for debugging purposes only. For user feedback, it is better to translate
     * the Guacamole status code into a message.
     * 
     * @type {string}
     */
    this.message = message;

    /**
     * Returns whether this status represents an error.
     *
     * @returns {!boolean}
     *     true if this status represents an error, false otherwise.
     */
    this.isError = function() {
        return guac_status.code < 0 || guac_status.code > 0x00FF;
    };

};

/**
 * Enumeration of all Guacamole status codes.
 */
Guacamole.Status.Code = {

    /**
     * The operation succeeded.
     *
     * @type {!number}
     */
    "SUCCESS": 0x0000,

    /**
     * The requested operation is unsupported.
     *
     * @type {!number}
     */
    "UNSUPPORTED": 0x0100,

    /**
     * The operation could not be performed due to an internal failure.
     *
     * @type {!number}
     */
    "SERVER_ERROR": 0x0200,

    /**
     * The operation could not be performed as the server is busy.
     *
     * @type {!number}
     */
    "SERVER_BUSY": 0x0201,

    /**
     * The operation could not be performed because the upstream server is not
     * responding.
     *
     * @type {!number}
     */
    "UPSTREAM_TIMEOUT": 0x0202,

    /**
     * The operation was unsuccessful due to an error or otherwise unexpected
     * condition of the upstream server.
     *
     * @type {!number}
     */
    "UPSTREAM_ERROR": 0x0203,

    /**
     * The operation could not be performed as the requested resource does not
     * exist.
     *
     * @type {!number}
     */
    "RESOURCE_NOT_FOUND": 0x0204,

    /**
     * The operation could not be performed as the requested resource is
     * already in use.
     *
     * @type {!number}
     */
    "RESOURCE_CONFLICT": 0x0205,

    /**
     * The operation could not be performed as the requested resource is now
     * closed.
     *
     * @type {!number}
     */
    "RESOURCE_CLOSED": 0x0206,

    /**
     * The operation could not be performed because the upstream server does
     * not appear to exist.
     *
     * @type {!number}
     */
    "UPSTREAM_NOT_FOUND": 0x0207,

    /**
     * The operation could not be performed because the upstream server is not
     * available to service the request.
     *
     * @type {!number}
     */
    "UPSTREAM_UNAVAILABLE": 0x0208,

    /**
     * The session within the upstream server has ended because it conflicted
     * with another session.
     *
     * @type {!number}
     */
    "SESSION_CONFLICT": 0x0209,

    /**
     * The session within the upstream server has ended because it appeared to
     * be inactive.
     *
     * @type {!number}
     */
    "SESSION_TIMEOUT": 0x020A,

    /**
     * The session within the upstream server has been forcibly terminated.
     *
     * @type {!number}
     */
    "SESSION_CLOSED": 0x020B,

    /**
     * The operation could not be performed because bad parameters were given.
     *
     * @type {!number}
     */
    "CLIENT_BAD_REQUEST": 0x0300,

    /**
     * Permission was denied to perform the operation, as the user is not yet
     * authorized (not yet logged in, for example).
     *
     * @type {!number}
     */
    "CLIENT_UNAUTHORIZED": 0x0301,

    /**
     * Permission was denied to perform the operation, and this permission will
     * not be granted even if the user is authorized.
     *
     * @type {!number}
     */
    "CLIENT_FORBIDDEN": 0x0303,

    /**
     * The client took too long to respond.
     *
     * @type {!number}
     */
    "CLIENT_TIMEOUT": 0x0308,

    /**
     * The client sent too much data.
     *
     * @type {!number}
     */
    "CLIENT_OVERRUN": 0x030D,

    /**
     * The client sent data of an unsupported or unexpected type.
     *
     * @type {!number}
     */
    "CLIENT_BAD_TYPE": 0x030F,

    /**
     * The operation failed because the current client is already using too
     * many resources.
     *
     * @type {!number}
     */
    "CLIENT_TOO_MANY": 0x031D

};

/**
 * Returns the Guacamole protocol status code which most closely
 * represents the given HTTP status code.
 *
 * @param {!number} status
 *     The HTTP status code to translate into a Guacamole protocol status
 *     code.
 *
 * @returns {!number}
 *     The Guacamole protocol status code which most closely represents the
 *     given HTTP status code.
 */
Guacamole.Status.Code.fromHTTPCode = function fromHTTPCode(status) {

    // Translate status codes with known equivalents
    switch (status) {

        // HTTP 400 - Bad request
        case 400:
            return Guacamole.Status.Code.CLIENT_BAD_REQUEST;

        // HTTP 403 - Forbidden
        case 403:
            return Guacamole.Status.Code.CLIENT_FORBIDDEN;

        // HTTP 404 - Resource not found
        case 404:
            return Guacamole.Status.Code.RESOURCE_NOT_FOUND;

        // HTTP 429 - Too many requests
        case 429:
            return Guacamole.Status.Code.CLIENT_TOO_MANY;

        // HTTP 503 - Server unavailable
        case 503:
            return Guacamole.Status.Code.SERVER_BUSY;

    }

    // Default all other codes to generic internal error
    return Guacamole.Status.Code.SERVER_ERROR;

};

/**
 * Returns the Guacamole protocol status code which most closely
 * represents the given WebSocket status code.
 *
 * @param {!number} code
 *     The WebSocket status code to translate into a Guacamole protocol
 *     status code.
 *
 * @returns {!number}
 *     The Guacamole protocol status code which most closely represents the
 *     given WebSocket status code.
 */
Guacamole.Status.Code.fromWebSocketCode = function fromWebSocketCode(code) {

    // Translate status codes with known equivalents
    switch (code) {

        // Successful disconnect (no error)
        case 1000: // Normal Closure
            return Guacamole.Status.Code.SUCCESS;

        // Codes which indicate the server is not reachable
        case 1006: // Abnormal Closure (also signalled by JavaScript when the connection cannot be opened in the first place)
        case 1015: // TLS Handshake
            return Guacamole.Status.Code.UPSTREAM_NOT_FOUND;

        // Codes which indicate the server is reachable but busy/unavailable
        case 1001: // Going Away
        case 1012: // Service Restart
        case 1013: // Try Again Later
        case 1014: // Bad Gateway
            return Guacamole.Status.Code.UPSTREAM_UNAVAILABLE;

    }

    // Default all other codes to generic internal error
    return Guacamole.Status.Code.SERVER_ERROR;

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A reader which automatically handles the given input stream, returning
 * strictly text data. Note that this object will overwrite any installed event
 * handlers on the given Guacamole.InputStream.
 * 
 * @constructor
 * @param {!Guacamole.InputStream} stream
 *     The stream that data will be read from.
 */
Guacamole.StringReader = function(stream) {

    /**
     * Reference to this Guacamole.InputStream.
     *
     * @private
     * @type {!Guacamole.StringReader}
     */
    var guac_reader = this;

    /**
     * Parser for received UTF-8 data.
     *
     * @type {!Guacamole.UTF8Parser}
     */
    var utf8Parser = new Guacamole.UTF8Parser();

    /**
     * Wrapped Guacamole.ArrayBufferReader.
     *
     * @private
     * @type {!Guacamole.ArrayBufferReader}
     */
    var array_reader = new Guacamole.ArrayBufferReader(stream);

    // Receive blobs as strings
    array_reader.ondata = function(buffer) {

        // Decode UTF-8
        var text = utf8Parser.decode(buffer);

        // Call handler, if present
        if (guac_reader.ontext)
            guac_reader.ontext(text);

    };

    // Simply call onend when end received
    array_reader.onend = function() {
        if (guac_reader.onend)
            guac_reader.onend();
    };

    /**
     * Fired once for every blob of text data received.
     * 
     * @event
     * @param {!string} text
     *     The data packet received.
     */
    this.ontext = null;

    /**
     * Fired once this stream is finished and no further data will be written.
     * @event
     */
    this.onend = null;

};
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * A writer which automatically writes to the given output stream with text
 * data.
 * 
 * @constructor
 * @param {!Guacamole.OutputStream} stream
 *     The stream that data will be written to.
 */
Guacamole.StringWriter = function(stream) {

    /**
     * Reference to this Guacamole.StringWriter.
     *
     * @private
     * @type {!Guacamole.StringWriter}
     */
    var guac_writer = this;

    /**
     * Wrapped Guacamole.ArrayBufferWriter.
     *
     * @private
     * @type {!Guacamole.ArrayBufferWriter}
     */
    var array_writer = new Guacamole.ArrayBufferWriter(stream);

    /**
     * Internal buffer for UTF-8 output.
     *
     * @private
     * @type {!Uint8Array}
     */
    var buffer = new Uint8Array(8192);

    /**
     * The number of bytes currently in the buffer.
     *
     * @private
     * @type {!number}
     */
    var length = 0;

    // Simply call onack for acknowledgements
    array_writer.onack = function(status) {
        if (guac_writer.onack)
            guac_writer.onack(status);
    };

    /**
     * Expands the size of the underlying buffer by the given number of bytes,
     * updating the length appropriately.
     * 
     * @private
     * @param {!number} bytes
     *     The number of bytes to add to the underlying buffer.
     */
    function __expand(bytes) {

        // Resize buffer if more space needed
        if (length+bytes >= buffer.length) {
            var new_buffer = new Uint8Array((length+bytes)*2);
            new_buffer.set(buffer);
            buffer = new_buffer;
        }

        length += bytes;

    }

    /**
     * Appends a single Unicode character to the current buffer, resizing the
     * buffer if necessary. The character will be encoded as UTF-8.
     * 
     * @private
     * @param {!number} codepoint
     *     The codepoint of the Unicode character to append.
     */
    function __append_utf8(codepoint) {

        var mask;
        var bytes;

        // 1 byte
        if (codepoint <= 0x7F) {
            mask = 0x00;
            bytes = 1;
        }

        // 2 byte
        else if (codepoint <= 0x7FF) {
            mask = 0xC0;
            bytes = 2;
        }

        // 3 byte
        else if (codepoint <= 0xFFFF) {
            mask = 0xE0;
            bytes = 3;
        }

        // 4 byte
        else if (codepoint <= 0x1FFFFF) {
            mask = 0xF0;
            bytes = 4;
        }

        // If invalid codepoint, append replacement character
        else {
            __append_utf8(0xFFFD);
            return;
        }

        // Offset buffer by size
        __expand(bytes);
        var offset = length - 1;

        // Add trailing bytes, if any
        for (var i=1; i<bytes; i++) {
            buffer[offset--] = 0x80 | (codepoint & 0x3F);
            codepoint >>= 6;
        }

        // Set initial byte
        buffer[offset] = mask | codepoint;

    }

    /**
     * Encodes the given string as UTF-8, returning an ArrayBuffer containing
     * the resulting bytes.
     * 
     * @private
     * @param {!string} text
     *     The string to encode as UTF-8.
     *
     * @return {!Uint8Array}
     *     The encoded UTF-8 data.
     */
    function __encode_utf8(text) {

        // Fill buffer with UTF-8
        for (var i=0; i<text.length; i++) {
            var codepoint = text.charCodeAt(i);
            __append_utf8(codepoint);
        }

        // Flush buffer
        if (length > 0) {
            var out_buffer = buffer.subarray(0, length);
            length = 0;
            return out_buffer;
        }

    }

    /**
     * Sends the given text.
     * 
     * @param {!string} text
     *     The text to send.
     */
    this.sendText = function(text) {
        if (text.length)
            array_writer.sendData(__encode_utf8(text));
    };

    /**
     * Signals that no further text will be sent, effectively closing the
     * stream.
     */
    this.sendEnd = function() {
        array_writer.sendEnd();
    };

    /**
     * Fired for received data, if acknowledged by the server.
     *
     * @event
     * @param {!Guacamole.Status} status
     *     The status of the operation.
     */
    this.onack = null;

};
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Provides cross-browser multi-touch events for a given element. The events of
 * the given element are automatically populated with handlers that translate
 * touch events into a non-browser-specific event provided by the
 * Guacamole.Touch instance.
 * 
 * @constructor
 * @augments Guacamole.Event.Target
 * @param {!Element} element
 *     The Element to use to provide touch events.
 */
Guacamole.Touch = function Touch(element) {

    Guacamole.Event.Target.call(this);

    /**
     * Reference to this Guacamole.Touch.
     *
     * @private
     * @type {!Guacamole.Touch}
     */
    var guacTouch = this;

    /**
     * The default X/Y radius of each touch if the device or browser does not
     * expose the size of the contact area.
     *
     * @private
     * @constant
     * @type {!number}
     */
    var DEFAULT_CONTACT_RADIUS = Math.floor(16 * window.devicePixelRatio);

    /**
     * The set of all active touches, stored by their unique identifiers.
     *
     * @type {!Object.<Number, Guacamole.Touch.State>}
     */
    this.touches = {};

    /**
     * The number of active touches currently stored within
     * {@link Guacamole.Touch#touches touches}.
     */
    this.activeTouches = 0;

    /**
     * Fired whenever a new touch contact is initiated on the element
     * associated with this Guacamole.Touch.
     * 
     * @event Guacamole.Touch#touchstart
     * @param {!Guacamole.Touch.Event} event
     *     A {@link Guacamole.Touch.Event} object representing the "touchstart"
     *     event.
     */

    /**
     * Fired whenever an established touch contact moves within the element
     * associated with this Guacamole.Touch.
     * 
     * @event Guacamole.Touch#touchmove
     * @param {!Guacamole.Touch.Event} event
     *     A {@link Guacamole.Touch.Event} object representing the "touchmove"
     *     event.
     */

    /**
     * Fired whenever an established touch contact is lifted from the element
     * associated with this Guacamole.Touch.
     * 
     * @event Guacamole.Touch#touchend
     * @param {!Guacamole.Touch.Event} event
     *     A {@link Guacamole.Touch.Event} object representing the "touchend"
     *     event.
     */

    element.addEventListener('touchstart', function touchstart(e) {

        // Fire "ontouchstart" events for all new touches
        for (var i = 0; i < e.changedTouches.length; i++) {

            var changedTouch = e.changedTouches[i];
            var identifier = changedTouch.identifier;

            // Ignore duplicated touches
            if (guacTouch.touches[identifier])
                continue;

            var touch = guacTouch.touches[identifier] = new Guacamole.Touch.State({
                id      : identifier,
                radiusX : changedTouch.radiusX || DEFAULT_CONTACT_RADIUS,
                radiusY : changedTouch.radiusY || DEFAULT_CONTACT_RADIUS,
                angle   : changedTouch.angle || 0.0,
                force   : changedTouch.force || 1.0 /* Within JavaScript changedTouch events, a force of 0.0 indicates the device does not support reporting changedTouch force */
            });

            guacTouch.activeTouches++;

            touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY);
            guacTouch.dispatch(new Guacamole.Touch.Event('touchmove', e, touch));

        }

    }, false);

    element.addEventListener('touchmove', function touchstart(e) {

        // Fire "ontouchmove" events for all updated touches
        for (var i = 0; i < e.changedTouches.length; i++) {

            var changedTouch = e.changedTouches[i];
            var identifier = changedTouch.identifier;

            // Ignore any unrecognized touches
            var touch = guacTouch.touches[identifier];
            if (!touch)
                continue;

            // Update force only if supported by browser (otherwise, assume
            // force is unchanged)
            if (changedTouch.force)
                touch.force = changedTouch.force;

            // Update touch area, if supported by browser and device
            touch.angle = changedTouch.angle || 0.0;
            touch.radiusX = changedTouch.radiusX || DEFAULT_CONTACT_RADIUS;
            touch.radiusY = changedTouch.radiusY || DEFAULT_CONTACT_RADIUS;

            // Update with any change in position
            touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY);
            guacTouch.dispatch(new Guacamole.Touch.Event('touchmove', e, touch));

        }

    }, false);

    element.addEventListener('touchend', function touchstart(e) {

        // Fire "ontouchend" events for all updated touches
        for (var i = 0; i < e.changedTouches.length; i++) {

            var changedTouch = e.changedTouches[i];
            var identifier = changedTouch.identifier;

            // Ignore any unrecognized touches
            var touch = guacTouch.touches[identifier];
            if (!touch)
                continue;

            // Stop tracking this particular touch
            delete guacTouch.touches[identifier];
            guacTouch.activeTouches--;

            // Touch has ended
            touch.force = 0.0;

            // Update with final position
            touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY);
            guacTouch.dispatch(new Guacamole.Touch.Event('touchend', e, touch));

        }

    }, false);

};

/**
 * The current state of a touch contact.
 *
 * @constructor
 * @augments Guacamole.Position
 * @param {Guacamole.Touch.State|object} [template={}]
 *     The object whose properties should be copied within the new
 *     Guacamole.Touch.State.
 */
Guacamole.Touch.State = function State(template) {

    template = template || {};

    Guacamole.Position.call(this, template);

    /**
     * An arbitrary integer ID which uniquely identifies this contact relative
     * to other active contacts.
     *
     * @type {!number}
     * @default 0
     */
    this.id = template.id || 0;

    /**
     * The Y radius of the ellipse covering the general area of the touch
     * contact, in pixels.
     *
     * @type {!number}
     * @default 0
     */
    this.radiusX = template.radiusX || 0;

    /**
     * The X radius of the ellipse covering the general area of the touch
     * contact, in pixels.
     *
     * @type {!number}
     * @default 0
     */
    this.radiusY = template.radiusY || 0;

    /**
     * The rough angle of clockwise rotation of the general area of the touch
     * contact, in degrees.
     *
     * @type {!number}
     * @default 0.0
     */
    this.angle = template.angle || 0.0;

    /**
     * The relative force exerted by the touch contact, where 0 is no force
     * (the touch has been lifted) and 1 is maximum force (the maximum amount
     * of force representable by the device).
     *
     * @type {!number}
     * @default 1.0
     */
    this.force = template.force || 1.0;

};

/**
 * An event which represents a change in state of a single touch contact,
 * including the creation or removal of that contact. If multiple contacts are
 * involved in a touch interaction, each contact will be associated with its
 * own event.
 *
 * @constructor
 * @augments Guacamole.Event.DOMEvent
 * @param {!string} type
 *     The name of the touch event type. Possible values are "touchstart",
 *     "touchmove", and "touchend".
 *
 * @param {!TouchEvent} event
 *     The DOM touch event that produced this Guacamole.Touch.Event.
 *
 * @param {!Guacamole.Touch.State} state
 *     The state of the touch contact associated with this event.
 */
Guacamole.Touch.Event = function TouchEvent(type, event, state) {

    Guacamole.Event.DOMEvent.call(this, type, [ event ]);

    /**
     * The state of the touch contact associated with this event.
     *
     * @type {!Guacamole.Touch.State}
     */
    this.state = state;

};

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Core object providing abstract communication for Guacamole. This object
 * is a null implementation whose functions do nothing. Guacamole applications
 * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
 * on this one.
 * 
 * @constructor
 * @see Guacamole.HTTPTunnel
 */
Guacamole.Tunnel = function() {

    /**
     * Connect to the tunnel with the given optional data. This data is
     * typically used for authentication. The format of data accepted is
     * up to the tunnel implementation.
     * 
     * @param {string} [data]
     *     The data to send to the tunnel when connecting.
     */
    this.connect = function(data) {};
    
    /**
     * Disconnect from the tunnel.
     */
    this.disconnect = function() {};
    
    /**
     * Send the given message through the tunnel to the service on the other
     * side. All messages are guaranteed to be received in the order sent.
     * 
     * @param {...*} elements
     *     The elements of the message to send to the service on the other side
     *     of the tunnel.
     */
    this.sendMessage = function(elements) {};

    /**
     * Changes the stored numeric state of this tunnel, firing the onstatechange
     * event if the new state is different and a handler has been defined.
     *
     * @private
     * @param {!number} state
     *     The new state of this tunnel.
     */
    this.setState = function(state) {

        // Notify only if state changes
        if (state !== this.state) {
            this.state = state;
            if (this.onstatechange)
                this.onstatechange(state);
        }

    };

    /**
     * Changes the stored UUID that uniquely identifies this tunnel, firing the
     * onuuid event if a handler has been defined.
     *
     * @private
     * @param {string} uuid
     *     The new state of this tunnel.
     */
    this.setUUID = function setUUID(uuid) {
        this.uuid = uuid;
        if (this.onuuid)
            this.onuuid(uuid);
    };

    /**
     * Returns whether this tunnel is currently connected.
     *
     * @returns {!boolean}
     *     true if this tunnel is currently connected, false otherwise.
     */
    this.isConnected = function isConnected() {
        return this.state === Guacamole.Tunnel.State.OPEN
            || this.state === Guacamole.Tunnel.State.UNSTABLE;
    };

    /**
     * The current state of this tunnel.
     * 
     * @type {!number}
     */
    this.state = Guacamole.Tunnel.State.CLOSED;

    /**
     * The maximum amount of time to wait for data to be received, in
     * milliseconds. If data is not received within this amount of time,
     * the tunnel is closed with an error. The default value is 15000.
     *
     * @type {!number}
     */
    this.receiveTimeout = 15000;

    /**
     * The amount of time to wait for data to be received before considering
     * the connection to be unstable, in milliseconds. If data is not received
     * within this amount of time, the tunnel status is updated to warn that
     * the connection appears unresponsive and may close. The default value is
     * 1500.
     * 
     * @type {!number}
     */
    this.unstableThreshold = 1500;

    /**
     * The UUID uniquely identifying this tunnel. If not yet known, this will
     * be null.
     *
     * @type {string}
     */
    this.uuid = null;

    /**
     * Fired when the UUID that uniquely identifies this tunnel is known.
     *
     * @event
     * @param {!string}
     *     The UUID uniquely identifying this tunnel.
     */
    this.onuuid = null;

    /**
     * Fired whenever an error is encountered by the tunnel.
     * 
     * @event
     * @param {!Guacamole.Status} status
     *     A status object which describes the error.
     */
    this.onerror = null;

    /**
     * Fired whenever the state of the tunnel changes.
     * 
     * @event
     * @param {!number} state
     *     The new state of the client.
     */
    this.onstatechange = null;

    /**
     * Fired once for every complete Guacamole instruction received, in order.
     * 
     * @event
     * @param {!string} opcode
     *     The Guacamole instruction opcode.
     *
     * @param {!string[]} parameters
     *     The parameters provided for the instruction, if any.
     */
    this.oninstruction = null;

};

/**
 * The Guacamole protocol instruction opcode reserved for arbitrary internal
 * use by tunnel implementations. The value of this opcode is guaranteed to be
 * the empty string (""). Tunnel implementations may use this opcode for any
 * purpose. It is currently used by the HTTP tunnel to mark the end of the HTTP
 * response, and by the WebSocket tunnel to transmit the tunnel UUID and send
 * connection stability test pings/responses.
 *
 * @constant
 * @type {!string}
 */
Guacamole.Tunnel.INTERNAL_DATA_OPCODE = '';

/**
 * All possible tunnel states.
 *
 * @type {!Object.<string, number>}
 */
Guacamole.Tunnel.State = {

    /**
     * A connection is in pending. It is not yet known whether connection was
     * successful.
     * 
     * @type {!number}
     */
    "CONNECTING": 0,

    /**
     * Connection was successful, and data is being received.
     * 
     * @type {!number}
     */
    "OPEN": 1,

    /**
     * The connection is closed. Connection may not have been successful, the
     * tunnel may have been explicitly closed by either side, or an error may
     * have occurred.
     * 
     * @type {!number}
     */
    "CLOSED": 2,

    /**
     * The connection is open, but communication through the tunnel appears to
     * be disrupted, and the connection may close as a result.
     *
     * @type {!number}
     */
    "UNSTABLE" : 3

};

/**
 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
 * 
 * @constructor
 * @augments Guacamole.Tunnel
 *
 * @param {!string} tunnelURL
 *     The URL of the HTTP tunneling service.
 *
 * @param {boolean} [crossDomain=false]
 *     Whether tunnel requests will be cross-domain, and thus must use CORS
 *     mechanisms and headers. By default, it is assumed that tunnel requests
 *     will be made to the same domain.
 *
 * @param {object} [extraTunnelHeaders={}]
 *     Key value pairs containing the header names and values of any additional
 *     headers to be sent in tunnel requests. By default, no extra headers will
 *     be added.
 */
Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {

    /**
     * Reference to this HTTP tunnel.
     *
     * @private
     * @type {!Guacamole.HTTPTunnel}
     */
    var tunnel = this;

    var TUNNEL_CONNECT = tunnelURL + "?connect";
    var TUNNEL_READ    = tunnelURL + "?read:";
    var TUNNEL_WRITE   = tunnelURL + "?write:";

    var POLLING_ENABLED     = 1;
    var POLLING_DISABLED    = 0;

    // Default to polling - will be turned off automatically if not needed
    var pollingMode = POLLING_ENABLED;

    var sendingMessages = false;
    var outputMessageBuffer = "";

    // If requests are expected to be cross-domain, the cookie that the HTTP
    // tunnel depends on will only be sent if withCredentials is true
    var withCredentials = !!crossDomain;

    /**
     * The current receive timeout ID, if any.
     *
     * @private
     * @type {number}
     */
    var receive_timeout = null;

    /**
     * The current connection stability timeout ID, if any.
     *
     * @private
     * @type {number}
     */
    var unstableTimeout = null;

    /**
     * The current connection stability test ping interval ID, if any. This
     * will only be set upon successful connection.
     *
     * @private
     * @type {number}
     */
    var pingInterval = null;

    /**
     * The number of milliseconds to wait between connection stability test
     * pings.
     *
     * @private
     * @constant
     * @type {!number}
     */
    var PING_FREQUENCY = 500;

    /**
     * Additional headers to be sent in tunnel requests. This dictionary can be
     * populated with key/value header pairs to pass information such as authentication
     * tokens, etc.
     *
     * @private
     * @type {!object}
     */
    var extraHeaders = extraTunnelHeaders || {};

    /**
     * The name of the HTTP header containing the session token specific to the
     * HTTP tunnel implementation.
     *
     * @private
     * @constant
     * @type {!string}
     */
    var TUNNEL_TOKEN_HEADER = 'Guacamole-Tunnel-Token';

    /**
     * The session token currently assigned to this HTTP tunnel. All distinct
     * HTTP tunnel connections will have their own dedicated session token.
     *
     * @private
     * @type {string}
     */
    var tunnelSessionToken = null;

    /**
     * Adds the configured additional headers to the given request.
     *
     * @private
     * @param {!XMLHttpRequest} request
     *     The request where the configured extra headers will be added.
     *
     * @param {!object} headers
     *     The headers to be added to the request.
     */
    function addExtraHeaders(request, headers) {
        for (var name in headers) {
            request.setRequestHeader(name, headers[name]);
        }
    }

    /**
     * Resets the state of timers tracking network activity and stability. If
     * those timers are not yet started, invoking this function starts them.
     * This function should be invoked when the tunnel is established and every
     * time there is network activity on the tunnel, such that the timers can
     * safely assume the network and/or server are not responding if this
     * function has not been invoked for a significant period of time.
     *
     * @private
     */
    var resetTimers = function resetTimers() {

        // Get rid of old timeouts (if any)
        window.clearTimeout(receive_timeout);
        window.clearTimeout(unstableTimeout);

        // Clear unstable status
        if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
            tunnel.setState(Guacamole.Tunnel.State.OPEN);

        // Set new timeout for tracking overall connection timeout
        receive_timeout = window.setTimeout(function () {
            close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
        }, tunnel.receiveTimeout);

        // Set new timeout for tracking suspected connection instability
        unstableTimeout = window.setTimeout(function() {
            tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
        }, tunnel.unstableThreshold);

    };

    /**
     * Closes this tunnel, signaling the given status and corresponding
     * message, which will be sent to the onerror handler if the status is
     * an error status.
     * 
     * @private
     * @param {!Guacamole.Status} status
     *     The status causing the connection to close;
     */
    function close_tunnel(status) {

        // Get rid of old timeouts (if any)
        window.clearTimeout(receive_timeout);
        window.clearTimeout(unstableTimeout);

        // Cease connection test pings
        window.clearInterval(pingInterval);

        // Ignore if already closed
        if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
            return;

        // If connection closed abnormally, signal error.
        if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) {

            // Ignore RESOURCE_NOT_FOUND if we've already connected, as that
            // only signals end-of-stream for the HTTP tunnel.
            if (tunnel.state === Guacamole.Tunnel.State.CONNECTING
                    || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND)
                tunnel.onerror(status);

        }

        // Reset output message buffer
        sendingMessages = false;

        // Mark as closed
        tunnel.setState(Guacamole.Tunnel.State.CLOSED);

    }


    this.sendMessage = function() {

        // Do not attempt to send messages if not connected
        if (!tunnel.isConnected())
            return;

        // Do not attempt to send empty messages
        if (!arguments.length)
            return;

        // Add message to buffer
        outputMessageBuffer += Guacamole.Parser.toInstruction(arguments);

        // Send if not currently sending
        if (!sendingMessages)
            sendPendingMessages();

    };

    function sendPendingMessages() {

        // Do not attempt to send messages if not connected
        if (!tunnel.isConnected())
            return;

        if (outputMessageBuffer.length > 0) {

            sendingMessages = true;

            var message_xmlhttprequest = new XMLHttpRequest();
            message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel.uuid);
            message_xmlhttprequest.withCredentials = withCredentials;
            addExtraHeaders(message_xmlhttprequest, extraHeaders);
            message_xmlhttprequest.setRequestHeader("Content-type", "application/octet-stream");
            message_xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken);

            // Once response received, send next queued event.
            message_xmlhttprequest.onreadystatechange = function() {
                if (message_xmlhttprequest.readyState === 4) {

                    resetTimers();

                    // If an error occurs during send, handle it
                    if (message_xmlhttprequest.status !== 200)
                        handleHTTPTunnelError(message_xmlhttprequest);

                    // Otherwise, continue the send loop
                    else
                        sendPendingMessages();

                }
            };

            message_xmlhttprequest.send(outputMessageBuffer);
            outputMessageBuffer = ""; // Clear buffer

        }
        else
            sendingMessages = false;

    }

    function handleHTTPTunnelError(xmlhttprequest) {

        // Pull status code directly from headers provided by Guacamole
        var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code"));
        if (code) {
            var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message");
            close_tunnel(new Guacamole.Status(code, message));
        }

        // Failing that, derive a Guacamole status code from the HTTP status
        // code provided by the browser
        else if (xmlhttprequest.status)
            close_tunnel(new Guacamole.Status(
                Guacamole.Status.Code.fromHTTPCode(xmlhttprequest.status),
                    xmlhttprequest.statusText));

        // Otherwise, assume server is unreachable
        else
            close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));

    }

    function handleResponse(xmlhttprequest) {

        var interval = null;
        var nextRequest = null;

        var dataUpdateEvents = 0;

        var parser = new Guacamole.Parser();
        parser.oninstruction = function instructionReceived(opcode, args) {

            // Switch to next request if end-of-stream is signalled
            if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE && args.length === 0) {

                // Reset parser state by simply switching to an entirely new
                // parser
                parser = new Guacamole.Parser();
                parser.oninstruction = instructionReceived;

                // Clean up interval if polling
                if (interval)
                    clearInterval(interval);

                // Clean up object
                xmlhttprequest.onreadystatechange = null;
                xmlhttprequest.abort();

                // Start handling next request
                if (nextRequest)
                    handleResponse(nextRequest);

            }

            // Call instruction handler.
            else if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction)
                tunnel.oninstruction(opcode, args);

        };

        function parseResponse() {

            // Do not handle responses if not connected
            if (!tunnel.isConnected()) {
                
                // Clean up interval if polling
                if (interval !== null)
                    clearInterval(interval);
                
                return;
            }

            // Do not parse response yet if not ready
            if (xmlhttprequest.readyState < 2) return;

            // Attempt to read status
            var status;
            try { status = xmlhttprequest.status; }

            // If status could not be read, assume successful.
            catch (e) { status = 200; }

            // Start next request as soon as possible IF request was successful
            if (!nextRequest && status === 200)
                nextRequest = makeRequest();

            // Parse stream when data is received and when complete.
            if (xmlhttprequest.readyState === 3 ||
                xmlhttprequest.readyState === 4) {

                resetTimers();

                // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
                if (pollingMode === POLLING_ENABLED) {
                    if (xmlhttprequest.readyState === 3 && !interval)
                        interval = setInterval(parseResponse, 30);
                    else if (xmlhttprequest.readyState === 4 && interval)
                        clearInterval(interval);
                }

                // If canceled, stop transfer
                if (xmlhttprequest.status === 0) {
                    tunnel.disconnect();
                    return;
                }

                // Halt on error during request
                else if (xmlhttprequest.status !== 200) {
                    handleHTTPTunnelError(xmlhttprequest);
                    return;
                }

                // Attempt to read in-progress data
                var current;
                try { current = xmlhttprequest.responseText; }

                // Do not attempt to parse if data could not be read
                catch (e) { return; }

                try {
                    parser.receive(current, true);
                }
                catch (e) {
                    close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, e.message));
                    return;
                }

            }

        }

        // If response polling enabled, attempt to detect if still
        // necessary (via wrapping parseResponse())
        if (pollingMode === POLLING_ENABLED) {
            xmlhttprequest.onreadystatechange = function() {

                // If we receive two or more readyState==3 events,
                // there is no need to poll.
                if (xmlhttprequest.readyState === 3) {
                    dataUpdateEvents++;
                    if (dataUpdateEvents >= 2) {
                        pollingMode = POLLING_DISABLED;
                        xmlhttprequest.onreadystatechange = parseResponse;
                    }
                }

                parseResponse();
            };
        }

        // Otherwise, just parse
        else
            xmlhttprequest.onreadystatechange = parseResponse;

        parseResponse();

    }

    /**
     * Arbitrary integer, unique for each tunnel read request.
     * @private
     */
    var request_id = 0;

    function makeRequest() {

        // Make request, increment request ID
        var xmlhttprequest = new XMLHttpRequest();
        xmlhttprequest.open("GET", TUNNEL_READ + tunnel.uuid + ":" + (request_id++));
        xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken);
        xmlhttprequest.withCredentials = withCredentials;
        addExtraHeaders(xmlhttprequest, extraHeaders);
        xmlhttprequest.send(null);

        return xmlhttprequest;

    }

    this.connect = function(data) {

        // Start waiting for connect
        resetTimers();

        // Mark the tunnel as connecting
        tunnel.setState(Guacamole.Tunnel.State.CONNECTING);

        // Start tunnel and connect
        var connect_xmlhttprequest = new XMLHttpRequest();
        connect_xmlhttprequest.onreadystatechange = function() {

            if (connect_xmlhttprequest.readyState !== 4)
                return;

            // If failure, throw error
            if (connect_xmlhttprequest.status !== 200) {
                handleHTTPTunnelError(connect_xmlhttprequest);
                return;
            }

            resetTimers();

            // Get UUID and HTTP-specific tunnel session token from response
            tunnel.setUUID(connect_xmlhttprequest.responseText);
            tunnelSessionToken = connect_xmlhttprequest.getResponseHeader(TUNNEL_TOKEN_HEADER);

            // Fail connect attempt if token is not successfully assigned
            if (!tunnelSessionToken) {
                close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
                return;
            }

            // Mark as open
            tunnel.setState(Guacamole.Tunnel.State.OPEN);

            // Ping tunnel endpoint regularly to test connection stability
            pingInterval = setInterval(function sendPing() {
                tunnel.sendMessage("nop");
            }, PING_FREQUENCY);

            // Start reading data
            handleResponse(makeRequest());

        };

        connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true);
        connect_xmlhttprequest.withCredentials = withCredentials;
        addExtraHeaders(connect_xmlhttprequest, extraHeaders);
        connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
        connect_xmlhttprequest.send(data);

    };

    this.disconnect = function() {
        close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
    };

};

Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();

/**
 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
 * 
 * @constructor
 * @augments Guacamole.Tunnel
 * @param {!string} tunnelURL
 *     The URL of the WebSocket tunneling service.
 */
Guacamole.WebSocketTunnel = function(tunnelURL) {

    /**
     * Reference to this WebSocket tunnel.
     *
     * @private
     * @type {Guacamole.WebSocketTunnel}
     */
    var tunnel = this;

    /**
     * The parser that this tunnel will use to parse received Guacamole
     * instructions. The parser is created when the tunnel is (re-)connected.
     * Initially, this will be null.
     *
     * @private
     * @type {Guacamole.Parser}
     */
    var parser = null;

    /**
     * The WebSocket used by this tunnel.
     * 
     * @private
     * @type {WebSocket}
     */
    var socket = null;

    /**
     * The current receive timeout ID, if any.
     *
     * @private
     * @type {number}
     */
    var receive_timeout = null;

    /**
     * The current connection stability timeout ID, if any.
     *
     * @private
     * @type {number}
     */
    var unstableTimeout = null;

    /**
     * The current connection stability test ping timeout ID, if any. This
     * will only be set upon successful connection.
     *
     * @private
     * @type {number}
     */
    var pingTimeout = null;

    /**
     * The WebSocket protocol corresponding to the protocol used for the current
     * location.
     *
     * @private
     * @type {!Object.<string, string>}
     */
    var ws_protocol = {
        "http:":  "ws:",
        "https:": "wss:"
    };

    /**
     * The number of milliseconds to wait between connection stability test
     * pings.
     *
     * @private
     * @constant
     * @type {!number}
     */
    var PING_FREQUENCY = 500;

    /**
     * The timestamp of the point in time that the last connection stability
     * test ping was sent, in milliseconds elapsed since midnight of January 1,
     * 1970 UTC.
     *
     * @private
     * @type {!number}
     */
    var lastSentPing = 0;

    // Transform current URL to WebSocket URL

    // If not already a websocket URL
    if (   tunnelURL.substring(0, 3) !== "ws:"
        && tunnelURL.substring(0, 4) !== "wss:") {

        var protocol = ws_protocol[window.location.protocol];

        // If absolute URL, convert to absolute WS URL
        if (tunnelURL.substring(0, 1) === "/")
            tunnelURL =
                protocol
                + "//" + window.location.host
                + tunnelURL;

        // Otherwise, construct absolute from relative URL
        else {

            // Get path from pathname
            var slash = window.location.pathname.lastIndexOf("/");
            var path  = window.location.pathname.substring(0, slash + 1);

            // Construct absolute URL
            tunnelURL =
                protocol
                + "//" + window.location.host
                + path
                + tunnelURL;

        }

    }

    /**
     * Sends an internal "ping" instruction to the Guacamole WebSocket
     * endpoint, verifying network connection stability. If the network is
     * stable, the Guacamole server will receive this instruction and respond
     * with an identical ping.
     *
     * @private
     */
    var sendPing = function sendPing() {
        var currentTime = new Date().getTime();
        tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', currentTime);
        lastSentPing = currentTime;
    };

    /**
     * Resets the state of timers tracking network activity and stability. If
     * those timers are not yet started, invoking this function starts them.
     * This function should be invoked when the tunnel is established and every
     * time there is network activity on the tunnel, such that the timers can
     * safely assume the network and/or server are not responding if this
     * function has not been invoked for a significant period of time.
     *
     * @private
     */
    var resetTimers = function resetTimers() {

        // Get rid of old timeouts (if any)
        window.clearTimeout(receive_timeout);
        window.clearTimeout(unstableTimeout);
        window.clearTimeout(pingTimeout);

        // Clear unstable status
        if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
            tunnel.setState(Guacamole.Tunnel.State.OPEN);

        // Set new timeout for tracking overall connection timeout
        receive_timeout = window.setTimeout(function () {
            close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
        }, tunnel.receiveTimeout);

        // Set new timeout for tracking suspected connection instability
        unstableTimeout = window.setTimeout(function() {
            tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
        }, tunnel.unstableThreshold);

        var currentTime = new Date().getTime();
        var pingDelay = Math.max(lastSentPing + PING_FREQUENCY - currentTime, 0);

        // Ping tunnel endpoint regularly to test connection stability, sending
        // the ping immediately if enough time has already elapsed
        if (pingDelay > 0)
            pingTimeout = window.setTimeout(sendPing, pingDelay);
        else
            sendPing();

    };

    /**
     * Closes this tunnel, signaling the given status and corresponding
     * message, which will be sent to the onerror handler if the status is
     * an error status.
     * 
     * @private
     * @param {!Guacamole.Status} status
     *     The status causing the connection to close;
     */
    function close_tunnel(status) {

        // Get rid of old timeouts (if any)
        window.clearTimeout(receive_timeout);
        window.clearTimeout(unstableTimeout);
        window.clearTimeout(pingTimeout);

        // Ignore if already closed
        if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
            return;

        // If connection closed abnormally, signal error.
        if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror)
            tunnel.onerror(status);

        // Mark as closed
        tunnel.setState(Guacamole.Tunnel.State.CLOSED);

        socket.close();

    }

    this.sendMessage = function(elements) {

        // Do not attempt to send messages if not connected
        if (!tunnel.isConnected())
            return;

        // Do not attempt to send empty messages
        if (!arguments.length)
            return;

        socket.send(Guacamole.Parser.toInstruction(arguments));

    };

    this.connect = function(data) {

        resetTimers();

        // Mark the tunnel as connecting
        tunnel.setState(Guacamole.Tunnel.State.CONNECTING);

        parser = new Guacamole.Parser();
        parser.oninstruction = function instructionReceived(opcode, args) {

            // Update state and UUID when first instruction received
            if (tunnel.uuid === null) {

                // Associate tunnel UUID if received
                if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE && args.length === 1)
                    tunnel.setUUID(args[0]);

                // Tunnel is now open and UUID is available
                tunnel.setState(Guacamole.Tunnel.State.OPEN);

            }

            // Call instruction handler.
            if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction)
                tunnel.oninstruction(opcode, args);

        };

        // Connect socket
        socket = new WebSocket(tunnelURL + "?" + data, "guacamole");

        socket.onopen = function(event) {
            resetTimers();
        };

        socket.onclose = function(event) {

            // Pull status code directly from closure reason provided by Guacamole
            if (event.reason)
                close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason));

            // Failing that, derive a Guacamole status code from the WebSocket
            // status code provided by the browser
            else if (event.code)
                close_tunnel(new Guacamole.Status(Guacamole.Status.Code.fromWebSocketCode(event.code)));

            // Otherwise, assume server is unreachable
            else
                close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));

        };
        
        socket.onmessage = function(event) {

            resetTimers();

            try {
                parser.receive(event.data);
            }
            catch (e) {
                close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, e.message));
            }

        };

    };

    this.disconnect = function() {
        close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
    };

};

Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();

/**
 * Guacamole Tunnel which cycles between all specified tunnels until
 * no tunnels are left. Another tunnel is used if an error occurs but
 * no instructions have been received. If an instruction has been
 * received, or no tunnels remain, the error is passed directly out
 * through the onerror handler (if defined).
 * 
 * @constructor
 * @augments Guacamole.Tunnel
 * @param {...Guacamole.Tunnel} tunnelChain
 *     The tunnels to use, in order of priority.
 */
Guacamole.ChainedTunnel = function(tunnelChain) {

    /**
     * Reference to this chained tunnel.
     * @private
     */
    var chained_tunnel = this;

    /**
     * Data passed in via connect(), to be used for
     * wrapped calls to other tunnels' connect() functions.
     * @private
     */
    var connect_data;

    /**
     * Array of all tunnels passed to this ChainedTunnel through the
     * constructor arguments.
     * @private
     */
    var tunnels = [];

    /**
     * The tunnel committed via commit_tunnel(), if any, or null if no tunnel
     * has yet been committed.
     *
     * @private
     * @type {Guacamole.Tunnel}
     */
    var committedTunnel = null;

    // Load all tunnels into array
    for (var i=0; i<arguments.length; i++)
        tunnels.push(arguments[i]);

    /**
     * Sets the current tunnel.
     * 
     * @private
     * @param {!Guacamole.Tunnel} tunnel
     *     The tunnel to set as the current tunnel.
     */
    function attach(tunnel) {

        // Set own functions to tunnel's functions
        chained_tunnel.disconnect  = tunnel.disconnect;
        chained_tunnel.sendMessage = tunnel.sendMessage;

        /**
         * Fails the currently-attached tunnel, attaching a new tunnel if
         * possible.
         *
         * @private
         * @param {Guacamole.Status} [status]
         *     An object representing the failure that occured in the
         *     currently-attached tunnel, if known.
         *
         * @return {Guacamole.Tunnel}
         *     The next tunnel, or null if there are no more tunnels to try or
         *     if no more tunnels should be tried.
         */
        var failTunnel = function failTunnel(status) {

            // Do not attempt to continue using next tunnel on server timeout
            if (status && status.code === Guacamole.Status.Code.UPSTREAM_TIMEOUT) {
                tunnels = [];
                return null;
            }

            // Get next tunnel
            var next_tunnel = tunnels.shift();

            // If there IS a next tunnel, try using it.
            if (next_tunnel) {
                tunnel.onerror = null;
                tunnel.oninstruction = null;
                tunnel.onstatechange = null;
                attach(next_tunnel);
            }

            return next_tunnel;

        };

        /**
         * Use the current tunnel from this point forward. Do not try any more
         * tunnels, even if the current tunnel fails.
         * 
         * @private
         */
        function commit_tunnel() {

            tunnel.onstatechange = chained_tunnel.onstatechange;
            tunnel.oninstruction = chained_tunnel.oninstruction;
            tunnel.onerror = chained_tunnel.onerror;

            // Assign UUID if already known
            if (tunnel.uuid)
                chained_tunnel.setUUID(tunnel.uuid);

            // Assign any future received UUIDs such that they are
            // accessible from the main uuid property of the chained tunnel
            tunnel.onuuid = function uuidReceived(uuid) {
                chained_tunnel.setUUID(uuid);
            };

            committedTunnel = tunnel;

        }

        // Wrap own onstatechange within current tunnel
        tunnel.onstatechange = function(state) {

            switch (state) {

                // If open, use this tunnel from this point forward.
                case Guacamole.Tunnel.State.OPEN:
                    commit_tunnel();
                    if (chained_tunnel.onstatechange)
                        chained_tunnel.onstatechange(state);
                    break;

                // If closed, mark failure, attempt next tunnel
                case Guacamole.Tunnel.State.CLOSED:
                    if (!failTunnel() && chained_tunnel.onstatechange)
                        chained_tunnel.onstatechange(state);
                    break;
                
            }

        };

        // Wrap own oninstruction within current tunnel
        tunnel.oninstruction = function(opcode, elements) {

            // Accept current tunnel
            commit_tunnel();

            // Invoke handler
            if (chained_tunnel.oninstruction)
                chained_tunnel.oninstruction(opcode, elements);

        };

        // Attach next tunnel on error
        tunnel.onerror = function(status) {

            // Mark failure, attempt next tunnel
            if (!failTunnel(status) && chained_tunnel.onerror)
                chained_tunnel.onerror(status);

        };

        // Attempt connection
        tunnel.connect(connect_data);
        
    }

    this.connect = function(data) {
       
        // Remember connect data
        connect_data = data;

        // Get committed tunnel if exists or the first tunnel on the list
        var next_tunnel = committedTunnel ? committedTunnel : tunnels.shift();

        // Attach first tunnel
        if (next_tunnel)
            attach(next_tunnel);

        // If there IS no first tunnel, error
        else if (chained_tunnel.onerror)
            chained_tunnel.onerror(Guacamole.Status.Code.SERVER_ERROR, "No tunnels to try.");

    };
    
};

Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();

/**
 * Guacamole Tunnel which replays a Guacamole protocol dump from a static file
 * received via HTTP. Instructions within the file are parsed and handled as
 * quickly as possible, while the file is being downloaded.
 *
 * @constructor
 * @augments Guacamole.Tunnel
 * @param {!string} url
 *     The URL of a Guacamole protocol dump.
 *
 * @param {boolean} [crossDomain=false]
 *     Whether tunnel requests will be cross-domain, and thus must use CORS
 *     mechanisms and headers. By default, it is assumed that tunnel requests
 *     will be made to the same domain.
 *
 * @param {object} [extraTunnelHeaders={}]
 *     Key value pairs containing the header names and values of any additional
 *     headers to be sent in tunnel requests. By default, no extra headers will
 *     be added.
 */
Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTunnelHeaders) {

    /**
     * Reference to this Guacamole.StaticHTTPTunnel.
     *
     * @private
     */
    var tunnel = this;

    /**
     * AbortController instance which allows the current, in-progress HTTP
     * request to be aborted. If no request is currently in progress, this will
     * be null.
     *
     * @private
     * @type {AbortController}
     */
    var abortController = null;

    /**
     * Additional headers to be sent in tunnel requests. This dictionary can be
     * populated with key/value header pairs to pass information such as authentication
     * tokens, etc.
     *
     * @private
     * @type {!object}
     */
    var extraHeaders = extraTunnelHeaders || {};

    /**
     * The number of bytes in the file being downloaded, or null if this is not
     * known.
     *
     * @type {number}
     */
    this.size = null;

    this.sendMessage = function sendMessage(elements) {
        // Do nothing
    };

    this.connect = function connect(data) {

        // Ensure any existing connection is killed
        tunnel.disconnect();

        // Connection is now starting
        tunnel.setState(Guacamole.Tunnel.State.CONNECTING);

        // Create Guacamole protocol and UTF-8 parsers specifically for this
        // connection
        var parser = new Guacamole.Parser();
        var utf8Parser = new Guacamole.UTF8Parser();

        // Invoke tunnel's oninstruction handler for each parsed instruction
        parser.oninstruction = function instructionReceived(opcode, args) {
            if (tunnel.oninstruction)
                tunnel.oninstruction(opcode, args);
        };

        // Allow new request to be aborted
        abortController = new AbortController();

        // Stream using the Fetch API
        fetch(url, {
            headers : extraHeaders,
            credentials : crossDomain ? 'include' : 'same-origin',
            signal : abortController.signal
        })
        .then(function gotResponse(response) {

            // Reset state and close upon error
            if (!response.ok) {

                if (tunnel.onerror)
                    tunnel.onerror(new Guacamole.Status(
                        Guacamole.Status.Code.fromHTTPCode(response.status), response.statusText));

                tunnel.disconnect();
                return;

            }

            // Report overall size of stream in bytes, if known
            tunnel.size = response.headers.get('Content-Length');

            // Connection is open
            tunnel.setState(Guacamole.Tunnel.State.OPEN);

            var reader = response.body.getReader();
            var processReceivedText = function processReceivedText(result) {

                // Clean up and close when done
                if (result.done) {
                    tunnel.disconnect();
                    return;
                }

                // Parse only the portion of data which is newly received
                parser.receive(utf8Parser.decode(result.value));

                // Continue parsing when next chunk is received
                reader.read().then(processReceivedText);

            };

            // Schedule parse of first chunk
            reader.read().then(processReceivedText);

        });

    };

    this.disconnect = function disconnect() {

        // Abort any in-progress request
        if (abortController) {
            abortController.abort();
            abortController = null;
        }

        // Connection is now closed
        tunnel.setState(Guacamole.Tunnel.State.CLOSED);

    };

};

Guacamole.StaticHTTPTunnel.prototype = new Guacamole.Tunnel();

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Parser that decodes UTF-8 text from a series of provided ArrayBuffers.
 * Multi-byte characters that continue from one buffer to the next are handled
 * correctly.
 *
 * @constructor
 */
Guacamole.UTF8Parser = function UTF8Parser() {

    /**
     * The number of bytes remaining for the current codepoint.
     *
     * @private
     * @type {!number}
     */
    var bytesRemaining = 0;

    /**
     * The current codepoint value, as calculated from bytes read so far.
     *
     * @private
     * @type {!number}
     */
    var codepoint = 0;

    /**
     * Decodes the given UTF-8 data into a Unicode string, returning a string
     * containing all complete UTF-8 characters within the provided data. The
     * data may end in the middle of a multi-byte character, in which case the
     * complete character will be returned from a later call to decode() after
     * enough bytes have been provided.
     *
     * @private
     * @param {!ArrayBuffer} buffer
     *     Arbitrary UTF-8 data.
     *
     * @return {!string}
     *     The decoded Unicode string.
     */
    this.decode = function decode(buffer) {

        var text = '';

        var bytes = new Uint8Array(buffer);
        for (var i=0; i<bytes.length; i++) {

            // Get current byte
            var value = bytes[i];

            // Start new codepoint if nothing yet read
            if (bytesRemaining === 0) {

                // 1 byte (0xxxxxxx)
                if ((value | 0x7F) === 0x7F)
                    text += String.fromCharCode(value);

                // 2 byte (110xxxxx)
                else if ((value | 0x1F) === 0xDF) {
                    codepoint = value & 0x1F;
                    bytesRemaining = 1;
                }

                // 3 byte (1110xxxx)
                else if ((value | 0x0F )=== 0xEF) {
                    codepoint = value & 0x0F;
                    bytesRemaining = 2;
                }

                // 4 byte (11110xxx)
                else if ((value | 0x07) === 0xF7) {
                    codepoint = value & 0x07;
                    bytesRemaining = 3;
                }

                // Invalid byte
                else
                    text += '\uFFFD';

            }

            // Continue existing codepoint (10xxxxxx)
            else if ((value | 0x3F) === 0xBF) {

                codepoint = (codepoint << 6) | (value & 0x3F);
                bytesRemaining--;

                // Write codepoint if finished
                if (bytesRemaining === 0)
                    text += String.fromCharCode(codepoint);

            }

            // Invalid byte
            else {
                bytesRemaining = 0;
                text += '\uFFFD';
            }

        }

        return text;

    };

};
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * The unique ID of this version of the Guacamole JavaScript API. This ID will
 * be the version string of the guacamole-common-js Maven project, and can be
 * used in downstream applications as a sanity check that the proper version
 * of the APIs is being used (in case an older version is cached, for example).
 *
 * @type {!string}
 */
Guacamole.API_VERSION = "1.5.3";

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * Abstract video player which accepts, queues and plays back arbitrary video
 * data. It is up to implementations of this class to provide some means of
 * handling a provided Guacamole.InputStream and rendering the received data to
 * the provided Guacamole.Display.VisibleLayer. Data received along the
 * provided stream is to be played back immediately.
 *
 * @constructor
 */
Guacamole.VideoPlayer = function VideoPlayer() {

    /**
     * Notifies this Guacamole.VideoPlayer that all video up to the current
     * point in time has been given via the underlying stream, and that any
     * difference in time between queued video data and the current time can be
     * considered latency.
     */
    this.sync = function sync() {
        // Default implementation - do nothing
    };

};

/**
 * Determines whether the given mimetype is supported by any built-in
 * implementation of Guacamole.VideoPlayer, and thus will be properly handled
 * by Guacamole.VideoPlayer.getInstance().
 *
 * @param {!string} mimetype
 *     The mimetype to check.
 *
 * @returns {!boolean}
 *     true if the given mimetype is supported by any built-in
 *     Guacamole.VideoPlayer, false otherwise.
 */
Guacamole.VideoPlayer.isSupportedType = function isSupportedType(mimetype) {

    // There are currently no built-in video players (and therefore no
    // supported types)
    return false;

};

/**
 * Returns a list of all mimetypes supported by any built-in
 * Guacamole.VideoPlayer, in rough order of priority. Beware that only the core
 * mimetypes themselves will be listed. Any mimetype parameters, even required
 * ones, will not be included in the list.
 *
 * @returns {!string[]}
 *     A list of all mimetypes supported by any built-in Guacamole.VideoPlayer,
 *     excluding any parameters.
 */
Guacamole.VideoPlayer.getSupportedTypes = function getSupportedTypes() {

    // There are currently no built-in video players (and therefore no
    // supported types)
    return [];

};

/**
 * Returns an instance of Guacamole.VideoPlayer providing support for the given
 * video format. If support for the given video format is not available, null
 * is returned.
 *
 * @param {!Guacamole.InputStream} stream
 *     The Guacamole.InputStream to read video data from.
 *
 * @param {!Guacamole.Display.VisibleLayer} layer
 *     The destination layer in which this Guacamole.VideoPlayer should play
 *     the received video data.
 *
 * @param {!string} mimetype
 *     The mimetype of the video data in the provided stream.
 *
 * @return {Guacamole.VideoPlayer}
 *     A Guacamole.VideoPlayer instance supporting the given mimetype and
 *     reading from the given stream, or null if support for the given mimetype
 *     is absent.
 */
Guacamole.VideoPlayer.getInstance = function getInstance(stream, layer, mimetype) {

    // There are currently no built-in video players
    return null;

};
module.exports = Guacamole;