/**!
 * project-site: http://plugins.jquery.com/project/AjaxManager
 * repository: http://github.com/aFarkas/Ajaxmanager
 * @author Alexander Farkas
 * @version 3.12
 * Copyright 2010, Alexander Farkas
 * Dual licensed under the MIT or GPL Version 2 licenses.
 */

(function($){
    "use strict";
    var managed = {},
        cache   = {}
    ;
    $.manageAjax = (function(){
        function create(name, opts){
            managed[name] = new $.manageAjax._manager(name, opts);
            return managed[name];
        }
        
        function destroy(name){
            if(managed[name]){
                managed[name].clear(true);
                delete managed[name];
            }
        }

        
        var publicFns = {
            create: create,
            destroy: destroy
        };
        
        return publicFns;
    })();
    
    $.manageAjax._manager = function(name, opts){
        this.requests = {};
        this.inProgress = 0;
        this.name = name;
        this.qName = name;
        
        this.opts = $.extend({}, $.manageAjax.defaults, opts);
        if(opts && opts.queue && opts.queue !== true && typeof opts.queue === 'string' && opts.queue !== 'clear'){
            this.qName = opts.queue;
        }
    };
    
    $.manageAjax._manager.prototype = {
        add: function(url, o){
            if(typeof url == 'object'){
                o = url;
            } else if(typeof url == 'string'){
                o = $.extend(o || {}, {url: url});
            }
            o = $.extend({}, this.opts, o);
            
            var origCom     = o.complete || $.noop,
                origSuc     = o.success || $.noop,
                beforeSend  = o.beforeSend || $.noop,
                origError   = o.error || $.noop,
                strData     = (typeof o.data == 'string') ? o.data : $.param(o.data || {}),
                xhrID       = o.type + o.url + strData,
                that        = this,
                ajaxFn      = this._createAjax(xhrID, o, origSuc, origCom)
            ;
            if(o.preventDoubleRequests && o.queueDuplicateRequests){
                if(o.preventDoubleRequests){
                    o.queueDuplicateRequests = false;
                }
                setTimeout(function(){
                    throw("preventDoubleRequests and queueDuplicateRequests can't be both true");
                }, 0);
            }
            if(this.requests[xhrID] && o.preventDoubleRequests){
                return;
            }
            ajaxFn.xhrID = xhrID;
            o.xhrID = xhrID;
            
            o.beforeSend = function(xhr, opts){
                var ret = beforeSend.call(this, xhr, opts);
                if(ret === false){
                    that._removeXHR(xhrID);
                }
                xhr = null;
                return ret;
            };
            o.complete = function(xhr, status){
                that._complete.call(that, this, origCom, xhr, status, xhrID, o);
                xhr = null;
            };
            
            o.success = function(data, status, xhr){
                that._success.call(that, this, origSuc, data, status, xhr, o);
                xhr = null;
            };
                        
            //always add some error callback
            o.error =  function(ahr, status, errorStr){
                var httpStatus  = '',
                    content     = ''
                ;
                if(status !== 'timeout' && ahr){
                    httpStatus = ahr.status;
                    content = ahr.responseXML || ahr.responseText;
                }
                if(origError) {
                    origError.call(this, ahr, status, errorStr, o);
                } else {
                    setTimeout(function(){
                        throw status + '| status: ' + httpStatus + ' | URL: ' + o.url + ' | data: '+ strData + ' | thrown: '+ errorStr + ' | response: '+ content;
                    }, 0);
                }
                ahr = null;
            };
            
            if(o.queue === 'clear'){
                $(document).clearQueue(this.qName);
            }
            
            if(o.queue || (o.queueDuplicateRequests && this.requests[xhrID])){
                $.queue(document, this.qName, ajaxFn);
                if(this.inProgress < o.maxRequests && (!this.requests[xhrID] || !o.queueDuplicateRequests)){
                    $.dequeue(document, this.qName);
                }
                return xhrID;
            }
            return ajaxFn();
        },
        _createAjax: function(id, o, origSuc, origCom){
            var that = this;
            return function(){
                if(o.beforeCreate.call(o.context || that, id, o) === false){return;}
                that.inProgress++;
                if(that.inProgress === 1){
                    $.event.trigger(that.name +'AjaxStart');
                }
                if(o.cacheResponse && cache[id]){
                    if(!cache[id].cacheTTL || cache[id].cacheTTL < 0 || ((new Date().getTime() - cache[id].timestamp) < cache[id].cacheTTL)){
                        that.requests[id] = {};
                        setTimeout(function(){
                            that._success.call(that, o.context || o, origSuc, cache[id]._successData, 'success', cache[id], o);
                            that._complete.call(that, o.context || o, origCom, cache[id], 'success', id, o);
                        }, 0);
                    } else {
                         delete cache[id];
                    }
                } 
                if(!o.cacheResponse || !cache[id]) {
                    if (o.async) {
                        that.requests[id] = $.ajax(o);
                    } else {
                        $.ajax(o);
                    }
                }
                return id;
            };
        },
        _removeXHR: function(xhrID){
            if(this.opts.queue || this.opts.queueDuplicateRequests){
                $.dequeue(document, this.qName);
            }
            this.inProgress--;
            this.requests[xhrID] = null;
            delete this.requests[xhrID];
        },
        clearCache: function () {
            cache = {};
        },
        _isAbort: function(xhr, status, o){
            if(!o.abortIsNoSuccess || (!xhr && !status)){
                return false;
            }
            var ret = !!(  ( !xhr || xhr.readyState === 0 || this.lastAbort === o.xhrID ) );
            xhr = null;
            return ret;
        },
        _complete: function(context, origFn, xhr, status, xhrID, o){
            if(this._isAbort(xhr, status, o)){
                status = 'abort';
                o.abort.call(context, xhr, status, o);
            }
            origFn.call(context, xhr, status, o);
            
            $.event.trigger(this.name +'AjaxComplete', [xhr, status, o]);
            
            if(o.domCompleteTrigger){
                $(o.domCompleteTrigger)
                    .trigger(this.name +'DOMComplete', [xhr, status, o])
                    .trigger('DOMComplete', [xhr, status, o])
                ;
            }
            
            this._removeXHR(xhrID);
            if(!this.inProgress){
                $.event.trigger(this.name +'AjaxStop');
            }
            xhr = null;
        },
        _success: function(context, origFn, data, status, xhr, o){
            var that = this;
            if(this._isAbort(xhr, status, o)){
                xhr = null;
                return;
            }
            if(o.abortOld){
                $.each(this.requests, function(name){
                    if(name === o.xhrID){
                        return false;
                    }
                    that.abort(name);
                });
            }
            if(o.cacheResponse && !cache[o.xhrID]){
                if(!xhr){
                    xhr = {};
                }
                cache[o.xhrID] = {
                    status: xhr.status,
                    statusText: xhr.statusText,
                    responseText: xhr.responseText,
                    responseXML: xhr.responseXML,
                    _successData: data,
                    cacheTTL: o.cacheTTL, 
                    timestamp: new Date().getTime()
                };
                if('getAllResponseHeaders' in xhr){
                    var responseHeaders = xhr.getAllResponseHeaders();
                    var parsedHeaders;
                    var parseHeaders = function(){
                        if(parsedHeaders){return;}
                        parsedHeaders = {};
                        $.each(responseHeaders.split("\n"), function(i, headerLine){
                            var delimiter = headerLine.indexOf(":");
                            parsedHeaders[headerLine.substr(0, delimiter)] = headerLine.substr(delimiter + 2);
                        });
                    };
                    $.extend(cache[o.xhrID], {
                        getAllResponseHeaders: function() {return responseHeaders;},
                        getResponseHeader: function(name) {
                            parseHeaders();
                            return (name in parsedHeaders) ? parsedHeaders[name] : null;
                        }
                    });
                }
            }
            origFn.call(context, data, status, xhr, o);
            $.event.trigger(this.name +'AjaxSuccess', [xhr, o, data]);
            if(o.domSuccessTrigger){
                $(o.domSuccessTrigger)
                    .trigger(this.name +'DOMSuccess', [data, o])
                    .trigger('DOMSuccess', [data, o])
                ;
            }
            xhr = null;
        },
        getData: function(id){
            if( id ){
                var ret = this.requests[id];
                if(!ret && this.opts.queue) {
                    ret = $.grep($(document).queue(this.qName), function(fn, i){
                        return (fn.xhrID === id);
                    })[0];
                }
                return ret;
            }
            return {
                requests: this.requests,
                queue: (this.opts.queue) ? $(document).queue(this.qName) : [],
                inProgress: this.inProgress
            };
        },
        abort: function(id){
            var xhr;
            if(id){
                xhr = this.getData(id);
                
                if(xhr && xhr.abort){
                    this.lastAbort = id;
                    xhr.abort();
                    this.lastAbort = false;
                } else {
                    $(document).queue(
                        this.qName, $.grep($(document).queue(this.qName), function(fn, i){
                            return (fn !== xhr);
                        })
                    );
                }
                xhr = null;
                return;
            }
            
            var that    = this,
                ids     = []
            ;
            $.each(this.requests, function(id){
                ids.push(id);
            });
            $.each(ids, function(i, id){
                that.abort(id);
            });
        },
        clear: function(shouldAbort){
            $(document).clearQueue(this.qName); 
            if(shouldAbort){
                this.abort();
            }
        }
    };
    $.manageAjax._manager.prototype.getXHR = $.manageAjax._manager.prototype.getData;
    $.manageAjax.defaults = {
        beforeCreate: $.noop,
        abort: $.noop,
        abortIsNoSuccess: true,
        maxRequests: 1,
        cacheResponse: false,
        async: true,
        domCompleteTrigger: false,
        domSuccessTrigger: false,
        preventDoubleRequests: true,
        queueDuplicateRequests: false,
        cacheTTL: -1,
        queue: false // true, false, clear
    };
    
    $.each($.manageAjax._manager.prototype, function(n, fn){
        if(n.indexOf('_') === 0 || !$.isFunction(fn)){return;}
        $.manageAjax[n] =  function(name, o){
            if(!managed[name]){
                if(n === 'add'){
                    $.manageAjax.create(name, o);
                } else {
                    return;
                }
            }
            var args = Array.prototype.slice.call(arguments, 1);
            managed[name][n].apply(managed[name], args);
        };
    });
    
})(jQuery);
