// @tag enterprise
/**
 * @class Ext.data.amf.Encoder
 * This class serializes data in the Action Message Format (AMF) format.
 * It can write simple and complex objects, to be used in conjunction with an
 * AMF-compliant server.
 * To encode a byte array, first construct an Encoder, optionally setting the format:
 *
 *     var encoder = Ext.create('Ext.data.amf.Encoder', {
 *       format: 3
 *     });
 *
 * Then use the writer methods to out data to the :
 *
 *     encoder.writeObject(1);
 *
 * And access the data through the #bytes property:
 *     encoder.bytes;
 *
 * You can also reset the class to start a new byte array:
 *
 *     encoder.clear();
 *
 * Current limitations:
 * AMF3 format (format:3)
 * - writeObject will write out XML object, not legacy XMLDocument objects. A
 *   writeXmlDocument method is provided for explicitly writing XMLDocument
 *   objects.
 * - Each object is written out explicitly, not using the reference tables
 *   supported by the AMF format. This means the function does NOT support
 *   circular reference objects.
 * - Array objects: only the numbered indices and data will be written out.
 *   Associative values will be ignored.
 * - Objects that aren't Arrays, Dates, Strings, Document (XML) or primitive
 *   values will be written out as anonymous objects with dynamic data.
 * - There's no JavaScript equivalent to the ByteArray type in ActionScript,
 *   hence data will never be searialized as ByteArrays by the writeObject
 *   function. A writeByteArray method is provided for writing out ByteArray objects.
 *
 * AMF0 format (format:0)
 * - Each object is written out explicitly, not using the reference tables
 *   supported by the AMF format. This means the function does NOT support
 *   circular reference objects.
 * - Array objects: the function always writes an associative array (following
 *   the behavior of flex).
 * - Objects that aren't Arrays, Dates, Strings, Document (XML) or primitive
 *   values will be written out as anonymous objects.
 *
 * For more information on working with AMF data please refer to the
 * [AMF Guide](#/guide/amf).
 */
