// @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](../guides/backend_connectors/amf.html).
 */
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 (= 0; j < 3; j++) {
                // 3 -XXXX segments
                uid += "-";
 
                for (= 0; i < 4; i++) {
                    uid += Ext.Number.randomInt(0, 15).toString(16).toUpperCase();
                }
            }
 
            uid += "-";
            
            // add timestamp
            // get the String representation of milliseconds in hex format
            // eslint-disable-next-line newline-per-chained-call
            t = new Number(new Date()).valueOf().toString(16).toUpperCase();
            j = 0;
 
            if (t.length < 8) { // pad with "0" if needed
                for (= 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 (= 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 = [],
            i, str;
 
        for (in array) {
            if (Ext.isNumeric(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 (= 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 (= firstNonOrdinal; 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:
        // iterate by counting since we need to guarantee the order
        for (= 0; i < ordinals.length; i++) {
            str += ordinals[i];
        }
 
        // Now add ECMA items
        for (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 `$flexType` 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 (in obj) {
            if (=== "$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 `$flexType` 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 (= 0; i < array.length; i++) {
                //<debug>
                if (!Ext.isNumber(array[i])) {
                    Ext.raise("Byte array contains a non-number: " + array[i] + " in index: " + i);
                }
 
                if (array[i] < 0 || array[i] > 255) {
                    Ext.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);
 
        if (=== "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);
        }
        // Can't use Ext.isNumeric since it accepts strings as well
        else if (=== "number" || item instanceof Number) {
            return this.encodeNumber(item);
        }
        else if (=== "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 str = '<amfx ver="3" xmlns="http://www.macromedia.com/2005/amfx"><body>' +
                  message.encodeMessage() +
                  '</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(message) {
        this.write(this.encodeAmfxRemotingPacket(message));
    },
 
    /**
     * 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;
    }
});