/*
This file is part of Ext JS 4.2

Copyright (c) 2011-2013 Sencha Inc

Contact:  http://www.sencha.com/contact

Commercial Usage
Licensees holding valid commercial licenses may use this file in accordance with the Commercial
Software License Agreement provided with the Software or, alternatively, in accordance with the
terms contained in a written agreement between you and Sencha.

If you are unsure which license is appropriate for your use, please contact the sales department
at http://www.sencha.com/contact.

Build date: 2013-09-18 17:18:59 (940c324ac822b840618a3a8b2b4b873f83a1a9b1)
*/
// @tag enterprise
/**
 * @class Ext.data.amf.XmlEncoder
 * This class serializes data in the Action Message Format XML (AMFX) format.
 * It can write simple and complex objects, to be used in conjunction with an
 * AMFX-compliant server.
 * To create an encoded XMl, first construct an Encoder:
 *
 *     var encoder = Ext.create('Ext.data.amf.XmlEncoder');
 *
 * Then use the writer methods to out data to the :
 *
 *     encoder.writeObject(1);
 *     encoder.writeObject({a: "b"});
 *
 * And access the data through the #bytes property:
 *     encoder.body;
 *
 * You can also reset the class to start a new body:
 *
 *     encoder.clear();
 *
 * Current limitations:
 * AMF3 format (format:3)
 * - Each object is written out explicitly, not using the reference tables
 *   supported by the AMFX format. This means the function does NOT support
 *   circular reference objects.
 * - Objects that aren't Arrays, Dates, Strings, Document (XML) or primitive
 *   values will be written out as anonymous objects with dynamic data.
 * - If the object has a $flexType field, that field will be used in signifying
 *   the object-type as an attribute, instead of being passed as 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.
 *
 * For more information on working with AMF data please refer to the
 * [AMF Guide](#/guide/amf).
 */

