// @tag enterprise
/**
 * @class Ext.data.amf.XmlDecoder
 * This class parses an XML-based AMFX message and returns the deserialized
 * objects. You should not need to use this class directly. It's mostly used by
 * the AMFX Direct implementation.
 * To decode a message, first construct a Decoder:
 *
 *      decoder = Ext.create('Ext.data.amf.XmlDecoder');
 *
 * Then ask it to read in the message :
 *
 *     resp = decoder.readAmfxMessage(str);
 *
 * For more information on working with AMF data please refer to the
 * [AMF Guide](#/guide/amf).
 */
Ext.define('Ext.data.amf.XmlDecoder', {

    alias: 'data.amf.xmldecoder',

    statics: {

        /**
         * Parses an xml string and returns an xml document
         * @private
         * @param {String} xml
         */
        readXml: 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;
        },

        /**
         * parses a node containing a byte array in hexadecimal format, returning the reconstructed array.
         * @param {HTMLElement/XMLElement} node the node
         * @return {Array} a byte array
         */
        readByteArray: function(node) {
            var bytes = [],
                c, i, str;
            str = node.firstChild.nodeValue;
            for (i = 0; i < str.length; i = i + 2) {
                c = str.substr(i, 2);
                bytes.push(parseInt(c, 16));
            }
            return bytes;
        },

        /**
         * Deserializes an AMF3 binary object from a byte array
         * @param {Array} bytes the byte array containing one AMF3-encoded value
         * @return {Object} the decoded value
         */
        readAMF3Value: function(bytes) {
            var packet;
            packet = Ext.create('Ext.data.amf.Packet');
            return packet.decodeValue(bytes);
        },

        /**
         * Accepts Flex-style UID and decodes the number in the first four bytes (8 hex digits) of data.
         * @param {String} messageId the message ID
         * @return {Number} the transaction ID
         */
        decodeTidFromFlexUID: function(messageId) {
            var str;
            str = messageId.substr(0,8);
            return parseInt(str, 16);
        }

    },

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

    /**
     * Clears the accumulated data and reference tables
     */
    clear: function() {
        // reset reference counters
        this.objectReferences=[];
        this.traitsReferences=[];
        this.stringReferences=[];
    },

    /**
     * Reads and returns a decoded AMFX packet.
     * @param {String} xml the xml of the message
     * @return {Object} the response object containing the message
     */
    readAmfxMessage: function(xml) {
        var doc, amfx, body,
            i, resp={};
        this.clear(); // reset counters
        doc = Ext.data.amf.XmlDecoder.readXml(xml);
        amfx = doc.getElementsByTagName('amfx')[0];
        //<debug>
        if (!amfx) {
            Ext.warn.log("No AMFX tag in message");
        }
        if (amfx.getAttribute('ver') != "3") {
            Ext.raise("Unsupported AMFX version: " + amfx.getAttribute('ver'));
        }
        //</debug>
        body = amfx.getElementsByTagName('body')[0];
        resp.targetURI = body.getAttribute('targetURI');
        resp.responseURI = body.getAttribute('responseURI'); // most likely empty string
        for (i = 0; i < body.childNodes.length; i++) {
            if (body.childNodes.item(i).nodeType != 1) {
                // only process element nodes, ignore white space and text nodes
                continue;
            }
            resp.message = this.readValue(body.childNodes.item(i));
            break; // no need to keep iterating
        }
        return resp;
    },

    /**
     * Parses an HTML element returning the appropriate JavaScript value from the AMFX data.
     * @param {HTMLElement} node The node to parse
     * @return {Object} a JavaScript object or value
     */
    readValue: function(node) {
        var val;
        if (typeof node.normalize === 'function') {
            node.normalize();
        }
        // 2DO: handle references!
        if (node.tagName == "null") {
            return null;
        } else if (node.tagName == "true") {
            return true;
        } else if (node.tagName == "false") {
            return false;
        } else if (node.tagName == "string") {
            return this.readString(node);
        } else if (node.tagName == "int") {
            return parseInt(node.firstChild.nodeValue);
        } else if (node.tagName == "double") {
            return parseFloat(node.firstChild.nodeValue);
        } else if (node.tagName == "date") {
            val = new Date(parseFloat(node.firstChild.nodeValue));
            // record in object reference table
            this.objectReferences.push(val);
            return val;
        } else if (node.tagName == "dictionary") {
            return this.readDictionary(node);
        } else if (node.tagName == "array") {
            return this.readArray(node);
        } else if (node.tagName == "ref") {
            return this.readObjectRef(node);
        } else if (node.tagName == "object") {
            return this.readObject(node);
        } else if (node.tagName == "xml") {
            // the CDATA content of the node is a parseable XML document. parse it.
            return Ext.data.amf.XmlDecoder.readXml(node.firstChild.nodeValue);
        } else if (node.tagName == "bytearray") {
            // a byte array is usually an AMF stream. Parse it to a byte array, then pass through the AMF decoder to get the objects inside
            return Ext.data.amf.XmlDecoder.readAMF3Value(Ext.data.amf.XmlDecoder.readByteArray(node));
        }
        //<debug>
        Ext.raise("Unknown tag type: " + node.tagName);
        //</debug>
        return null;
    },

    /**
     * Reads a string or string reference and return the value
     * @param {HTMLElement/XMLElement} node the node containing a string object
     * @return {String} the parsed string
     */
    readString: function(node) {
        var val;
        if (node.getAttributeNode('id')) {
            return this.stringReferences[parseInt(node.getAttribute('id'))];
        }
        val = (node.firstChild ? node.firstChild.nodeValue : "") || "";
        this.stringReferences.push(val);
        return val;
    },

    /**
     * Parses and returns an ordered list of trait names
     * @param {HTMLElement/XMLElement} node the traits node from the XML doc
     * @return {Array} an array of ordered trait names or null if it's an externalizable object
     */
    readTraits: function(node) {
        var traits = [], i, rawtraits;
        if (node === null) {
            return null;
        }
        if (node.getAttribute('externalizable') == "true") {
            // no traits since it's an externalizable or a null object.
            return null;
        }
        if (node.getAttributeNode('id')) {
            // return traits reference
            return this.traitsReferences[parseInt(node.getAttributeNode('id').value)];
        }
        /* // empty anonymous objects still seem to get their empty traits in the reference table
         if (!node.hasChildNodes()) {
         var className = node.parentNode.getElementsByTagName('type');
         if (className.length == 0) {
         return traits; // special case of an anonymous object with no traits. Does not get reference counted
         }
         }
         */
        rawtraits = node.childNodes;
        for (i = 0; i < rawtraits.length; i++) {
            if (rawtraits.item(i).nodeType != 1) {
                // only process element nodes, ignore white space and text nodes
                continue;
            }
            // this will be a string, but let the readValue function handle it nonetheless
            traits.push(this.readValue(rawtraits.item(i)));
        }

        // register traits in ref table:
        this.traitsReferences.push(traits);
        return traits;
    },

    /**
     * Parses and return an object / array / dictionary / date from reference
     * @param {HTMLElement/XMLElement} node the ref node
     * @return {Object} the previously instantiated object referred to by the ref node
     */
    readObjectRef: function(node) {
        var id;
        id = parseInt(node.getAttribute('id'));
        return this.objectReferences[id];
    },

    /**
     * Parses and returns an AMFX object.
     * @param {HTMLElement/XMLElement} node the `<object>` node to parse
     * @return {Object} the deserialized object
     */
    readObject: function(node) {
        var obj,
            traits = [],
            traitsNode,
            i, j, n,
            key, val,
            klass = null, className;

        className = node.getAttribute('type');
        if (className) {
            klass = Ext.ClassManager.getByAlias('amfx.' + className); // check if special case for class
        }
        obj = klass ? new klass() : (className ? {$className: className} : {}); // if there is no klass, mark the classname for easier parsing of returned results

        // check if we need special handling for this class
        if ((!klass) && this.converters[className]) {
            obj = this.converters[className](this,node);
            return obj; // we're done
        }

        traitsNode = node.getElementsByTagName('traits')[0];
        traits = this.readTraits(traitsNode);
        //<debug>
        if (traits === null) {
            Ext.raise("No support for externalizable object: " + className);
        }
        //</debug>
        // Register object if ref table, in case there's a cyclical reference coming
        this.objectReferences.push(obj);


        // Now we expect an item for each trait name we have. We assume it's an ordered list. We'll skip the first (traits) tag
        j = 0;
        for (i = 0; i < node.childNodes.length; i++) {
            n = node.childNodes.item(i);
            if (n.nodeType != 1) {
                // Ignore text nodes and non-element nodes
                continue;
            }
            if (n.tagName == "traits") {
                // ignore the traits node. We've already covered it.
                continue;
            }
            key = traits[j];
            val = this.readValue(n);
            j = j + 1;
            obj[key] = val;
            //<debug>
            if (j > traits.length) {
                Ext.raise("Too many items for object, not enough traits: " + className);
            }
            //</debug>
        }
        return obj;
    },

    /**
     * Parses and returns an AMFX array.
     * @param {HTMLElement/XMLElement} node the array node
     * @return {Array} the deserialized array
     */
    readArray: function(node) {
        var arr=[],
            n,i,j,l,name, val, len, childnodes, cn;

        // register array in object references table before we parse, in case of circular references
        this.objectReferences.push(arr);

        len = parseInt(node.getAttributeNode('length').value);
        i = 0;
        // the length only accounts for the ordinal values. For the rest, we'll read them as ECMA key-value pairs
        for (l = 0; l < node.childNodes.length; l++) {
            n = node.childNodes.item(l);
            if (n.nodeType != 1) {
                // Ignore text nodes and non-element nodes
                continue;
            }
            if (n.tagName == "item") {
                // parse item node
                name = n.getAttributeNode('name').value;
                childnodes = n.childNodes;
                for (j = 0; j < childnodes.length; j++) {
                    cn = childnodes.item(j);
                    if (cn.nodeType != 1) {
                        // Ignore text nodes and non-element nodes
                        continue;
                    }
                    val = this.readValue(cn);
                    break; // out of loop. We've found our value
                }
                arr[name] = val;
            } else {
                // ordinal node
                arr[i] = this.readValue(n);
                i++;
                //<debug>
                if (i > len) {
                    Ext.raise("Array has more items than declared length: " + i + " > " + len);
                }
                //</debug>
            }
        }
        //<debug>
        if (i < len) {
            Ext.raise("Array has less items than declared length: " + i + " < " + len);
        }
        //</debug>
        return arr;
    },

    /**
     * Parses and returns an AMFX dictionary.
     * @param {HTMLElement/XMLElement} node the `<dictionary>` node
     * @return {Object} a javascript object with the dictionary value-pair elements
     */
    readDictionary: function(node) {
        // For now, handle regular objects
        var dict = {},
            key, val,
            i, j, n, len;

        len = parseInt(node.getAttribute('length'));
        // Register dictionary in the ref table, in case there's a cyclical reference coming
        this.objectReferences.push(dict);


        // now find pairs of keys and values
        key = null;
        val = null;
        j = 0;
        for (i = 0; i < node.childNodes.length; i++) {
            n = node.childNodes.item(i);
            if (n.nodeType != 1) {
                // Ignore text nodes and non-element nodes
                continue;
            }
            if (!key) {
                key = this.readValue(n);
                continue; // next element is the value
            }
            val = this.readValue(n);
            j = j + 1;
            dict[key] = val;
            key = null;
            val = null;
        }
        //<debug>
        if (j != len) {
            Ext.raise("Incorrect number of dictionary values: " + j + " != " + len);
        }
        //</debug>
        return dict;
    },


    /**
     * Converts externalizable flex objects with a source array to a regular array.
     * @private
     */
    convertObjectWithSourceField: function(node) {
        var i, n, val;
        for (i = 0; i < node.childNodes.length; i++) {
            n = node.childNodes.item(i);
            if (n.tagName == "bytearray") {
                val = this.readValue(n);
                this.objectReferences.push(val);
                return val;
            }
        }
        return null; // we shouldn't reach here, but just in case
    },

    /**
     * Converters used in converting specific typed Flex classes to JavaScript usable form.
     * @private
     */

    converters: {
        'flex.messaging.io.ArrayCollection': function(decoder,node) {
            return decoder.convertObjectWithSourceField(node);
        },
        'mx.collections.ArrayList':  function(decoder,node) {
            return decoder.convertObjectWithSourceField(node);
        },
        'mx.collections.ArrayCollection':  function(decoder,node) {
            return decoder.convertObjectWithSourceField(node);
        }
    }
});
