YUI.add('aui-io-request', function (A, NAME) {

/**
 * The IORequest Utility - Provides response data normalization for 'xml', 'json',
 * JavaScript and cache option.
 *
 * @module aui-io
 * @submodule aui-io-request
 */

var L = A.Lang,
    isBoolean = L.isBoolean,
    isFunction = L.isFunction,
    isString = L.isString,

    defaults = A.namespace('config.io'),

    getDefault = function(attr) {
        return function() {
            return defaults[attr];
        };
    },

    ACCEPTS = {
        all: '*/*',
        html: 'text/html',
        json: 'application/json, text/javascript',
        text: 'text/plain',
        xml: 'application/xml, text/xml'
    };

/**
 * A base class for IORequest, providing:
 *
 * - Response data normalization for XML, JSON, JavaScript
 * - Cache options
 *
 * Check the [live demo](http://alloyui.com/examples/io/).
 *
 * @class A.IORequest
 * @extends Plugin.Base
 * @param {Object} config Object literal specifying widget configuration
 *     properties.
 * @uses io
 * @constructor
 * @include http://alloyui.com/examples/io/basic.js
 */
var IORequest = A.Component.create({
    /**
     * Static property provides a string to identify the class.
     *
     * @property NAME
     * @type String
     * @static
     */
    NAME: 'IORequest',

    /**
     * Static property used to define the default attribute
     * configuration for the IORequest.
     *
     * @property ATTRS
     * @type Object
     * @static
     */
    ATTRS: {

        /**
         * If `true` invoke the [start](A.IORequest.html#method_start) method
         * automatically, initializing the IO transaction.
         *
         * @attribute autoLoad
         * @default true
         * @type Boolean
         */
        autoLoad: {
            value: true,
            validator: isBoolean
        },

        /**
         * If `false` the current timestamp will be appended to the
         * url, avoiding the url to be cached.
         *
         * @attribute cache
         * @default true
         * @type Boolean
         */
        cache: {
            value: true,
            validator: isBoolean
        },

        /**
         * The type of the request (i.e., could be xml, json, javascript, text).
         *
         * @attribute dataType
         * @default null
         * @type String
         */
        dataType: {
            setter: function(v) {
                return (v || '').toLowerCase();
            },
            value: null,
            validator: isString
        },

        /**
         * This is a normalized attribute for the response data. It's useful to
         * retrieve the correct type for the
         * [dataType](A.IORequest.html#attr_dataType) (i.e., in json requests
         * the `responseData`) is a JSONObject.
         *
         * @attribute responseData
         * @default null
         * @type String | JSONObject | XMLDocument
         */
        responseData: {
            setter: function(v) {
                return this._setResponseData(v);
            },
            value: null
        },

        /**
         * URI to be requested using AJAX.
         *
         * @attribute uri
         * @default null
         * @type String
         */
        uri: {
            setter: function(v) {
                return this._parseURL(v);
            },
            value: null,
            validator: isString
        },

        // User readOnly variables

        /**
         * Whether the transaction is active or not.
         *
         * @attribute active
         * @default false
         * @type Boolean
         */
        active: {
            value: false,
            validator: isBoolean
        },

        /**
         * Object containing all the [IO Configuration Attributes](A.io.html).
         * This Object is passed to the `A.io` internally.
         *
         * @attribute cfg
         * @default Object containing all the [IO Configuration
         *     Attributes](A.io.html).
         * @readOnly
         * @type String
         */
        cfg: {
            getter: function() {
                var instance = this;

                // keep the current cfg object always synchronized with the
                // mapped public attributes when the user call .start() it
                // always retrieve the last set values for each mapped attr
                return {
                    arguments: instance.get('arguments'),
                    context: instance.get('context'),
                    data: instance.getFormattedData(),
                    form: instance.get('form'),
                    headers: instance.get('headers'),
                    method: instance.get('method'),
                    on: {
                        complete: A.bind(instance.fire, instance, 'complete'),
                        end: A.bind(instance._end, instance),
                        failure: A.bind(instance.fire, instance, 'failure'),
                        start: A.bind(instance.fire, instance, 'start'),
                        success: A.bind(instance._success, instance)
                    },
                    sync: instance.get('sync'),
                    timeout: instance.get('timeout'),
                    xdr: instance.get('xdr')
                };
            },
            readOnly: true
        },

        /**
         * Stores the IO Object of the current transaction.
         *
         * @attribute transaction
         * @default null
         * @type Object
         */
        transaction: {
            value: null
        },

        // Configuration Object mapping
        // To take advantages of the Attribute listeners of A.Base
        // See: http://yuilibrary.com/yui/docs/io/

        /**
         * See [IO
         * Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
         *
         * @attribute arguments
         * @default Value mapped on YUI.AUI.defaults.io.
         * @type Object
         */
        arguments: {
            valueFn: getDefault('arguments')
        },

        /**
         * See [IO
         * Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
         *
         * @attribute context
         * @default Value mapped on YUI.AUI.defaults.io.
         * @type Object
         */
        context: {
            valueFn: getDefault('context')
        },

        /**
         * See [IO
         * Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
         *
         * @attribute data
         * @default Value mapped on YUI.AUI.defaults.io.
         * @type Object
         */
        data: {
            valueFn: getDefault('data')
        },

        /**
         * See [IO
         * Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
         *
         * @attribute form
         * @default Value mapped on YUI.AUI.defaults.io.
         * @type Object
         */
        form: {
            valueFn: getDefault('form')
        },

        /**
         * Set the correct ACCEPT header based on the dataType.
         *
         * @attribute headers
         * @default Object
         * @type Object
         */
        headers: {
            getter: function(value) {
                var header = [];
                var instance = this;
                var dataType = instance.get('dataType');

                if (dataType) {
                    header.push(
                        ACCEPTS[dataType]
                    );
                }

                // always add *.* to the accept header
                header.push(
                    ACCEPTS.all
                );

                return A.merge(
                    value, {
                        Accept: header.join(', ')
                    }
                );
            },
            valueFn: getDefault('headers')
        },

        /**
         * See [IO
         * Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
         *
         * @attribute method
         * @default Value mapped on YUI.AUI.defaults.io.
         * @type String
         */
        method: {
            setter: function(val) {
                return val.toLowerCase();
            },
            valueFn: getDefault('method')
        },

        /**
         * A selector to be used to query against the response of the
         * request. Only works if the response is XML or HTML.
         *
         * @attribute selector
         * @default null
         * @type String
         */
        selector: {
            value: null
        },

        /**
         * See [IO
         * Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
         *
         * @attribute sync
         * @default Value mapped on YUI.AUI.defaults.io.
         * @type Boolean
         */
        sync: {
            valueFn: getDefault('sync')
        },

        /**
         * See [IO
         * Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
         *
         * @attribute timeout
         * @default Value mapped on YUI.AUI.defaults.io.
         * @type Number
         */
        timeout: {
            valueFn: getDefault('timeout')
        },

        /**
         * See [IO
         * Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
         *
         * @attribute xdr
         * @default Value mapped on YUI.AUI.defaults.io.
         * @type Object
         */
        xdr: {
            valueFn: getDefault('xdr')
        }
    },

    /**
     * Static property used to define which component it extends.
     *
     * @property EXTENDS
     * @type Object
     * @static
     */
    EXTENDS: A.Plugin.Base,

    prototype: {

        /**
         * Construction logic executed during IORequest instantiation.
         * Lifecycle.
         *
         * @method initializer
         * @param config
         * @protected
         */
        init: function() {
            var instance = this;

            IORequest.superclass.init.apply(this, arguments);

            instance._autoStart();
        },

        /**
         * Destructor lifecycle implementation for the `IORequest` class.
         * Purges events attached to the node (and all child nodes).
         *
         * @method destructor
         * @protected
         */
        destructor: function() {
            var instance = this;

            instance.stop();

            instance.set('transaction', null);
        },

        /**
         * Applies the `YUI.AUI.defaults.io.dataFormatter` if
         * defined and return the formatted data.
         *
         * @method getFormattedData
         * @return {String}
         */
        getFormattedData: function() {
            var instance = this;
            var value = instance.get('data');
            var dataFormatter = defaults.dataFormatter;

            if (isFunction(dataFormatter)) {
                value = dataFormatter.call(instance, value);
            }

            return value;
        },

        /**
         * Starts the IO transaction. Used to refresh the content also.
         *
         * @method start
         */
        start: function() {
            var instance = this;

            instance.destructor();

            instance.set('active', true);

            var ioObj = instance._yuiIOObj;

            if (!ioObj) {
                ioObj = new A.IO();

                instance._yuiIOObj = ioObj;
            }

            var transaction = ioObj.send(
                instance.get('uri'),
                instance.get('cfg')
            );

            instance.set('transaction', transaction);
        },

        /**
         * Stops the IO transaction.
         *
         * @method stop
         */
        stop: function() {
            var instance = this;
            var transaction = instance.get('transaction');

            if (transaction) {
                transaction.abort();
            }
        },

        /**
         * Invoke the `start` method (autoLoad attribute).
         *
         * @method _autoStart
         * @protected
         */
        _autoStart: function() {
            var instance = this;

            if (instance.get('autoLoad')) {
                instance.start();
            }
        },

        /**
         * Parse the [uri](A.IORequest.html#attr_uri) to add a
         * timestamp if [cache](A.IORequest.html#attr_cache) is
         * `true`. Also applies the `YUI.AUI.defaults.io.uriFormatter`.
         *
         * @method _parseURL
         * @param {String} url
         * @protected
         * @return {String}
         */
        _parseURL: function(url) {
            var instance = this;
            var cache = instance.get('cache');
            var method = instance.get('method');

            // reusing logic to add a timestamp on the url from jQuery 1.3.2
            if ((cache === false) && (method === 'get')) {
                var ts = +new Date();
                // try replacing _= if it is there
                var ret = url.replace(/(\?|&)_=.*?(&|$)/, '$1_=' + ts + '$2');
                // if nothing was replaced, add timestamp to the end
                url = ret + ((ret === url) ? (url.match(/\?/) ? '&' : '?') + '_=' + ts : '');
            }

            // formatting the URL with the default uriFormatter after the cache
            // timestamp was added
            var uriFormatter = defaults.uriFormatter;

            if (isFunction(uriFormatter)) {
                url = uriFormatter.apply(instance, [url]);
            }

            return url;
        },

        /**
         * Internal end callback for the IO transaction.
         *
         * @method _end
         * @param {Number} id ID of the IO transaction.
         * @param {Object} args Custom arguments, passed to the event handler.
         *     See [IO](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
         * @protected
         */
        _end: function(id, args) {
            var instance = this;

            instance.set('active', false);
            instance.set('transaction', null);

            instance.fire('end', id, args);
        },

        /**
         * Internal success callback for the IO transaction.
         *
         * @method _success
         * @param {Number} id ID of the IO transaction.
         * @param {Object} obj IO transaction Object.
         * @param {Object} args Custom arguments, passed to the event handler.
         *     See [IO](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
         * @protected
         */
        _success: function(id, obj, args) {
            var instance = this;

            // update the responseData attribute with the new data from xhr
            instance.set('responseData', obj);

            instance.fire('success', id, obj, args);
        },

        /**
         * Setter for [responseData](A.IORequest.html#attr_responseData).
         *
         * @method _setResponseData
         * @protected
         * @param {Object} xhr XHR Object.
         * @return {Object}
         */
        _setResponseData: function(xhr) {
            var data = null;
            var instance = this;

            if (xhr) {
                var dataType = instance.get('dataType');
                var contentType = xhr.getResponseHeader('content-type') || '';

                // if the dataType or the content-type is XML...
                if ((dataType === 'xml') ||
                    (!dataType && contentType.indexOf('xml') >= 0)) {

                    // use responseXML
                    data = xhr.responseXML;

                    // check if the XML was parsed correctly
                    if (data.documentElement.tagName === 'parsererror') {
                        throw 'Parser error: IO dataType is not correctly parsing';
                    }
                }
                else {
                    // otherwise use the responseText
                    data = xhr.responseText;
                }

                // empty string is not a valid 'json', convert it to null
                if (data === '') {
                    data = null;
                }

                // trying to parse to JSON if dataType is a valid json
                if (dataType === 'json') {
                    try {
                        data = A.JSON.parse(data);
                    }
                    catch (e) {
                        // throw 'Parser error: IO dataType is not correctly parsing';
                    }
                }
                else {
                    var selector = instance.get('selector');

                    if (data && selector) {
                        var tempRoot;

                        if (data.documentElement) {
                            tempRoot = A.one(data);
                        }
                        else {
                            tempRoot = A.Node.create(data);
                        }

                        data = tempRoot.all(selector);
                    }
                }
            }

            return data;
        }
    }
});

A.IORequest = IORequest;

/**
 * Alloy IO extension
 *
 * @class A.io
 * @static
 */

/**
 * Static method to invoke the [IORequest](A.IORequest.html).
 * Likewise [IO](A.io.html).
 *
 * @method A.io.request
 * @for A.io
 * @param {String} uri URI to be requested.
 * @param {Object} config Configuration Object for the [IO](A.io.html).
 * @return {IORequest}
 */
A.io.request = function(uri, config) {
    return new A.IORequest(
        A.merge(config, {
            uri: uri
        })
    );
};


}, '3.1.0-deprecated.95', {"requires": ["io-base", "json", "plugin", "querystring-stringify", "aui-component"]});
