/**
 * This class manages a pending Ajax request. Instances of this type are created by the
 * `{@link Ext.data.Connection#request}` method.
 * @since 6.0.0
 */
Ext.define('Ext.data.request.Ajax', {
    extend: 'Ext.data.request.Base',
    alias:  'request.ajax',
    
    requires: [
        'Ext.data.flash.BinaryXhr'
    ],
 
    statics: {
 
        /**
         * Checks if the response status was successful
         * @param {Number} status The status code
         * @param {Object} response The Response object
         * @return {Object} An object containing success/status state
         * @private
         */
        parseStatus: function(status, response) {
            var len;
 
            if (response) {
                //We have to account for binary response type 
                if (response.responseType === 'arraybuffer') {
                    len = response.byteLength;
                } else if (response.responseText) {
                    len = response.responseText.length;
                }
            }
 
            // see: https://prototype.lighthouseapp.com/projects/8886/tickets/129-ie-mangles-http-response-status-code-204-to-1223 
            status = status == 1223 ? 204 : status;
 
            var success = (status >= 200 && status < 300) || status == 304 || (status == 0 && Ext.isNumber(len)),
                isException = false;
 
            if (!success) {
                switch (status) {
                    case 12002:
                    case 12029:
                    case 12030:
                    case 12031:
                    case 12152:
                    case 13030:
                        isException = true;
                        break;
                }
            }
 
            return {
                success: success,
                isException: isException
            };
        }
    },
    
    start: function(data) {
        var me = this,
            options = me.options,
            requestOptions = me.requestOptions,
            isXdr = me.isXdr,
            xhr, headers;
        
        xhr = me.xhr = me.openRequest(options, requestOptions, me.async, me.username, me.password);
 
        // XDR doesn't support setting any headers 
        if (!isXdr) {
            headers = me.setupHeaders(xhr, options, requestOptions.data, requestOptions.params);
        }
 
        if (me.async) {
            if (!isXdr) {
                xhr.onreadystatechange = me.bindStateChange();
            }
        }
 
        if (isXdr) {
            me.processXdrRequest(me, xhr);
        }
        
        // Parent will set the timeout if needed 
        me.callParent([data]);
        
        // start the request! 
        xhr.send(data);
        
        if (!me.async) {
            return me.onComplete();
        }
        
        return me;
    },
    
    /**
     * Aborts an active request.
     */
    abort: function(force) {
        var me = this,
            xhr = me.xhr;
 
        if (force || me.isLoading()) {
            /*
             * Clear out the onreadystatechange here, this allows us
             * greater control, the browser may/may not fire the function
             * depending on a series of conditions.
             */
            try {
                xhr.onreadystatechange = null;
            }
            catch (e) {
                // Setting onreadystatechange to null can cause problems in IE, see 
                // http://www.quirksmode.org/blog/archives/2005/09/xmlhttp_notes_a_1.html 
                xhr.onreadystatechange = Ext.emptyFn;
            }
            
            xhr.abort();
            
            me.callParent([force]);
            
            me.onComplete();
            me.cleanup();
        }
    },
    
    /**
     * Cleans up any left over information from the request
     */
    cleanup: function() {
        this.xhr = null;
        delete this.xhr;
    },
    
    isLoading: function() {
        var me = this,
            xhr = me.xhr,
            state = xhr && xhr.readyState,
            C = Ext.data.flash && Ext.data.flash.BinaryXhr;
 
        if (!xhr || me.aborted || me.timedout) {
            return false;
        }
 
        // if there is a connection and readyState is not 0 or 4, or in case of 
        // BinaryXHR, not 4 
        if (&& xhr instanceof C) {
            return state !== 4;
        }
 
        return state !== 0 && state !== 4;
    },
 
    /**
     * Creates and opens an appropriate XHR transport for a given request on this browser.
     * This logic is contained in an individual method to allow for overrides to process all
     * of the parameters and options and return a suitable, open connection.
     * @private
     */
    openRequest: function(options, requestOptions, async, username, password) {
        var me = this,
            xhr = me.newRequest(options);
 
        if (username) {
            xhr.open(requestOptions.method, requestOptions.url, async, username, password);
        }
        else {
            if (me.isXdr) {
                xhr.open(requestOptions.method, requestOptions.url);
            }
            else {
                xhr.open(requestOptions.method, requestOptions.url, async);
            }
        }
 
        if (options.binary || me.binary) {
            if (window.Uint8Array) {
                xhr.responseType = 'arraybuffer';
            }
            else if (xhr.overrideMimeType) {
                // In some older non-IE browsers, e.g. ff 3.6, that do not 
                // support Uint8Array, a mime type override is required so that 
                // the unprocessed binary data can be read from the responseText 
                // (see createResponse()) 
                xhr.overrideMimeType('text\/plain; charset=x-user-defined');
            //<debug> 
            }
            else if (!Ext.isIE) {
                Ext.log.warn("Your browser does not support loading binary data using Ajax.");
            //</debug> 
            }
        }
 
        if (options.withCredentials || me.withCredentials) {
            xhr.withCredentials = true;
        }
 
        return xhr;
    },
 
    /**
     * Creates the appropriate XHR transport for a given request on this browser. On IE
     * this may be an `XDomainRequest` rather than an `XMLHttpRequest`.
     * @private
     */
    newRequest: function(options) {
        var me = this,
            xhr;
 
        if (options.binaryData) {
            // This is a binary data request. Handle submission differently for differnet browsers 
            if (window.Uint8Array) {
                // On browsers that support this, use the native XHR object 
                xhr = me.getXhrInstance();
            }
            else {
                // catch all for all other browser types 
                xhr = new Ext.data.flash.BinaryXhr();
            }
        }
        else if (me.cors && Ext.isIE9m) {
            xhr = me.getXdrInstance();
            me.isXdr = true;
        }
        else {
            xhr = me.getXhrInstance();
            me.isXdr = false;
        }
 
        return xhr;
    },
 
    /**
     * Setup all the headers for the request
     * @private
     * @param {Object} xhr The xhr object
     * @param {Object} options The options for the request
     * @param {Object} data The data for the request
     * @param {Object} params The params for the request
     */
    setupHeaders: function(xhr, options, data, params) {
        var me = this,
            headers = Ext.apply({}, options.headers || {}, me.defaultHeaders),
            contentType = me.defaultPostHeader,
            jsonData = options.jsonData,
            xmlData = options.xmlData,
            type = 'Content-Type',
            useHeader = me.useDefaultXhrHeader,
            key, header;
 
        if (!headers.hasOwnProperty(type) && (data || params)) {
            if (data) {
                if (options.rawData) {
                    contentType = 'text/plain';
                }
                else {
                    if (xmlData && Ext.isDefined(xmlData)) {
                        contentType = 'text/xml';
                    }
                    else if (jsonData && Ext.isDefined(jsonData)) {
                        contentType = 'application/json';
                    }
                }
            }
            
            headers[type] = contentType;
        }
 
        if (useHeader && !headers['X-Requested-With']) {
            headers['X-Requested-With'] = me.defaultXhrHeader;
        }
 
        // If undefined/null, remove it and don't set the header. 
        // Allow the browser to do so. 
        if (headers[type] === undefined || headers[type] === null) {
            delete headers[type];
        }
 
        // set up all the request headers on the xhr object 
        try {
            for (key in headers) {
                if (headers.hasOwnProperty(key)) {
                    header = headers[key];
                    xhr.setRequestHeader(key, header);
                }
            }
        }
        catch(e) {
            // TODO Request shouldn't fire events from its owner 
            me.owner.fireEvent('exception', key, header);
        }
        
        return headers;
    },
 
    /**
     * Creates the appropriate XDR transport for this browser.
     * - IE 7 and below don't support CORS
     * - IE 8 and 9 support CORS with native XDomainRequest object
     * - IE 10 (and above?) supports CORS with native XMLHttpRequest object
     * @private
     */
    getXdrInstance: function() {
        var xdr;
 
        if (Ext.ieVersion >= 8) {
            xdr = new XDomainRequest();
        }
        else {
            Ext.raise({
                msg: 'Your browser does not support CORS'
            });
        }
 
        return xdr;
    },
 
    /**
     * Creates the appropriate XHR transport for this browser.
     * @private
     */
    getXhrInstance: (function() {
        var options = [function() {
            return new XMLHttpRequest();
        }, function() {
            return new ActiveXObject('MSXML2.XMLHTTP.3.0'); // jshint ignore:line 
        }, function() {
            return new ActiveXObject('MSXML2.XMLHTTP'); // jshint ignore:line 
        }, function() {
            return new ActiveXObject('Microsoft.XMLHTTP'); // jshint ignore:line 
        }], i = 0,
            len = options.length,
            xhr;
 
        for (; i < len; ++i) {
            try {
                xhr = options[i];
                xhr();
                break;
            } catch(e) {
            }
        }
        return xhr;
    }()),
 
    processXdrRequest: function(request, xhr) {
        var me = this;
 
        // Mutate the request object as per XDR spec. 
        delete request.headers;
 
        request.contentType = request.options.contentType || me.defaultXdrContentType;
 
        xhr.onload = me.bindStateChange(true);
        xhr.onerror = xhr.ontimeout = me.bindStateChange(false);
    },
 
    processXdrResponse: function(response, xhr) {
        // Mutate the response object as per XDR spec. 
        response.getAllResponseHeaders = function() {
            return [];
        };
        
        response.getResponseHeader = function() {
            return '';
        };
        
        response.contentType = xhr.contentType || this.defaultXdrContentType;
    },
 
    bindStateChange: function (xdrResult) {
        var me = this;
 
        return function () {
            Ext.elevate(function () {
                me.onStateChange(xdrResult);
            });
        };
    },
 
    onStateChange: function(xdrResult) {
        var me = this,
            xhr = me.xhr;
 
        // Using CORS with IE doesn't support readyState so we fake it. 
        if ((xhr && xhr.readyState == 4) || me.isXdr) {
            me.clearTimer();
            
            me.onComplete(xdrResult);
            
            me.cleanup();
        }
    },
    
    /**
     * To be called when the request has come back from the server
     * @param {Object} xdrResult 
     * @return {Object} The response
     * @private
     */
    onComplete: function(xdrResult) {
        var me = this,
            owner = me.owner,
            options = me.options,
            xhr = me.xhr,
            failure = { success: false, isException: false },
            result, success, response;
        
        if (!xhr || me.destroyed) {
            return me.result = failure;
        }
        
        try {
            result = Ext.data.request.Ajax.parseStatus(xhr.status, xhr);
            
            if (result.success) {
                // This is quite difficult to reproduce, however if we abort a request 
                // just before it returns from the server, occasionally the status will be 
                // returned correctly but the request is still yet to be complete. 
                result.success = xhr.readyState === 4;
            }
        }
        catch (e) {
            // In some browsers we can't access the status if the readyState is not 4, 
            // so the request has failed 
            result = failure;
        }
        
        success = me.success = me.isXdr ? xdrResult : result.success;
 
        if (success) {
            response = me.createResponse(xhr);
            
            if (owner.hasListeners.requestcomplete) {
                owner.fireEvent('requestcomplete', owner, response, options);
            }
            
            if (options.success) {
                Ext.callback(options.success, options.scope, [response, options]);
            }
        }
        else {
            if (result.isException || me.aborted || me.timedout) {
                response = me.createException(xhr);
            }
            else {
                response = me.createResponse(xhr);
            }
            
            if (owner.hasListeners.requestexception) {
                owner.fireEvent('requestexception', owner, response, options);
            }
            
            if (options.failure) {
                Ext.callback(options.failure, options.scope, [response, options]);
            }
        }
        
        me.result = response;
        
        if (options.callback) {
            Ext.callback(options.callback, options.scope, [options, success, response]);
        }
        
        owner.onRequestComplete(me);
        
        me.callParent([xdrResult]);
        
        return response;
    },
 
    /**
     * Creates the response object
     * @param {Object} xhr 
     * @private
     */
    createResponse: function(xhr) {
        var me = this,
            isXdr = me.isXdr,
            headers = {},
            lines = isXdr ? [] : xhr.getAllResponseHeaders().replace(/\r\n/g, '\n').split('\n'),
            count = lines.length,
            line, index, key, response, byteArray;
 
        while (count--) {
            line = lines[count];
            index = line.indexOf(':');
            
            if (index >= 0) {
                key = line.substr(0, index).toLowerCase();
                
                if (line.charAt(index + 1) == ' ') {
                    ++index;
                }
                
                headers[key] = line.substr(index + 1);
            }
        }
        
        response = {
            request: me,
            requestId: me.id,
            status: xhr.status,
            statusText: xhr.statusText,
            getResponseHeader: function(header) {
                return headers[header.toLowerCase()];
            },
            getAllResponseHeaders: function() {
                return headers;
            }
        };
 
        if (isXdr) {
            me.processXdrResponse(response, xhr);
        }
 
        if (me.binary) {
            response.responseBytes = me.getByteArray(xhr);
        }
        else {
            // an error is thrown when trying to access responseText or responseXML 
            // on an xhr object with responseType of 'arraybuffer', so only attempt 
            // to set these properties in the response if we're not dealing with 
            // binary data 
            response.responseText = xhr.responseText;
            response.responseXML = xhr.responseXML;
        }
 
        return response;
    },
 
    destroy: function() {
        this.xhr = null;
        
        this.callParent();
    },
    
    privates: {
        /**
         * Gets binary data from the xhr response object and returns it as a byte array
         * @param {Object} xhr the xhr response object
         * @return {Uint8Array/Array}
         * @private
         */
        getByteArray: function(xhr) {
            var response = xhr.response,
                responseBody = xhr.responseBody,
                Cls = Ext.data.flash && Ext.data.flash.BinaryXhr,
                byteArray, responseText, len, i;
            
            if (xhr instanceof Cls) {
                // If this was a BinaryXHR request via flash, we already have the bytes ready 
                byteArray = xhr.responseBytes;
            }
            else if (window.Uint8Array) {
                // Modern browsers (including IE10) have a native byte array 
                // which can be created by passing the ArrayBuffer (returned as 
                // the xhr.response property) to the Uint8Array constructor. 
                byteArray = response ? new Uint8Array(response) : [];
            }
            else if (Ext.isIE9p) {
                // In IE9 and below the responseBody property contains a byte array 
                // but it is not directly accessible using javascript. 
                // In IE9p we can get the bytes by constructing a VBArray 
                // using the responseBody and then converting it to an Array. 
                try {
                    byteArray = new VBArray(responseBody).toArray(); // jshint ignore:line 
                }
                catch(e) {
                    // If the binary response is empty, the VBArray constructor will 
                    // choke on the responseBody.  We can't simply do a null check 
                    // on responseBody because responseBody is always falsy when it 
                    // contains binary data. 
                    byteArray = [];
                }
            }
            else if (Ext.isIE) {
                // IE8 and below also have a VBArray constructor, but throw a 
                // "VBArray Expected" error if you try to pass the responseBody to 
                // the VBArray constructor. 
                // http://msdn.microsoft.com/en-us/library/ye3x9by3%28v=vs.71%29.aspx 
                // so we have to use vbscript injection to access the bytes 
                if (!this.self.vbScriptInjected) {
                    this.injectVBScript();
                }
                
                getIEByteArray(xhr.responseBody, byteArray = []); // jshint ignore:line 
            }
            else {
                // in other older browsers make a best-effort attempt to read the 
                // bytes from responseText 
                byteArray = [];
                responseText = xhr.responseText;
                len = responseText.length;
                
                for (= 0; i < len; i++) {
                    // Some characters have an extra byte 0xF7 in the high order 
                    // position. Throw away the high order byte and then push the 
                    // result onto the byteArray. 
                    byteArray.push(responseText.charCodeAt(i) & 0xFF);
                }
            }
            
            return byteArray;
        },
 
        /**
         * Injects a vbscript tag containing a 'getIEByteArray' method for reading
         * binary data from an xhr response in IE8 and below.
         * @private
         */
        injectVBScript: function() {
            var scriptTag = document.createElement('script');
            
            scriptTag.type = 'text/vbscript';
            scriptTag.text = [
                'Function getIEByteArray(byteArray, out)',
                    'Dim len, i',
                    'len = LenB(byteArray)',
                    'For i = 1 to len',
                        'out.push(AscB(MidB(byteArray, i, 1)))',
                    'Next',
                'End Function'
            ].join('\n');
            
            Ext.getHead().dom.appendChild(scriptTag);
            
            this.self.vbScriptInjected = true;
        }
    }
});