/**
 * ajax module - pulp javascript framework
 * @link {http://pulpjs.org}
 * @license MIT-style license http://pulpjs.org/license
 */
/** var Ajax = pulp.ajax; */
pulp.Modules.ajax = 'Ajax';
/** var AjaxJson = pulp.ajax.json; */
pulp.Modules['ajax.json'] = 'AjaxJson';
/** var AjaxXml = pulp.ajax.xml; */
pulp.Modules['ajax.xml'] = 'AjaxXml';
/** var AjaxForm = pulp.ajax.form; */
pulp.Modules['ajax.form'] = 'AjaxForm';
/** var AjaxUpdater = pulp.ajax.updater; */
pulp.Modules['ajax.updater'] = 'AjaxUpdater';

(function() {

  var $p = pulp.base;

  // List of valid events - the first five are standard XMLHttpRequest statuses
  var events = 'uninitialized loading loaded interactive complete success failure abort error'.split(' ');
  
  /**
   * The ajax module performs get and post requests and serializes forms
   * responseText can be returned as HTML, XML, JSON or plain text
   * 
   * @requires base
   * @requires cls
   * @requires cls.event
   */
  var self = pulp.ajax = pulp.cls.create(/** @lends pulp.ajax# */{
    /**
     * Create a pulp.ajax object to send a GET or POST request to the server 
     * see {@link pulp.ajax#setProperties} for argument description
     * 
     * @constructs
     */ 
    initialize: function(url, data, options) {
      this.transport = null;
      this.data = {};
      this.url = '';
      this.options = {};
      this.setProperties(url, data, options);
      this.subscribeInline();
      this.refreshTimeout = false;
      return this;
    },
    /**
     * Clear url, data, and options
     * 
     * @return {pulp.ajax}
     * @chainable
     */
    reset: function() {
      return this.setProperties('', '', {});
    },
    /**
     * Set new values for url, data, and options
     *
     * @param {String} [url]  The url to which to make the request
     * @param {String|Object} [data]  The key-value pairs to send to the server; 
     *   in POST/GET requests, string should be in format  key1=value&key2=value&arr[]=value&assoc[key]=value
     *   and Object should be an object: {key1: 'value', key2: 'value', arr: ['value'], assoc: {key: 'value'}}
     * @param {Object} [options]  Object containing ajax options @see pulp.ajax.defaultOptions
     *   @config method: post or get
     *   @config async: if false, all code execution will stop until the request returns or aborts
     *   @config contentType: the mime type of data being sent
     *   @config encoding: the encoding of the string sent to the server
     *   @config parameters: the query string or object with name-value pairs representing the data to send
     *   @config timeout: the number of seconds after which to automatically abort the request
     *   @config requestHeaders: the http headers to send to the server 
     * @return {pulp.ajax}
     * @chainable
     */
    setProperties: function(url, data, options) {    
      if (typeof url == 'string') { this.setUrl(url); }
      if (data) { this.setData(data); }
      this.setOptions(options);
      return this;
    },
    /**
     * Set the url to which the ajax request will be made
     * @param {String} url
     * @return {this}
     * @chainable
     */
    setUrl: function(url) {
      this.url = url;
      return this;
    },
    /**
     * Set the data parameters to send
     * @param {Object|String} data  Object with param-value pairs or http get string
     * @return {this}
     * @chainable
     */
    setData: function(data) {
      if (typeof data == 'string') {
        data = self.queryToObject(data);
      }
      this.data = data;
      return this;
    },
    /**
     * Add aditional data to the data parameters to send
     * @param {Object|String} data  Object with param-value pairs or http get string
     * @return {this}
     * @chainable
     */
    extendData: function(data) {
      if (typeof data == 'string') {
        data = self.queryToObject(data);
      }
      this.data = $p.extend({}, this.data || {}, data);
      return this;      
    },
    /**
     * Set options of the ajax object see {@link pulp.ajax#setProperties}
     * @param {Object} options  Object with key-value pairs of options
     * @return {this}
     * @chainable
     */
    setOptions: function(options) {
      this.options = $p.extend({}, self.defaultOptions, this.options, options || {});
      this.options.method = this.options.method.toUpperCase();
      return this;
    },
    /**
     * Send a GET or POST request to the server 
     * 
     * see {@link pulp.ajax#setProperties} for argument description
     * @return {this}
     * @chainable
     */      
    send: function(url, data, options) {
      this.setProperties(url, data, options);
      this.transport = new self.transport(this.options);
      var query = self.objectToQuery(this.data);
      var url = this.url;      
      if (this.options.method == 'GET') {
        url += (query ? '?' + query : '');        
        query = null;
      }
      // webkit minus Chrome?
      if (query && (/Konqueror|Safari|KHTML/).test(navigator.userAgent)) {
        query += '&_=';
      }      
      this.transport.open(this.options.method, url, this.options.async, this.options.username, this.options.password);
      // observing readystatechange after opening allows the XMLHttpRequest object to be reused
      this.transport.after('readystatechange', $p.bind(this._respond, this));
      this.notify('send', function() {
        this.transport.send(query);
        this.on('complete', function() { window.clearTimeout(this.abortTimeout); });
        this.abortTimeout = window.setTimeout($p.bind(this._timeout, this), this.options.timeout * 1000);
        if (this.refreshSeconds) {
          window.clearTimeout(this.refreshTimeout);
          this.refreshTimeout = window.setTimeout($p.bind(this.send, this), this.refreshSeconds * 1000);
        }
      }, this.data);
      return this;
    },
    /**
     * Abort the Ajax Request (can be called directly or called after this.options.timeout seconds)
     * 
     * @return {pulp.ajax}
     * @chainable
     */
    abort: function() {
      return this.notify('abort', function() {
        this.transport.abort();
        this.notify('complete', null, this.transport);
        this.notify('failure', null, this.transport);
      }, this.transport);
    },
    /**
     * Called on timeout; fires the timeout event and aborts
     * 
     * @return {Undefiend}
     */
    _timeout: function() {
      this.notify('timeout', function() {
        this.abort();
      }, this.transport);
    },
    /**
     * Send a POST request to the server
     * 
     * see {@link pulp.ajax#setProperties} for argument description
     * @return {this}
     * @chainable
     */
    post: function(url, data, options) {
      options = options || {};
      options.method = 'post';
      return this.send(url, data, options);
    },
    /**
     * Send a GET request to the server for argument description
     * 
     * see {@link pulp.ajax#setProperties} for argument description
     * @return {this}
     * @chainable
     */
    get: function(url, data, options) {
      options = options || {};
      options.method = 'get';      
      return this.send(url, data, options);
    },
    /**
     * Respond to a ready state change, fire events for each state
     *   Fired on the transports onreadystatechange event
     *
     * @return {Undefined}
     */
    _respond: function() {
      var state = this.transport.readyState;
      this.notify(events[state] || 'error', function() {
        if (state == 4) {
          // network error timeout status codes: normalize to 0
          // from "Ajax: the Complete Reference" by Thomas Powell - http://books.google.com/books?id=p9hrjVMAeYkC&pg=PA180
          // 12002: internet timeout
          // 12007: network error
          // 12029: dropped connection
          // 12030: dropped connection
          // 12031: dropped connection
          // 12152: connection closed by server
          // 13030: exception code (Browser threw exception when attempting to read status)
          if (' 12002 12007 12029 12030 12031 12152 13030 0 '.indexOf(' ' + this.transport.status + ' ') > -1) {
            this.transport.status = 0;
            this.notify('error', null, this.transport);
          }
          else {
            this.notify(this.transport.status, null, this.transport);
          }
          this.notify(this.transport.isSuccessful() ? 'success' : 'failure', null, this.transport);
          if (this.transport.raw) {
            // clean up to allow reuse
            this.transport.raw.onreadystatechange = pulp.EmptyFunction;
            //this.transport.raw.i = this.transport.raw.i ? this.transport.raw.i : id++;
            if (!$p.inArray(self._reusableTransports, this.transport.raw)) {
              self._reusableTransports.push(this.transport.raw);
            }
            this.transport.raw = null;
          }
        }
      }, this.transport);
    }
   });
  /**
   * Static properties for pulp.ajax
   */
  self.extend(/** @scope pulp.ajax */{
    /**
     * The default options for pulp.ajax requests; see {@link pulp.ajax#setProperties} for description
     * 
     * @type {Object}
     */ 
    defaultOptions: {
      method: 'post',
      async: true,
      contentType: 'application/x-www-form-urlencoded',
      encoding: 'UTF-8',
      parameters: '',
      timeout: 30, // number of seconds after which to abort the request
      requestHeaders: {
        'X-Requested-With': 'XMLHttpRequest',
        'User-Agent': 'XMLHTTP/1.0',
        'X-Pulp-Version': pulp.Version,
        'Accept': 'text/javascript, text/html, application/xml, text/xml, *' + '/' + '*'
      }
    },
    /**
     * Convert a query string to an object with name-value pairs
     * 
     * @param {String} string  The query string
     * @param {String} [delimiter="="]  The character separating name and value
     * @oaran {String} [glue="&"]  The character separating each pair
     * @return {Object}
     */
    queryToObject: function(string, delimiter, glue) {
      glue = glue || '=';
      var pairs = string.split(delimiter || '&');
      var object = {}, keyValue, key, value;
      for (var i = 0, len = pairs.length; i < len; i++) {
        if (pairs[i].length) {
          keyValue = pairs[i].split(glue);
          key = decodeURIComponent(keyValue.shift().replace(/\[\]$/, ''));
          if (key) {
            value = keyValue.length ? decodeURIComponent(keyValue.join(glue)) : undefined;
            if (key in object) {
              if (object[key].constructor === Array) {
                object[key].push(value);
              } else {
                object[key] = [object[key], value];
              }
            } else {
              object[key] = value;
            }
          }
        }
      }
      return object;
    },   
    /**
     * Convert an object with name-value pairs to a query string
     * 
     * @param {Object} object  The object containing name-value pairs
     * @param {String} [delimiter="="]  The character separating name and value
     * @oaran {String} [glue="&"]  The character separating each pair
     * @return {String}
     */
    objectToQuery: function(object, delimiter, glue) {
      var pairs = [], glue = glue || '=', key, value, escValue;
      for (var prop in object) {
        key = encodeURIComponent($p.castAsString(prop));
        value = object[prop];
        if ($p.isArray(value)) {
          if (value.length) {
            for (var i in value) {
              escValue = encodeURIComponent($p.castAsString(value[i]));
              pairs.push(key + '[]' + (escValue ? glue + escValue : ''));
            }
          } else {
           pairs.push(key + '[]');
          }
        } else {
          escValue = encodeURIComponent($p.castAsString(value));
          pairs.push(key + (escValue ? glue + escValue : ''));
        }
      }
      return pairs.join(delimiter || '&');
    },
    _reusableTransports: [],
    /**
     * Convenience method to create an ajax object and send request
     *
     * @param {Object} [options={}]  Configure the ajax request
     * @param {String} [options.type]  The flavor of ajax request (json, xml, form, updater); defaults to a regular pulp.ajax object
     * @param {String} [options.url='']  The url to which to send the ajax request
     * @param {String} [options.params={}]  Key-value pairs to send with request
     * see {@link pulp.ajax#setProperties} for other option names, values and descriptions
     * @return {pulp.ajax}  Or child class depending on options.type (pulp.ajax.json, pulp.ajax.xml, pulp.ajax.form, pulp.ajax.updater)
     */
    request: function (options) {
      options = options || {};
      var type;
      var ctor = (options.type && ((type = options.type.toLowerCase()) in self) ? self[type] : self);
      if (options.container) {
        ctor = 'updater';
      }
      var url = options.url || '';
      var params = options.params || {};
      return new ctor(url, params, options).send();
    }
  });   
   
  /**
   * Wrapper for XMLHttpRequest, normalizing behavior for all browsers and handling non-HTTP requests
   * Separate from pulp.ajax to allow reusing XMLHttpRequest objects while allowing scripts
   *   to cache this object with all the core properties of the original XMLHttpRequest  
   *
   * @name pulp.ajax.transport
   * @extends pulp.cls.Base
   */
  self.transport = pulp.cls.create(/** @lends pulp.ajax.transport# */{
    /**
     * 
     * @param {Object} [options]
     * @param {String} [options.contentType="application/x-www-form-urlencoded"]
     * @param {String} [options.encoding="UTF-8"]
     * @param {Object} [options.requestHeaders={...}]
     * @constructs
     */
    initialize: function(options) {
      /**
       * @type {Object}  Includes contentType, encoding, requestHeaders
       */
      this.options = $p.clone(options || {});
    },
    /**
     * Get a transport from the reusable array or instantiate a new one
     * 
     * @return {XMLHttpRequest}
     */
    _loadTransport: function() {
      /**
       * @type {XMLHttpRequest}  The wrapped XMLHttpRequest object
       */
      this.raw = self._reusableTransports.shift() || this._instantiateTransport();
    },
    /**
     * Instantiate an XMLHttpRequest Object
     * 
     * @return {XMLHttpRequest}
     */
    _instantiateTransport: function() {
      try { return new XMLHttpRequest(); } catch(e) {}
      try { return new ActiveXObject('Msxml2.XMLHTTP'); } catch(e) {}
      try { return new ActiveXObject('Msxml3.XMLHTTP'); } catch(e) {}
      try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch(e) {}
      throw 'Browser does not support ajax.';
    },
    /**
     * Wrapper for XMLHttpRequest#open that also assesses wheter the request is local
     * 
     * @param {String} method  get or post
     * @param {String} url  The url to which to send the request
     * @param {Boolean} [async]  If false, all code execution will stop until the request returns or aborts
     * @param {String} [username]  The username to send if http authentication is required
     * @param {String} [password]  The password to send if http authentication is required
     * @return {pulp.ajax.transport}
     * @chainable
     */
    open: function(method, url, async, username, password) {
      this._loadTransport();
      this.method = method;
      this.url = url;
      this.async = !!async;
      this.raw.open(this.method, this.url, async, username, password);
      this._sendHeaders();
      return this;
    },
    /**
     * Return true if request is through the http protocol
     *
     * @return {Boolean}
     */
    isHttp: function() {
      var pcol = String(this.url).match(/^([a-z]+)?\:/i);
      if (!pcol) {
        return window.location.protocol.match(/^https?\:/);
      }
      if (pcol[1] == 'http') {
    		return true;
    	}
    },
    /**
     * Wrapper for XMLHttpRequest#send that also binds the class function to the transport's onreadystatechange event
     * 
     * @param {String} query  The data to send in get or post
     * @return {pulp.ajax.transport}
     * @chainable
     */
    send: function(query) {
      this.aborted = false;
      this.query = query || null;
      this.raw.send(query);
      // will this allow object reuse in IE?
      this.raw.onreadystatechange = $p.bind(this._onreadystatechange, this);
      return this;
    },
    /**
     * Wrapper for XMLHttpRequest#abort that also sets properties as undefined
     * 
     * @return {pulp.ajax.transport}
     * @chainable
     */
    abort: function() {
      if (this.raw && !this.aborted) {
        this.notify('abort', function() {
          this.aborted = true;
          this.raw.abort();
        });
      }
      return this;
    },
    /**
     * The class function bound to the transport's onreadystatechange event
     * - Copies crucial properties from the transport object to this object
     * - Fires a readystatechange event to allow multiple listeners
     * - Stores the transport to be reused
     * 
     * @return {undefined}
     */
    _onreadystatechange: function() {
      if (this.aborted) { return; }
      $p.each('readyState responseText responseXML statusText', function(p) {
        this[p] = this.raw[p];
      }, this);
      this.status = this._getStatus();
      if (this.status == 1223) { // IE specific status
        this.statusText = 'No Content';
      }      
      this.notify('readystatechange', null, {readyState: this.readyState});
    },
    /**
     * Send http headers based on options
     * 
     * @return {Undefined}
     */
    _sendHeaders: function() {
      var headers = {};
      if (this.method == 'POST') {
        headers['Content-type'] = this.options.contentType + (this.options.encoding ? '; charset=' + this.options.encoding: '');
  
        /* Force "Connection: close" for older Mozilla browsers to work
         * around a bug where XMLHttpRequest sends an incorrect
         * Content-length header. See Mozilla Bugzilla #246651.
         */
        if (this.raw.overrideMimeType && (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0, 2005])[1] < 2005) {
          headers.Connection = 'close';
        }
      }
  
      // user-defined headers
      headers = $p.extend(headers, self.defaultOptions.requestHeaders, this.options.requestHeaders || {});
  
      for (var name in headers) {
        this.raw.setRequestHeader(name, headers[name]);
      }
      this.requestHeaders = headers;
    },
    /**
     * Return the raw http headers that were sent to the server
     * 
     * @return {String}
     */
    getAllRequestHeaders: function() {
      var headers = this.requestHeaders;
      var raw = [];
      for (var name in headers) {
        raw.push(name + ': ' + headers[name]);
      }
      return raw.join('\n');
    },
    /**
     * Get the value of a particular http header that was sent to the server
     * 
     * @param {String} name  The name of the header to get
     * @return {String}
     */
    getRequestHeader: function(name) {
      return this.requestHeaders[name];
    },
    /**
     * Get the raw http headers received from the server
     * 
     * @return {String}
     */
    getAllResponseHeaders: function() {
      return this.raw.getAllResponseHeaders();
    },
    /**
     * Get the value of a particular http header received from the server
     * 
     * @param {String} name  The name of the header to get
     * @return {String}
     */
    getResponseHeader: function(name) {
      try {
        return this.raw.getResponseHeader(name) || null;
      } catch (e) { return null }
    },    
    /**
     * Get the http status; normalize IE's 1223 code to 204; set status to 13030 if Mozilla throws an exception
     *
     * @return {Number}  The http status code (e.g. 200, 404)
     */
    _getStatus: function() {
      var status;
      try { // Mozilla will throw an exception if status is not available so use a try-catch block
        // normalize IE's 1223 code to 204
        status = (this.aborted ? undefined :
          this.raw.status == 1223) ? 204 : this.raw.status;
          
      } catch (e) {
        status = 13030;
      }
      // non-http requests fire 0 regardless of success or failure
      if (!this.isHttp() && status === 0) {
        // define success on non-http requests as something with response text
        status = (this.responseText || '').length ? 200 : 404;        
      }			
      return status;
    },
    /**
     * Return true if transport status is between 200 and 300
     * 
     * return {Boolean}
     */
    isSuccessful: function() {
      return this.status >= 200 && this.status < 300;
    }    
  });
  
  /**
   * Class that automatically evaluates response as JSON and stores the result as responseObject
   * @name pulp.ajax.json
   * @extends pulp.ajax
   */
  self.json = self.createSubclass(/** @lends pulp.ajax.json# */{
    /**
     * Call parent constructor and observe complete callback that evaluates responseText
     * 
     * see {@link pulp.ajax#setProperties} for params
     * @constructs
     */
    initialize: function(/*url, data, options*/) {
      this.after('complete', function() {
        this.transport.responseJson = this.transport.responseText;
        if ('json' in pulp) {
          this.transport.responseObject = pulp.json.decode(this.transport.responseText);
        } else {
          var str = this.transport.responseText.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
          if((/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str)) {
            this.transport.responseObject = eval(this.transport.responseText);
          }
        }
      });
      this.applyParent('initialize', arguments);
      // TODO: set request header "Accept" to "text/json, application/json" or the like 
    }
   });
   
  /**
   * Class that automatically evaluates response as XML and stores an XML Document Object in responseXML
   * @name pulp.ajax.xml
   * @extends pulp.ajax
   */
  self.xml = self.createSubclass(/** @lends pulp.ajax.xml# */{
    /**
     * Call parent constructor and observe complete callback that converts responseText to XML Document
     * 
     * see {@link pulp.ajax#setProperties} for params
     * @constructs
     */
    initialize: function(/*url, data, options*/) {
      this.after('complete', function() {            
        if ('xml' in pulp) {
          this.transport.responseXML = new pulp.xml(this.transport.responseText);
          
        } else {
          if (window.DOMParser) {
            //EOMB            
            var doc = new DOMParser().parseFromString(this.transport.responseText, "text/xml");
            
          } else {
            //Internet Explorer
            var doc = new ActiveXObject("Microsoft.XMLDOM");
            doc.async = false;
            doc.loadXML(this.transport.responseText);
          }
          this.transport.responseXML = doc;
        }
      });
      this.applyParent('initialize', arguments);
    }
  });   

  /**
   * Class that sends form data from options.form (defaults to document.forms[0])
   * options.form can be a form object, HTML id attribute, or form name/number
   * @name pulp.ajax.form
   * @extends pulp.ajax
   */
  self.form = self.createSubclass(/** @lends pulp.ajax.form# */{
    /**
     * Call parent constructor and observe form submission
     * 
     * see {@link pulp.ajax#setProperties} for params
     * @constructs
     */
    initialize: function(/*url, data, options*/) {
      this.setDefaultAction('send', $p.bind(this.serializeForm, this));
      this.applyParent('initialize', arguments);
      var formId = options.form || '';
      if (typeof formId == 'string') {
        // Determine if the argument is a form id or a form name
        this.form = (document.getElementById(formId) || document.forms[formId]);
      } else if (formId.nodeType == 1) {
        // Treat argument as an HTML form object.
        this.form = formId;
      } else {
        this.form = document.forms[0];
      }
      this.form.onsubmit = $p.bind(this.send, this);
    },
    /**
     * This method assembles the form label and value pairs and
     * constructs an encoded string. (from YUI)
     *.
     * @return {Object} form field name and value pairs.
     */
    serializeForm: function() {
      var el, item = 0, name, selected, opt, value;
  
      // Iterate over the form elements collection to construct the
      // label-value pairs.
      while ((el = this.form.elements[item++])) {
        // Do not submit fields that are disabled or
        // do not have a name attribute value.
        if (el.disabled == false && (name = el.name)) {
          switch (el.type) {
            // Safari, Opera, FF all default opt.value from .text if
            // value attribute not specified in markup
            case 'select-one':
              if (el.selectedIndex > -1) {
                opt = el.options[el.selectedIndex];
                value = (opt.attributes.value && opt.attributes.value.specified ? opt.value : opt.text);
              }
              break;
            case 'select-multiple':
              selected = [];
              var i = 0;
              while (opt = el.options[i++]) {
                if (opt.selected) {
                  selected.push(opt.attributes.value && opt.attributes.value.specified ? opt.value : opt.text);
                }
              }
              value = selected;
              break;
            case 'radio':
            case 'checkbox':
              if (el.checked) {
                value = el.value;
              }
              break;
            case 'file':
              // stub case as XMLHttpRequest will only send the file path as a string.
            case undefined:
              // stub case for fieldset element which returns undefined.
            case 'reset':
              // stub case for input type reset button.
            case 'button':
              // stub case for input type button elements.
              break;
            case 'submit':
              // TODO: determine which button was clicked??
              if (hasSubmit === false) {
                if (this._hasSubmitListener && this._submitElementValue){
                  data[item++] = this._submitElementValue;
                } else {
                  data[item++] = oName + oValue;
                }
                hasSubmit = true;
              }
              break;
              
            default:
              value = el.value;
          }
          if ($p.isArray(data[name])) {
            data[name].push(value);
          } else if (name in data) {
            data[name] = [data[name], value];
          } else {
            data[name] = value;
          }
        }
      }
      
      if (this.form.enctype) {
        this.initHeader('Content-Type', this.form.enctype);
      }
      this.options.method = this.form.method;
  
      this.data = data;
      return this;
    }  
  });
  
  /**
   * Class that sets the innerHTML of the given node to the responseText
   * @name pulp.ajax.updater
   * @extends pulp.ajax
   */
  self.updater = self.createSubclass(/** @lends pulp.ajax.updater# */{
    /**
     * Call parent constructor and observe complete callback that sets options.container's innerHTML
     * 
     * see {@link pulp.ajax#setProperties} for params
     * @constructor
     */  
    initialize: function(/*url, data, options*/) {
      this.applyParent('initialize', arguments);
      this.after('complete', function(event) {
        var t = this.transport, div;
        if (typeof this.options.container == 'string') {
          div = document.getElementById(this.options.container);
        }
        else if (this.options.container && this.options.container.nodeType == 1) {
          div = this.options.container;
        }
        (div || document.body).innerHTML = t.isSuccessful() ? t.responseText : (this.options.defaultText || '');
      });
    }
  });
  
})();