/**
 * 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'
    ],
    
    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 = Ext.Function.bind(me.onStateChange, me);
            }
        }

        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 (C && 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 = Ext.Function.bind(me.onStateChange, me, [true]);
        xhr.onerror = xhr.ontimeout = Ext.Function.bind(me.onStateChange, me, [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;
    },

    onStateChange: function(xdrResult) {
        var me = this,
            xhr = me.xhr,
            globalEvents = Ext.GlobalEvents;

        // 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();
            
            if (globalEvents.hasListeners.idle) {
                globalEvents.fireEvent('idle');
            }
        }
    },
    
    /**
     * To be called when the request has come back from the server
     * @param {Object} request
     * @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 = me.parseStatus(xhr.status);
            
            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);
            
            owner.fireEvent('requestcomplete', owner, response, options);
            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);
            }
            
            owner.fireEvent('requestexception', owner, response, options);
            Ext.callback(options.failure, options.scope, [response, options]);
        }
        
        me.result = response;
        
        Ext.callback(options.callback, options.scope, [options, success, response]);
        
        owner.onRequestComplete(me);
        
        me.callParent([xdrResult]);
        
        return response;
    },

    /**
     * Checks if the response status was successful
     * @param {Number} status The status code
     * @return {Object} An object containing success/status state
     */
    parseStatus: function(status) {
        // 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,
            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
        };
    },

    /**
     * Creates the response object
     * @param {Object} request
     * @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: me._getHeader,
            getAllResponseHeaders: me._getHeaders
        };

        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 (i = 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;
        }
    }
});
