//=============================================================================
// XML HTTP Request with Cache     Version 0.1
//
// Copyright (C) 2005           Masaki Sano,      All rights reserved.
//
//   This program is free software; you can redistribute it and/or modify
//   it under the terms of the GNU General Public License as published by
//    the Free Software Foundation; either version 2 of the License, or
//   (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with JSXMLRPCTiny; if not, write to the Free Software
//    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//=============================================================================


//=============================================================================
// CacheAlgorithmElement Object
//=============================================================================

// var el = new CacheAlgorithmElement(key, data, prev, next);
//    key : should not be neither null nor undefined
//
function CacheAlgorithmElement(k, d, p, n)
{
  this.key     = k;
  this.data    = d;          // {param1: val1, param2: val2, ...}
  this.prev    = p || null;
  this.next    = n || null;
}

CacheAlgorithmElement.prototype.toString = function() {
    var s = this.key + " : {";
    if(typeof this.data == typeof {})
      for(var a in this.data) s += a + ":" + this.data[a] + ", ";
    else 
      s += this.data;
    s += "}<"  + (this.prev ? this.prev.key : "null") 
      + " | " + (this.next ? this.next.key : "null") + ">  ";
    return s;
  };


//=============================================================================
// CacheAlgorithm Object
//=============================================================================

// var cache = new CacheAlgorithm(10);            // LRU
// var cache = new CacheAlgorithm(10, 'FIFO');    // FIFO
function CacheAlgorithm(s, t)
{
  if(t) this.type_  = t;

  this.size_  = s || 1;
  this.count_ = 0;
  this.begin_ = null;
  this.end_   = null;
  this.hash_  = {};  // a hash of CacheAlgorithmElement objects;
  //    key should not be neither null nor undefined

  this.nref_  = 0;
  this.nhit_  = 0;
}

//--------------------------------------------------------------------------
// Property
//--------------------------------------------------------------------------

// default property
CacheAlgorithm.prototype.type_ = 'LRU';  // default cache algorithm

// class property
CacheAlgorithm.DEBUG = function() {};
// [sample]
//CacheAlgorithm.DEBUG = function() {
//  for(var i = 0; i < arguments.length; ++i) alert(arguments[i]);
//};


CacheAlgorithm.pushFunc = {
  LRU  : function (cache, key, data) {
    if(! cache._raw_push_back(key, data)) return false;
      
      CacheAlgorithm.referFunc[cache.type_](cache, key);
      cache.popIfOverFlow();
        
      return true;
  },
  FIFO : function (cache, key, data) {
    if(! cache._raw_push_back(key, data)) return false;
      cache.popIfOverFlow();
        
      return true;
  }
};

// nref_ is not incremented in this function.
CacheAlgorithm.referFunc = {
  LRU  : function (cache, key) {
    var el = cache._delete(key);
    if(el) cache._raw_push_back(el.key, el.data);
    return cache._raw_refer(key);
  },
  FIFO : function (cache, key) {
    return cache._raw_refer(key);
  }
};

//--------------------------------------------------------------------------
// Method
//--------------------------------------------------------------------------

CacheAlgorithm.prototype.toString = function() {
  var s = 'type          : ' + this.type_ + "\n"
    + "count / size : " + this.count_ + " / " + this.size_ + "\n"
    + "begin        : " + (this.begin_ ? this.begin_.key : "null") + "\n"
    + "end          : " + (this.end_   ? this.end_.key   : "null") + "\n"
    + "hit / ref    : " + this.nhit_ + " / " + this.nref_ + "\n";
  s+= "hash         : {";
  for(var a in this.hash_) s += a + ", ";
  s+= "}\n";
  return s;
};

// var r = c.hitRate();
CacheAlgorithm.prototype.hitRate = function() {
  return this.nref_ > 0 ? this.nhit_ / this.nref_ : 0;
};

// c.type('LRU');
// var t = c.type();
CacheAlgorithm.prototype.type = function() {
  var t = arguments[0];
  if(t) this.type_ = t;
  return this.type_;
};


// var new_size = c.resize(5);
CacheAlgorithm.prototype.resize = function(size) {
  if(!isNaN(size)) 
    this.size_ = size;
  
  this.popIfOverFlow();
  CacheAlgorithm.DEBUG("resize --> " + this.size_ + "\n", this);
  return this.size_;
};

// c.hasKey('http://.../') ;
// 
CacheAlgorithm.prototype.hasKey = function(key) {
  return this.hash_[key] != null && this.hash_[key] != undefined;
};

CacheAlgorithm.prototype._raw_refer = function(key) {
  return this.hasKey(key) ? this.hash_[key] : null;
};

// var el = c._delete(key); 
CacheAlgorithm.prototype._delete = function(key) {
  var el = this._raw_refer(key);
  if(el) {
    delete this.hash_[key];
    
    var prev_obj = el.prev;
    var next_obj = el.next;
    prev_obj ? prev_obj.next = next_obj : this.begin_ = next_obj;
    next_obj ? next_obj.prev = prev_obj : this.end_   = prev_obj;

    el.prev = null;
    el.next = null;

    this.count_--;
  }
    
  return el;
};