Ext.define('Ext.data.amf.XmlEncoder', {

    alias: 'data.amf.xmlencoder',

    /**
     * @property {String} body - The output string
     */
    body: "",

    statics: {
        /**
         * Utility function to generate a flex-friendly UID
         * @param {Number} id used in the first 8 chars of the id. If not provided, a random number will be used.
         * @return {String} a string-encoded opaque UID
         */
        generateFlexUID: function(id) {
            var uid = "",
                i, j, t;
            if (id === undefined) {
                id = Ext.Number.randomInt(0, 0xffffffff);
            }
            // The format of a UID is XXXXXXXX-XXXX-XXXX-XXXX-YYYYYYYYXXXX
            // where each X is a random hex digit and each Y is a hex digit from the least significant part of a time stamp.
            t =  (id + 0x100000000).toString(16).toUpperCase(); // padded
            uid = t.substr(t.length - 8, 8); // last 8 chars

            for (j = 0; j < 3; j++) {
                // 3 -XXXX segments
                uid += "-";
                for (i = 0; i < 4; i++) {
                    uid += Ext.Number.randomInt(0, 15).toString(16).toUpperCase();
                }
            }
            uid += "-";
            // add timestamp
            t = new Number(new Date()).valueOf().toString(16).toUpperCase(); // get the String representation of milliseconds in hex format
            j = 0;
            if (t.length < 8) { // pad with "0" if needed
                for (i = 0; i < t.length - 8; i++) {
                    j++;
                    uid += "0";
                }
            }
            // actual timestamp:
            uid += t.substr(-(8-j)); // last few chars
            // and last 4 random digits
            for (i = 0; i < 4; i++) {
                uid += Ext.Number.randomInt(0, 15).toString(16).toUpperCase();
            }
            return uid;
        }
    },

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

    /**
     * Clears the accumulated data, starting with an empty string
     */
    clear: function() {
        this.body = "";
    },

    /**
     * Returns the encoding for undefined (which is the same as the encoding for null)
     */
    encodeUndefined: function() {
        return this.encodeNull();
    },

    /**
     * Writes the undefined value to the string
     */
    writeUndefined: function() {
        this.write(this.encodeUndefined());
    },

    /**
     * Returns the encoding for null
     */
    encodeNull: function() {
        return "<null />";
    },

    /**
     * Writes the null value to the string
     */
    writeNull: function() {
        this.write(this.encodeNull());
    },

    /**
     * Returns an encoded boolean
     * @param {Boolean} val a boolean value
     */
    encodeBoolean: function(val) {
        var str;
        if (val) {
            str = "<true />";
        } else {
            str = "<false />";
        }
        return str;
    },

    /**
     * Writes a boolean value to the string
     * @param {Boolean} val a boolean value
     */
    writeBoolean: function(val) {
        this.write(this.encodeBoolean(val));
    },


    /**
     * Returns an encoded string
     * @param {String} str the string to encode
     */
    encodeString: function(str) {
        var ret;
        if (str === "") {
            ret = "<string />";
        } else {
            ret ="<string>"+str+"</string>";
        }
        return ret;
    },

    /**
     * Writes a string tag with the string content.
     * @param {String} str the string to encode
     */
    writeString: function(str) {
        this.write(this.encodeString(str));
    },

    /**
     * Returns an encoded int
     * @param {Number} num the integer to encode
     */
    encodeInt: function(num) {
        return "<int>" + num.toString() + "</int>";
    },

    /**
     * Writes a int tag with the content.
     * @param {Number} num the integer to encode
     */
    writeInt: function(num) {
        this.write(this.encodeInt(num));
    },

    /**
     * Returns an encoded double
     * @param {Number} num the double to encode
     */
    encodeDouble: function(num) {
        return "<double>" + num.toString() + "</double>";
    },

    /**
     * Writes a double tag with the content.
     * @param {Number} num the double to encode
     */
    writeDouble: function(num) {
        this.write(this.encodeDouble(num));
    },

    /**
     * Returns an encoded number. Decides wheter to use int or double encoding.
     * @param {Number} num the number to encode
     */
    encodeNumber: function(num) {
        var maxInt = 0x1fffffff,
            minSignedInt = -0xfffffff;
        //<debug>
        if (typeof(num) !== "number" && !(num instanceof Number)) {
            Ext.log.warn("Encoder: writeNumber argument is not numeric. Can't coerce.");
        }
        // </debug>

        // switch to the primitive value for handling:
        if (num instanceof Number) {
            num = num.valueOf();
        }
        // Determine if this is an integer or a float.
        if (num % 1 === 0 && num >= minSignedInt && num <= maxInt) {
            // The number has no decimal point and is within bounds. Let's encode it.
            return this.encodeInt(num);
        } else {
            return this.encodeDouble(num);
        }
    },

    /**
     * Writes a number, deciding if to use int or double as the tag
     * @param {Number} num the number to encode
     */
    writeNumber: function(num) {
        this.write(this.encodeNumber(num));
    },

    /**
     * Encode a date
     * @param {Date} date the date to encode
     */
    encodeDate: function(date) {
        return "<date>" + (new Number(date)).toString() + "</date>";
    },

    /**
     * Write a date to the string
     * @param {Date} date the date to encode
     */
    writeDate: function(date) {
        this.write(this.encodeDate(date));
    },

    /**
     * @private
     * Encodes one ECMA array element
     * @param {String} key the name of the element
     * @param {Object} value the value of the element
     * @return {String} the encoded key-value pair
     */
    encodeEcmaElement: function(key, value) {
        var str = '<item name="' + key.toString() + '">' + this.encodeObject(value) + '</item>';
        return str;
    },

    /**
     * Encodes an array, marking it as an ECMA array if it has associative (non-ordinal) indices
     * @param {Array} array the array to encode
     */
    encodeArray: function(array) {
        var ordinals=[],
            firstNonOrdinal,
            ecmaElements=[],
            length = array.length, // length is of ordinal section only
            i, str;
        for (i in array) {
            if (Ext.isNumeric(i) && (i % 1 == 0)) {
                //this is an integer. Add to ordinals array
                ordinals[i] = this.encodeObject(array[i]);
            } else {
                ecmaElements.push(this.encodeEcmaElement(i, array[i]));
            }
        }
        firstNonOrdinal=ordinals.length;
        // now, check if we have consecutive numbers in the ordinals array
        for (i = 0; i < ordinals.length; i++) {
            if (ordinals[i] === undefined) {
                //  we have a gap in the array. Mark it - the rest of the items become ECMA elements
                firstNonOrdinal = i;
                break;
            }
        }
        if (firstNonOrdinal < ordinals.length) {
            // transfer some of the elements to the ecma array
            for (i = firstNonOrdinals; i < ordinals.length; i++) {
                if (ordinals[i] !== undefined) {
                    ecmaElements.push(this.encodeEcmaElement(i, ordinals[i]));
                }
            }
            ordinals = ordinals.slice(0, firstNonOrdinal);
        }

        // finally start constructing the string
        str = '<array length="' + ordinals.length + '"';
        if (ecmaElements.length > 0) {
            str += ' ecma="true"';
        }
        str += '>';

        // first add the oridnals in consecutive order:
        for (i = 0; i < ordinals.length; i++) { // iterate by counting since we need to guarantee the order
            str += ordinals[i];
        }
        // Now add ECMA items
        for (i in ecmaElements) {
            str += ecmaElements[i];
        }
        // And close the array:
        str += '</array>';
        return str;
    },

    /**
     * Writes an array to the string, marking it as an ECMA array if it has associative (non-ordinal) indices
     * @param {Array} array the array to encode
     */
    writeArray: function(array) {
        this.write(this.encodeArray(array));
    },

    /**
     * Encodes an xml document into a CDATA section
     * @param {XMLElement/HTMLElement} xml an XML document or element (Document type in some browsers)
     */
    encodeXml: function(xml) {
        var str = this.convertXmlToString(xml);
        return "<xml><![CDATA[" + str + "]]></xml>";
    },

    /**
     * Write an XML document to the string
     * @param {XMLElement/HTMLElement} xml an XML document or element (Document type in some browsers)
     */
    writeXml: function(xml) {
        this.write(this.encodeXml(xml));
    },

    /**
     * Encodes a generic object into AMFX format. If a <tt>$flexType</tt> member is defined, list that as the object type.
     * @param {Object} obj the object to encode
     * @return {String} the encoded text
     */
    encodeGenericObject: function(obj) {
        var traits = [],
            values = [],
            flexType = null,
            i, str;
        for (i in obj) {
            if (i == "$flexType") {
                flexType = obj[i];
            } else {
                traits.push(this.encodeString(new String(i)));
                values.push(this.encodeObject(obj[i]));
            }
        }
        if (flexType) {
            str = '<object type="' +flexType + '">';
        } else {
            str="<object>";
        }
        if (traits.length > 0) {
            str += "<traits>";
            str += traits.join("");
            str += "</traits>";
        } else {
            str += "<traits />";
        }
        str += values.join("");
        str += "</object>";
        return str;
    },

    /**
     * Writes a generic object to the string. If a <tt>$flexType</tt> member is defined, list that as the object type.
     * @param {Object} obj the object to encode
     */
    writeGenericObject: function(obj) {
        this.write(this.encodeGenericObject(obj));
    },

    /**
     * Encodes a byte arrat in AMFX format
     * @param {Array} array the byte array to encode
     */
    encodeByteArray: function(array) {
        var str, i, h;
        if (array.length > 0) {
            str = "<bytearray>";
            for (i = 0; i < array.length; i++) {
                //<debug>
                if (!Ext.isNumber(array[i])) {
                    Ext.Error.raise("Byte array contains a non-number: " + array[i]  + " in index: " + i);
                }
                if (array[i] < 0 || array[i] > 255) {
                    Ext.Error.raise("Byte array value out of bounds: " + array[i]);
                }
                //</debug>
                h = array[i].toString(16).toUpperCase();
                if (array[i] < 0x10) {
                    h = "0" + h;
                }
                str += h;
            }
            str += "</bytearray>";
        } else {
            str = "<bytearray />";
        }
        return str;
    },

    /**
     * Writes an AMFX byte array to the string. This is for convenience only and is not called automatically by writeObject.
     * @param {Array} array the byte array to encode
     */
    writeByteArray: function(array) {
        this.write(this.encodeByteArray(array));
    },

    /**
     * encode the appropriate data item. Supported types:
     * - undefined
     * - null
     * - boolean
     * - integer
     * - double
     * - UTF-8 string
     * - XML Document (identified by being instaneof Document. Can be generated with: new DOMParser()).parseFromString(xml, "text/xml");
     * - Date
     * - Array
     * - Generic object
     * @param {Object} item A primitive or object to write to the stream
     * @return {String} the encoded object in AMFX format
     */
    encodeObject: function(item) {
        var t = typeof(item);
        //Ext.log("Writing " + item + " of type " + t);
        if (t === "undefined") {
            return this.encodeUndefined();
        } else if (item === null) { // can't check type since typeof(null) returns "object"
            return this.encodeNull();
        } else if (Ext.isBoolean(item)) {
            return this.encodeBoolean(item);
        } else if (Ext.isString(item)) {
            return this.encodeString(item);
        } else if (t === "number" || item instanceof Number) { // Can't use Ext.isNumeric since it accepts strings as well
            return this.encodeNumber(item);
        } else if (t === "object") {
            // Figure out which object this is
            if (item instanceof Date) {
                return this.encodeDate(item);
            } else if (Ext.isArray(item)) {
                return this.encodeArray(item);
            } else if (this.isXmlDocument(item)) {
                return this.encodeXml(item);
            } else {
                // Treat this as a generic object with name/value pairs of data.
                return this.encodeGenericObject(item);
            }
        } else {
            //<debug>
            Ext.log.warn("AMFX Encoder: Unknown item type " + t + " can't be written to stream: " + item);
            //</debug>
        }
        return null; // if we reached here, return null
    },

    /**
     * Writes the appropriate data item to the string. Supported types:
     * - undefined
     * - null
     * - boolean
     * - integer
     * - double
     * - UTF-8 string
     * - XML Document (identified by being instaneof Document. Can be generated with: new DOMParser()).parseFromString(xml, "text/xml");
     * - Date
     * - Array
     * - Generic object
     * @param {Object} item A primitive or object to write to the stream
     */
    writeObject: function(item) {
        this.write(this.encodeObject(item));
    },

    /**
     * Encodes an AMFX remoting message with the AMFX envelope.
     * @param {Ext.data.amf.RemotingMessage} message the message to pass on to serialize.
     */
    encodeAmfxRemotingPacket: function(message) {
        var msg, str;
        str = '<amfx ver="3" xmlns="http://www.macromedia.com/2005/amfx"><body>';
        str += message.encodeMessage();
        str += '</body></amfx>';
        return str;
    },

    /**
     * Writes an AMFX remoting message with the AMFX envelope to the string.
     * @param {Ext.data.amf.RemotingMessage} message the message to pass on to serialize.
     */
    writeAmfxRemotingPacket: function(params) {
        this.write(this.encodeAmfxRemotingPacket(params));
    },

    /**
     * Converts an XML Document object to a string.
     * @param {Object} xml XML document to convert (typically Document object)
     * @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;
    },

    /**
     * Appends a string to the body of the message
     * @param {String} str the string to append
     * @private
     */
    write: function(str) {
        this.body += str;
    }
});
