﻿function dbg(s){
  //var dbg_wrap = document.getElementById("dbg_wrap");
  //dbg_wrap.innerHTML += s +"<br />";
}

//NOTE: IE6 is deprecated
var ajax = function(opts){
  //TODO const define
  this.READY_STATE_UNINITIALIZED = 0;
  this.READY_STATE_LOADING = 1;
  this.READY_STATE_LOADED = 2;
  this.READY_STATE_INTERACTIVE = 3;
  this.READY_STATE_COMPLETE = 4;
  this.STATUS_UNKNOW = 0; //when no response or unknow format
  //Hypertext Transfer Protocol -- HTTP/1.1
  this.STATUS_UNKNOW = 0;
  //1.  Informational 1xx 
  this.STATUS_CONTINUE = 100;
  this.STATUS_SWITCHING_PROTOCOLS = 101;
  //2._Successful_2xx_
  this.STATUS_OK = 200;
  this.STATUS_CREATED = 201;
  this.STATUS_ACCEPTED = 202;
  this.STATUS_NON_AUTHORITATIVE_INFORMATION = 203;
  this.STATUS_NO_CONTENT = 204;
  this.STATUS_RESET_CONTENT = 205;
  this.STATUS_PARTIAL_CONTENT = 206;
  //3._Redirection_3xx_
  this.STATUS_MULTIPLE_CHOICES = 300;
  this.STATUS_MOVED_PERMANENTLY = 301;
  this.STATUS_FOUND = 302;
  this.STATUS_SEE_OTHER = 303;
  this.STATUS_NOT_MODIFIED = 304;
  this.STATUS_USE_PROXY = 305;
  //this.STATUS_UNUSED = 306;
  this.STATUS_TEMPORARY_REDIRECT = 307;
  //4._Client_Error_4xx_
  this.STATUS_BAD_REQUEST = 400;
  this.STATUS_UNAUTHORIZED = 401;
  this.STATUS_PAYMENT_REQUIRED = 402;
  this.STATUS_FORBIDDEN = 403;
  this.STATUS_NOT_FOUND = 404;
  this.STATUS_METHOD_NOT_ALLOWED = 405;
  this.STATUS_NOT_ACCEPTABLE = 406;
  this.STATUS_PROXY_AUTHENTICATION_REQUIRED = 407;
  this.STATUS_REQUEST_TIMEOUT = 408;
  this.STATUS_CONFLICT = 409;
  this.STATUS_GONE = 410;
  this.STATUS_LENGTH_REQUIRED = 411;
  this.STATUS_PRECONDITION_FAILED = 412;
  this.STATUS_REQUEST_ENTITY_TOO_LARGE = 413;
  this.STATUS_REQUEST_URI_TOO_LONG = 414;
  this.STATUS_UNSUPPORTED_MEDIA_TYPE = 415;
  this.STATUS_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
  this.STATUS_EXPECTATION_FAILED = 417;
  //5._Server_Error_5xx_
  this.STATUS_INTERNAL_SERVER_ERROR = 500;
  this.STATUS_NOT_IMPLEMENTED = 501;
  this.STATUS_BAD_GATEWAY = 502;
  this.STATUS_SERVICE_UNAVAILABLE = 503;
  this.STATUS_GATEWAY_TIMEOUT = 504;
  this.STATUS_HTTP_VERSION_NOT_SUPPORTED = 505;

  //TODO private property
  var factory = new this._factory();
  this._ajax = factory.ajax;
  this._ajax_assistant = new this._factory().ajax;
  this.version = factory.version;
  this.loadbytes = 0; //for progress procese
  this.totalbytes = -1; //for progress procese, -1 for unknown size
  this.opts = { };//options for ajax, and initilize from _opts first
  
  //extends use define parameters to this.opts
  this._opts = {
    //define default events handler
    onreadystatechange:this._onreadystatechange,
    oninit:this._oninit,
    onloading:this._onloading,
    onloaded:this._onloaded,
    oninteractive:this._oninteractive,
    oncomplete:this._oncomplete,
    onsuccess:this._onsuccess,
    onerror:this._onerror,
    onfailure:this._onfailure,
    onprogress:this._onprogress,
    oncountdown:this._oncountdown,
    onexpire:this._onexpire,
    //define default parameters
    url:"",
    timeout:9,
    method:"post", //define default method
    paras:"", //no default params
    stamp:true, //append stamp for each request, set true to avoid cache problems
    headers:[],
    //null - no request, false - requesting, true - request completed 
    _completed:null,
    _tickid:null,
    _tick:null
  };
  this._opts_events_empty = {
    onreadystatechange:function(){},
    oninit:function(){},
    onloading:function(){},
    onloaded:function(){},
    oninteractive:function(){},
    oncomplete:function(){},
    onsuccess:function(){},
    onerror:function(){},
    onfailure:function(){},
    onprogress:function(){},
    oncountdown:function(){},
    onexpire:function(){}
  };
  this._opts_events_null = {
    onreadystatechange:null,
    oninit:null,
    onloading:null,
    onloaded:null,
    oninteractive:null,
    oncomplete:null,
    onsuccess:null,
    onerror:null,
    onfailure:null,
    onprogress:null,
    oncountdown:null,
    onexpire:null
  };
  this._ext(this._opts,this.opts); //copy the default options
  this._ext(opts,this.opts);
  this._tick; //tick for oncountdown event, initialized while a new request provoke.
  this._tickid; //tick's timer handle
  //sentinal for oncountdown, initizlized white a new request provoke.
  //indicator for interactive process
  this.canInteractive = false; 

  //TODO define local varialbes
  //story this in context name _ct
  var _ct = this;

  //TODO software test area
  return this;

  //_ext test case
  this.opts.obj = {a:99};
  var opts_t = this._ext(this.opts,new Object());
  var s = "";
  for(var i in opts_t) s += i +"="+opts_t[i]+'\n';
  alert(s);
  opts_t.obj.a = null;
  alert(this.opts.obj.a);

};
ajax.prototype = {
  //public method
  connect:function(url,opts){
    //TODO connect implementation
    this._ext(opts, this.opts);
    this.opts.method = "connect";
    this.opts.url = url!=null? url:this.opts.url;
    this._proxy();
  },
  del:function(url,opts){
    //TODO delete implementation
    this._ext(opts, this.opts);
    this.opts.method = "delete";
    this.opts.url = url!=null? url:this.opts.url;
    this._proxy();
  },
  get:function(url,opts){
    //TODO get implementation
    this._ext(opts, this.opts);
    this.opts.method = "get";
    this.opts.url = url!=null? url:this.opts.url;
    this._proxy();
  },
  head:function(url,opts){
    //TODO get header information only
    //NOTE: a head method of http is implementate natively in window.XMLHttpRequest
    //      but Msxml2.XMLHTTP[.x.x] or MSXML2.DOMDocument. The good thing is that 
    //      window.XMLHttpRequest exists in IE7 plus, and the bed news is that XMLHttpRequest
    //      in IE is not support head method, enjoy it.
    this._ext(opts, this.opts);
    this.opts.method = "head";
    this.opts.url = url!=null? url:this.opts.url;
    this._proxy();
  },
  options:function(url,opts){
    //TODO options implementation
    this._ext(opts, this.opts);
    this.opts.method = "options";
    this.opts.url = url!=null? url:this.opts.url;
    this._proxy();
  },
  post:function(url,opts){
    //TODO post implementation
    this._ext(opts, this.opts);
    this.opts.method = "post";
    this.opts.url = url!=null? url:this.opts.url;
    this._proxy();
  },
  put:function(url,opts){
    //TODO put implementation
    this._ext(opts, this.opts);
    this.opts.method = "put";
    this.opts.url = url!=null? url:this.opts.url;
    this._proxy();
  },
  trace:function(url,opts){
    //TODO trace implementation
    this._ext(opts, this.opts);
    this.opts.method = "trace";
    this.opts.url = url!=null? url:this.opts.url;
    this._proxy();
  },
  json:function(url,callback){
    //TODO cross domain data receiver
    var js = this.script(url);
    //alert("loading json:"+js);
    var check = setTimeout(function(){
      if(ajax.json_data&&callback){
        var data = ajax.json_data;
        ajax.json_data = null;
        //alert("load json:"+callback);
        document.getElementsByTagName("head")[0].removeChild(js);
        callback.call(data);
      }else{
        check = setTimeout(arguments.callee,0);
      }
    },0);
  },
  style:function(url,isUnload,media){
    //TODO dynamic style sheet loader
    var loaded = false;
    var css = document.getElementsByTagName("link");
    dbg("style unload:"+isUnload+" "+css.length);
    for(var i=0;i<css.length;i++){
      dbg(css[i].href);
      if((css[i].type=="text/css"||/\.css$/.test(css[i].href))&&(css[i].href+"").indexOf(url)!=-1){
        if(isUnload) {
          dbg("style remive:"+url);
          document.getElementsByTagName("head")[0].removeChild(css[i]);
          return isUnload;
        }
        loaded=true;
        break;
      }
    }

    if(loaded==false&&isUnload!=true){
      var css = document.createElement('link');
      css.href = url;
      css.rel = 'stylesheet';
      css.type = 'text/css';
      css.media = media? media:"all";
      document.getElementsByTagName('HEAD')[0].appendChild(css);
    }
  },
  script:function(url){
    //TODO dynamic script loader
    var head = document.getElementsByTagName('head')[0];
    //var js = document.getElementsByTagName("script");
    var js = head.getElementsByTagName("script");
    for(var i=0; i<js.length; i++){
      //dbg("script url:"+js[i].src);
      if(js[i].src.indexOf(url)!=-1){
        return js[i];
        break;
      }
    }

    //if script specify in url unload, then load it
    var js = document.createElement("script"); //be carefull reuse of js
    js.src = url;
    js.type = "text/javascript";
    head.appendChild(js);
    return js;
    //先将文件载入并解析完毕后才执行
  },
  addHeader:function(name,content){
    //TODO modify header information
    var opts = this.opts;
    var headers = opts.headers;
    headers.push({name:name,content:content});
  },
  overHeader:function(name,content){
    //TODO modify header information
    var opts = this.opts;
    var headers = opts.headers;
    var news = [];
    for(var i=0,last=0; i<headers.length; i++){
      if(headers[i].name==name){
        news.concat(headers.slice(last,i));
        last = i;
      }
    }
    news.concat(headers.slice(last));
    news.push({name:name,content:content});
    opts.headers = news; //not headers = news;
  },
  
  //default event handlers
  _oncountdown:function(tick){
    dbg("_oncountdown:"+tick);
  },
  _onerror:function(err){
    dbg("_onerror:"+err.message);
  },
  _onexpire:function(){
    dbg("_onexpire:expire for request");
  },
  _onreadystatechange:function(o){
    //alert((arguments.callee+"")+":"+arguments.length);
    //dbg("_onreadystatechange:"+o.readyState);
    //var s = "";
    //for (var i in o) s +=i+"="+o[i];
    //alert(o);
  },
  _oninit:function(o){
    dbg("_oninit:"+o.readyState);
  },
  _onloading:function(o){
    dbg("_onloading:"+o.readyState);
  },
  _onloaded:function(o){
    dbg("_onloaded:"+o.readyState);
  },
  _oninteractive:function(o){
    //alert(arguments.callee+":"+arguments.length);
    //dbg("_oninteractive:"+o.readyState+" load/total:"+this.loadbytes+"/"+this.totalbytes);
  },
  _onprogress:function(load,total,o){
    //dbg("_oninteractive:"+o.readyState);
    dbg("_progress:"+load+"/"+total);
  },
  _oncomplete:function(o){
    dbg("_oncomplete:"+o.responseText.length);
  },
  _onsuccess:function(o){
    //TODO success
    dbg("_onsuccess:"+o.readyState);
  },
  _onfailure:function(o){
    //TODO failure
    dbg("_onfailure:"+o.readyState);
  },
    
  //private method
  //dispatcher for events delivery 
  _dispatcher:function(interval,opts,o){
    var ct = this;
    //dbg("_oninteractive:"+o.readyState+"=="+ct.READY_STATE_INTERACTIVE);
    if(opts.onreadystatechange) opts.onreadystatechange.call(ct,o);
    switch (o.readyState)
    {
    case ct.READY_STATE_UNINITIALIZED:
      if(opts.oninit) opts.oninit.call(ct,o);
      break;
    case ct.READY_STATE_LOADING:
      try{
        //dbg("dispatcher this.loadbytes = "+o.responseText.length);
        var loadbytes = o.responseText.length;
        ct.canInteractive = true;
      }catch(ex){
        dbg("no interactive:"+ex.message);
        ct.canInteractive = false;
      }
      if(opts.onloading) opts.onloading.call(ct,o);
      break;
    case ct.READY_STATE_LOADED:
      if(opts.onloaded) opts.onloaded.call(ct,o);
      break;
    case ct.READY_STATE_INTERACTIVE:
      var dt = new Date().getTime();
      var delay = dt - interval.time;
      //dbg("start:"+interval+"-"+new Date().getTime());
      if(delay>100){
        interval.time = dt;
        //dbg("_oninteractive:length="+o.responseText.length+" can interactive:"+ct.canInteractive);
        if(ct.canInteractive) this.loadbytes = o.responseText.length;
        if(opts.oninteractive) opts.oninteractive.call(ct,o);
        if(opts.onprogress&&ct.totalbytes!=-1) opts.onprogress.call(ct,ct.loadbytes,ct.totalbytes,o);
      }
      break;
    case ct.READY_STATE_COMPLETE:
      //ct.completed = true; //avoid associate setting, this to be change
      opts._completed = true;
      clearTimeout(opts._tickid);
      if(opts.oncomplete) opts.oncomplete.call(ct,o);
      if(o.status == ct.STATUS_OK && opts.onsuccess){
        opts.onsuccess.call(ct,o);
      }else if(opts.onfailure){
        opts.onfailure.call(ct,o)
      }
      break;
    }
  },
  //method to extends object
  _ext:function (os,od){
    /*
    //ok no matter about deep copy
    var a = [1,2];
    var b = new Array().concat(a);
    b[0] = 0;

    //a problem like this code demo, it need deep copy
    var c = [{n:1,m:2},{o:3,p:4}];
    var d = new Array().concat(c);
    d[0].n = 5;
    */
    for(var p in os){
      if(os[p] instanceof Array){
        od[p] = new Array().concat(os[p]);
      }else if(typeof os[p]=="object"){
        od[p] = new Object();
        this._ext(os[p],od[p]);
      }else{
        od[p] = os[p];
      }
    }
    return od;
  },
  _abort:function(){
    var _ajax = this._ajax;
    var opts = this.opts;
    _ajax.abort();
    if(opts.onabort) opts.onabort.call(this,_ajax);
  },
  _getSize:function(url){
    //TODO get header for file size information
    var ct = this;
    var opts = this._ext(this._opts,new Object());
    this._ext(this._opts_events_empty,opts);
    //if(opts.onreadystatechange)alert(opts.onreadystatechange);
    opts.method = "head";
    opts.url = url;
    opts.oncomplete = function(o){
      var length = o.getResponseHeader("Content-Length");
      var ctype = o.getResponseHeader("Content-Type");
      var status = o.status;
      dbg("_getSize:"+length+"bytes, "+ctype+", status "+status);
      switch(status){
        case ct.STATUS_OK:
        ct.totalbytes = length;
        break;
        case ct.STATUS_UNKNOW:
        ct.totalbytes = length;
        break;
        default:
        ct.totalbytes = -1;
      }
      //ct.totalbytes  = 6451;
    };
    this._assistant(this._ajax_assistant,opts);
  },
  //ajax factory method to generate core XMLHttp object
  _factory:function(){
    var version = "";
    if (window.XMLHttpRequest){
      var xd = new XMLHttpRequest();
      return {ajax:xd,version:"XMLHttpRequest"};
    } else if (window.ActiveXObject){
      var vers = new Array("Msxml2.XMLHTTP.6.0","MSXML2.DOMDocument.4.0","Microsoft.XMLHTTP");
      for (var i in vers){
        try {
          var xd = new ActiveXObject(vers[i]);
          version = vers[i];
          break;
        } catch (ex){
          throw("Error " + vers[i] + ": " + ex.message);
        }
      }
      if (xd == null){ 
        throw("无法构造MS平台的XMLHTTP。");
      }
      return {ajax:xd,version:version};
    } else {
      throw("无法构造XMLHttpRequest对象。");
      return null;
    }
  },
  //ajax proxy method to generate url and action
  _proxy:function (){
    var _ajax = this._ajax;
    var ct = this;
    var opts = this.opts;
    var url = opts.url;
    var method = opts.method;
    var headers = opts.headers;
    var paras = opts.paras;
    var stamp = opts.stamp;
    if(stamp){
      paras = (paras==""? "stamp=":paras+"&stamp=")+new Date().getTime();
    }
    if(method=="get"){
      url = paras==""? url:url.indexOf("?")!=-1? url+"&"+paras:url+"?"+paras;
      paras="";
    }
    //get header information
    this._getSize(url);

    if(ct.completed==false) this._abort();

    try{
      //注意open方法为_ajax的初始化方法，可能会将onreadystatechange置null，因此open之后设置很重要
      //_ajax.open("get","http://www.baidu.com?s=1",true); //跨域不可访问
      //prototype open("method","URL",async,"uname","pswd")
      _ajax.open(method,url,true);
      //append header information
      for(var i=0; i<headers.length; i++){
        //alert("head:"+headers[i].name);
        _ajax.setRequestHeader(headers[i].name, headers[i].content);
      }
      //An event handler for an event that fires at every state change
      var stamp = {time:new Date().getTime()-100};
      //dbg("begin:"+stamp+"-"+new Date().getTime());
      _ajax.onreadystatechange = function(){
        //dbg("before:"+stamp.time);
        ct._dispatcher.call(ct,stamp,opts,_ajax); //pass object for a returmn value
        //dbg("after:"+stamp.time);
       };
      //XHR binary charset opt by Marcus Granado 2006 [http://mgran.blogspot.com]   
      //this.req.overrideMimeType('text/plain; charset=x-user-defined'); 

      //open方法中的true，默认值启动了异步收发，到这里只要等待onreadystatechange事件。
      //如果是false，则是同步调用，即send方法要等到服务器回头数据后才返回。
      //打开对象后方可设置RequestHeader
      //_ajax.setRequestHeader("label","value"); //Adds a label/value pair to the http header to be sent 
      //var contentType="xml",content={xml:"application/xml",html:"text/html"};
      //_ajax.setRequestHeader('Content-Type',content[contentType]+';charset=GB2312');
      //_ajax.setRequestHeader ("CONTENT-TYPE", "multipart/form-data ");
      _ajax.setRequestHeader ("CONTENT-TYPE", "application/x-www-form-urlencoded"); //发送参数时要求设置
      if(paras) _ajax.setRequestHeader("Content-length", paras.length);//发送参数时要求设置
      _ajax.send(paras);//send("a=1&b=2..."); Sends the request, can cause Access to restricted URI denied
      //_ajax.abort(); //Cancels the current request
      opts._completed = false; //indicate a request start.
    }catch(e){
      _ajax.onreadystatechange = function(){}; //reset handler
      if(opts.onerror) opts.onerror.call(ct,e,_ajax);
    }

    //expires event manager
    if(opts._completed==false){
      opts._tick = opts.timeout;
      var start = new Date().getTime();
      //dbg("tick begin:"+opts._tick +" completed:"+opts._completed);
      opts._tickid = setTimeout(function(){
        opts._tick = opts.timeout-(new Date().getTime()-start)/1000;
        //dbg("tick:"+opts._tick +" completed:"+opts._completed);
        //dbg("countdown:"+opts._tick+(new Date().getTime()-start)/1000);
        if(opts._tick>0){
          if(opts._completed==false) opts._tickid = setTimeout(arguments.callee,10);
          if(opts.oncountdown) opts.oncountdown.call(ct,opts._tick,opts.timeout,_ajax);
        }else{
          _ajax.abort();
          if(opts.onexpire) opts.onexpire.call(ct,opts.timeout,_ajax);
        }
      },10);
    }
  },
  //assistant is _proxy like method that can specify ajax and opts
  _assistant:function(_ajax,opts){
    var ct = this;
    var url = opts.url;
    var method = opts.method;
    var headers = opts.headers;
    var paras = opts.paras;
    var stamp = opts.stamp;
    if(stamp){
      paras = (paras==""? "stamp=":"&stamp=")+new Date().getTime();
    }
    if(method=="get"){
      url = paras==""? url:url+"?"+paras;
      paras="";
    }

    //注意open方法为_ajax的初始化方法，可能会将onreadystatechange置null，因此open之后设置很重要
    //_ajax.open("get","http://www.baidu.com?s=1",true); //跨域不可访问
    //prototype open("method","URL",async,"uname","pswd")
    try{
      _ajax.open(method,url,true);
      //append header information
      for(var i=0; i<headers.length; i++){
        _ajax.setRequestHeader(headers[i].name, headers[i].content);
      }
      //An event handler for an event that fires at every state change
      var stamp = {time:new Date().getTime()-100};
      _ajax.onreadystatechange = function(){
        //if(opts.onreadystatechange) opts.onreadystatechange.call(ct,_ajax,opts);
        ct._dispatcher.call(ct,stamp,opts,_ajax); //pass object for a returmn value
      }

      //_ajax.setRequestHeader ("CONTENT-TYPE", "application/x-www-form-urlencoded"); //发送参数时要求设置
      if(paras) _ajax.setRequestHeader("Content-length", paras.length);//发送参数时要求设置
      _ajax.send(paras);//send("a=1&b=2..."); Sends the request
    }catch(e){
      if(opts.onerror) opts.onerror.call(ct,_ajax);
    }
  }
}


