/**
 * The Form panel presents a set of form fields and provides convenient ways to load and save data. Usually a form
 * panel just contains the set of fields you want to display, ordered inside the items configuration like this:
 *
 *     @example
 *     var form = Ext.create('Ext.form.Panel', {
 *         fullscreen: true,
 *         items: [
 *             {
 *                 xtype: 'textfield',
 *                 name: 'name',
 *                 label: 'Name'
 *             },
 *             {
 *                 xtype: 'emailfield',
 *                 name: 'email',
 *                 label: 'Email'
 *             },
 *             {
 *                 xtype: 'passwordfield',
 *                 name: 'password',
 *                 label: 'Password'
 *             }
 *         ]
 *     });
 *
 * Here we just created a simple form panel which could be used as a registration form to sign up to your service. We
 * added a plain {@link Ext.field.Text text field} for the user's Name, an {@link Ext.field.Email email field} and
 * finally a {@link Ext.field.Password password field}. In each case we provided a {@link Ext.field.Field#name name}
 * config on the field so that we can identify it later on when we load and save data on the form.
 *
 * ##Loading data
 *
 * Using the form we created above, we can load data into it in a few different ways, the easiest is to use
 * {@link #setValues}:
 *
 *     form.setValues({
 *         name: 'Peter',
 *         email: 'peter.venkman@gb.com',
 *         password: 'secret'
 *     });
 *
 * It's also easy to load {@link Ext.data.Model Model} instances into a form - let's say we have a User model and want
 * to load a particular instance into our form:
 *
 *     Ext.define('MyApp.model.User', {
 *         extend: 'Ext.data.Model',
 *         config: {
 *             fields: ['name', 'email', 'password']
 *         }
 *     });
 *
 *     var ed = Ext.create('MyApp.model.User', {
 *         name: 'Peter',
 *         email: 'peter.venkman@gb.com',
 *         password: 'secret'
 *     });
 *
 *     form.setRecord(ed);
 *
 * ##Retrieving form data
 *
 * Getting data out of the form panel is simple and is usually achieve via the {@link #getValues} method:
 *
 *     var values = form.getValues();
 *
 *     //values now looks like this:
 *     {
 *         name: 'Peter',
 *         email: 'peter.venkman@gb.com',
 *         password: 'secret'
 *     }
 *
 * It's also possible to listen to the change events on individual fields to get more timely notification of changes
 * that the user is making. Here we expand on the example above with the User model, updating the model as soon as
 * any of the fields are changed:
 *
 *     var form = Ext.create('Ext.form.Panel', {
 *         listeners: {
 *             '> field': {
 *                 change: function(field, newValue, oldValue) {
 *                     ed.set(field.getName(), newValue);
 *                 }
 *             }
 *         },
 *         items: [
 *             {
 *                 xtype: 'textfield',
 *                 name: 'name',
 *                 label: 'Name'
 *             },
 *             {
 *                 xtype: 'emailfield',
 *                 name: 'email',
 *                 label: 'Email'
 *             },
 *             {
 *                 xtype: 'passwordfield',
 *                 name: 'password',
 *                 label: 'Password'
 *             }
 *         ]
 *     });
 *
 * The above attached a listener to the {@link Ext.field.Text#change change} event of each form
 * field that is a direct child of the form panel. Our listener gets the name of the field that fired the change event,
 * and updates our {@link Ext.data.Model Model} instance with the new value. For example, changing the email field
 * in the form will update the Model's email field.
 *
 * ##Submitting forms
 *
 * There are a few ways to submit form data. In our example above we have a Model instance that we have updated, giving
 * us the option to use the Model's {@link Ext.data.Model#save save} method to persist the changes back to our server,
 * without using a traditional form submission. Alternatively, we can send a normal browser form submit using the
 * {@link #method} method:
 *
 *     form.submit({
 *         url: 'url/to/submit/to',
 *         method: 'POST',
 *         success: function() {
 *             alert('form submitted successfully!');
 *         }
 *     });
 *
 * In this case we provided the `url` to submit the form to inside the submit call - alternatively you can just set the
 * {@link #url} configuration when you create the form. We can specify other parameters (see {@link #method} for a
 * full list), including callback functions for success and failure, which are called depending on whether or not the
 * form submission was successful. These functions are usually used to take some action in your app after your data
 * has been saved to the server side.
 */