Ext.define('Ext.data.amf.Encoder', {

    alias: 'data.amf.Encoder',

    config: {
        format: 3
    },

    /**
     * @property {Array} bytes
     * @readonly
     * The constructed byte array.
     */
    bytes: [],

    /**
     * Creates new Encoder.
     * @param {Object} config Configuration options
     */
    constructor: function(config) {
        this.initConfig(config);
        this.clear();
    },

    /**
     * Reset all class states and starts a new empty array for encoding data.
     * The method generates a new array for encoding, so it's safe to keep a
     * reference to the old one.
     */
    clear: function() {
        this.bytes = [];
    },

    /**
     * Sets the functions that will correctly serialize for the relevant
     * protocol version.
     * @param {Number} protocol_version the protocol version to support
     */
    applyFormat: function(protocol_version) {
        var funcs = {
            0: {
                writeUndefined: this.write0Undefined,
                writeNull: this.write0Null,
                writeBoolean: this.write0Boolean,
                writeNumber: this.write0Number,
                writeString: this.write0String,
                writeXml: this.write0Xml,
                writeDate: this.write0Date,
                writeArray: this.write0Array,
                writeGenericObject: this.write0GenericObject
            },

            3: {
                writeUndefined: this.write3Undefined,
                writeNull: this.write3Null,
                writeBoolean: this.write3Boolean,
                writeNumber: this.write3Number,
                writeString: this.write3String,
                writeXml: this.write3Xml,
                writeDate: this.write3Date,
                writeArray: this.write3Array,
                writeGenericObject: this.write3GenericObject
            }

        }[protocol_version];
        if (funcs) {
            Ext.apply(this, funcs);
            return protocol_version;
        } else {
            //<debug>
            Ext.Error.raise("Unsupported AMF format: " + protocol_version + ". Only '3' (AMF3) is supported at this point.");
            //</debug>
            return; // return nothing
        }
    },

    /**
     * Write the appropriate data items to the byte array. Supported types:
     * - undefined
     * - null
     * - boolean
     * - integer (if AMF3 - limited by 29-bit int, otherwise passed as double)
     * - double
     * - UTF-8 string
     * - XML Document (identified by being instaneof Document. Can be generated with: new DOMParser()).parseFromString(xml, "text/xml");
     * @param {Object} item A primitive or object to write to the stream
     */
    writeObject: function(item) {
        var t = typeof(item);
        //Ext.log("Writing " + item + " of type " + t);
        if (t === "undefined") {
            this.writeUndefined();
        } else if (item === null) { // can't check type since typeof(null) returns "object"
            this.writeNull();
        } else if (Ext.isBoolean(item)) {
            this.writeBoolean(item);
        } else if (Ext.isString(item)) {
            this.writeString(item);
        } else if (t === "number" || item instanceof Number) { // Can't use Ext.isNumeric since it accepts strings as well
            this.writeNumber(item);
        } else if (t === "object") {
            // Figure out which object this is
            if (item instanceof Date) {
                this.writeDate(item);
            } else if (Ext.isArray(item)) { // this won't catch associative arrays deserialized by the Packet class!
                this.writeArray(item);
            } else if (this.isXmlDocument(item)) {
                this.writeXml(item);
            } else {
                // Treat this as a generic object with name/value pairs of data.
                this.writeGenericObject(item);
            }
        } else {
            //<debug>
            Ext.log.warn("AMF Encoder: Unknown item type " + t + " can't be written to stream: " + item);
            //</debug>
        }
    },

    /**
     * Writes the AMF3 undefined value to the byte array.
     * @private
     */
    write3Undefined: function() {
        this.writeByte(0x00); // AMF3 undefined
    },

    /**
     * Writes the AMF0 undefined value to the byte array.
     * @private
     */
    write0Undefined: function() {
        this.writeByte(0x06); // AMF0 undefined
    },

    /**
     * Writes the AMF3 null value to the byte array.
     * @private
     */
    write3Null: function() {
        this.writeByte(0x01); // AMF3 null
    },

    /**
     * Writes the AMF0 null value to the byte array.
     * @private
     */
    write0Null: function() {
        this.writeByte(0x05); // AMF0 null
    },

    /**
     * Writes the appropriate AMF3 boolean value to the byte array.
     * @param {boolean} item The value to write
     * @private
     */
    write3Boolean: function(item) {
        //<debug>
        if (typeof(item) !== "boolean") {
            Ext.log.warn("Encoder: writeBoolean argument is not a boolean. Coercing.");
        }
        // </debug>
        if (item) {
            this.writeByte(0x03); // AMF3 true
        } else {
            this.writeByte(0x02); // AMF3 false
        }
    },

    /**
     * Writes the appropriate AMF0 boolean value to the byte array.
     * @param {boolean} item The value to write
     * @private
     */
    write0Boolean: function(item) {
        //<debug>
        if (typeof(item) !== "boolean") {
            Ext.log.warn("Encoder: writeBoolean argument is not a boolean. Coercing.");
        }
        // </debug>
        this.writeByte(0x01); // AMF0 boolean marker
        if (item) {
            this.writeByte(0x01); // AMF0 true
        } else {
            this.writeByte(0x00); // AMF0 false
        }
    },

    /**
     * Encodes a U29 int, returning a byte array with the encoded number.
     * @param item - unsigned int value
     * @private
     */
    encode29Int: function(item) {
        var data = [], // prepare the bytes, then send them to the array
            num = item,
            nibble,
            i;
        if (num == 0) {
            return [0]; // no other data
        }
        // we have a special case if the number is 4-nibbles in U29 encoding
        if (num > 0x001fffff) {
            // last nibble is an 8-bit value
            nibble = num & 0xff;
            data.unshift(nibble);
            num = num >> 8;
        }
        // get all the 7-bit parts ready
        while (num > 0) {
            nibble = num & 0x7f; // 7 bits
            data.unshift(nibble);
            num = num >> 7;
        }
        // now we need to mark each MSb of a 7-bit byte with a 1, except the absolute last one which has a 0.
        // If there's an 8-bit byte, the 7-bit byte before it is marked with 1 as well.
        for (i = 0; i < data.length - 1; i++) {
            data[i] = data[i] | 0x80;
        }
        return data;
    },

    /**
     * Writes a numberic value to the byte array in AMF3 format
     * @param item A native numeric value, Number instance or one of Infinity, -Infinity or NaN
     * @private
     */
    write3Number: function(item) {
        var data;
        var maxInt = 0x1fffffff,
            minSignedInt = -0xfffffff;
        //<debug>
        if (typeof(item) !== "number" && !(item instanceof Number)) {
            Ext.log.warn("Encoder: writeNumber argument is not numeric. Can't coerce.");
        }
        // </debug>

        // switch to the primitive value for handling:
        if (item instanceof Number) {
            item = item.valueOf();
        }
        // First we need to determine if this is an integer or a float.
        // AMF3 allows integers between -2^28 < item < 2^29, else they need to be passed as doubles.
        if (item % 1 === 0 && item >= minSignedInt && item <= maxInt) {
            // The number has no decimal point and is within bounds. Let's encode it.
            item = item & maxInt; // get an unsigned value to work with - we only care about 29 bits.
            data = this.encode29Int(item);
            // And , mark it as an integer
            data.unshift(0x04); // AMF3 integer marker
            // write it!
            this.writeBytes(data);

        } else {
            data = this.encodeDouble(item);
            data.unshift(0x05); // AMF3 double marker
            this.writeBytes(data);
        }
    },

    /**
     * Writes a numberic value to the byte array in AMF0 format
     * @param item A native numeric value, Number instance or one of Infinity, -Infinity or NaN
     * @private
     */
    write0Number: function(item) {
        var data;
        //<debug>
        if (typeof(item) !== "number" && !(item instanceof Number)) {
            Ext.log.warn("Encoder: writeNumber argument is not numeric. Can't coerce.");
        }
        // </debug>

        // switch to the primitive value for handling:
        if (item instanceof Number) {
            item = item.valueOf();
        }
        //In AMF0 numbers are always serialized as double-float values.
        data = this.encodeDouble(item);
        data.unshift(0x00); // AMF0 double marker
        this.writeBytes(data);
    },

    /**
     * Convert a UTF 16 char to a UTF 8 char
     * @param {Number} c char 16-bit code to convert
     * @return {Array} byte array with the UTF 8 values
     */
    encodeUtf8Char: function(c) {
        var data = [],
            val, b, i,
            marker;
        //<debug>
        if (c > 0x10FFFF) {
            //<debug>
            Ext.Error.raise("UTF 8 char out of bounds");
            //</debug>
        }
        //</debug>
        if (c <= 0x7F) {
            // One byte UTF8
            data.push(c);
        } else {
            // Multi-byte UTF8. Figure out how many bytes:
            if (c <= 0x7ff) {
                b = 2;
            } else if (c <= 0xffff) {
                b = 3;
            } else {
                b = 4;
            }
            // encode LSBs of value
            marker = 0x80; // MSB marker
            for (i = 1; i < b; i++) {
                val = (c & 0x3F) | 0x80; // lowest 6 bits of number, plus a flag to mark the byte
                data.unshift(val);
                c = c >> 6; // drop 6 LSbs
                marker = (marker >> 1) | 0x80; // add one more bit for every byte
            }
            // the final byte is now ready, but we need to mark it to show how many other bytes follow
            val = c | marker;
            data.unshift(val);
        }
        return data;
    },

    /**
     * Accepts a string and returns a byte array encoded in UTF-8
     * @param {String} str String to encode
     * @return {Array} byte array with string encoded in UTF-8 format
     * @private
     */
    encodeUtf8String: function(str) {
        var i,
            utf8Data = [];
        for (i = 0; i < str.length; i++) {
            var data = this.encodeUtf8Char(str.charCodeAt(i));
            Ext.Array.push(utf8Data, data);
        }
        return utf8Data;

        // quicker conversion, doesn't work in IE:
        //          utf8String = unescape(encodeURIComponent(str)),
        //          utf8Data = [];


    },

    /**
     * Encode the length of a UTF-8 string in AMF3 format.
     * @param {Array} utf8Data byte array with the encoded data
     * @return {Array} byte array encoding of length
     *
     * @private
     */
    encode3Utf8StringLen: function(utf8Data) {
        var len = utf8Data.length,
            data = [];
        if (len <= 0xFFFFFFF) {
            // String is under max allowed length in AMF3
            // AMF3 strings use the LSb to mark whether it's a string reference or a string value. For now we only pass values:
            len = len << 1;
            len = len | 1; // mark as value
            // push length value to the array
            data =this.encode29Int(len);
        } else {
            //<debug>
            Ext.Error.raise("UTF8 encoded string too long to serialize to AMF: " + len);
            //</debug>
        }
        return data;
    },

    /**
     * Write an AMF3 UTF-8 string to the byte array
     * @param {String} item The string to write
     * @private
     */
    write3String: function(item) {
        //<debug>
        if (!Ext.isString(item)) {
            Ext.log.warn("Encoder: writString argument is not a string.");
        }
        // </debug>
        if (item == "") { // special case for the empty string
            this.writeByte(0x06); // AMF3 string marker
            this.writeByte(0x01); // zero length string
        } else {
            // first encode string to UTF-8.
            var utf8Data = this.encodeUtf8String(item);
            var lenData = this.encode3Utf8StringLen(utf8Data);
            // only write encoding once we got here, i.e. length of string is legal
            this.writeByte(0x06); // AMF3 string marker, only if we can actually write a string
            this.writeBytes(lenData);
            this.writeBytes(utf8Data);
        }
    },

    /**
     * Encode 16- or 32-bit integers into big-endian (network order) bytes
     * @param {Number} value the number to encode.
     * @param {Number} byte_count 2 or 4 byte encoding
     * @return {Array} byte array with encoded number
     */
    encodeXInt: function(value, byte_count) {
        var data = [],
            i;
        for (i = 0; i < byte_count; i++) {
            data.unshift(value & 0xff);
            value = value >> 8;
        }
        return data;
    },

    /**
     * Write an AMF0 UTF-8 string to the byte array
     * @param {String} item The string to write
     * @private
     */
    write0String: function(item) {
        //<debug>
        if (!Ext.isString(item)) {
            Ext.log.warn("Encoder: writString argument is not a string.");
        }
        // </debug>
        if (item == "") { // special case for the empty string
            this.writeByte(0x02); // AMF0 short string marker
            this.writeBytes([0x00, 0x00]); // zero length string
        } else {
            // first encode string to UTF-8.
            var utf8Data = this.encodeUtf8String(item);
            var encoding;
            var lenData;
            if (utf8Data.length <= 0xffff) {
                // short string
                encoding = 0x02; // short string
                lenData = this.encodeXInt(utf8Data.length, 2);
            } else {
                // long string
                encoding = 0x0C; // long string
                lenData = this.encodeXInt(utf8Data.length, 4);
            }
            this.writeByte(encoding); // Approperiate AMF0 string marker
            this.writeBytes(lenData);
            this.writeBytes(utf8Data);
        }
    },

    /**
     * Writes an XML document in AMF3 format.
     * @param {Object} xml XML document (type Document typically)
     * @param {number} amfType Either 0x07 or 0x0B - the AMF3 object type to use
     * @private
     */
    write3XmlWithType: function(xml, amfType) {
        //<debug>
        // We accept XML Documents, or strings
        if (amfType !== 0x07 && amfType !== 0x0B) {
            Ext.Error.raise("write XML with unknown AMF3 code: " + amfType);
        }
        if (!this.isXmlDocument(xml)) {
            Ext.log.warn("Encoder: write3XmlWithType argument is not an xml document.");
        }
        // </debug>
        var xmlStr = this.convertXmlToString(xml);
        if (xmlStr == "") { // special case for the empty string
            this.writeByte(amfType); // AMF3 XML marker
            this.writeByte(0x01); // zero length string
        } else {
            // first encode string to UTF-8.
            var utf8Data = this.encodeUtf8String(xmlStr);
            var lenData = this.encode3Utf8StringLen(utf8Data);
            // only write encoding once we got here, i.e. length of string is legal
            this.writeByte(amfType); // AMF3 XML marker, only if we can actually write the string
            this.writeBytes(lenData);
            this.writeBytes(utf8Data);
        }
    },

    /**
     * Writes an Legacy XMLDocument (ActionScript Legacy XML object) in AMF3
     * format. Must be called explicitly.
     * The writeObject method will call writeXml and not writeXmlDocument.
     * @param {Object} xml XML document (type Document typically) to write
     */
    write3XmlDocument: function(xml) {
        this.write3XmlWithType(xml, 0x07);
    },

    /**
     * Writes an XML object (ActionScript 3 new XML object) in AMF3 format.
     * @param {Object} xml XML document (type Document typically) to write
     * @private
     */
    write3Xml: function(xml) {
        this.write3XmlWithType(xml, 0x0B);
    },

    /**
     * Writes an XMLDocument in AMF0 format.
     * @param {Object} xml XML document (type Document typically) to write
     * @private
     */
    write0Xml: function(xml) {
        //<debug>
        // We accept XML Documents, or strings
        if (!this.isXmlDocument(xml)) {
            Ext.log.warn("Encoder: write0Xml argument is not an xml document.");
        }
        // </debug>
        var xmlStr = this.convertXmlToString(xml);
        this.writeByte(0x0F); // AMF0 XML marker

        // Always encoded as a long string
        var utf8Data = this.encodeUtf8String(xmlStr);
        var lenData = this.encodeXInt(utf8Data.length, 4);
        this.writeBytes(lenData);
        this.writeBytes(utf8Data);

    },

    /**
     * Writes a date in AMF3 format.
     * @param {Date} date the date object
     * @private
     */
    write3Date: function(date) {

        //<debug>
        if (!(date instanceof Date)) {
            Ext.Error.raise("Serializing a non-date object as date: " + date);
        }
        //</debug>
        // For now, we don't use object references to just encode the date.
        this.writeByte(0x08); // AMF3 date marker
        this.writeBytes(this.encode29Int(0x1)); // mark this as a date value - we don't support references yet
        this.writeBytes(this.encodeDouble(new Number(date)));
    },

    /**
     * Writes a date in AMF0 format.
     * @param {Date} date the date object
     * @private
     */
    write0Date: function(date) {

        //<debug>
        if (!(date instanceof Date)) {
            Ext.Error.raise("Serializing a non-date object as date: " + date);
        }
        //</debug>
        // For now, we don't use object references to just encode the date.
        this.writeByte(0x0B); // AMF0 date marker
        this.writeBytes(this.encodeDouble(new Number(date)));
        this.writeBytes([0x00, 0x00]); // placeholder for timezone, standard says to keep 0, flash actually writes data here
    },

    /**
     * Writes an array in AMF3 format. Only the ordered part of the array use handled.
     * Unordered parts are ignored (e.g. a["hello"] will not be encoded).
     * @param {Array} arr the array to serialize.
     * @private
     */
    write3Array: function(arr) {

        //<debug>
        if (!Ext.isArray(arr)) {
            Ext.Error.raise("Serializing a non-array object as array: " + arr);
        }
        if (arr.length > 0xFFFFFFF) {
            Ext.Error.raise("Array size too long to encode in AMF3: " + arr.length);
        }
        //</debug>
        // For now, we don't use object references to just encode the array.
        this.writeByte(0x09); // AMF3 array marker

        // encode ordered part of array's length
        var len = arr.length;
        len = len << 1; // right-most bit marks this as size
        len = len | 0x1; // mark it a size
        this.writeBytes(this.encode29Int(len));

        // The associative part of the array is ignored, so mark it as empty
        this.writeByte(0x01); // equivalent to an empty UTF-8 string

        // now iterate over the array, writing each element
        Ext.each(arr, function(x) {this.writeObject(x);}, this);
    },

    /**
     * Writes a key-value pair in AMF0 format.
     * @param {String} key the name of the property
     * @param {Object} value to write in AMF0 format
     */
    write0ObjectProperty: function(key, value) {
        if (!(key instanceof String) && (typeof(key) !== "string")) {
            // coerce to a string
            key = key + "";
        }
        // first encode the key to a short UTF-8.
        var utf8Data = this.encodeUtf8String(key);
        var lenData;
        lenData = this.encodeXInt(utf8Data.length, 2);
        this.writeBytes(lenData);
        this.writeBytes(utf8Data);
        // and now write out the object
        this.writeObject(value);
    },

    /**
     * Writes an associative array in AMF0 format.
     * @param {Array} arr the array to serialize.
     * @private
     */
    write0Array: function(arr) {
        var key;
        //<debug>
        if (!Ext.isArray(arr)) {
            Ext.Error.raise("Serializing a non-array object as array: " + arr);
        }
        //</debug>

        /* This writes a strict array, but it seems Flex always writes out associative arrays, so mimic behavior

         // For now, we don't use object references to just encode the array.
         this.writeByte(0x0A); // AMF0 strict array marker

         // encode ordered part of array's length
         var len = arr.length;
         this.writeBytes(this.encodeXInt(len, 4));

         // now iterate over the array, writing each element
         Ext.each(arr, function(x) {this.writeObject(x);}, this);
         */
        // Use ECMA (associative) array type
        this.writeByte(0x08); // AMF0 ECMA-array marker
        // we need to know the length of the array before we write the serialized data
        // to the array. Better to traverse it twice than to copy all the byte data afterwards
        var total = 0;
        for (key in arr) {
            total++;
        }
        // Now write out the length of the array
        this.writeBytes(this.encodeXInt(total, 4));
        // then write out the data
        for (key in arr) {
            Ext.Array.push(this.write0ObjectProperty(key, arr[key]));
        }
        // And finally the object end marker
        this.writeBytes([0x00, 0x00, 0x09]);
    },

    /**
     * Writes a strict-array in AMF0 format. Unordered parts are ignored (e.g.
     * a["hello"] will not be encoded). This function is included for
     * completeness and will never be called by writeObject.
     * @param {Array} arr the array to serialize.
     */
    write0StrictArray: function(arr) {

        //<debug>
        if (!Ext.isArray(arr)) {
            Ext.Error.raise("Serializing a non-array object as array: " + arr);
        }
        //</debug>

        // For now, we don't use object references to just encode the array.
        this.writeByte(0x0A); // AMF0 strict array marker

        // encode ordered part of array's length
        var len = arr.length;
        this.writeBytes(this.encodeXInt(len, 4));

        // now iterate over the array, writing each element
        Ext.each(arr, function(x) {this.writeObject(x);}, this);
    },


    /**
     * Write a byte array in AMF3 format. This function is never called directly
     * by writeObject since there's no way to distinguish a regular array from a
     * byte array.
     * @param {Array} arr the object to serialize.
     */
    write3ByteArray: function(arr) {

        //<debug>
        if (!Ext.isArray(arr)) {
            Ext.Error.raise("Serializing a non-array object as array: " + arr);
        }
        if (arr.length > 0xFFFFFFF) {
            Ext.Error.raise("Array size too long to encode in AMF3: " + arr.length);
        }
        //</debug>
        this.writeByte(0x0c); // Byte array marker

        // for now no support for references, so just dump the length and data

        // encode array's length
        var len = arr.length;
        len = len << 1; // right-most bit marks this as size
        len = len | 0x1; // mark it a size
        this.writeBytes(this.encode29Int(len));
        // and finally, dump the byte data
        this.writeBytes(arr);
    },

    /**
     * Write an object to the byte array in AMF3 format.
     * Since we don't have the class information form Flex, the object
     * is written as an anonymous object.
     * @param {Array} obj the object to serialize.
     * @private
     */
    write3GenericObject: function(obj) {
        var name;

        //<debug>
        if (!Ext.isObject(obj)) {
            Ext.Error.raise("Serializing a non-object object: " + obj);
        }
        //</debug>
        // For now, we don't use object references so just encode the object.
        this.writeByte(0x0A); // AMF3 object marker
        // The following 29-int is marked as follows (LSb to MSb) to signify a
        // "U29O-traits":
        // 1 - LSb marks an object value (1) or an object reference (0) which
        //     is not yet supported.
        // 1 - trait values (1) or trait references (0) which are not supported
        //     yet.
        // 0 - AMF3 format (0) or externalizable, i.e. object handles own
        //     serialization (1) which is not supported.
        // 1 - dynamic (1) or not dynamic (0) object which is not relevant since
        //     we pass all data as dynamic fields.
        // The reset of the bits signify how many sealed traits the object has.
        //     we pass 0 since all data is passed as dynamic fields
        var oType = 0x0b; // binary 1011
        this.writeByte(oType);
        // Next we pass the class name, which is the empty string for anonymous
        // objects
        this.writeByte(0x01);
        // Next are the sealed traits (of which we have none)
        // And now the name / value pairs of dynamic fields
        for (name in obj) {
            // Ensure that name is actually a string
            var newName = new String(name).valueOf();
            if (newName == "") {
                //<debug>
                Ext.Error.raise("Can't encode non-string field name: " + name);
                //</debug>
            }
            var nameData = (this.encodeUtf8String(name));
            this.writeBytes(this.encode3Utf8StringLen(name));
            this.writeBytes(nameData);
            this.writeObject(obj[name]);
        }
        // And mark the end of the dynamic field section with the empty string
        this.writeByte(0x01);
    },

    /**
     * Write an object to the byte array in AMF0 format.
     * Since we don't have the class information form Flex, the object
     * is written as an anonymous object.
     * @param {Array} obj the object to serialize.
     * @private
     */
    write0GenericObject: function(obj) {
        var typed, amfType, key;
        //<debug>
        if (!Ext.isObject(obj)) {
            Ext.Error.raise("Serializing a non-object object: " + obj);
        }
        //</debug>
        // For now, we don't use object references so just encode the object.
        // if the object is typed, the ID changes and we need to send the type ahead of the data
        typed = !!obj.$flexType;
        amfType = typed ? 0x10 : 0x03; // typed vs. untyped object
        this.writeByte(amfType); // AMF0 object marker
        // if typed, send object type
        if (typed) {
            this.write0ShortUtf8String(obj.$flexType);
        }
        // then write out the data. There's no counter, but other than that it's the same as an ECMA array
        for (key in obj) {
            if (key != "$flexType") {
                Ext.Array.push(this.write0ObjectProperty(key, obj[key]));
            }
        }
        // And finally the object end marker
        this.writeBytes([0x00, 0x00, 0x09]);
    },

    /**
     * Writes a byte to the byte array
     * @param {number} b Byte to write to the array
     * @private
     */
    writeByte: function(b) {

        //<debug>
        if (b < 0 || b > 255) {
            Ex.Error.raise('ERROR: Value being written outside byte range: ' + b);
        }
        //</debug>
        Ext.Array.push(this.bytes, b);
    },

    /**
     * Writes a byte array to the byte array
     * @param {number} b Byte array to append to the array
     * @private
     */
    writeBytes: function(b) {
        var i;
        //<debug>
        if (!Ext.isArray(b)) {
            Ext.Error.raise("Decoder: writeBytes parameter is not an array: " + b);
        }
        for (i = 0; i < b.length; i++) {
            if (b[i] < 0 || b[i] > 255 || !Ext.isNumber(b[i])) {
                Ext.Error.raise("ERROR: Value " + i + " being written outside byte range: " + b[i]);
            }
        }
        //</debug>
        Ext.Array.push(this.bytes, b);
    },

    /**
     * Converts an XML Document object to a string.
     * @param {Object} xml XML document (type Document typically) to convert
     * @return {String} A string representing the document
     * @private
     */
    convertXmlToString: function(xml) {
        var str;
        if (window.XMLSerializer) {
            // this is not IE, so:
            str = new window.XMLSerializer().serializeToString(xml);
        } else {
            //no XMLSerializer, might be an old version of IE
            str = xml.xml;
        }
        return str;
    },


    /**
     * Tries to determine if an object is an XML document
     * @param {Object} item to identify
     * @return {Boolean} true if it's an XML document, false otherwise
     */
    isXmlDocument: function(item) {
        // We can't test if Document is defined since IE just throws an exception. Instead rely on the DOMParser object
        if (window.DOMParser) {
            if (Ext.isDefined(item.doctype)) {
                return true;
            }
        }
        // Otherwise, check if it has an XML field
        if (Ext.isString(item.xml)) {
            // and we can get the xml
            return true;
        }
        return false;
    },

    /*
     * The encodeDouble function is derived from code from the typedarray.js library by Linden Research, Inc.
     *

     Copyright (c) 2010, Linden Research, Inc.

     Permission is hereby granted, free of charge, to any person obtaining a copy
     of this software and associated documentation files (the "Software"), to deal
     in the Software without restriction, including without limitation the rights
     to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     copies of the Software, and to permit persons to whom the Software is
     furnished to do so, subject to the following conditions:

     The above copyright notice and this permission notice shall be included in
     all copies or substantial portions of the Software.

     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
     THE SOFTWARE.
     */

    /**
     * Encodes an IEEE-754 double-precision number.
     * @param {Number} num the number to encode
     * @return {Array} byte array containing the encoded number
     * @private
     */
    encodeDouble: function(v) {
        var ebits = 11, fbits = 52; // double
        var bias = (1 << (ebits - 1)) - 1,
            s, e, f, ln,
            i, bits, str, data = [];

        // Precalculated values
        var K_INFINITY=[127,240,0,0,0,0,0,0],
            K_NINFINITY=[255,240,0,0,0,0,0,0],
            K_NAN=[255,248,0,0,0,0,0,0];


        // Compute sign, exponent, fraction
        if (isNaN(v)) {
            data = K_NAN;
        } else if (v === Infinity) {
            data = K_INFINITY;
        } else if (v == -Infinity) {
            data = K_NINFINITY;
        } else {
            // not a special case, so encode
            if (v === 0) {
                e = 0; f = 0; s = (1 / v === -Infinity) ? 1 : 0;
            }
            else {
                s = v < 0;
                v = Math.abs(v);

                if (v >= Math.pow(2, 1 - bias)) {
                    // Normalized
                    ln = Math.min(Math.floor(Math.log(v) / Math.LN2), bias);
                    e = ln + bias;
                    f = Math.round(v * Math.pow(2, fbits - ln) - Math.pow(2, fbits));
                }
                else {
                    // Denormalized
                    e = 0;
                    f = Math.round(v / Math.pow(2, 1 - bias - fbits));
                }
            }

            // Pack sign, exponent, fraction
            bits = [];
            for (i = fbits; i; i -= 1) { bits.push(f % 2 ? 1 : 0); f = Math.floor(f / 2); }
            for (i = ebits; i; i -= 1) { bits.push(e % 2 ? 1 : 0); e = Math.floor(e / 2); }
            bits.push(s ? 1 : 0);
            bits.reverse();
            str = bits.join('');

            // Bits to bytes
            data = [];
            while (str.length) {
                data.push(parseInt(str.substring(0, 8), 2));
                str = str.substring(8);
            }
        }
        return data;
    },

    /**
     * Writes a short UTF8 string preceded with a 16-bit length.
     * @param {String} str the string to write
     */
    write0ShortUtf8String: function(str) {
        var utf8Data = this.encodeUtf8String(str),
            lenData;
        lenData = this.encodeXInt(utf8Data.length, 2);
        this.writeBytes(lenData);
        this.writeBytes(utf8Data);
    },

    /**
     * Writes an AMF packet to the byte array
     * @param {Array} headers the headers to serialize. Each item in the array
     *                should be an object with three fields:
     *                name, mustUnderstand, value
     * @param {Array} messages the messages to serialize. Each item in the array
     *                should be an object with three fields:
     *                targetUri, responseUri, body
     */
    writeAmfPacket: function(headers, messages) {
        var i;
        //<debug>
        if (this.config.format != 0) {
            Ext.Error.raise ("Trying to write a packet on an AMF3 Encoder. Only AMF0 is supported!");
        }
        if (!Ext.isArray(headers)) {
            Ext.Error.raise("headers is not an array: " + headers);
        }
        if (!Ext.isArray(messages)) {
            Ext.Error.raise("messages is not an array: " + messages);
        }
        //</debug>
        // Write Packet marker
        this.writeBytes([0x00, 0x00]); // AMF 0 version for this packet.
        // Write header count
        this.writeBytes(this.encodeXInt(headers.length, 2));
        // And actual headers
        for (i in headers) {
            this.writeAmfHeader(headers[i].name, headers[i].mustUnderstand, headers[i].value);
        }
        // Write message count
        this.writeBytes(this.encodeXInt(messages.length, 2));
        // And actual messages
        for (i in messages) {
            this.writeAmfMessage(messages[i].targetUri, messages[i].responseUri, messages[i].body);
        }
    },

    /**
     * Write an AMF header to the byte array. AMF headers are always encoded in AMF0.
     * @param {String} headerName the header name
     * @param {Boolean} mustUnderstand true if the receiver must understand this header or else reject it, false otherwise
     * @param {Object} value the value to serialize. Must be an object that can be serialized by AMF
     * @private
     */
    writeAmfHeader: function(headerName, mustUnderstand, value) {
        //<debug>
        if (this.config.format != 0) {
            Ext.Error.raise ("Trying to write a header on an AMF3 Encoder. Only AMF0 is supported!");
        }
        if (!Ext.isString(headerName)) {
            Ext.Error.raise("targetURI is not a string: " + targetUri);
        }
        if ((typeof(mustUnderstand) !== "boolean") && !Ext.isBoolean(mustUnderstand)) {
            Ext.Error.raise("mustUnderstand is not a boolean value: " + mustUnderstand);
        }
        //</debug>
        // write header name
        this.write0ShortUtf8String(headerName);
        // write must understand byte
        var mu = mustUnderstand ? 0x01 : 0x00;
        this.writeByte(mu);
        // next write the header length of -1 (undetermined) to the stream
        this.writeBytes(this.encodeXInt(-1, 4));
        // write value
        this.writeObject(value);
    },

    /**
     * Writes an AMF message to the byte array. AMF messages are always encoded in AMF0.
     * @param {String} targetUri the class / method to call
     * @param {String} responseUri the response should call here
     * @param {Array} body the parameters to pass to the called method, wrapped in an array
     * @private
     */
    writeAmfMessage: function(targetUri, responseUri, body) {
        //<debug>
        if (this.config.format != 0) {
            Ext.Error.raise ("Trying to write a message on an AMF3 Encoder. Only AMF0 is supported!");
        }
        if (!Ext.isString(targetUri)) {
            Ext.Error.raise("targetURI is not a string: " + targetUri);
        }
        if (!Ext.isString(responseUri)) {
            Ext.Error.raise("targetURI is not a string: " + responseUri);
        }
        if (!Ext.isArray(body)) {
            Ext.Error.raise("body is not an array: " + typeof(body));
        }
        //</debug>
        // write target URI
        this.write0ShortUtf8String(targetUri);
        // write response URI
        this.write0ShortUtf8String(responseUri);
        // next write the message length of -1 (undetermined) to the stream
        this.writeBytes(this.encodeXInt(-1, 4));
        // write the paramters
        this.write0StrictArray(body);
    }

});