// @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](../guides/backend_connectors/amf.html).
 */
/* global ActiveXObject */
/* eslint-disable eqeqeq */
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 (= 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 = 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 = 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,
            resp = {},
            i;
 
        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 (= 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)];
        }
 
        rawtraits = node.childNodes;
 
        for (= 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 = 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) {
            // check if special case for class
            klass = Ext.ClassManager.getByAlias('amfx.' + className);
        }
 
        // if there is no klass, mark the classname for easier parsing of returned results
        obj = klass ? new klass() : (className ? { $className: className } : {});
 
        // 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 (= 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 (> 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 (= 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 (= 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 (> len) {
                    Ext.raise("Array has more items than declared length: " + i + " > " + len);
                }
                //</debug>
            }
        }
 
        //<debug>
        if (< 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 (= 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 (!= 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 (= 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);
        }
    }
});