// @tag enterprise
/**
 * @class Ext.data.amf.Packet
 * This class represents an Action Message Format (AMF) Packet.  It contains all
 * the logic required to decode an AMF Packet from a byte array.
 * To decode a Packet, first construct a Packet:
 *
 *     var packet = Ext.create('Ext.data.amf.Packet');
 *
 * Then use the decode method to decode an AMF byte array:
 *
 *     packet.decode(bytes);
 *
 * where "bytes" is a Uint8Array or an array of numbers representing the binary
 * AMF data.
 *
 * To access the decoded data use the #version, #headers, and #messages properties:
 *
 *     console.log(packet.version, packet.headers, packet.messages);
 *
 * For more information on working with AMF data please refer to the
 * [AMF Guide](../guides/backend_connectors/amf.html).
 */
/* global ActiveXObject */
Ext.define('Ext.data.amf.Packet', function() {
    var twoPowN52 = Math.pow(2, -52),
        twoPow8 = Math.pow(2, 8),
        pos = 0,
        bytes, strings, objects, traits;
 
    return {
        /**
         * @property {Array} headers
         * @readonly
         * The decoded headers. Each header has the following properties:
         *
         * - `name`: String
         * The header name. Typically identifies a remote operation or method to
         * be invoked by this context header.
         * - `mustUnderstand`: Boolean
         * If `true` this flag instructs the endpoint to abort and generate an
         * error if the header is not understood.
         * - `byteLength`: Number
         * If the byte-length of a header is known it can be specified to optimize
         * memory allocation at the remote endpoint.
         * - `value`: Mixed
         * The header value
         */
 
        /**
         * @property {Array} messages
         * @readonly
         * The decoded messages. Each message has the following properties:
         *
         * - `targetURI`: String
         * Describes which operation, function, or method is to be remotely
         * invoked.
         * - `responseURI`: String
         * A unique operation name
         * - `byteLength`: Number
         * Optional byte-length of the message body
         * - `body`: Mixed
         * The message body
         */
 
        /**
         * @property {Number} version
         * @readonly
         * The AMF version number (0 or 3)
         */
 
        /**
         * Mapping of AMF data types to the names of the methods responsible for
         * reading them.
         * @private
         */
        typeMap: {
            // AMF0 mapping
            0: {
                0: 'readDouble',
                1: 'readBoolean',
                2: 'readAmf0String',
                3: 'readAmf0Object',
                5: 'readNull',
                6: 'readUndefined',
                7: 'readReference',
                8: 'readEcmaArray',
                10: 'readStrictArray',
                11: 'readAmf0Date',
                12: 'readLongString',
                13: 'readUnsupported',
                15: 'readAmf0Xml',
                16: 'readTypedObject'
            },
            // AMF3 mapping
            3: {
                0: 'readUndefined',
                1: 'readNull',
                2: 'readFalse',
                3: 'readTrue',
                4: 'readUInt29',
                5: 'readDouble',
                6: 'readAmf3String',
                7: 'readAmf3Xml',
                8: 'readAmf3Date',
                9: 'readAmf3Array',
                10: 'readAmf3Object',
                11: 'readAmf3Xml',
                12: 'readByteArray'
            }
        },
 
        /**
         * Decodes an AMF btye array and sets the decoded data as the
         * Packet's #version, #headers, and #messages properties
         * @param {Array} byteArray A byte array containing the encoded AMF data.
         * @return {Ext.data.amf.Packet} this AMF Packet
         */
        decode: function(byteArray) {
            var me = this,
                headers = me.headers = [],
                messages = me.messages = [],
                headerCount, messageCount;
 
            pos = 0;
 
            bytes = me.bytes = byteArray;
 
            // The strings array holds references to all of the deserialized
            // AMF3 strings for a given header value or message body so that
            // repeat instances of the same string can be deserialized by
            // reference
            strings = me.strings = [];
 
            // The objects array holds references to deserialized objects so
            // that repeat occurrences of the same object instance in the byte
            // array can be deserialized by reference.
            // If version is AMF0 this array holds anonymous objects, typed
            // objects, arrays, and ecma-arrays.
            // If version is AMF3 this array holds instances of Object, Array, XML,
            // XMLDocument, ByteArray, Date, and instances of user defined Classes
            objects = me.objects = [];
 
            // The traits array holds references to the "traits" (the
            // characteristics of objects that define a strong type such as the
            // class name and public member names) of deserialized AMF3 objects
            // so that if they are repeated they can be deserialized by reference.
            traits = me.traits = [];
 
            // The first two bytes of an AMF packet contain the AMF version
            // as an unsigned 16 bit integer.
            me.version = me.readUInt(2);
 
            // the next 2 bytes contain the header count
            for (headerCount = me.readUInt(2); headerCount--;) {
                headers.push({
                    name: me.readAmf0String(),
                    mustUnderstand: me.readBoolean(),
                    byteLength: me.readUInt(4),
                    value: me.readValue()
                });
 
                // reset references (reference indices are local to each header)
                strings = me.strings = [];
                objects = me.objects = [];
                traits = me.traits = [];
            }
 
            // The 2 bytes immediately after the header contain the message count.
            for (messageCount = me.readUInt(2); messageCount--;) {
                messages.push({
                    targetURI: me.readAmf0String(),
                    responseURI: me.readAmf0String(),
                    byteLength: me.readUInt(4),
                    body: me.readValue()
                });
 
                // reset references (reference indices are local to each message)
                strings = me.strings = [];
                objects = me.objects = [];
                traits = me.traits = [];
            }
 
            // reset the pointer
            pos = 0;
 
            // null the bytes array and reference arrays to free up memory.
            bytes = strings = objects = traits =
                me.bytes = me.strings = me.objects = me.traits = null;
 
            return me;
        },
 
        /**
         * Decodes an AMF3 byte array and that has one value and returns it.
         * Note: Destroys previously stored data in this Packet.
         * @param {Array} byteArray A byte array containing the encoded AMF data.
         * @return {Object} the decoded object
         */
        decodeValue: function(byteArray) {
            var me = this;
 
            bytes = me.bytes = byteArray;
 
            // reset the pointer
            pos = 0;
 
            // The first two bytes of an AMF packet contain the AMF version
            // as an unsigned 16 bit integer.
            me.version = 3;
 
            // The strings array holds references to all of the deserialized
            // AMF3 strings for a given header value or message body so that
            // repeat instances of the same string can be deserialized by
            // reference
            strings = me.strings = [];
 
            // The objects array holds references to deserialized objects so
            // that repeat occurrences of the same object instance in the byte
            // array can be deserialized by reference.
            // If version is AMF0 this array holds anonymous objects, typed
            // objects, arrays, and ecma-arrays.
            // If version is AMF3 this array holds instances of Object, Array, XML,
            // XMLDocument, ByteArray, Date, and instances of user defined Classes
            objects = me.objects = [];
 
            // The traits array holds references to the "traits" (the
            // characteristics of objects that define a strong type such as the
            // class name and public member names) of deserialized AMF3 objects
            // so that if they are repeated they can be deserialized by reference.
            traits = me.traits = [];
 
            // read one value and return it
            return me.readValue();
        },
 
        /**
         * Parses an xml string and returns an xml document
         * @private
         * @param {String} xml 
         */
        parseXml: function(xml) {
            var doc;
 
            if (window.DOMParser) {
                doc = (new DOMParser()).parseFromString(xml, "text/xml");
            }
            else {
                doc = new ActiveXObject("Microsoft.XMLDOM");
                doc.loadXML(xml);
            }
 
            return doc;
        },
 
        /**
         * Reads an AMF0 date from the byte array
         * @private
         */
        readAmf0Date: function() {
            var date = new Date(this.readDouble());
 
            // An AMF0 date type ends with a 16 bit integer time-zone, but
            // according to the spec time-zone is "reserved, not supported,
            // should be set to 0x000".
            pos += 2; // discard the time zone
 
            return date;
        },
 
        /**
         * Reads an AMF0 Object from the byte array
         * @private
         */
        readAmf0Object: function(obj) {
            var me = this,
                key;
 
            obj = obj || {};
 
            // add the object to the objects array so that the AMF0 reference
            // type decoder can refer to it by index if needed.
            objects.push(obj);
 
            // An AMF0 object consists of a series of string keys and variable-
            // type values.  The end of the series is marked by an empty string
            // followed by the object-end marker (9).
            while ((key = me.readAmf0String()) || bytes[pos] !== 9) {
                obj[key] = me.readValue();
            }
 
            // move the pointer past the object-end marker
            pos++;
 
            return obj;
        },
 
        /**
         * Reads an AMF0 string from the byte array
         * @private
         */
        readAmf0String: function() {
            // AMF0 strings begin with a 16 bit byte-length header.
            return this.readUtf8(this.readUInt(2));
        },
 
        readAmf0Xml: function() {
            return this.parseXml(this.readLongString());
        },
 
        readAmf3Array: function() {
            var me = this,
                header = me.readUInt29(),
                count, key, array, i;
 
            // AMF considers Arrays in two parts, the dense portion and the
            // associative portion. The binary representation of the associative
            // portion consists of name/value pairs (potentially none) terminated
            // by an empty string. The binary representation of the dense portion
            // is the size of the dense portion (potentially zero) followed by an
            // ordered list of values (potentially none).
            if (header & 1) {
                // If the first (low) bit is a 1 read an array instance.  The
                // remaining 1-28 bits are used to encode the length of the
                // dense portion of the array.
                count = (header >> 1);
 
                // First read the associative portion of the array (if any).  If
                // there is an associative portion, the array will be read as a
                // javascript object, otherwise it will be a javascript array.
                key = me.readAmf3String();
 
                if (key) {
                    // First key is not an empty string - this is an associative
                    // array.  Read keys and values from the byte array until
                    // we get to an empty string key
                    array = {};
                    objects.push(array);
 
                    do {
                        array[key] = me.readValue();
                    } while ((key = me.readAmf3String()));
 
                    // The dense portion of the array is then read into the
                    // associative object, keyed by ordinal index.
                    for (= 0; i < count; i++) {
                        array[i] = me.readValue();
                    }
                }
                else {
                    // First key is an empty string - this is an array with
                    // ordinal indices.
                    array = [];
                    objects.push(array);
 
                    for (= 0; i < count; i++) {
                        array.push(me.readValue());
                    }
                }
            }
            else {
                // If the first (low) bit is a 0 read an array reference. The
                // remaining 1-28 bits are used to encode the reference index
                array = objects[header >> 1];
            }
 
            return array;
        },
 
        /**
         * Reads an AMF3 date from the byte array
         * @private
         */
        readAmf3Date: function() {
            var me = this,
                header = me.readUInt29(),
                date;
 
            if (header & 1) {
                // If the first (low) bit is a 1, this is a date instance.
                date = new Date(me.readDouble());
                objects.push(date);
            }
            else {
                // If the first (low) bit is a 0, this is a date reference.
                // The remaining 1-28 bits encode the reference index
                date = objects[header >> 1];
            }
 
            return date;
        },
 
        /**
         * Reads an AMF3 object from the byte array
         * @private
         */
        readAmf3Object: function() {
            var me = this,
                header = me.readUInt29(),
                members = [],
                headerLast3Bits, memberCount, className,
                dynamic, objectTraits, obj, key, klass, i;
 
            // There are 3 different types of object headers, distinguishable
            // by the 1-3 least significant bits.  All object instances have
            // a 1 in the low bit position, while references have a 0:
            //
            // 0    : object reference
            // 011  : traits
            // 01   : traits-ref
            // 111  : traits-ext
            if (header & 1) {
                // first (low) bit of 1, denotes an encoded object instance
                // The next string is the class name.
                headerLast3Bits = (header & 0x07);
 
                if (headerLast3Bits === 3) {
                    // If the 3 least significant bits of the header are "011"
                    // then trait information follows.
 
                    className = me.readAmf3String();
                    // A 1 in the header's 4th least significant byte position
                    // indicates that dynamic members may follow the sealed
                    // members.
                    dynamic = !!(header & 0x08);
 
                    // Shift off the 4 least significant bits, and the remaining
                    // 1-25 bits encode the number of sealed member names. Read
                    // as many strings from the byte array as member names.
                    memberCount = (header >> 4);
 
                    for (= 0; i < memberCount; i++) {
                        members.push(me.readAmf3String());
                    }
 
                    objectTraits = {
                        className: className,
                        dynamic: dynamic,
                        members: members
                    };
 
                    // An objects traits are cached in the traits array enabling
                    // the traits for a given class to only be encoded once for
                    // a series of instances.
                    traits.push(objectTraits);
                }
                else if ((header & 0x03) === 1) {
                    // If the 2 least significant bits are "01", then a traits
                    // reference follows.  The remaining 1-27 bits are used
                    // to encode the trait reference index.
                    objectTraits = traits[header >> 2];
                    className = objectTraits.className;
                    dynamic = objectTraits.dynamic;
                    members = objectTraits.members;
                    memberCount = members.length;
                }
                else if (headerLast3Bits === 7) {
                    // if the 3 lease significant bits are "111" then
                    // externalizable trait data follows
 
                    // TODO: implement externalizable traits
                }
 
                if (className) {
                    klass = Ext.ClassManager.getByAlias('amf.' + className);
                    obj = klass ? new klass() : { $className: className };
                }
                else {
                    obj = {};
                }
 
                objects.push(obj);
 
                // read the sealed member values
                for (= 0; i < memberCount; i++) {
                    obj[members[i]] = me.readValue();
                }
 
                if (dynamic) {
                    // If the dynamic flag is set, dynamic members may follow
                    // the sealed members. Read key/value pairs until we
                    // encounter an empty string key signifying the end of the
                    // dynamic members.
                    while ((key = me.readAmf3String())) {
                        obj[key] = me.readValue();
                    }
                }
 
                // finally, check if we need to convert this class
                if ((!klass) && this.converters[className]) {
                    obj = this.converters[className](obj);
                }
 
            }
            else {
                // If the first (low) bit of the header is a 0, this is an
                // object reference. The remaining 1-28 significant bits are
                // used to encode an object reference index.
                obj = objects[header >> 1];
            }
 
            return obj;
        },
 
        /**
         * Reads an AMF3 string from the byte array
         * @private
         */
        readAmf3String: function() {
            var me = this,
                header = me.readUInt29(),
                value;
 
            if (header & 1) {
                // If the first (low) bit is a 1, this is a string literal.
                // Discard the low bit.  The remaining 1-28 bits are used to
                // encode the string's byte-length.
                value = me.readUtf8(header >> 1);
 
                if (value) {
                    // the emtpy string is never encoded by reference
                    strings.push(value);
                }
 
                return value;
            }
            else {
                // If the first (low) bit is a 0, this is a string reference.
                // Discard the low bit, then look up and return the reference
                // from the strings array using the remaining 1-28 bits as the
                // index.
                return strings[header >> 1];
            }
        },
 
        /**
         * Reads an AMF3 XMLDocument type or XML type from the byte array
         * @private
         */
        readAmf3Xml: function() {
            var me = this,
                header = me.readUInt29(),
                doc;
 
            if (header & 1) {
                // If the first (low) bit is a 1, this is an xml instance. The
                // remaining 1-28 bits encode the byte-length of the xml string.
                doc = me.parseXml(me.readUtf8(header >> 1));
                objects.push(doc);
            }
            else {
                // if the first (low) bit is a 1, this is an xml reference. The
                // remaining 1-28 bits encode the reference index.
                doc = objects[header >> 1];
            }
 
            return doc;
        },
 
        /**
         * Reads an AMF0 boolean from the byte array
         * @private
         */
        readBoolean: function() {
            return !!bytes[pos++];
        },
 
        /**
         * Reads an AMF3 ByteArray type from the byte array
         * @private
         */
        readByteArray: function() {
            var header = this.readUInt29(),
                byteArray, end;
 
            if (header & 1) {
                // If the first (low) bit is a 1, this is a ByteArray instance.
                // The remaining 1-28 bits encode the ByteArray's byte-length.
                end = pos + (header >> 1);
 
                // Depending on the browser, "bytes" may be either a Uint8Array
                // or an Array.  Uint8Arrays don't have Array methods, so
                // we have to use Array.prototype.slice to get the byteArray
                byteArray = Array.prototype.slice.call(bytes, pos, end);
                objects.push(byteArray);
 
                // move the pointer to the first byte after the byteArray that
                // was just read
                pos = end;
            }
            else {
                // if the first (low) bit is a 1, this is a ByteArray reference.
                // The remaining 1-28 bits encode the reference index.
                byteArray = objects[header >> 1];
            }
 
            return byteArray;
        },
 
        /**
         * Reads a IEEE 754 double-precision binary floating-point number
         * @private
         */
        readDouble: function() {
            var byte1 = bytes[pos++],
                byte2 = bytes[pos++],
                // the first bit of byte1 is the sign (0 = positive, 1 = negative.
                // We read this bit by shifting the 7 least significant bits of
                // byte1 off to the right.
                sign = (byte1 >> 7) ? -1 : 1,
 
                // the exponent takes up the next 11 bits.
                exponent =
                    // extract the 7 least significant bits from byte1 and then
                    // shift them left by 4 bits to make room for the 4 remaining
                    // bits from byte 2
                    (((byte1 & 0x7F) << 4) |
                     // add the 4 most significant bits from byte 2 to complete
                     // the exponent
                     (byte2 >> 4)),
 
                // the remaining 52 bits make up the significand. read the 4
                // least significant bytes of byte 2 to begin the significand
                significand = (byte2 & 0x0F),
 
                // The most significant bit of the significand is always 1 for
                // a normalized number, therefore it is not stored. This bit is
                // referred to as the "hidden bit". The true bit width of the
                // significand is 53 if you include the hidden bit. An exponent
                // of 0 indicates that this is a subnormal number, and subnormal
                // numbers always have a 0 hidden bit.
                hiddenBit = exponent ? 1 : 0,
                i = 6;
 
            // The operands of all bitwise operators in javascript are converted
            // to signed 32 bit integers.  It is therefore impossible to construct
            // the 52 bit significand by repeatedly shifting its bits and then
            // bitwise OR-ing the result with the the next byte. To work around
            // this issue, we repeatedly multiply the significand by 2^8 which
            // produces the same result as (significand << 8), then we add the
            // next byte, which has the same result as a bitwise OR.
            while (i--) {
                significand = (significand * twoPow8) + bytes[pos++];
            }
 
            if (!exponent) {
                if (!significand) {
                    // if both exponent and significand are 0, the number is 0
                    return 0;
                }
 
                // If the exponent is 0, but the significand is not 0, this
                // is a subnormal number. Subnormal numbers are encoded with a
                // biased exponent of 0, but are interpreted with the value of
                // the smallest allowed exponent, which is one greater.
                exponent = 1;
            }
 
            // 0x7FF (2047) is a special exponent value that represents infinity
            // if the significand is 0, and NaN if the significand is not 0
            if (exponent === 0x7FF) {
                return significand ? NaN : (Infinity * sign);
            }
 
            return sign *
                // The exponent is encoded using an offset binary
                // representation with the zero offset being 0x3FF (1023),
                // so we have to subtract 0x3FF to get the true exponent
                Math.pow(2, exponent - 0x3FF) *
 
                // convert the significand to its decimal value by multiplying
                // it by 2^52 and then add the hidden bit
                (hiddenBit + twoPowN52 * significand);
        },
 
        /**
         * Reads an AMF0 ECMA Array from the byte array
         * @private
         */
        readEcmaArray: function() {
            // An ecma array type is encoded exactly like an anonymous object
            // with the exception that it has a 32 bit "count" at the beginning.
            // We handle emca arrays by just throwing away the count and then
            // letting the object decoder handle the rest.
            pos += 4;
 
            return this.readAmf0Object();
        },
 
        /**
         * Returns false.  Used for reading the false type
         * @private
         */
        readFalse: function() {
            return false;
        },
 
        /**
         * Reads a long string (longer than 65535 bytes) from the byte array
         * @private
         */
        readLongString: function() {
            // long strings begin with a 32 bit byte-length header.
            return this.readUtf8(this.readUInt(4));
        },
 
        /**
         * Returns null.  Used for reading the null type
         * @private
         */
        readNull: function() {
            return null;
        },
 
        /**
         * Reads a reference from the byte array.  Reference types are used to
         * avoid duplicating data if the same instance of a complex object (which
         * is defined in AMF0 as an anonymous object, typed object, array, or
         * ecma-array) is included in the data more than once.
         * @private
         */
        readReference: function() {
            // a reference type contains a single 16 bit integer that represents
            // the index of an already deserialized object in the objects array
            return objects[this.readUInt(2)];
        },
 
        /**
         * Reads an AMF0 strict array (an array with ordinal indices)
         * @private
         */
        readStrictArray: function() {
            var me = this,
                len = me.readUInt(4),
                arr = [];
 
            objects.push(arr);
 
            while (len--) {
                arr.push(me.readValue());
            }
 
            return arr;
        },
 
        /**
         * Returns true.  Used for reading the true type
         * @private
         */
        readTrue: Ext.returnTrue,
 
        /**
         * Reads an AMF0 typed object from the byte array
         * @private
         */
        readTypedObject: function() {
            var me = this,
                className = me.readAmf0String(),
                klass, instance, modified;
 
            klass = Ext.ClassManager.getByAlias('amf.' + className);
 
            // if there is no klass, mark the classname for easier parsing of returned results
            instance = klass ? new klass() : { $className: className };
 
            modified = me.readAmf0Object(instance);
 
            // check if we need to convert this class
            if ((!klass) && this.converters[className]) {
                modified = this.converters[className](instance);
            }
 
            return modified;
        },
 
        /**
         * Reads an unsigned integer from the byte array
         * @private
         * @param {Number} byteCount the number of bytes to read, e.g. 2 to read
         * a 16 bit integer, 4 to read a 32 bit integer, etc.
         * @return {Number} 
         */
        readUInt: function(byteCount) {
            var i = 1,
                result;
 
            // read the first byte
            result = bytes[pos++];
 
            // if this is a multi-byte int, loop over the remaining bytes
            for (; i < byteCount; ++i) {
                // shift the result 8 bits to the left and add the next byte.
                result = (result << 8) | bytes[pos++];
            }
 
            return result;
        },
 
        /**
         * Reads an unsigned 29-bit integer from the byte array.
         * AMF 3 makes use of a special compact format for writing integers to
         * reduce the number of bytes required for encoding. As with a normal
         * 32-bit integer, up to 4 bytes are required to hold the value however
         * the high bit of the first 3 bytes are used as flags to determine
         * whether the next byte is part of the integer. With up to 3 bits of
         * the 32 bits being used as flags, only 29 significant bits remain for
         * encoding an integer. This means the largest unsigned integer value
         * that can be represented is 2^29-1.
         *
         *           (hex)         :                (binary)
         * 0x00000000 - 0x0000007F :  0xxxxxxx
         * 0x00000080 - 0x00003FFF :  1xxxxxxx 0xxxxxxx
         * 0x00004000 - 0x001FFFFF :  1xxxxxxx 1xxxxxxx 0xxxxxxx
         * 0x00200000 - 0x3FFFFFFF :  1xxxxxxx 1xxxxxxx 1xxxxxxx xxxxxxxx
         * @private
         * @return {Number} 
         */
        readUInt29: function() {
            var value = bytes[pos++],
                nextByte;
 
            if (value & 0x80) {
                // if the high order bit of the first byte is a 1, the next byte
                // is also part of this integer.
                nextByte = bytes[pos++];
 
                // remove the high order bit from both bytes before combining them
                value = ((value & 0x7F) << 7) | (nextByte & 0x7F);
 
                if (nextByte & 0x80) {
                    // if the high order byte of the 2nd byte is a 1, then
                    // there is a 3rd byte
                    nextByte = bytes[pos++];
 
                    // remove the high order bit from the 3rd byte before
                    // adding it to the value
                    value = (value << 7) | (nextByte & 0x7F);
 
                    if (nextByte & 0x80) {
                        // 4th byte is also part of the integer
                        nextByte = bytes[pos++];
 
                        // use all 8 bits of the 4th byte
                        value = (value << 8) | nextByte;
                    }
 
                }
            }
 
            return value;
        },
 
        /**
         * @method
         * Returns undefined.  Used for reading the undefined type
         * @private
         */
        readUndefined: Ext.emptyFn,
 
        /**
         * @method
         * Returns undefined.  Used for reading the unsupported type
         * @private
         */
        readUnsupported: Ext.emptyFn,
 
        /**
         * Reads a UTF-8 string from the byte array
         * @private
         * @param {Number} byteLength The number of bytes to read
         * @return {String} 
         */
        readUtf8: function(byteLength) {
            var end = pos + byteLength, // the string's end position
                chars = [],
                charCount = 0,
                maxCharCount = 65535,
                charArrayCount = 1,
                result = [],
                i = 0,
                charArrays, byteCount, charCode;
 
            charArrays = [chars];
 
            // UTF-8 characters may be encoded using 1-4 bytes. The number of
            // bytes that a character consumes is determined by reading the
            // leading byte.  Values 0-127 in the leading byte indicate a single-
            // byte ASCII-compatible character. Values 192-223 (bytes with "110"
            // in the high-order position) indicate a 2-byte character, values
            // 224-239 (bytes with "1110" in the high-order position) indicate a
            // 3-byte character, and values 240-247 (bytes with "11110" in the
            // high-order position) indicate a 4-byte character. The remaining
            // bits of the leading byte hold bits of the encoded character, with
            // leading zeros if necessary.
            //
            // The continuation bytes all have "10" in the high-order position,
            // which means only the 6 least significant bits of continuation
            // bytes are available to hold the bits of the encoded character.
            //
            // The following table illustrates the binary format of UTF-8
            // characters:
            //
            // Bits     Byte 1      Byte 2      Byte 3      Byte 4
            // -----------------------------------------------------
            //  7       0xxxxxxx
            // 11       110xxxxx    10xxxxxx
            // 16       1110xxxx    10xxxxxx    10xxxxxx
            // 21       11110xxx    10xxxxxx    10xxxxxx    10xxxxxx
            while (pos < end) {
                // read a byte from the byte array - if the byte's value is less
                // than 128 we are dealing with a single byte character
                charCode = bytes[pos++];
 
                if (charCode > 127) {
                    // if the byte's value is greater than 127 we are dealing
                    // with a multi-byte character.
                    if (charCode > 239) {
                        // a leading-byte value greater than 239 means this is a
                        // 4-byte character
                        byteCount = 4;
 
                        // Use only the 3 least-significant bits of the leading
                        // byte of a 4-byte character. This is achieved by
                        // applying the following bit mask:
                        // (charCode & 0x07)
                        // which is equivalent to:
                        //     11110xxx (the byte)
                        // AND 00000111 (the mask)
                        charCode = (charCode & 0x07);
                    }
                    else if (charCode > 223) {
                        // a leading-byte value greater than 223 but less than
                        // 240 means this is a 3-byte character
                        byteCount = 3;
 
                        // Use only the 4 least-significant bits of the leading
                        // byte of a 3-byte character. This is achieved by
                        // applying the following bit mask:
                        // (charCode & 0x0F)
                        // which is equivalent to:
                        //     1110xxxx (the byte)
                        // AND 00001111 (the mask)
                        charCode = (charCode & 0x0F);
                    }
                    else {
                        // a leading-byte value less than 224 but (implicitly)
                        // greater than 191 means this is a 2-byte character
                        byteCount = 2;
 
                        // Use only the 5 least-significant bits of the first
                        // byte of a 2-byte character. This is achieved by
                        // applying the following bit mask:
                        // (charCode & 0x1F)
                        // which is equivalent to:
                        //     110xxxxx (the byte)
                        // AND 00011111 (the mask)
                        charCode = (charCode & 0x1F);
                    }
 
                    while (--byteCount) {
                        // get one continuation byte. then strip off the leading
                        // "10" by applying the following bit mask:
                        // (b & 0x3F)
                        // which is equialent to:
                        //     10xxxxxx (the byte)
                        // AND 00111111 (the mask)
                        // That leaves 6 remaining bits on the continuation byte
                        // which are concatenated onto the character's bits
                        charCode = ((charCode << 6) | (bytes[pos++] & 0x3F));
                    }
                }
 
                chars.push(charCode);
 
                if (++charCount === maxCharCount) {
                    charArrays.push(chars = []);
                    charCount = 0;
                    charArrayCount ++;
                }
            }
 
            // At this point we end up with an array of char arrays, each char
            // array being no longer than 65,535 characters, the fastest way to
            // turn these char arrays into strings is to pass them as the
            // arguments to fromCharCode (fortunately all currently supported
            // browsers can handle at least 65,535 function arguments).
            for (; i < charArrayCount; i++) {
                // create a result array containing the strings converted from
                // the individual character arrays.
                result.push(String.fromCharCode.apply(String, charArrays[i]));
            }
 
            return result.join('');
        },
 
        /**
         * Reads an AMF "value-type" from the byte array.  Automatically detects
         * the data type by reading the "type marker" from the first byte after
         * the pointer.
         * @private
         */
        readValue: function() {
            var me = this,
                marker = bytes[pos++];
 
            // With the introduction of AMF3, a special type marker was added to
            // AMF0 to signal a switch to AMF3 serialization. This allows a packet
            // to start out in AMF 0 and switch to AMF 3 on the first complex type
            // to take advantage of the more the efficient encoding of AMF 3.
            if (marker === 17) {
                // change the version to AMF3 when we see a 17 marker
                me.version = 3;
                marker = bytes[pos++];
            }
 
            return me[me.typeMap[me.version][marker]]();
        },
 
        /**
         * Converters used in converting specific typed Flex classes to JavaScript usable form.
         * @private
         */
        converters: {
            'flex.messaging.io.ArrayCollection': function(obj) {
                // array collections have a source var that contains the actual data
                return obj.source || [];
            }
        }
    };
});