/*
Script: Request.js
  Powerful all purpose Request Class. Uses XMLHTTPRequest.

License:
  MIT-style license.
*/

var Request = new Class({

  Implements: [Chain, Events, Options],

  options: {
    /*onRequest: $empty,
    onSuccess: $empty,
    onFailure: $empty,
    onException: $empty,*/
    url: '',
    data: '',
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
    },
    async: true,
    format: false,
    method: 'post',
    link: 'ignore',
    isSuccess: null,
    emulation: true,
    urlEncoded: true,
    encoding: 'utf-8',
    evalScripts: false,
    evalResponse: false
  },

  initialize: function(options){
    this.xhr = new Browser.Request();
    this.setOptions(options);
    this.options.isSuccess = this.options.isSuccess || this.isSuccess;
    this.headers = new Hash(this.options.headers);
  },

  onStateChange: function(){
    if (this.xhr.readyState != 4 || !this.running) return;
    this.running = false;
    this.status = 0;
    $try(function(){
      this.status = this.xhr.status;
    }.bind(this));
    if (this.options.isSuccess.call(this, this.status)){
      this.response = {text: this.xhr.responseText, xml: this.xhr.responseXML};
      this.success(this.response.text, this.response.xml);
    } else {
      this.response = {text: null, xml: null};
      this.failure();
    }
    this.xhr.onreadystatechange = $empty;
  },

  isSuccess: function(){
    return ((this.status >= 200) && (this.status < 300));
  },

  processScripts: function(text){
    if (this.options.evalResponse || (/(ecma|java)script/).test(this.getHeader('Content-type'))) return $exec(text);
    return text.stripScripts(this.options.evalScripts);
  },

  success: function(text, xml){
    this.onSuccess(this.processScripts(text), xml);
  },

  onSuccess: function(){
    this.fireEvent('complete', arguments).fireEvent('success', arguments).callChain();
  },

  failure: function(){
    this.onFailure();
  },

  onFailure: function(){
    this.fireEvent('complete').fireEvent('failure', this.xhr);
  },

  setHeader: function(name, value){
    this.headers.set(name, value);
    return this;
  },

  getHeader: function(name){
    return $try(function(){
      return this.xhr.getResponseHeader(name);
    }.bind(this));
  },

  check: function(caller){
    if (!this.running) return true;
    switch (this.options.link){
      case 'cancel': this.cancel(); return true;
      case 'chain': this.chain(caller.bind(this, Array.slice(arguments, 1))); return false;
    }
    return false;
  },

  send: function(options){
    if (!this.check(arguments.callee, options)) return this;
    this.running = true;

    var type = $type(options);
    if (type == 'string' || type == 'element') options = {data: options};

    var old = this.options;
    options = $extend({data: old.data, url: old.url, method: old.method}, options);
    var data = options.data, url = options.url, method = options.method;

    switch ($type(data)){
      case 'element': data = $(data).toQueryString(); break;
      case 'object': case 'hash': data = Hash.toQueryString(data);
    }

    if (this.options.format){
      var format = 'format=' + this.options.format;
      data = (data) ? format + '&' + data : format;
    }

    if (this.options.emulation && ['put', 'delete'].contains(method)){
      var _method = '_method=' + method;
      data = (data) ? _method + '&' + data : _method;
      method = 'post';
    }

    if (this.options.urlEncoded && method == 'post'){
      var encoding = (this.options.encoding) ? '; charset=' + this.options.encoding : '';
      this.headers.set('Content-type', 'application/x-www-form-urlencoded' + encoding);
    }

    if (data && method == 'get'){
      url = url + (url.contains('?') ? '&' : '?') + data;
      data = null;
    }

    this.xhr.open(method.toUpperCase(), url, this.options.async);

    this.xhr.onreadystatechange = this.onStateChange.bind(this);

    this.headers.each(function(value, key){
      if (!$try(function(){
        this.xhr.setRequestHeader(key, value);
        return true;
      }.bind(this))) this.fireEvent('exception', [key, value]);
    }, this);

    this.fireEvent('request');
    this.xhr.send(data);
    if (!this.options.async) this.onStateChange();
    return this;
  },

  cancel: function(){
    if (!this.running) return this;
    this.running = false;
    this.xhr.abort();
    this.xhr.onreadystatechange = $empty;
    this.xhr = new Browser.Request();
    this.fireEvent('cancel');
    return this;
  }

});

(function(){

var methods = {};
['get', 'post', 'put', 'delete', 'GET', 'POST', 'PUT', 'DELETE'].each(function(method){
  methods[method] = function(){
    var params = Array.link(arguments, {url: String.type, data: $defined});
    return this.send($extend(params, {method: method.toLowerCase()}));
  };
});

Request.implement(methods);

})();

Element.Properties.send = {

  set: function(options){
    var send = this.retrieve('send');
    if (send) send.cancel();
    return this.eliminate('send').store('send:options', $extend({
      data: this, link: 'cancel', method: this.get('method') || 'post', url: this.get('action')
    }, options));
  },

  get: function(options){
    if (options || !this.retrieve('send')){
      if (options || !this.retrieve('send:options')) this.set('send', options);
      this.store('send', new Request(this.retrieve('send:options')));
    }
    return this.retrieve('send');
  }

};

Element.implement({

  send: function(url){
    var sender = this.get('send');
    sender.send({data: this, url: url || sender.options.url});
    return this;
  }

});