Ext.define('Ext.form.Panel', {
    alternateClassName: 'Ext.form.FormPanel',
    extend  : 'Ext.Panel',
    xtype   : 'formpanel',
    requires: ['Ext.XTemplate', 'Ext.field.Checkbox', 'Ext.Ajax'],

    /**
     * @event submit
     * @preventable doSubmit
     * Fires upon successful (Ajax-based) form submission.
     * @param {Ext.form.Panel} this This FormPanel.
     * @param {Object} result The result object as returned by the server.
     * @param {Ext.event.Event} e The event object.
     */

    /**
     * @event beforesubmit
     * @preventable doBeforeSubmit
     * Fires immediately preceding any Form submit action.
     * Implementations may adjust submitted form values or options prior to execution.
     * A return value of `false` from this listener will abort the submission
     * attempt (regardless of `standardSubmit` configuration).
     * @param {Ext.form.Panel} this This FormPanel.
     * @param {Object} values A hash collection of the qualified form values about to be submitted.
     * @param {Object} options Submission options hash (only available when `standardSubmit` is `false`).
     * @param {Ext.event.Event} e The event object if the form was submitted via a HTML5 form submit event.
     */

    /**
     * @event exception
     * Fires when either the Ajax HTTP request reports a failure OR the server returns a `success:false`
     * response in the result payload.
     * @param {Ext.form.Panel} this This FormPanel.
     * @param {Object} result Either a failed Ext.data.Connection request object or a failed (logical) server.
     * response payload.
     */

    config: {
        /**
         * @cfg {String} baseCls
         * @inheritdoc
         */
        baseCls: Ext.baseCSSPrefix + 'form',

        /**
         * @cfg {Boolean} standardSubmit
         * Whether or not we want to perform a standard form submit.
         * @accessor
         */
        standardSubmit: false,

        /**
         * @cfg {String} url
         * The default url for submit actions.
         * @accessor
         */
        url: null,

        /**
         * @cfg {String} enctype
         * The enctype attribute for the form, specifies how the form should be encoded when submitting
         */
        enctype: null,

        /**
         * @cfg {Object} baseParams
         * Optional hash of params to be sent (when `standardSubmit` configuration is `false`) on every submit.
         * @accessor
         */
        baseParams: null,

        /**
         * @cfg {Object} submitOnAction
         * When this is set to `true`, the form will automatically submit itself whenever the `action`
         * event fires on a field in this form. The action event usually fires whenever you press
         * go or enter inside a textfield.
         * @accessor
         */
        submitOnAction: false,

        /**
         * @cfg {Ext.data.Model} record The model instance of this form. Can by dynamically set at any time.
         * @accessor
         */
        record: null,

        /**
         * @cfg {String} method
         * The method which this form will be submitted. `post` or `get`.
         */
        method: 'post',

        /**
         * @inheritdoc
         */
        scrollable: {},
        
        /**
         * @cfg {Boolean} trackResetOnLoad
         * If set to true, {@link #reset}() resets to the last loaded or {@link #setValues}() data instead of
         * when the form was first created.
         */
        trackResetOnLoad:false,

        /**
         * @cfg {Object} api
         * If specified, load and submit actions will be loaded and submitted via Ext.Direct.  Methods which have been imported by
         * {@link Ext.direct.Manager} can be specified here to load and submit forms. API methods may also be
         * specified as strings and will be parsed into the actual functions when the first submit or load has occurred. Such as the following:
         *
         *     api: {
         *         load: App.ss.MyProfile.load,
         *         submit: App.ss.MyProfile.submit
         *     }
         *
         *     api: {
         *         load: 'App.ss.MyProfile.load',
         *         submit: 'App.ss.MyProfile.submit'
         *     }
         *
         * Load actions can use {@link #paramOrder} or {@link #paramsAsHash} to customize how the load method
         * is invoked.  Submit actions will always use a standard form submit. The `formHandler` configuration
         * (see Ext.direct.RemotingProvider#action) must be set on the associated server-side method which has
         * been imported by {@link Ext.direct.Manager}.
         */
        api: null,

        /**
         * @cfg {String/String[]} paramOrder
         * A list of params to be executed server side. Only used for the {@link #api} `load`
         * configuration.
         *
         * Specify the params in the order in which they must be executed on the
         * server-side as either (1) an Array of String values, or (2) a String of params
         * delimited by either whitespace, comma, or pipe. For example,
         * any of the following would be acceptable:
         *
         *     paramOrder: ['param1','param2','param3']
         *     paramOrder: 'param1 param2 param3'
         *     paramOrder: 'param1,param2,param3'
         *     paramOrder: 'param1|param2|param'
         */
        paramOrder: null,

        /**
         * @cfg {Boolean} paramsAsHash
         * Only used for the {@link #api} `load` configuration. If true, parameters will be sent as a
         * single hash collection of named arguments. Providing a {@link #paramOrder} nullifies this
         * configuration.
         */
        paramsAsHash: null,

        /**
         * @cfg {Number} timeout
         * Timeout for form actions in seconds.
         */
        timeout: 30,

        /**
         * @cfg {Boolean} multipartDetection
         * If this is enabled the form will automatically detect the need to use 'multipart/form-data' during submission.
         */
        multipartDetection: true,

        /**
         * @cfg {Boolean} enableSubmissionForm
         * The submission form is generated but never added to the dom. It is a submittable version of your form panel, allowing for fields
         * that are not simple textfields to be properly submitted to servers. It will also send values that are easier to parse
         * with server side code.
         *
         * If this is false we will attempt to subject the raw form inside the form panel.
         */
        enableSubmissionForm: true
    },

    getElementConfig: function() {
        var config = this.callParent();
        config.tag = "form";
        // Added a submit input for standard form submission. This cannot have "display: none;" or it will not work
        config.children.push({
            tag: 'input',
            type: 'submit',
            style: 'visibility: hidden; width: 0; height: 0; position: absolute; right: 0; bottom: 0;'
        });

        return config;
    },

    /**
     * @private
     */
    initialize: function() {
        var me = this;
        me.callParent();

        me.element.on({
            submit: 'onSubmit',
            scope : me
        });
    },

    applyEnctype: function(newValue) {
        var  form = this.element.dom || null;
        if(form) {
            if (newValue) {
                form.setAttribute("enctype", newValue);
            } else {
                form.setAttribute("enctype");
            }
        }
    },

    applyScrollable: function(scrollable, oldScrollable) {
        var translatable;

        if (Ext.isObject(scrollable) && !scrollable.isScroller && Ext.supports.touchScroll === 2) {
            // Use scrollposition as a default to prevent issues in iOS with scrolling
            // when using the next/prev softkeys.
            scrollable.translatable = scrollable.translatable || {
                translationMethod: 'scrollposition'
            }
        }
        return this.callParent([scrollable, oldScrollable]);
    },

    updateRecord: function(newRecord) {
        var fields, values, name;

        if (newRecord) {
            values = this.getValues();
            for (name in values) {
                if (values.hasOwnProperty(name) && newRecord.getField(name)) {
                    newRecord.set(name, values[name]);
                }
            }
        }
        return this;
    },

    /**
     * Loads matching fields from a model instance into this form.
     * @param {Ext.data.Model} record The model instance.
     * @return {Ext.form.Panel} This form.
     */
    setRecord: function(record) {
        var me = this;

        if (record && record.data) {
            me.setValues(record.data);
        }

        me._record = record;

        return this;
    },

    /**
     * @private
     */
    onSubmit: function(e) {
        var me = this;
        if (e && !me.getStandardSubmit()) {
            e.stopEvent();
        } else {
            this.submit(null, e);
        }
    },

    updateSubmitOnAction: function(newSubmitOnAction) {
        if (newSubmitOnAction) {
            this.on({
                action: 'onFieldAction',
                scope: this
            });
        } else {
            this.un({
                action: 'onFieldAction',
                scope: this
            });
        }
    },

    /**
     * @private
     */
    onFieldAction: function(field) {
        if (this.getSubmitOnAction()) {
            field.blur();
            this.submit();
        }
    },

    /**
     * Performs a Ajax-based submission of form values (if {@link #standardSubmit} is false) or otherwise
     * executes a standard HTML Form submit action.
     *
     * **Notes**
     *
     *  1. Only the first parameter is implemented. Put all other parameters inside the first
     *  parameter:
     *
     *     submit({params: "" ,headers: "" etc.})
     *
     *  2. Submit example:
     *
     *     myForm.submit({
     *       url: 'PostMyData/To',
     *       method: 'Post',
     *       success: function() { Ext.Msg.alert("success"); },
     *       failure: function() { Ext.Msg.alert("error"); }
     *     });
     *
     *  3. Parameters and values only submit for a POST and not for a GET.
     *
     * @param {Object} options
     * The configuration when submitting this form.
     *
     * The following are the configurations when submitting via Ajax only:
     *
     * @param {String} options.url
     * The url for the action (defaults to the form's {@link #url}).
     *
     * @param {String} options.method
     * The form method to use (defaults to the form's {@link #method}, or POST if not defined).
     *
     * @param {Object} options.headers
     * Request headers to set for the action.
     *
     * @param {Boolean} [options.autoAbort=false]
     * `true` to abort any pending Ajax request prior to submission.
     * __Note:__ Has no effect when `{@link #standardSubmit}` is enabled.
     *
     * @param {Number} options.timeout
     * The number is seconds the loading will timeout in.
     *
     * The following are the configurations when loading via Ajax or Direct:
     *
     * @param {String/Object} options.params
     * The params to pass when submitting this form (defaults to this forms {@link #baseParams}).
     * Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode}.
     *
     * @param {Boolean} [options.submitDisabled=false]
     * `true` to submit all fields regardless of disabled state.
     * __Note:__ Has no effect when `{@link #standardSubmit}` is enabled.
     *
     * @param {String/Object} [options.waitMsg]
     * If specified, the value which is passed to the loading {@link #masked mask}. See {@link #masked} for
     * more information.
     *
     * @param {Function} options.success
     * The callback that will be invoked after a successful response. A response is successful if
     * a response is received from the server and is a JSON object where the `success` property is set
     * to `true`, `{"success": true}`.
     *
     * The function is passed the following parameters and can be used for submitting via Ajax or Direct:
     *
     * @param {Ext.form.Panel} options.success.form
     * The {@link Ext.form.Panel} that requested the action.
     *
     * @param {Object/Ext.direct.Event} options.success.result
     * The result object returned by the server as a result of the submit request. If the submit is sent using Ext.Direct,
     * this will return the {@link Ext.direct.Event} instance, otherwise will return an Object.
     *
     * @param {Object} options.success.data
     * The parsed data returned by the server.
     *
     * @param {Function} options.failure
     * The callback that will be invoked after a failed transaction attempt.
     *
     * The function is passed the following parameters and can be used for submitting via Ajax or Direct:
     *
     * @param {Ext.form.Panel} options.failure.form
     * The {@link Ext.form.Panel} that requested the submit.
     *
     * @param {Ext.form.Panel} options.failure.result
     * The failed response or result object returned by the server which performed the operation.
     *
     * @param {Object} options.success.data
     * The parsed data returned by the server.
     *
     * @param {Object} options.scope
     * The scope in which to call the callback functions (The `this` reference for the callback functions).
     *
     * @return {Ext.data.Connection} The request object if the {@link #standardSubmit} config is false.
     * If the standardSubmit config is true, then the return value is undefined.
     */
    submit: function(options, e) {
        options = options || {};

        var me = this,
            formValues = me.getValues(me.getStandardSubmit() || !options.submitDisabled),
            form = me.element.dom || {};

        if(this.getEnableSubmissionForm()) {
            form = this.createSubmissionForm(form, formValues);
        }

        options = Ext.apply({
            url : me.getUrl() || form.action,
            submit: false,
            form: form,
            method : me.getMethod() || form.method || 'post',
            autoAbort : false,
            params : null,
            waitMsg : null,
            headers : null,
            success : null,
            failure : null
        }, options || {});

        return me.fireAction('beforesubmit', [me, formValues, options, e], 'doBeforeSubmit');
    },

    createSubmissionForm: function(form, values) {
        var fields = this.getFields(),
            name, input, field, fileinputElement, inputComponent;

        if(form.nodeType === 1) {
            form = form.cloneNode(false);

            for (name in values) {
                input = document.createElement("input");
                input.setAttribute("type", "text");
                input.setAttribute("name", name);
                input.setAttribute("value", values[name]);
                form.appendChild(input);
            }
        }

        for (name in fields) {
            if (fields.hasOwnProperty(name)) {
                field = fields[name];
                if(field.isFile) {
                    if(!form.$fileswap) form.$fileswap = [];

                    inputComponent = field.getComponent().input;
                    fileinputElement = inputComponent.dom;
                    input = fileinputElement.cloneNode(true);
                    fileinputElement.parentNode.insertBefore(input, fileinputElement.nextSibling);
                    form.appendChild(fileinputElement);
                    form.$fileswap.push({original: fileinputElement, placeholder: input});
                } else if(field.isPassword) {
                    if(field.getComponent().getType !== "password") {
                        field.setRevealed(false);
                    }
                }
            }
        }

        return form;
    },

    doBeforeSubmit: function(me, formValues, options) {
        var form = options.form || {},
            multipartDetected = false;

        if(this.getMultipartDetection() === true) {
            this.getFieldsAsArray().forEach(function(field) {
                if(field.isFile === true) {
                    multipartDetected = true;
                    return false;
                }
            });

            if(multipartDetected) {
                form.setAttribute("enctype", "multipart/form-data");
            }
        }

        if(options.enctype) {
            form.setAttribute("enctype", options.enctype);
        }

        if (me.getStandardSubmit()) {
            if (options.url && Ext.isEmpty(form.action)) {
                form.action = options.url;
            }

            // Spinner fields must have their components enabled *before* submitting or else the value
            // will not be posted.
            var fields = this.query('spinnerfield'),
                ln = fields.length,
                i, field;

            for (i = 0; i < ln; i++) {
                field = fields[i];
                if (!field.getDisabled()) {
                    field.getComponent().setDisabled(false);
                }
            }

            form.method = (options.method || form.method).toLowerCase();
            form.submit();
        } else {
            var api = me.getApi(),
                url = options.url || me.getUrl(),
                scope = options.scope || me,
                waitMsg = options.waitMsg,
                failureFn = function(response, responseText) {
                    if (Ext.isFunction(options.failure)) {
                        options.failure.call(scope, me, response, responseText);
                    }

                    me.fireEvent('exception', me, response);
                },
                successFn = function(response, responseText) {
                    if (Ext.isFunction(options.success)) {
                        options.success.call(options.scope || me, me, response, responseText);
                    }

                    me.fireEvent('submit', me, response);
                },
                submit;

            if (options.waitMsg) {
                if (typeof waitMsg === 'string') {
                    waitMsg = {
                        xtype   : 'loadmask',
                        message : waitMsg
                    };
                }

                me.setMasked(waitMsg);
            }

            if (api) {
                submit = api.submit;

                if (typeof submit === 'string') {
                    submit = Ext.direct.Manager.parseMethod(submit);

                    if (submit) {
                        api.submit = submit;
                    }
                }

                if (submit) {
                    return submit(this.element, function(data, response, success) {
                        me.setMasked(false);

                        if (success) {
                            if (data.success) {
                                successFn(response, data);
                            } else {
                                failureFn(response, data);
                            }
                        } else {
                            failureFn(response, data);
                        }
                    }, this);
                }
            } else {
                var request = Ext.merge({},
                    {
                        url: url,
                        timeout: this.getTimeout() * 1000,
                        form: form,
                        scope: me
                    },
                    options
                );
                delete request.success;
                delete request.failure;

                request.params = Ext.merge(me.getBaseParams() || {}, options.params);
                request.header = Ext.apply(
                    {
                        'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8'
                    },
                    options.headers || {}
                );
                request.callback = function(callbackOptions, success, response) {
                    var me = this,
                        responseText = response.responseText,
                        responseXML = response.responseXML,
                        statusResult = Ext.Ajax.parseStatus(response.status, response);

                    if(form.$fileswap) {
                        var original, placeholder;
                        Ext.each(form.$fileswap, function(item) {
                            original = item.original;
                            placeholder = item.placeholder;

                            placeholder.parentNode.insertBefore(original, placeholder.nextSibling);
                            placeholder.parentNode.removeChild(placeholder);
                        });
                        form.$fileswap = null;
                        delete form.$fileswap;
                    }

                    me.setMasked(false);

                    if(response.success === false) success = false;
                    if (success) {
                        if (statusResult && responseText && responseText.length === 0) {
                            success = true;
                        } else {
                            if(!Ext.isEmpty(response.responseBytes)) {
                                success = statusResult.success;
                            }else {
                                if(Ext.isString(responseText) && response.request.options.responseType === "text") {
                                    response.success = true;
                                } else if(Ext.isString(responseText)) {
                                    try {
                                        response = Ext.decode(responseText);
                                    }catch (e){
                                        response.success = false;
                                        response.error = e;
                                        response.message = e.message;
                                    }
                                } else if(Ext.isSimpleObject(responseText)) {
                                    response = responseText;
                                    Ext.applyIf(response, {success:true});
                                }

                                if(!Ext.isEmpty(responseXML)){
                                    response.success = true;
                                }
                                success = !!response.success;
                            }
                        }
                        if (success) {
                            successFn(response, responseText);
                        } else {
                            failureFn(response, responseText);
                        }
                    }
                    else {
                        failureFn(response, responseText);
                    }
                };

                if(Ext.feature.has.XHR2 && request.xhr2) {
                    delete request.form;
                    var formData = new FormData(form);
                    if (request.params) {
                        Ext.iterate(request.params, function(name, value) {
                            if (Ext.isArray(value)) {
                                Ext.each(value, function(v) {
                                    formData.append(name, v);
                                });
                            } else {
                                formData.append(name, value);
                            }
                        });
                        delete request.params;
                    }
                    request.data = formData;
                }

                return Ext.Ajax.request(request);
            }
        }
    },

    /**
     * Performs an Ajax or Ext.Direct call to load values for this form.
     *
     * @param {Object} options
     * The configuration when loading this form.
     *
     * The following are the configurations when loading via Ajax only:
     *
     * @param {String} options.url
     * The url for the action (defaults to the form's {@link #url}).
     *
     * @param {String} options.method
     * The form method to use (defaults to the form's {@link #method}, or GET if not defined).
     *
     * @param {Object} options.headers
     * Request headers to set for the action.
     *
     * @param {Number} options.timeout
     * The number is seconds the loading will timeout in.
     *
     * The following are the configurations when loading via Ajax or Direct:
     *
     * @param {Boolean} [options.autoAbort=false]
     * `true` to abort any pending Ajax request prior to loading.
     *
     * @param {String/Object} options.params
     * The params to pass when submitting this form (defaults to this forms {@link #baseParams}).
     * Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode}.
     *
     * @param {String/Object} [options.waitMsg]
     * If specified, the value which is passed to the loading {@link #masked mask}. See {@link #masked} for
     * more information.
     *
     * @param {Function} options.success
     * The callback that will be invoked after a successful response. A response is successful if
     * a response is received from the server and is a JSON object where the `success` property is set
     * to `true`, `{"success": true}`.
     *
     * The function is passed the following parameters and can be used for loading via Ajax or Direct:
     *
     * @param {Ext.form.Panel} options.success.form
     * The {@link Ext.form.Panel} that requested the load.
     *
     * @param {Object/Ext.direct.Event} options.success.result
     * The result object returned by the server as a result of the load request. If the loading was done via Ext.Direct,
     * will return the {@link Ext.direct.Event} instance, otherwise will return an Object.
     *
     * @param {Object} options.success.data
     * The parsed data returned by the server.
     *
     * @param {Function} options.failure
     * The callback that will be invoked after a failed transaction attempt.
     *
     * The function is passed the following parameters and can be used for loading via Ajax or Direct:
     *
     * @param {Ext.form.Panel} options.failure.form
     * The {@link Ext.form.Panel} that requested the load.
     *
     * @param {Ext.form.Panel} options.failure.result
     * The failed response or result object returned by the server which performed the operation.
     *
     * @param {Object} options.success.data
     * The parsed data returned by the server.
     *
     * @param {Object} options.scope
     * The scope in which to call the callback functions (The `this` reference for the callback functions).
     *
     * @return {Ext.data.Connection} The request object.
     */
    load : function(options) {
        options = options || {};

        var me = this,
            api = me.getApi(),
            url = me.getUrl() || options.url,
            waitMsg = options.waitMsg,
            successFn = function(response, data) {
                me.setValues(data.data);

                if (Ext.isFunction(options.success)) {
                    options.success.call(options.scope || me, me, response, data);
                }

                me.fireEvent('load', me, response);
            },
            failureFn = function(response, data) {
                if (Ext.isFunction(options.failure)) {
                    options.failure.call(scope, me, response, data);
                }

                me.fireEvent('exception', me, response);
            },
            load, method, args;

        if (options.waitMsg) {
            if (typeof waitMsg === 'string') {
                waitMsg = {
                    xtype   : 'loadmask',
                    message : waitMsg
                };
            }

            me.setMasked(waitMsg);
        }

        if (api) {
            load = api.load;

            if (typeof load === 'string') {
                load = Ext.direct.Manager.parseMethod(load);

                if (load) {
                    api.load = load;
                }
            }

            if (load) {
                method = load.directCfg.method;
                args = method.getArgs(me.getParams(options.params), me.getParamOrder(), me.getParamsAsHash());

                args.push(function(data, response, success) {
                    me.setMasked(false);

                    if (success) {
                        successFn(response, data);
                    } else {
                        failureFn(response, data);
                    }
                }, me);

                return load.apply(window, args);
            }
        } else if (url) {
            return Ext.Ajax.request({
                url: url,
                timeout: (options.timeout || this.getTimeout()) * 1000,
                method: options.method || 'GET',
                autoAbort: options.autoAbort,
                headers: Ext.apply(
                    {
                        'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8'
                    },
                    options.headers || {}
                ),
                callback: function(callbackOptions, success, response) {
                    var responseText = response.responseText,
                        statusResult = Ext.Ajax.parseStatus(response.status, response);

                    me.setMasked(false);

                    if (success) {
                        if (statusResult && responseText.length === 0) {
                            success = true;
                        } else {
                            response = Ext.decode(responseText);
                            success = !!response.success;
                        }
                        if (success) {
                            successFn(response, responseText);
                        } else {
                            failureFn(response, responseText);
                        }
                    }
                    else {
                        failureFn(response, responseText);
                    }
                }
            });
        }
    },

    /**
     * @private
     */
    getParams : function(params) {
        return Ext.apply({}, params, this.getBaseParams());
    },

    /**
     * Sets the values of form fields in bulk. Example usage:
     *
     *     myForm.setValues({
     *         name: 'Ed',
     *         crazy: true,
     *         username: 'edspencer'
     *     });
     *
     * If there groups of checkbox fields with the same name, pass their values in an array. For example:
     *
     *     myForm.setValues({
     *         name: 'Jacky',
     *         crazy: false,
     *         hobbies: [
     *             'reading',
     *             'cooking',
     *             'gaming'
     *         ]
     *     });
     *
     * @param {Object} values field name => value mapping object.
     * @return {Ext.form.Panel} This form.
     */
    setValues: function(values) {
        var fields = this.getFields(),
            me = this,
            name, field, value, ln, i, f;

        values = values || {};

        for (name in values) {
            if (values.hasOwnProperty(name)) {
                field = fields[name];
                value = values[name];

                if (field) {
                    // If there are multiple fields with the same name. Checkboxes, radio fields and maybe event just normal fields..
                    if (Ext.isArray(field)) {
                        ln = field.length;

                        // Loop through each of the fields
                        for (i = 0; i < ln; i++) {
                            f = field[i];

                            if (f.isRadio) {
                                // If it is a radio field just use setGroupValue which will handle all of the radio fields
                                f.setGroupValue(value);
                                break;
                            } else if (f.isCheckbox) {
                                if (Ext.isArray(value)) {
                                   f.setChecked((value.indexOf(f._value) != -1));
                               } else {
                                   f.setChecked((value == f._value));
                               }
                            } else {
                                // If it is a bunch of fields with the same name, check if the value is also an array, so we can map it
                                // to each field
                                if (Ext.isArray(value)) {
                                    f.setValue(value[i]);
                                }
                            }
                        }
                    } else {
                        if (field.isRadio || field.isCheckbox) {
                            // If the field is a radio or a checkbox
                            field.setChecked(value);
                        } else {
                            // If just a normal field
                            field.setValue(value);
                        }
                    }

                    if (me.getTrackResetOnLoad()) {
                       field.resetOriginalValue();
                    }
                }
            }
        }

        return this;
    },

    /**
     * Returns an object containing the value of each field in the form, keyed to the field's name.
     * For groups of checkbox fields with the same name, it will be arrays of values. For example:
     *
     *     {
     *         name: "Jacky Nguyen", // From a TextField
     *         favorites: [
     *             'pizza',
     *             'noodle',
     *             'cake'
     *         ]
     *     }
     *
     * @param {Boolean} [enabled] `true` to return only enabled fields.
     * @param {Boolean} [all] `true` to return all fields even if they don't have a
     * {@link Ext.field.Field#name name} configured.
     * @return {Object} Object mapping field name to its value.
     */
    getValues: function(enabled, all) {
        var fields = this.getFields(),
            values = {},
            isArray = Ext.isArray,
            field, value, addValue, bucket, name, ln, i;

        // Function which you give a field and a name, and it will add it into the values
        // object accordingly
        addValue = function(field, name) {
            if (!all && (!name || name === 'null') || field.isFile) {
                return;
            }

            if (field.isCheckbox) {
                value = field.getSubmitValue();
            } else {
                value = field.getValue();
            }


            if (!(enabled && field.getDisabled())) {
                // RadioField is a special case where the value returned is the fields valUE
                // ONLY if it is checked
                if (field.isRadio) {
                    if (field.isChecked()) {
                        values[name] = value;
                    }
                } else {
                    // Check if the value already exists
                    bucket = values[name];
                    if (!Ext.isEmpty(bucket)) {
                        // if it does and it isn't an array, we need to make it into an array
                        // so we can push more
                        if (!isArray(bucket)) {
                            bucket = values[name] = [bucket];
                        }

                        // Check if it is an array
                        if (isArray(value)) {
                            // Concat it into the other values
                            bucket = values[name] = bucket.concat(value);
                        } else {
                            // If it isn't an array, just pushed more values
                            bucket.push(value);
                        }
                    } else {
                        values[name] = value;
                    }
                }
            }
        };

        // Loop through each of the fields, and add the values for those fields.
        for (name in fields) {
            if (fields.hasOwnProperty(name)) {
                field = fields[name];

                if (isArray(field)) {
                    ln = field.length;
                    for (i = 0; i < ln; i++) {
                        addValue(field[i], name);
                    }
                } else {
                    addValue(field, name);
                }
            }
        }

        return values;
    },

    /**
     * Resets all fields in the form back to their original values.
     * @return {Ext.form.Panel} This form.
     */
    reset: function() {
        this.getFieldsAsArray().forEach(function(field) {
            field.reset();
        });

        return this;
    },

    /**
     * A convenient method to disable all fields in this form.
     * @return {Ext.form.Panel} This form.
     */
    updateDisabled: function(newDisabled) {
        this.getFieldsAsArray().forEach(function(field) {
            field.setDisabled(newDisabled);
        });

        return this;
    },

    /**
     * @private
     */
    getFieldsAsArray: function() {
        var fields = [],
            getFieldsFrom = function(item) {
                if (item.isField) {
                    fields.push(item);
                }

                if (item.isContainer) {
                    item.getItems().each(getFieldsFrom);
                }
            };

        this.getItems().each(getFieldsFrom);

        return fields;
    },

    /**
     * Returns all {@link Ext.field.Field field} instances inside this form.
     * @param {Boolean} byName return only fields that match the given name, otherwise return all fields.
     * @return {Object/Array} All field instances, mapped by field name; or an array if `byName` is passed.
     */
    getFields: function(byName) {
        var fields = {},
            itemName;

        var getFieldsFrom = function(item) {
            if (item.isField) {
                itemName = item.getName();

                if ((byName && itemName == byName) || typeof byName == 'undefined') {
                    if (fields.hasOwnProperty(itemName)) {
                        if (!Ext.isArray(fields[itemName])) {
                            fields[itemName] = [fields[itemName]];
                        }

                        fields[itemName].push(item);
                    } else {
                        fields[itemName] = item;
                    }
                }

            }

            if (item.isContainer) {
                item.items.each(getFieldsFrom);
            }
        };

        this.getItems().each(getFieldsFrom);

        return (byName) ? (fields[byName] || []) : fields;
    },

    /**
     * Returns an array of fields in this formpanel.
     * @return {Ext.field.Field[]} An array of fields in this form panel.
     * @private
     */
    getFieldsArray: function() {
        var fields = [];

        var getFieldsFrom = function(item) {
            if (item.isField) {
                fields.push(item);
            }

            if (item.isContainer) {
                item.items.each(getFieldsFrom);
            }
        };

        this.items.each(getFieldsFrom);

        return fields;
    },

    getFieldsFromItem: Ext.emptyFn,

    /**
     * Shows a generic/custom mask over a designated Element.
     * @param {String/Object} cfg Either a string message or a configuration object supporting
     * the following options:
     *
     *     {
     *         message : 'Please Wait',
     *         cls : 'form-mask'
     *     }
     *
     * @param {Object} target
     * @return {Ext.form.Panel} This form
     * @deprecated 2.0.0 Please use {@link #setMasked} instead.
     */
    showMask: function(cfg, target) {
        //<debug>
        Ext.Logger.warn('showMask is now deprecated. Please use Ext.form.Panel#setMasked instead');
        //</debug>

        cfg = Ext.isObject(cfg) ? cfg.message : cfg;

        if (cfg) {
            this.setMasked({
                xtype: 'loadmask',
                message: cfg
            });
        } else {
            this.setMasked(true);
        }

        return this;
    },

    /**
     * Hides a previously shown wait mask (See {@link #showMask}).
     * @return {Ext.form.Panel} this
     * @deprecated 2.0.0 Please use {@link #unmask} or {@link #setMasked} instead.
     */
    hideMask: function() {
        this.setMasked(false);
        return this;
    },

    /**
     * Returns the currently focused field
     * @return {Ext.field.Field} The currently focused field, if one is focused or `null`.
     * @private
     */
    getFocusedField: function() {
        var fields = this.getFieldsArray(),
            ln = fields.length,
            field, i;

        for (i = 0; i < ln; i++) {
            field = fields[i];
            if (field.isFocused) {
                return field;
            }
        }

        return null;
    },

    /**
     * @return {Boolean/Ext.field.Field} The next field if one exists, or `false`.
     * @private
     */
    getNextField: function() {
        var fields = this.getFieldsArray(),
            focusedField = this.getFocusedField(),
            index;

        if (focusedField) {
            index = fields.indexOf(focusedField);

            if (index !== fields.length - 1) {
                index++;
                return fields[index];
            }
        }

        return false;
    },

    /**
     * Tries to focus the next field in the form, if there is currently a focused field.
     * @return {Boolean/Ext.field.Field} The next field that was focused, or `false`.
     * @private
     */
    focusNextField: function() {
        var field = this.getNextField();
        if (field) {
            field.focus();
            return field;
        }

        return false;
    },

    /**
     * @private
     * @return {Boolean/Ext.field.Field} The next field if one exists, or `false`.
     */
    getPreviousField: function() {
        var fields = this.getFieldsArray(),
            focusedField = this.getFocusedField(),
            index;

        if (focusedField) {
            index = fields.indexOf(focusedField);

            if (index !== 0) {
                index--;
                return fields[index];
            }
        }

        return false;
    },

    /**
     * Tries to focus the previous field in the form, if there is currently a focused field.
     * @return {Boolean/Ext.field.Field} The previous field that was focused, or `false`.
     * @private
     */
    focusPreviousField: function() {
        var field = this.getPreviousField();
        if (field) {
            field.focus();
            return field;
        }

        return false;
    }
});