// var bool_val = c._raw_push_back('http://.../', {content: '...', lastmod: date_obj});
// 
CacheAlgorithm.prototype._raw_push_back = function(key, data) {
  if(!key) return false;
  
  var el = new CacheAlgorithmElement(key, data);
  if(this.hasKey(key)) {
  // update
    this.hash_[key] = el;
  }
  else {
  // add
    this.hash_[key] = el;
    this.count_++;

    el.prev         = this.end_;
    el.next         = null;
    if(this.end_) // add not-first element
      this.end_.next  = el;
    else // add first element
      this.begin_     = el;
    this.end_ = el;
  }

  return true;
};

// var bool_val = c.push('http://.../', {content: '...', lastmod: date_obj});
// 
CacheAlgorithm.prototype.push = function(key, data) {
  var bool_val = CacheAlgorithm.pushFunc[this.type_](this, key, data);
  CacheAlgorithm.DEBUG("push("+key+", "+data+") ---> " + bool_val + "\n", this);

  return bool_val;
};

//var el = c._raw_pop();
CacheAlgorithm.prototype._raw_pop = function() {
  if(this.count_ < 1) return null;

  var first  = this.begin_;
  var second = first.next;
    
  first.prev  = null;
  first.next  = null;
  if(second)
    second.prev = null;
  else
    this.end_ = null;
  this.begin_ = second;

  delete this.hash_[first.key];
    
  this.count_--;
    
  return first;
};

// var element_array = this.popIfOverFlow();
CacheAlgorithm.prototype.popIfOverFlow = function() {
  var ar = new Array();
  while (this.count_ > this.size_ && this.count_ > 0) {
    ar.push(this._raw_pop());
  }

  CacheAlgorithm.DEBUG("popIfOverFlow() ---> ", ar);
  return ar;
};

CacheAlgorithm.prototype.pop = function() {
  var el = this._raw_pop();
  CacheAlgorithm.DEBUG("pop() ---> ", el, "\n", this);
  return el ? el.data : null;
};

// var data = c.refer('http://..../');
CacheAlgorithm.prototype.refer = function(key) {
  this.nref_++;
  var el = CacheAlgorithm.referFunc[this.type_](this, key);
  if(el) {
    this.nhit_++;
    CacheAlgorithm.DEBUG("hit("+key+") \n", this);
    return el.data;
  }
  else {
    CacheAlgorithm.DEBUG("not hit("+key+") \n", this);
    return null;
  }
};



CacheAlgorithm.UT = function() {
  CacheAlgorithm.DEBUG = function() {
    for(var i = 0; i < arguments.length; ++i) WScript.Echo(arguments[i]);
  };

  var f = function(c) {
    CacheAlgorithm.DEBUG(c);
    c.resize(3);
    c.push('key1', {param1:"hoge1",param2:"1990"});
    c.push('key2', {param1:"hoge2",param2:"1991"});
    c.push('key3', {param1:"hoge3",param2:"1992"});
    c.refer('key1');
    c.refer('key5');
    c.push('key4', {param1:"hoge4",param2:"1993"});
    c.resize(2);
    c.refer('key1');
    c.push('key5', {param1:"hoge4",param2:"1994"});
    c.pop();
    c.pop();
    c.pop();
    CacheAlgorithm.DEBUG("hit rate --> " + c.hitRate());
  };
  var cache = new CacheAlgorithm();
  f(cache);
  cache.type('FIFO');
  f(cache);
}

// [windows]
// cscript //nologo xmlhttprequestwithcache.js
//
//CacheAlgorithm.UT();


//=============================================================================
// XMLHttpRequestWithCache Object
//=============================================================================

