/* STEPS TO USE
  
1 - enable html elements using the ajax-enabled class, all enabled elements require a unique id, ex <form id="myform" class="ajax-enabled">
  - you should only give this to one root element per active area, ie on the <form> element only, instead of all inputs, results and paging controls.
  - this is designed to allow for more than 1 active area on the page
  
2 - all enabled elements are given historyAdd and historyRead methods and an event listener for ajax:history-action
  
3 - To add a new entry into the history storage, call the elements historyAdd method.
  - ex. calling $('myform').historyAdd(); in the onComplete callback handler of an ajax call
  
4 - upon the user clicking the browser back/forward buttons, the AjaxHistory object fires an event to the appropriate element.
   - e.x Event.observe($('myform'), 'ajax:history-action', function(e){
            //do something
        });
   - the AjaxHistory object does not try to assume how to present the particular page state, so if additional ajax calls need to be made in order to fetch the apropriat econtent, they should should be invoked at this point.
   - the script does assume that it will be using the same method to invoke the ajax call, so blocks are automatically put in place to suppress a new history entry being written from a history driven event
  
*/

var AjaxHistory = {
  // find the elements which should apply and set up some basic parameters
  enabledElems:$$('.ajax-enabled'),
  
  //standard settings, these don't change through basic checks, build, etc
  debug:false,
  useHashEvent:('onhashchange' in window)? true : false,
  useSStorage:('sessionStorage' in window)? true : false,
  
  //settings that can be modified externally and within AjaxHistory
  alwaysStoreUrl:true,
  hashPfx:'ajx-set=',
  storagePfx:'ajx-fs-',
  hashAlert:true,
  historyDriven:false,
  loaded:false,
  built:false,
  
  build:function(){
    //set up dom element containers
    this.rootObj, this.historyContainer, this.ajaxIframe, this.ajaxForm, this.activeElem
    
    //create common properties
    this.updateCount = 0;
    this.lastC = 0;
    this.hashString = '';
    this.checkString = '';
    this.readHash = {};
    this.readHashString = '';
    this.setHash = {
      id:'',
      c:''
    };
    AjaxHistory.that = this;
    this.built = true;
  },
  
  //initialize the object
  load:function(){
    //create storage mechanisms - createHistoryMicro generates the html objects, setBehaviours interacts with them and does the event handling
    if(this.loaded==false){
      this.createHistoryMicro();
      this.multiElem = (this.enabledElems[1]) ? true : false;
      this.setBehaviours();
      
      //check to see if the process should fire initially on page load
      this.checkLoadedHash();
      this.loaded = true;
    }
  },
  
  // fire if hash is set on load
  preCheckLoadedHash:function(){
    var precheckloadDate = new Date();
    if(location.hash.indexOf(this.hashPfx)!=-1){
      //alert('yes!');
      var tmpHashString = new String(location.hash);
      var preHashString = tmpHashString.substr(1,tmpHashString.length).replace(this.hashPfx, '');
      var preTmpHash = modules.sanitizeResponse(preHashString, {});
      
      for(props in preTmpHash){
        try{
          if(('params' in preTmpHash[props]) && (preTmpHash[props].params!='')){
            //alert(props)
            Event.fire($(props), 'ajax:history-load-action');
          }
        }
        catch(e){}
      }
    }
  },
  
  
  checkLoadedHash:function(){
    if(location.hash.indexOf(this.hashPfx)!=-1){
      this.allReadEvent();
    }
  },
  
  // define the history microformat
  createHistoryMicro:function(){
      this.rootObj = $$('body')[0];
      
      Element.insert(this.rootObj, '<div class="history-container" style="display:none;" ></div>' );
      this.historyContainer = $$('.history-container')[0];
      
      Element.insert(this.historyContainer, '<form class="history-form" action="blank.html" id="ajax-history-form" target="ajax-history-frame" style="display:none;" ></form>' );
      this.ajaxForm = $('ajax-history-form');
      
      /* if(!this.useHashEvent){
        Element.insert(this.historyContainer, '<iframe class="history-iframe" name="ajax-history-frame" id="ajax-history-frame" src="/ajax-history/blank.html"></iframe>' );
        this.ajaxIframe = $('ajax-history-frame');
      } */
  },
  
  // register the data, methods and events for the enabled elements
  setBehaviours:function(){
    this.enabledElems.each(function(elem){
        
        //populate the global storage object with an entry for this element
        if(!$(this.storagePfx + elem.id)){
          Element.insert(this.ajaxForm, '<fieldset id="' + this.storagePfx + elem.id + '"></fieldset>');
        }
        elem.storageSet = $(this.storagePfx + elem.id);
        elem.allStorage = this.createStorageEntry(elem, true);
        elem.baseStorage = elem.allStorage[0].value;
        elem.currentStorage = elem.allStorage[1].value;
        elem.historyDriven = false;
        elem.setHash = {};
        elem.readHash = {};
        
        elem.historyAdd = function(opts){
          this.historyAdd(elem, opts);
        }.bind(this)
        
        elem.historyRead = function(){
          this.historyRead(elem);
        }.bind(this)
        
        elem.getHashAsJSON = function(){
          var returnElemJson = this.getHashAsJSON(elem);
          return returnElemJson;
        }.bind(this)
        
    }.bind(this));
    
    if(this.loaded==true){
    }
    else if(this.useHashEvent==true && this.loaded==false){
      
      Event.observe(window,'hashchange', function(e){
          if(this.hashAlert == true){
            this.historyDriven = true;
            this.timedCall(this, 'readEntries', 100);
          }
          this.hashAlert=true;
      }.bind(this));
    }
    else if (this.useHashEvent==false && this.loaded==false){
      window.setInterval(function(){
            AjaxHistory.that.readEntries();
      }, 150);
    }
  },
  
  resetBehaviours:function(){
    Event.stopObserving(window, 'hashchange');
  },
  
  historyAdd:function(elem, opts){
    if(this.historyDriven != true && elem.historyDriven != true){
      
      this.activeElem = elem;
      this.setHash.id = elem.id;
      this.getParameters(opts);
      if('params' in elem.setHash){
        this.setParameters();
      }
      this.buildHashString();
      this.setHashEntries();
    }
    elem.historyDriven = false;
    this.historyDriven = false;
  },
  
  historyRead:function(elem){
    this.historyDriven = true;
    switch(elem.nodeName){
    case 'FORM':
      var tmpElemParams = elem.readHash.params.split('&');
    
      elem.currentStorage = elem.baseStorage;
      
      for(x=tmpElemParams.length-1; x>=0; x--){
        var tmpElemParamsKey = tmpElemParams[x].substring(0, tmpElemParams[x].indexOf('='));
        var tmpElemParamsValue = tmpElemParams[x].substring(tmpElemParams[x].indexOf('=')+1, tmpElemParams[x].length);
        regTest = new RegExp('('+tmpElemParamsKey+'=)([^&]*)(&)');
        elem.currentStorage = elem.currentStorage.replace(regTest, '$1'+tmpElemParamsValue+'$3');
      }
      
      elem.readHash.params = elem.currentStorage;
      
      break;
    default:
      elem.readHash.params = ('params' in elem.readHash) ? elem.readHash.params : '';
      break;
    }
    
    Event.fire(elem, 'ajax:history-action'); //observers can now be written anywhere
  },
  
  //Setting all the storage - building placemarks, form inputs, hash entries
  
  // create a new permanent entry for the hash and return a reference to that object
  createStorageEntry:function(elem , check){
    var useElem = (this.activeElem!=null) ? this.activeElem : elem;
    if((!Element.select(elem.storageSet, '.storage')[0] && check==true) || check==false){
      
      var switchVal;
      switch(useElem.nodeName){
      case 'FORM':
        switchVal = new String(Form.serialize(useElem));
        break;
      case 'A':
        switchVal = useElem.href;
        break;
      default:
        switchVal = '';
        break;
      }
      
      switchVal = unescape(switchVal);
      storeVal = switchVal.replace(/&amp;/, '&');
      
      Element.insert(elem.storageSet, '<textarea class="baseline storage" title="' + this.updateCount + '" >' + storeVal + '</textarea>');
      Element.insert(elem.storageSet, '<textarea class="storage" title="' + this.updateCount + '" >' + storeVal + '</textarea>');
    }
    
    return Element.select(elem.storageSet, '.storage');
  },
  
  //gets the parameters required for storage
  getParameters:function(obj){
    this.enabledElems.each(function(elem){
        if(this.activeElem.id == elem.id){
          switch(elem.nodeName){
          case 'FORM':
            if(this.alwaysStoreUrl == true){
              elem.setHash.url = ((obj) && ('url' in obj))? obj.url : new String(elem.action);
              elem.setHash.url = unescape(elem.setHash.url);
            }
            else if((obj) && ('url' in obj)){
              elem.setHash.url = obj.url;
              elem.setHash.url = unescape(elem.setHash.url);
            }
            elem.setHash.params = ((obj) && ('params' in obj)) ? obj.params : new String(Form.serialize(elem));
            break;
          default:
            if((obj) && ('params' in obj)){
              elem.setHash.params = obj.params;
            }
            break;
          }
        }
        else {
          switch(elem.nodeName){
          case 'FORM':
            elem.setHash.params = ('params' in elem.setHash) ? elem.setHash.params : new String(Form.serialize(elem));
            break;
          default:
            elem.setHash.params = '';
            break;
          }
        }
        
        elem.setHash.params = unescape(elem.setHash.params);
        if(elem.setHash.params.indexOf('&amp;')!=-1){
          elem.setHash.params = elem.setHash.params.replace(/&amp;/gi, '&');
        }
        if(elem.setHash.params.indexOf('&#34;')!=-1){
          elem.setHash.params = elem.setHash.params.replace(/&#34;/gi, '"');
        }
    }.bind(this));
    this.setHash.c = this.updateCount;
    this.updateCount++;
  },
  
  // set the hashes for current locations and microformat history entry
  setParameters:function(){
    
    this.enabledElems.each(function(elem){
        if('params' in elem.setHash){
          elem.baseArray = elem.baseStorage.split('&');
          elem.chkArray = elem.setHash.params.split('&');
          
          elem.setHash.params = '';
          for(x=elem.baseArray.length; x>=0; x--){
            if(elem.chkArray[x]!=elem.baseArray[x] && typeof(elem.chkArray[x])!='undefined' && elem.chkArray[x]!=''){
              elem.setHash.params += elem.chkArray[x] + '&';
            }
          }
          elem.setHash.params = elem.setHash.params.substr(0, elem.setHash.params.length-1);
          elem.setHash.params = unescape(elem.setHash.params);
        }
    });
  },
  
  buildHashString:function(){
    this.hashString = this.hashPfx + '{ ';
    this.enabledElems.each(function(elem){
        this.hashString += '"' + elem.id + '" : {'
        for(props in elem.setHash){
          this.hashString += '"' + props + '" : "' + encodeURI(elem.setHash[props])+ '", ';
        }
        this.hashString = this.hashString.substr(0, this.hashString.length-2);
        this.hashString += ' }, ';
    }.bind(this));
    for(props in this.setHash){
      this.hashString += '"' + props + '" : "' + encodeURI(this.setHash[props])+ '", ';
    }
    this.hashString = this.hashString.substr(0, this.hashString.length-2);
    this.hashString += ' }';
  },
  
  // set the values created in AjaxHistory.setPlaceMark into the location.hash
  setHashEntries:function(){
    this.hashAlert = false;
    
    var newHashString = new String(this.hashString);
    this.checkString = newHashString;
    
    location.hash = newHashString;
    this.checkString = location.hash;
  },
  
  // Reading the entries and generating custom events for the appropriate elements to act upon
  // called every time there's a change to hash - if the values don't match, call AjaxHistory.fireReadEvent
  readEntries:function(){
    
    this.readHash = this.readPlaceMark();
    
    if((location.hash!='') && (typeof(this.activeElem) == 'undefined')){
      
      //debugAlert('first check');
      
      this.activeElem = $(this.readHash.id);
      this.setHash.id = this.activeElem.id;
      this.fireReadEvent();
    }
    else if((location.hash!='') && (location.hash.indexOf('"id" : "'+this.activeElem.id)==-1)){
      
      //debugAlert('second check');
      
      this.activeElem = $(this.readHash.id);
      this.setHash.id = this.activeElem.id;
      
      this.hashAlert = false;
      var hashDir = (this.readHash.c < this.lastC) ? -1 : 1; 
      history.go(hashDir);
      
    }
    else if(this.checkString == '' || location.hash == ''){
      this.fireReadEvent();
    }
    else if(location.hash != this.checkString){
      this.fireReadEvent();
    }
    else if((location.hash == this.checkString) && (this.historyDriven == true)){
      this.fireReadEvent();
    }
    this.lastC = this.readHash.c;
  },
  
  // called when the location and stored hashes don't match
  fireReadEvent:function(){
    this.readHash = this.readPlaceMark();
    this.enabledElems.each(function(elem){
        if(elem.id == this.readHash.id){
          for(props in this.readHash){
            if(elem.id == props){
              elem.readHash = this.readHash[props]
            }
            else if(props == 'tmpParams'){
              elem.readHash = this.readHash[props]
            }
          }
          elem.readHash.params = ('params' in elem.readHash) ? elem.readHash.params : '';
          this.historyRead(elem);
        }
    }.bind(this));
  },
  
  allReadEvent:function(){
    this.readHash = this.readPlaceMark();
    
    for(props in this.readHash){
      try{
        if(($(props)) && ('params' in this.readHash[props]) && (this.readHash[props].params!='')){
          var tmpElem = $(props);
          tmpElem.readHash.params = this.readHash[props].params;
          tmpElem.historyDriven = true;
          this.historyRead(tmpElem);
         }
       }catch(e){}
      }
  },
  
  // Utils for helping out with the logic
  
  // read the hash, try to parse and return it
  readPlaceMark:function(){
    try{
      var hashString = this.returnHash();
      
      if(hashString){
        return modules.sanitizeResponse(hashString, {});
      }
      else if('id' in this.activeElem){
        //last active elem
        
        var tmpMark = {
          id:this.activeElem.id,
          tmpParams:{
            params:''
          }
        }
        return tmpMark;
      }
      else{
        return false;
      }
    }
    catch(e){
     //alert(e);
     return false;
    }
  },
  
  //descide what directives to send
  getHashAsJSON:function(elem){
    if(location.hash!='' || 'id' in elem.readHash){
      
      var readHashString = '{ ';
    
      for(props in elem.readHash){
        readHashString += '"' + props + '" : "' + encodeURI(elem.readHash[props])+ '", ';
      }
      readHashString = readHashString.substr(0, readHashString.length-2);
      readHashString += ' }';
      
      var returnJson = modules.sanitizeResponse(readHashString, {});
      
      return returnJson;
    }
    else{
      return false;
    }
  },
  
  // return the most recent available location.hash object as JSON compatibale string
  returnHash:function(){
    try{
      var strReturnHash = new String(location.hash);
      var sendHash = strReturnHash.substr(1,strReturnHash.length).replace(this.hashPfx, '');
      return sendHash
    }catch(e){
      return false;
    }
  },
  
  timedCall:function(bd, fn, ms){
    var t = ms;
    var d = new Date();
    var pD = Date.parse(d);
    var eD = pD + t;
    var r = new Date();
    var pR = Date.parse(r);
    
    while(eD > pR){
      r=null;
      pR=null;
      r = new Date();
      pR = Date.parse(r);
    }
    if(typeof(bd[fn])=='function'){
      try{
        bd[fn]().bind(bd);
      }
      catch(e){}
    }
  },
  
  debugAlert:function(step){
   var debug = step + '\n';
    for(props in this){
      if(typeof(this[props])!='function'){
      debug += ' '+props+' : '+this[props]+' \n'; 
      }
    }
    alert(debug); 
  }
  
}

history.navigationMode = 'compatible';

Event.observe(window, 'dom:loaded', function(){
    AjaxHistory.build();
    AjaxHistory.preCheckLoadedHash();
}.bind(AjaxHistory));

Event.observe(window, 'load', function(e){
    if(AjaxHistory.built == false){
      AjaxHistory.build();
      AjaxHistory.preCheckLoadedHash();
    }
    AjaxHistory.enabledElems = $$('.ajax-enabled');
    if(AjaxHistory.enabledElems[0]){
      AjaxHistory.load();
    }
    else{
      delete AjaxHistory.enabledElems;
    }
}.bind(AjaxHistory));

Event.observe(window, 'unload', function(e){
    AjaxHistory.loaded == false;
    //prevents scripts from not firing after using back/forward buttons.   
});

