/**
 * This class manages a pending form submit. Instances of this type are created by the
 * `{@link Ext.data.Connection#request}` method.
 * @since 6.0.0
 */
Ext.define('Ext.data.request.Form', {
    extend: 'Ext.data.request.Base',
    alias:  'request.form',
 
    start: function(data) {
        var me = this,
            options = me.options,
            requestOptions = me.requestOptions;
        
        // Parent will set the timeout 
        me.callParent([data]);
        
        me.form = me.upload(options.form, requestOptions.url, requestOptions.data, options);
        
        return me;
    },
 
    abort: function(force) {
        var me = this,
            frame;
        
        if (me.isLoading()) {
            
            try {
                frame = me.frame.dom;
                
                if (frame.stop) {
                    frame.stop();
                }
                else {
                    frame.document.execCommand('Stop');
                }
            }
            catch (e) {
                // ignore 
            }
        }
        
        me.callParent([force]);
        
        me.onComplete();
        me.cleanup();
    },
    
    /*
     * Clean up any left over information from the form submission.
     */
    cleanup: function() {
        var me = this,
            frame = me.frame;
        
        if (frame) {
            // onComplete hasn't fired yet if frame != null so need to clean up 
            frame.un('load', me.onComplete, me);
            Ext.removeNode(frame);
        }
        
        me.frame = me.form = null;
    },
    
    isLoading: function() {
        return !!this.frame;
    },
    
    /**
     * Uploads a form using a hidden iframe.
     * @param {String/HTMLElement/Ext.dom.Element} form The form to upload
     * @param {String} url The url to post to
     * @param {String} params Any extra parameters to pass
     * @param {Object} options The initial options
     * @private
     */
    upload: function(form, url, params, options) {
        form = Ext.getDom(form);
        options = options || {};
 
        var frameDom = document.createElement('iframe'),
            frame = Ext.get(frameDom),
            id = frame.id,
            hiddens = [],
            encoding = 'multipart/form-data',
            buf = {
                target: form.target,
                method: form.method,
                encoding: form.encoding,
                enctype: form.enctype,
                action: form.action
            },
            addField = function(name, value) {
                hiddenItem = document.createElement('input');
                Ext.fly(hiddenItem).set({
                    type: 'hidden',
                    value: value,
                    name: name
                });
                form.appendChild(hiddenItem);
                hiddens.push(hiddenItem);
            },
            hiddenItem, obj, value, name, vLen, v, hLen, h, request;
 
        /*
         * Originally this behaviour was modified for Opera 10 to apply the secure URL after
         * the frame had been added to the document. It seems this has since been corrected in
         * Opera so the behaviour has been reverted, the URL will be set before being added.
         */
        frame.set({
            name: id,
            cls: Ext.baseCSSPrefix + 'hidden-display',
            src: Ext.SSL_SECURE_URL,
            tabIndex: -1
        });
 
        document.body.appendChild(frameDom);
        document.body.appendChild(form);
 
        // This is required so that IE doesn't pop the response up in a new window. 
        if (document.frames) {
            document.frames[id].name = id;
        }
 
        Ext.fly(form).set({
            target: id,
            method: 'POST',
            enctype: encoding,
            encoding: encoding,
            action: url || buf.action
        });
 
        // add dynamic params 
        if (params) {
            obj = Ext.Object.fromQueryString(params) || {};
 
            for (name in obj) {
                if (obj.hasOwnProperty(name)) {
                    value = obj[name];
                    if (Ext.isArray(value)) {
                        vLen = value.length;
                        for (= 0; v < vLen; v++) {
                            addField(name, value[v]);
                        }
                    } else {
                        addField(name, value);
                    }
                }
            }
        }
        
        this.frame = frame;
        
        frame.on({
            load: this.onComplete,
            scope: this,
            // Opera introduces multiple 'load' events, so account for extras as well 
            single: !Ext.isOpera
        });
 
        form.submit();
        document.body.removeChild(form);
 
        // Restore form to previous settings 
        Ext.fly(form).set(buf);
 
        for (hLen = hiddens.length, h = 0; h < hLen; h++) {
            Ext.removeNode(hiddens[h]);
        }
 
        return form;
    },
 
    getDoc: function() {
        var frame = this.frame.dom;
 
        return (frame && (frame.contentWindow.document || frame.contentDocument)) ||
                (window.frames[frame.id] || {}).document;
    },
 
    getTimeout: function() {
        // For a form post, since it can include large file uploads, we do not use the 
        // default timeout from the owner. Only explicit timeouts passed in the options 
        // are meaningful here. 
        return this.options.timeout;
    },
 
    /**
     * Callback handler for the upload function. After we've submitted the form via the
     * iframe this creates a bogus response object to simulate an XHR and populates its
     * responseText from the now-loaded iframe's document body (or a textarea inside the
     * body). We then clean up by removing the iframe.
     * @private
     */
    onComplete: function() {
        var me = this,
            frame = me.frame,
            owner = me.owner,
            options = me.options,
            callback, doc, success, contentNode, response;
        
        // Nulled out frame means onComplete was fired already 
        if (!frame) {
            return;
        }
        
        if (me.aborted || me.timedout) {
            me.result = response = me.createException();
            response.responseXML = null;
            response.responseText = '{success:false,message:"' + Ext.String.trim(response.statusText) + '"}';
           
            response.request = me;
            callback = options.failure;
            success = false;
        }
        else {
            try {
                doc = me.getDoc();
                
                // bogus response object 
                me.result = response = {
                    responseText: '',
                    responseXML: null,
                    request: me
                };
                
                // Opera will fire an extraneous load event on about:blank 
                // We want to ignore this since the load event will be fired twice 
                if (doc) {
                    //TODO: See if this still applies vs Current opera-webkit releases 
                    if (Ext.isOpera && doc.location == Ext.SSL_SECURE_URL) {
                        return;
                    }
                    
                    if (doc.body) {
                        // Response sent as Content-Type: text/json or text/plain. 
                        // Browser will embed it in a <pre> element. 
                        // Note: The statement below tests the result of an assignment. 
                        if ((contentNode = doc.body.firstChild) && /pre/i.test(contentNode.tagName)) {
                            response.responseText = contentNode.textContent || contentNode.innerText;
                        }
                        // Response sent as Content-Type: text/html. We must still support 
                        // JSON response wrapped in textarea. 
                        // Note: The statement below tests the result of an assignment. 
                        else if ((contentNode = doc.getElementsByTagName('textarea')[0])) {
                            response.responseText = contentNode.value;
                        }
                        // Response sent as Content-Type: text/html with no wrapping. Scrape 
                        // JSON response out of text 
                        else {
                            response.responseText = doc.body.textContent || doc.body.innerText;
                        }
                    }
                    
                    //in IE the document may still have a body even if returns XML. 
                    // TODO What is this about? 
                    response.responseXML = doc.XMLDocument || doc;
                    callback = options.success;
                    success = true;
                    response.status = 200;
                }
                else {
                    Ext.raise("Could not acquire a suitable connection for the file upload service.");
                }
            }
            catch (e) {
                me.result = response = me.createException();
                
                // Report any error in the message property 
                response.status = 400;
                response.statusText = (e.message || e.description) + '';
                response.responseText = '{success:false,message:"' + Ext.String.trim(response.statusText) + '"}';
                response.responseXML = null;
                
                callback = options.failure;
                success = false;
            }
        }
 
        me.frame = null;
        me.success = success;
 
        owner.fireEvent(success ? 'requestcomplete' : 'requestexception', owner, response, options);
 
        Ext.callback(callback, options.scope, [response, options]);
        Ext.callback(options.callback, options.scope, [options, success, response]);
        
        owner.onRequestComplete(me);
        
        // Must defer slightly to permit full exit from load event before destruction 
        Ext.asap(frame.destroy, frame);
 
        me.callParent();
    },
    
    destroy: function() {
        this.cleanup();
        this.callParent();
    }
});