// var creq = XMLHttpRequestWithCache(new XMLHttpRequest());
// 
// the following properties/methods are supported:
//   [ReadOnlyProperty]  readyState,responseText,responseXML,status,statusText,
//   [ReadWriteProperty] onreadystatechange, onload
//   [Method]    open, abort, send, setRequestHeader, getResponseHeader, 
//               getAllResponseHeaders
//
//  XMLHttpRequestWithCache is inspired by "XMLHttpRequest for IE" by ma.la.
//   URL     : http://la.ma.la/blog/diary_200509031529.htm
//   License : GPL(http://www.gnu.org/copyleft/gpl.html)
//
function XMLHttpRequestWithCache(xmlhttprequest)
{
  // only available in this constructor
  var __req = this;
  var properties 
    = ['readyState', 'responseText', 'responseXML', 'status', 'statusText'];


  this.readyState = 0;
  this.__request__ = xmlhttprequest;
  XMLHttpRequestWithCache.DEBUG("XMLHttpRequest : " + typeof this.__request__);
  this.current_url_ = '';
  this.current_method_ = '';

  this.onreadystatechange = function(){};
  this.onload             = function(){};
  this.__request__.onreadystatechange = function() {
    for(var i = 0; i < properties.length; ++i){
      try {
        __req[properties[i]] 
          = __req.__request__[properties[i]];
      }
      catch(err) {
      }
    }
    XMLHttpRequestWithCache.DEBUG("readyState : " + __req.readyState);

    if(__req.readyState==4 && __req.status==304 && __req.current_url_) { 
    // use cached content
      var d = XMLHttpRequestWithCache.cache.refer(__req.current_url_);
      if(d) { 
        if(d.dom)     __req.responseXML  = d.dom;
        if(d.content) {
          __req.responseText = d.content;
          __req.status       = 200;
          __req.statusText   = 'OK';
        }
        XMLHttpRequestWithCache.DEBUG("[use cached content]\n\n"
          + 'URL           : ' + __req.current_url_ + "\n"
          + 'Last-Modified : ' + d.last_modified + "\n"
          + "content       : \n" + d.content.substring(0,200) + "\n...\n\n");
      }
    }

    var ret = __req.onreadystatechange();
    if(__req.readyState == 4) { // complete
      __req.onload();

      if(__req.current_url_ && __req.__request__.status!=304) { 
      // cache data 
        var url       = __req.current_url_;
        var method    = __req.current_method_;
        var content_  = __req.responseText;
        var dom_      = null;
        try { dom_ = __req.responseXML.cloneNode(true); }
        catch(err) {}

        var lm        = __req.getResponseHeader("Last-Modified");
        if(method.toLowerCase() == 'get' && lm && content_) {
          XMLHttpRequestWithCache.cache.push(url, 
            {content:content_, dom:dom_, last_modified:lm});

          XMLHttpRequestWithCache.DEBUG("[cache] \n"
            + [url,lm,content_.substring(0,200)+"\n..."].join("\n\n"));
        }
      }

      __req.current_url_    = '';
      __req.current_method_ = '';
    }

    return ret;
  };
}

//--------------------------------------------------------------------------
// Class Property
//--------------------------------------------------------------------------
XMLHttpRequestWithCache.cache = new CacheAlgorithm(100);

//--------------------------------------------------------------------------
// Class Method
//--------------------------------------------------------------------------

// var new_size = XMLHttpRequestWithCache.resizeCache(5);
XMLHttpRequestWithCache.resizeCache
  = function(n) {return XMLHttpRequestWithCache.cache.resize(n);};
XMLHttpRequestWithCache.cacheAlgorithm
  = function(t) {return XMLHttpRequestWithCache.cache.type(t);};

// for debug
XMLHttpRequestWithCache.DEBUG = function() {};
//XMLHttpRequestWithCache.DEBUG = function() { alert(arguments[0]);};


//--------------------------------------------------------------------------
// Method
//--------------------------------------------------------------------------
XMLHttpRequestWithCache.prototype.setRequestHeader = function(l,v) {
  this.__request__.setRequestHeader(l,v);
  XMLHttpRequestWithCache.DEBUG("setRequestHeader(" + l + " => " + v + ")\n");
};

XMLHttpRequestWithCache.prototype.open = function() {
  var url    = arguments[1];
  var method = arguments[0];
  var d = XMLHttpRequestWithCache.cache.refer(url);
  if(!this.current_url_    && url)    this.current_url_    = url;
  if(!this.current_method_ && method) this.current_method_ = method;
  var lm = (d&&d.last_modified) ? d.last_modified:'01 Jan 1970 00:00:00 GMT';   
  var arg = [];
  for(var i = 0; i < arguments.length; ++i)
    arg[i] = 'arguments[' + i + ']';
  var open_str = 'var ret = this.__request__.open('+arg.join(', ')+');';
  //XMLHttpRequestWithCache.DEBUG("open_str : " + open_str + "\n");
  eval(open_str);
  if(arguments[0].toLowerCase() == 'get') 
    this.setRequestHeader("If-Modified-Since", lm);

  XMLHttpRequestWithCache.DEBUG("open() : " + method + "\n" + url + "\n");
  
  return ret;
};

XMLHttpRequestWithCache.prototype.abort = function() {
  this.current_url_    = '';
  this.current_method_ = '';
  return this.__request__.abort();
};

XMLHttpRequestWithCache.prototype.send = function(c) {
  XMLHttpRequestWithCache.DEBUG("send(" + c + ") : \n");
  return this.__request__.send(c);
};


XMLHttpRequestWithCache.prototype.getResponseHeader = function(h) {
  try { return this.__request__.getResponseHeader(h);}
  catch (err) {return null;}
};

XMLHttpRequestWithCache.prototype.getAllResponseHeaders = function() {
  return this.__request__.getAllResponseHeaders();
};


//=============================================================================
// Override Ajax.transport
//   required : prototype.js 1.3.1 or later
//=============================================================================

if(typeof Prototype != "undefined" && typeof Ajax != "undefined") {

// prototype.js has already been loaded.
  var orig_get_transport = Ajax.getTransport;
  XMLHttpRequestWithCache.DEBUG(orig_get_transport);
  Ajax.getTransport = function() {
    return new XMLHttpRequestWithCache(orig_get_transport.apply(Ajax, arguments));
  };
  XMLHttpRequestWithCache.DEBUG("Ajax.getTransport() : \n\n", Ajax.getTransport);
}

