
// ----------------------------------------------------
//  Add new methods to String/Element etc...
// ----------------------------------------------------

/** 
 * Convenient method used to generate namespace 
 * for a given string/separator (http://thinkweb2.com/projects/prototype/namespacing-made-easy/))
 *
 * Usage:
 * 'foo.bar.baz.quux'.namespace({
 *   say: function(){ alert('Hello World !');) }
 * }); 
 *
 * @param separator the namespace separator (default is '.')
 * @return An object tree for the given namespace
 */
String.prototype.namespace = function(source) {
  Object.extend(this.split('.').inject(window, function(parent, child) {
    return (parent[child] = parent[child] || { });
  }), source || { });
}

String.prototype.fastStrip = function() {
  var str = this.replace(/^\s\s*/, ''),
       ws = /\s/,
       i  = str.length;
  while (ws.test(str.charAt(--i)));
  return str.slice(0, i + 1);
}

Array.prototype.addAll = function() {
    for (var a = 0;  a < arguments.length;  a++) {
        var arr = arguments[a];
        for (var i = 0;  i < arr.length;  i++) {
            this.push(arr[i]);
        }
    }
}

Object.extend(Element, (function() {

  function _fastMatch(elm, tags, className) {
    if (tags && tags.indexOf(elm.tagName) == -1) {
      return false;
    }
    if (className && (!elm.className || elm.className.indexOf(className) < 0)) {
      return false;
    }
    return elm;
  }
  
  return {
    _fastMatch: _fastMatch
  }
})());

Element.addMethods({

  /**
   * Retrieves the ID of the JCMS Data bound to this element by looking
   * in this element's classname, using syntax "ID_{dataId}".
   */
  getJcmsId : function(element) {
    var idClass = $w(element.className).find(function(elm){ 
      return elm.startsWith('ID_');
    });
    return idClass && idClass.substring(3);
  },
  
  /**
   * Retrieves the ID of the JCMS Data bound to this element by looking
   * in this element's classname, using syntax "ID_{dataId}".
   * @param pfx the prefix (optional default is ID_)
   */
  getJcmsIds : function(element, pfx) {
    var pfx = pfx ? pfx : "ID_";
    var len = pfx.length;
    var ids = $w(element.className).findAll(function(str){ 
      return str.startsWith(pfx);
    }).collect(function(str){
      return str.substring(len);
    });
    return ids;
  },
  
  /**
   * Retrieves the ID of the JCMS Data bound to this element by looking
   * in this element's classname or parent elements, using syntax "ID_{dataId}".
   */
  findJcmsId: function(element){
    
    var jcmsId = element.getJcmsId();
    if (jcmsId){
      return jcmsId;
    }
    
    var parentNode = element.parentNode;
    return parentNode && $(parentNode).findJcmsId();
  },
  
  /**
   * Looks for an element matching the specified type (and optionnal classname)
   * in the DOM parent hierarchy of this element.
   *
   * @param tagName the tag type to look for, eg. 'A', 'UL', 'TABLE', ...
   * can be an array of tags e.g ['A', 'UL']
   * @param classname the classname the element must match in order to be returned
   * @param checkThis a boolean indicating whether to include this element in the search
   * @param max the maximum number of node to ride up to find a matching node, default is 100
   * @return the element matching tagName and className or undefined if no element
   *         could be found.
   */
  fastUp : function(element, tagName, className, checkThis, max) {
    var tags = (!tagName || Object.isArray(tagName)) ? tagName : [ tagName ];
    max = max || 100;
    for (var elm = checkThis ? element : element.parentNode; elm; elm = elm.parentNode) {
      if (max <= 0) { return; } max--;
      if (Element._fastMatch(elm, tags, className)){ return $(elm); }
    }
  },
  
  fastNext: function(element, tagName, className, checkThis, max) {
    var tags = (!tagName || Object.isArray(tagName)) ? tagName : [ tagName ];
    max = max || 100;
    for (var elm = checkThis ? element : element.nextSibling; elm; elm = elm.nextSibling) {
      if (elm.nodeType != 1){ continue; }
      if (max <= 0) { return; } max--;
      if (Element._fastMatch(elm, tags, className)){ return $(elm); }
    }
  },
  
  fastPrevious: function(element, tagName, className, checkThis, max) {
    var tags = (!tagName || Object.isArray(tagName)) ? tagName : [ tagName ];
    max = max || 100;
    for (var elm = checkThis ? element : element.previousSibling; elm; elm = elm.previousSibling) {
      if (elm.nodeType != 1){ continue; }
      if (max <= 0) { return; } max--;
      if (Element._fastMatch(elm, tags, className)){ return $(elm); }
    }
  },
  
  fastChild: function(element, tagName, className, count) {
    var tags = (!tagName || Object.isArray(tagName)) ? tagName : [ tagName ];
    var count = count || 0;    
    for (var child = element.firstChild; child; child = child.nextSibling){
      if (child.nodeType != 1){ continue; }
      if (Element._fastMatch(child, tags, className)){ count--; }
      if (count < 0){ return $(child); }
    }
  },
  
  fastEach: function(element, tagName, className, func) {
    var tags = (!tagName || Object.isArray(tagName)) ? tagName : [ tagName ];
    var idx = 0;
    var result = $A([]);
    for (var child = element.firstChild; child; child = child.nextSibling){
      if (child.nodeType != 1){ continue; }
      if (Element._fastMatch(child, tags, className)){ 
        result.push(func($(child),++idx));
      }
    }
    return result;
  },
  
  // Inspired by JQuery. Works on all browsers
  fastVisible : function(element) {
    return element.offsetWidth > 0 || element.offsetHeight > 0;
  }
});


// ----------------------------------------------------
//  AJAX COMMON JSON-OBJECT PACKAGE
// ----------------------------------------------------

/**
 * Usage:
 * -----
 * 
 * // Init Json Request
 * var jcmsRequest = new JcmsAjaxRequest();
 * 
 * // Init RPC with jcmsRequest callback
 * var funcRPC = function(){
 *   // Do stuff  
 *   jcmsRequest.asyncJsonCallBack(value);
 * 
 *   // Or call JSON method
 *   JcmsJsContext.getJsonRPC().callRPCMethod(jcmsRequest.asyncJsonCallBack(value).bind(jcmsRequest), param1, param2);
 * }
 * 
 * // Init CallBack
 * var funcCallBack = function(returnValue, returnEffect){ 
 *   // Do stuff
 * }
 * 
 * // Init Effect (Optional)
 * var functEffect = function(){
 *   // Do Stuff
 *   jcmsRequest.asyncEffectCallBack(effect);
 * }
 * 
 * jcmsRequest.rpc      = funcRPC;
 * jcmsRequest.callback = funcCallBack;
 * jcmsRequest.effect   = functEffect;
 * jcmsRequest.asyncJsonCall();
 * 
 * Note: 
 * - jcmsRequest.exception = funcException;   // Handle custom rpc exception.
 * - jcmsRequest.waitState = false;           // Do not display wait state
 * - jcmsRequest.asyncJsonCallPeriodical(10); // Periodical RPC
 * - jcmsRequest.timeout = 10000;              // Request timeout
 * - jcmsRequest.quiet = true;                // Will not display alerts on error. Default is false.
 *  */

JcmsAjaxRequest = Class.create();
JcmsAjaxRequest.prototype = {
  initialize: function(elm) {
    this.elm         = elm;
    this.effect      = null;
    this.exception   = null;
    this.callback    = null;
    this.rpc         = null;
    this._periodexec = null; 
    this.waitState   = true; // Display wait state
    this.timeout     = 20000;
    this.quiet       = false;
    
    this.isOk        = false;
    this.isDone      = false;
    this.isUpdate    = false;
    this.isEffect    = false;
    this.isTimeout   = false;
  },
  
  asyncJsonCall: function(){

     // Init request status
     this.isOk       = false;
     this.isDone     = false;
     this.isUpdate   = false;
     this.isEffect   = false;
     this.isTimeout  = false;
    
     // Set browser to waiting mode
     if (this.waitState){
       Ajax.setWaitState(true,this.elm);
     }
     
     try{
       // Call Effect
       if (this.effect)
         this.effect();
       else
         this.isEffect = true;
     
       // Call Timeout
       if (this._timeoutFunc){
         clearTimeout(this._timeoutFunc);
       }
       this._timeoutFunc = setTimeout(function(){  
         this.isTimeout = true;
         this._asyncResponseCallBack();
       }.bind(this),this.timeout);
     
       // Call Json
       if (this.rpc)
         this.rpc();
     }
     catch(ex){
       this._handleException();
    }
  },
  
  asyncJsonCallPeriodical: function(frequency){
    // Todo: Should check if PeriodicalExecuter is running twice ?
    var func = function(){
      if (this.isDone)
        return;
      this.asyncJsonCall.bind(this).defer();
    };
    
    this._periodexec = new PeriodicalExecuter(func.bind(this), frequency);
  },
    
  stopPeriodical: function(){
    if (this._periodexec){
      this._periodexec.stop();
    }
    this.dispose();
  },
  
  /**
   * Must be Called by given function JSON
   * @param value the return value
   */
  asyncJsonCallBack: function(value, exception){
    if (value){ 
      this.returnValue = value;
    }
    else{
      this.isOk = true;
    }

    // Handle server exception
    if (exception){
      this.returnException = exception;
    }
    
    // Response is done
    this.isDone = true;
    
    // Call response callback
    this._asyncResponseCallBack();
    
    if (this._periodexec){
      // Reset response for PeriodicalUpdater
      this.isDone = false;
    }
  },
  
  /**
   * Must be Called by given function Effect
   * @param effect the running effect
   */
  asyncEffectCallBack: function(effect){
    // Effect is finished
    this.isEffect      = true;
    this.workingEffect = effect;
    
    // Call response callback
    this._asyncResponseCallBack();
  },
  
  /**
   * Called by both Effect or JSON to do the
   * real callbaack
   */
  _asyncResponseCallBack: function(){
    
    // Handle Timeout
    if (this.isTimeout){
      if (!this._timeoutFunc){
        return;
      }

      this._handleException();
      return;
    }
    
    
    if (!this.isDone)
      return;
      
    if (this.isUpdate)
      return;
      
    if (!this.isEffect)
      return;
      
    // Handle Exception
    if (this.returnException){
      JcmsLogger.error('JcmsAjaxRequest', this.returnException.message, ' Error: ['+this.returnException.code+']', 
                                          this.returnException.name , '\n'+this.returnException.javaStack);
      this._handleException();
      return;
    }
      
      
    if (this._timeoutFunc)
      clearTimeout(this._timeoutFunc);  
      
    this.isUpdate = true;
    
    // Call callback
    try {
      if (this.callback){
        this.callback(this.returnValue, this.workingEffect);
      }
    }
    catch (ex){ 
      /* Trap CallBack error to clean jcmsRequest */ 
      JcmsLogger.error('JcmsAjaxRequest',ex);
    }
    
    this._disposeResponse();
  },
  
  _disposeResponse: function(){
    // Remove browser from waiting mode
    if (this.waitState){
      Ajax.setWaitState(false,this.elm);
    }
    
    // Clean stuff
    if (!this._periodexec){
      this.dispose();
    }
  },
  
  _handleException: function(){
    if (!this.quiet){
      alert(I18N.glp('warn.json.sessiontimeout'));
    }
    if (this.exception){
      this.exception(this.returnException);
    }
    this._disposeResponse();
  },
  
  /**
   * Clean pointer to reduce memory leaks
   * and helps Garbage Collector
   */
  dispose: function(){
    this.elm         = null;
    this.effect      = null;
    this.exception   = null;
    this.callback    = null;
    this.rpc         = null;
    this._timeoutFunc= null;
    this._periodexec = null; 
    this.timeout     = 20000;
    this.quiet       = false;
  }
};

JcmsJsonRequest = Class.create();
Object.extend(JcmsJsonRequest.prototype, JcmsAjaxRequest.prototype);


// ----------------------------------------------------
//  AJAX COMMON PACKAGE
// ----------------------------------------------------

if (!window.Ajax) {
  var Ajax = new Object();
}

Object.extend(Ajax,{
    // ----------------------------
    //  Common usefull AJAX Method
    // ----------------------------
    
    setWaitState: function(wait, elm){
      
      var wnd  = parent ? parent : window;
      var doc  = document;
      try { doc  = (parent && parent.document) ? parent.document : doc; } catch(e){ /* Security catch */ }
      
      var body = doc.getElementsByTagName('body')[0];
      if (!body){ return; }
      
      if (!Ajax.waitDiv){
        Ajax.waitDiv = doc.createElement('DIV');
        Ajax.waitDiv.innerHTML     = I18N.glp('info.msg.loading');
        Ajax.waitDiv.className     = 'ajaxwait';
        Ajax.waitDiv.style.display = 'none';
        body.appendChild(Ajax.waitDiv);
      }
      
      // Set vars
      var wndstatus ='';
      var wndcursor ='';
      
      if (wait){
        wndcursor = 'wait';
        wndstatus = I18N.glp('info.msg.loading');
        Ajax.waitDiv.style.display = 'block';
      }
      else{ // Reset cursor
        Ajax.waitDiv.style.display = 'none';
      }
      
      // Set Window status
      try { if (wnd.status){ wnd.status=wndstatus; }} catch(e){ /* Security catch */ }
      
      // Set cursor
      if (elm){ elm.style.cursor = wndcursor; }
      body.style.cursor = wndcursor;
    },
    
    /**
     * Convenient method building a JcmsAjaxRequest to call a given url
     * @param uri the url to call (or url with params that will be splited)
     * @param params the url parameters
     * @param callback the callback to call at the end of the request
     */
    performAjaxRequest: function(uri, params, callback){
      
      var url    = uri;
      var pos    = url.indexOf('?');
      
      if (pos >= 0){
        uri  = url.substr(0,pos);
        params = url.substr(pos+1);
      }
      
      // Init Json Request
      var jcmsRequest = new JcmsAjaxRequest();
      
      // Init RPC with jcmsRequest callback
      var funcRPC = function(){
        new Ajax.Request(uri, {
          parameters:  params,
          onComplete:  jcmsRequest.asyncJsonCallBack.bind(jcmsRequest),
          onException: jcmsRequest._handleException.bind(jcmsRequest),
          onFailure:   jcmsRequest._handleException.bind(jcmsRequest)
        });
      }
      
      // Init CallBack
      var funcCallBack = callback || function(returnValue, returnEffect){  /* nothing */  }
  
      jcmsRequest.rpc      = funcRPC;
      jcmsRequest.callback = funcCallBack;
      jcmsRequest.asyncJsonCall();
    },
    
  _styleSheetsAdded : $H(),
  _javaScriptsAdded : $H(),

  /**
   * Load a StyleSheet file from its relative path.
   */
  loadStyleSheet : function (path) {
    if (Ajax._styleSheetsAdded.get(path) === true) {
      JcmsLogger.debug('Ajax', 'StyleSheet already imported: ', path);
      return;
    }
    JcmsLogger.info('Ajax', 'Import StyleSheet', path);
    Ajax.markStyleSheetLoaded(path);
    
    var headID = document.getElementsByTagName("head")[0];         
    var cssNode = document.createElement('link');
    cssNode.type = 'text/css';
    cssNode.rel = 'stylesheet';
    cssNode.media = 'screen';
    cssNode.href = path;
    headID.appendChild(cssNode);
    
    if (Prototype.Browser.IE) {
      var deferedFunc = function() {
        $(document.body).toggleClassName("fixIERenderingBugOnDynamicCssLoad");
      };
      deferedFunc.defer();
    }
  },
  
  /**
   * Indicates that the specified StyleSheet file has been loaded.
   */  
  markStyleSheetLoaded : function (path) {
    JcmsLogger.debug('Ajax', 'Mark StyleSheet loaded: ', path);
    Ajax._styleSheetsAdded.set(path, true);
  },
  
  /**
   * Indicates that the specified StyleSheets files have been loaded.
   */  
  markStyleSheetsLoaded : function () {
    $A(arguments).each(function(path) { Ajax.markStyleSheetLoaded(path); } );
  },
  
  /**
   * Load a javascript file from its relative path.
   */
  loadJavaScript : function (path) {
    if (Ajax._javaScriptsAdded.get(path) === true) {
      JcmsLogger.debug('Ajax', 'JavaScript already imported: ', path);
      return;
    }
    JcmsLogger.info('Ajax', 'Import JavaScript', path);
    Ajax.markJavaScriptLoaded(path);
    
    var headID = document.getElementsByTagName("head")[0];
    var newScript = document.createElement('script');
    newScript.type = 'text/javascript';
    newScript.src = path;
    headID.appendChild(newScript);
  },
  
  /**
   * Indicates that the specified JavaScript file has been loaded.
   */  
  markJavaScriptLoaded : function (path) {
    JcmsLogger.debug('Ajax', 'Mark JavaScript loaded: ', path);
    Ajax._javaScriptsAdded.set(path, true);
  },
  
  /**
   * Indicates that the specified JavaScript files have been loaded.
   */  
  markJavaScriptsLoaded : function () {
    $A(arguments).each(function(path) { Ajax.markJavaScriptLoaded(path); } );
  }
  
});

/**
 * Register AJAX Responder to handle Prototype AJAX Error Requests
 */
Ajax.Responders.register({
  onException: function(transport, ex) {
    alert(I18N.glp('warn.json.sessiontimeout'));
  }
});


// ----------------------------------------------------
//  INPUT UTIL
// ----------------------------------------------------

if (!window.InputUtil) {
  var InputUtil = new Object();
}

Object.extend(InputUtil,{
  
  /**
   * Focus the given Input and set the caret at the end of 
   * the input text (to be compliant between IE and FF)
   * @param input the input
   */
  focus: function(input){
    if (input.type && input.type.toLowerCase() == 'file')
      return;
    
    if (input.focus){
      try { input.focus(); } catch(e){}
    }
    
    if (input.value){
      input.value = input.value;
    }
  },
  
  blur: function(input){
    if (input.type && input.type.toLowerCase() == 'file')
      return;
    
    if (input.blur){
      try { input.blur(); } catch(e){}
    }
  },
  
  /**
   * Return an object reprensenting a cross-browser selection
   * @param input the working input
   * @param trim indicate to trim or not selection
   */
  getSelection: function(input, trim){
    var inputSelection = new Object();
    inputSelection.input = input;
    // Gecko browser
    if (input.setSelectionRange) {
      inputSelection.start      = input.selectionStart;
      inputSelection.end        = input.selectionEnd;
      inputSelection.value      = input.value.substring(input.selectionStart, input.selectionEnd);
      inputSelection.gecko      = true;
      inputSelection.scrolltop  = input.scrollTop;
      inputSelection.scrollleft = input.scrollLeft;
    }
    // Internet Explorer
    else if (input.createTextRange) {
      if (! input.ownerDocument) { input.ownerDocument = document; }
      var selection = input.ownerDocument.selection.createRange();
      
      if ( selection.parentElement().tagName!="TEXTAREA" ) {
        inputSelection.start = 0;
        inputSelection.end = 0;
      }
      // Insert a text to find caret position
      else if (selection.text.length == 0) {
        var backup = input.value ;
        var bookmark = "~JCMSwiki~";
        selection.text = bookmark ;
        var index = input.value.search( bookmark );
        input.value = backup ;
        index -= input.value.substr(0,index).split('\n').length - 1;
        
        inputSelection.value = "";
        InputUtil._setSelection(inputSelection,index,index);
        
        // Update selection
        inputSelection.start = index;
        inputSelection.end   = index;
      } 
      // Check selected text
      else {
        var bookmark       = selection.getBookmark();
        var range          = input.createTextRange();
        var selectionRange = input.createTextRange();
        var fulltext       = range.text;
          
        // Select and retrieve selected text
        selectionRange.moveToBookmark(bookmark);
        var text           = selectionRange.text;
          
        // Retrieve text before selection
        range.setEndPoint('EndToStart', selectionRange);
        var start          = range.text.length;
        
        // Find the real location of text
        var index          = fulltext.indexOf(text,start);
         
        // Find the end location of text
        var end            = index + text.length;
          
        // Count \n in text before index
        var startLines     = fulltext.substring(0,index).split('\n').length - 1;
         
        // Count \n in text before ends of selection
        var endLines       = startLines + text.split('\n').length - 1;
          
        // Update start and end postion
        start  = index - startLines;
        end    = end   - endLines;
        
        // Backup selection
        inputSelection.start = start;
        inputSelection.end   = end;
        inputSelection.value = selectionRange.text;
        
        // Reset selection
        InputUtil._setSelection(inputSelection,start,end);
      }
    }
    // Logger
    if (JcmsLogger.isDebug && JcmsLogger.InputUtil){
      JcmsLogger.debug('InputUtil',"getSelection: "+inputSelection.start+","+inputSelection.end+": "+inputSelection.value+"("+inputSelection.scrolltop+","+inputSelection.scrollleft+")");
    }
    return trim ? InputUtil._trimSelection(inputSelection) : inputSelection;
  },
  
  /**
   * Replace the current selection by the given string.
   * If called by internal method the third parameter is selection (avoid to compute twice)
   * @param input the working input
   * @param replace the text to replace
   * @param trim indicate to trim or not selection
   * @param selection the object return by getSelection() set by internal method call
   */
  replaceSelection: function(input, replace, trim, selection){
    input.focus(); 
    var selection  = selection || InputUtil.getSelection(input,trim);
    var inputValue = input.value;
    if (selection.gecko){
      input.value = inputValue.substring(0, selection.start) + replace + inputValue.substring(selection.end);
      var oldLength = selection.value ? selection.value.length : 0;
      InputUtil._setSelection(selection, selection.start, selection.end + (replace.length-oldLength));
    }
    else{ 
      var range       = input.ownerDocument.selection.createRange();
      var isCollapsed = (range.text.length == 0);
      range.text = replace;
      
      if (!isCollapsed){ // Make a better selection
        var rlen = replace.length - (replace.split('\n').length - 1);
        range.moveStart('character', -rlen);
        range.select();
      }
    }
  },
  
  /**
   * Check the current selection if it match "match" then
   * replace it by "replace" otherwise replace it by "backward"
   * @param input the working input
   * @param match a regular expression to check
   * @param backward the txt to replace if regexp doesn't match (work on .+)
   * @param replace the txt to replace if regexp match
   * @param trim  true to trim selection
   * @param caret true to work even if there is no selection (use backward without $1)
   * @param selection the object return by getSelection() set by internal method call
   */
  replaceRegexp: function(input, match, replace, backward, trim, caret, selection){
    
    var selection  = selection || InputUtil.getSelection(input,trim);
    
    
    var selectText = selection.value;
    if (!selectText && !caret) return;
    
    var replaceText = selectText;
    if (!selectText){
      replaceText = backward.replace(/\$1/g,'');
    }
    else if (selectText.match(match)){ 
      replaceText = selectText.replace(match,replace);
    }
    else {
      replaceText = selectText.replace(/\s*([\S ]+)(\s*)/g,backward+"$2");
    }
    
    if (replaceText == selectText) return;
    InputUtil.replaceSelection(input, replaceText, trim, selection);
  },
  
  /**
   * Selects the selection from start to end
   * @param selection the object return by getSelection() set by internal method call
   * @param start the new selection start point
   * @param end the new selection end point
   */
  _setSelection: function(selection, start, end){
    selection.input.focus();
    
    if (selection.gecko){
      // Set caret
      selection.input.setSelectionRange(start, start);
      
      // Fix Scrollbar
      if (selection.scrollleft){
        selection.input.scrollLeft = selection.scrollleft;
      }
      if (selection.scrolltop){
        selection.input.scrollTop = selection.scrolltop;
      }
      else{// Fake event 'esc' key to set scroll
        Element.fire(selection.input,'keypress'); /* Fix for Chrome */
      }
      
      selection.input.setSelectionRange(start, end);
    }
    else if (selection.input.createTextRange){
      var range   = selection.input.createTextRange();
      var text    = selection.input.value;
      range.move('character', start);
      range.moveEnd('character', end-start);
      range.select();
    }

    var substart = start-selection.start; 
    var subend   = selection.value.length - (selection.end-end);
    selection.value = selection.value.substring(substart, subend);
    selection.start = start;
    selection.end   = end;
  },
  
  /**
   * Trims white space in selection 
   * and adjsute to new size 
   * @param selection the object return by getSelection() set by internal method call
   */
  _trimSelection: function(selection){
    if (selection.start >= selection.end){
      return selection;
    }
    // Trim spaces
    var start = 0;
    var end   = 0;
    while (selection.value.charAt(start) == " ") { start++; }
    while (selection.value.charAt(selection.value.length-end-1) == " ") { end++; }
    
    if ((start ==0) && (end ==0)){
      return selection;
    }
    
    // Adjuste new selection area
    InputUtil._setSelection(selection, selection.start+start, selection.end-end);
    return selection;
  }
});


// ----------------------------------------------------
//  STYLESHEET UTIL
// ----------------------------------------------------

/** 
 * Notes:
 * - Do not put @import
 * - Do not put expr, expr, expr{}
 */

'JCMS.util.StyleSheet'.namespace({

  _ss : false,
  
  _getStyleSheet: function(){
    var ss = JCMS.util.StyleSheet._ss;
    if (ss){ return ss; }
    
    if (document.createStyleSheet){ // IE
      JCMS.util.StyleSheet._ss = document.createStyleSheet();
    } 
    else {
      var head  = document.getElementsByTagName('head')[0];
      var style = document.createElement('style');
      style.type = 'text/css';
      head.appendChild(style);
      JCMS.util.StyleSheet._ss = style.sheet;
    }
    return JCMS.util.StyleSheet._ss;
  },

  _addRule: function(expr, rule){ /* alert('_addRule:'+expr+' , '+rule); */
    var ss = JCMS.util.StyleSheet._getStyleSheet();
  
    if (ss.addRule){ // IE
      ss.addRule(expr, rule);
    } else { 
      ss.insertRule(expr + '{' +  rule  + '}',ss.cssRules.length);
    }
  },
  
  putRule: function(expr, rule){
  
    var ss    = JCMS.util.StyleSheet._getStyleSheet();
    var rules = $A(ss.rules || ss.cssRules);
    var match = rules.find(function(ru, idx){
      if (ru.selectorText != expr){ return false; }
      
      // Remove
      if (!rule){
        if (ss.deleteRule) { ss.deleteRule(idx); }
        else { ss.removeRule(idx); }
        return true;
      }
      
      // IE
      if (ru.style.cssText){ ru.style.cssText = rule; } 
      
      // CSS2
      else { ru.cssText = rule; }
      
      return true;
    });
    
    if (!match && rule){
      JCMS.util.StyleSheet._addRule(expr, rule);
    }
  },
  
  removeRule: function(expr){
    JCMS.util.StyleSheet.putRule(expr, false);
  }
});


// ----------------------------------------------------
//  FORM UTIL
// ----------------------------------------------------

if (!window.FormUtil) {
  var FormUtil = new Object();
}

Object.extend(FormUtil,{

  // disable or enable all the input of the given array, by finding them in the
  // given form name according to the boolean
  toggleInputs: function(formName, inputsArray, enable) {
    $A(inputsArray).each(function(str, idx) {
      Form.getInputs(formName, false, str).each(function(element) {
        element.disabled = !enable;
      });
    });
  },
  
  // Sets some values of text input in the given form, 
  // using a map of key/value (key being the input name, value
  // the new value for the input)
  setInputValues: function(formName, inputToValueMap) {
    Form.getInputs(formName, 'text', false).each(function(element) {
      var newValue = inputToValueMap[element.name];
      if (newValue != undefined) {
        element.value = newValue;
      }
    });
  },
  
  // Cause maxLength is not available on textarea, emule the behaviour
  imposeMaxLength: function(element, maxLength) {
    if (element.value.length < maxLength) {
      return true;
    } else if (element.value.length > maxLength) {
      element.value = '';
      return false; 
    } else {
      element.value = element.value.substr(0, maxLength-1); 
      return false;
    }
  },
  
  clearFields: function(event){ 
    var elm = Event.element(event);
    if (!elm){ return; }
    elm.previousSiblings ().findAll(function(item){ return item.clear; }).invoke('clear');
  },
  
  getCheckedValues: function(formName , widgetName){
    var form   = $(document.forms[formName]);
    var inputs = form.getInputs('checkbox', widgetName).concat(form.getInputs('radio', widgetName));
        inputs = inputs.findAll(function(elm) { return elm.checked; });
         
    return inputs.pluck('value').compact();
  }
});

// ----------------------------------------------------
//  JCMSPREFS OBJECT
// ----------------------------------------------------

if (!window.JcmsPrefs) {
  var JcmsPrefs = new Object();
}


Object.extend(JcmsPrefs,{
  
  items: null,
  
  put: function(key,value){
    JcmsPrefs._init();
    JcmsPrefs.items[key] = value;
    JcmsPrefs._store();
  },
  
  get: function(key,defaultValue){
    JcmsPrefs._init();
    if (JcmsPrefs.items[key] != undefined){
      return JcmsPrefs.items[key];
    }
    return defaultValue;
  },
  
  _init: function(){
    
    // Escape if nothing
    if (JcmsPrefs.items){
      return;
    }
    
    // Init cookie value
    var cookieString  = Cookie.get('JcmsPrefs');
    if (!cookieString){
      JcmsPrefs.items = new Object();
      return;
    }
    
    // Unmarshal JSON Value
    eval("JcmsPrefs.items = "+cookieString);
    
  },
  
  _store: function(){
    if (!JcmsPrefs.items){
      return;
    }
    
    var cookieString = toJSON(JcmsPrefs.items); //alert(cookieString);
    Cookie.write([{
      name: 'JcmsPrefs', 
      value: cookieString,
      expires: new Date((new Date()).getTime() + (1000 * 60 * 60 * 24 * 30)), // 30 days
      path: '/',
      domain: ''
    }]);
  }
});

// ----------------------------------------------------
//  UTIL OBJECT
// ----------------------------------------------------

// Util 'static Class'
if (!window.Util) {
  var Util = new Object();
}

Object.extend(Util,{
  
  /**
   * Check if the current window is in an iframe or not.
   */
  isInIFrame: function() {
    return window != top; // DO NOT use "!==" as it would not work in IE
  },
  
  /**
   * Fix IE rendering bug
   * Warning slow IE6 ! Use instead CSS (position: relative; zoom:1)
   */
  shakeIE: function(){
    if (Prototype.Browser.IE) {
      $(document.body).toggleClassName("fixIERenderingBug");
      JcmsLogger.debug('ShakeIE','Warning slow IE6 ! Use instead CSS (position: relative; zoom:1)');
    }
  },

  /**
   * Convert the given object in to a boolean value, or use
   * default value if object cannot be converted
   */
  toBoolean: function(object, defaultValue) {
    if (typeof object == 'boolean') { return object; }
    if (object == 'false' || object == 'no' ) { return false; }
    if (object == 'true'  || object == 'yes') { return true;  }
    return defaultValue;
  },

  /**
   * Remove all child nodes under the given element.
   * Current implementation call: removeChild() recursively.
   * 
   * @param elm the root element to work with
   * @param deep should this method called recursively
   * @param filter String optional classname of element branch to remove
   */
  cleanDOMElements: function(elm, deep, filter,_status){
    var removed = $A([]);
    for (var child = elm.firstChild; child; child = child.nextSibling){
      if (child.nodeType != 1){ continue; }
      
      var remove = _status || !filter || child.className.indexOf(filter) >= 0;
      if (deep){ Util.cleanDOMElements(child,deep,filter, remove); }
      
      child.removeAttribute('id');
      
      if (remove){ 
        child.removeAttribute('onclick');
        removed.push(child);
      }
    }
    removed.each(function(child, idx){ elm.removeChild(child); });
  },
  
  /**
   * Convenient wrapper that returns true if it is a left click (or IE)
   * @param event Mouse Event
   */
  isLeftClick: function(event){
    return JcmsJsContext.isIE || Event.isLeftClick(event);
  },
  
  /**
   * This is much faster than using (el.innerHTML = str) when there are many
   * existing descendants, because in some browsers, innerHTML spends much longer
   * removing existing elements than it does creating new ones.
   * http://ajaxian.com/archives/replacehtml-for-when-innerhtml-dogs-you-down
   */
  replaceHtml : function(el, html) {
    var oldEl = (typeof el === "string" ? document.getElementById(el) : el);
    var newEl = document.createElement(oldEl.nodeName);

    // Preserve the element's id and class (other properties are lost)
    newEl.id = oldEl.id;
    newEl.className = oldEl.className;
    // Replace the old with the new
    newEl.innerHTML = html;
    oldEl.parentNode.replaceChild(newEl, oldEl);
    /* Since we just removed the old element from the DOM, return a reference
    to the new element, which can be used to restore variable references. */
    return newEl;
  },
  

  /**
   * Retrieves "position" and "dimension" of the given window's
   * viewport (or the current window if no window argument is given).
   * 
   * returns an object containing the following value :
   *   obj.x      = viewport X position in the screen
   *   obj.y      = viewport Y position in the screen
   *   obj.width  = viewport width
   *   obj.height = viewport height
   * 
   * Warning: the position returned in internet explorer is the position
   * of the window not the viewport (i.e. the viewport position being the
   * position of the window + title and toolbars offset)
   * 
   * cf. http://www.quirksmode.org/viewport/compatibility.html
   */
  getViewportBounds: function(win) {
    var vpWidth = 0; 
    var vpHeight = 0;
    var vpXposInScreen = 0;
    var vpYposInScreen = 0;
    
    if (!win) {
      win = self;
    }
    var doc = win.document;
    
    // 1. Viewport Position

    // all but mozilla
    if (win.screenTop){
      vpXposInScreen = win.screenLeft;
      vpYposInScreen = win.screenTop;
    }
    // mozilla
    else if (win.screenX){
      vpXposInScreen = win.screenX;
      vpYposInScreen = win.screenY;
    }
    
    // 2. Viewport Dimension
    
    //   all except Explorer
    if (win.innerHeight) {
      vpWidth = win.innerWidth;
      vpHeight = win.innerHeight;
    }
    //   Explorer 6 Strict Mode
    else if (doc.documentElement && win.document.documentElement.clientHeight) {
      vpWidth = doc.documentElement.clientWidth;
      vpHeight = doc.documentElement.clientHeight;
    }
    //   other Explorers
    else if (document.body) {
      vpWidth = doc.body.clientWidth;
      vpHeight = doc.body.clientHeight;
    }
    
    return { x: vpXposInScreen,
             y: vpYposInScreen,
             width: vpWidth,
             height: vpHeight };
  },
  
  /**
   * Resize the Iframe to the size of it's own content according to document scroll height.
   * It should be sometimes defer (eg Util.resizeIframeToViewport.defer(iframe);)
   * 
   * @param iframe (element or id)
   */
  resizeIframeToViewport: function(iframe){
    iframe = $(iframe);
    if (!iframe){
      return;
    }
    var height = 400;
    try {
      if       (iframe.contentDocument ){ doc = iframe.contentDocument; } // For NS6
      else if (iframe.contentWindow){    doc = iframe.contentWindow.document; } // For IE5.5 and IE6 // ERROR HERE 
      else if (iframe.document ){        doc = iframe.document; } // For IE5
      else { return; }
    
      height =  doc.body.scrollHeight; // doc.documentElement.scrollHeight
      
      Event.observe(iframe.contentWindow, "unload", function(){ 
        Util.resizeIframeToViewport.delay(2,this);
      }.bindAsEventListener(iframe));
    }
    catch(ex){
      JcmsLogger.error('Util', 'resizeIframeToViewport', ex);
    }
    iframe.style.height = Math.max(150, height) + "px";
  },
  
  _initIframe : function(event){
    
    if (event && event.memo){ // FIXME: Working on given wrapper
      var wrapper = $(event.memo.wrapper);
      if (!wrapper){ return; }
      wrapper.select('IFRAME[height=100%]').each(function(elm, idx){
        Util.resizeIframeToViewport.defer(elm);
      })
      return;
    }
    
    var t0 = new Date().getTime();
    
    $$('IFRAME[height=100%]').each(function(elm, idx){
      Util.resizeIframeToViewport.defer(elm);
    });
    
    var t1 = new Date().getTime();
    JcmsLogger.info('InitIFrame', 'Init IFrame', ' in '+(t1-t0)+' ms');
  },
  
  
  /**
   * Observe event (click) on document then call callback. This method delegate on Prototype EventObserver.
   * The callback MUST be bindAsEventListener.
   *
   * @param eventName the Event name
   * @param callback the function MUST be bindAsEventListener
   */
  observeDocument: function(eventName, callback){
    if (Prototype.Browser.IE || Prototype.Browser.Chrome){
      Event.observe(document, eventName , callback); // InternetExplorer or Google Chrome
    } else {
      Event.observe(window,   eventName , callback); // FireFox
    }
  },

  observeFocus: function(focusInHandler, focusOutHandler){
    if (document.addEventListener){
      document.addEventListener("focus", focusInHandler, true);
      document.addEventListener("blur", focusOutHandler, true);
    } else {
      document.observe("focusin", focusInHandler);
      document.observe("focusout", focusOutHandler);
    }
  },

  _classToCallBack : $H(), // classname => function
  _tagToCallBack   : $H(), // tagname   => function
  _focusToCallBack : $H(), // classname   => function
  
  _onLoadCB: function() {
    var method = Util._onClickCB.bindAsEventListener(this);
    Util.observeDocument('click', method);
    Util.observeDocument('jcms:click', method);
    
    var methodfIn  = Util._onFocusCB.bindAsEventListener(this,'focus:in');
    var methodfOut = Util._onFocusCB.bindAsEventListener(this,'focus:out');
    Util.observeFocus(methodfIn, methodfOut);
  },
  
   _onFocusCB: function(event, eventName) {
     var elm = Event.element(event);
     if (!elm || !elm.fastUp) { return; }
     
     var elm = elm.fastUp(['INPUT', 'TEXTAREA', 'SELECT'], null, true, 2);
     if (!elm) { return; }
     
     if (elm.type && elm.type == 'hidden') { return; }
     
     // Call Init
     if(!elm._init){
       elm._init = true;
       document.fire('jcms:init-focus', { elmId: elm.identify() });
     }
     
     // Call CallBack bind to each classname
     var classNames = $w(elm.className);
     classNames.each(function(className,idx) {
       var cb = Util._focusToCallBack.get(className);
       if (Object.isFunction(cb)) {
         cb(event, elm, className, eventName);
       }
     }.bind(this));
   },
  
  _onClickCB: function(event) {
    if (!(event.eventName === 'jcms:click' || Util.isLeftClick(event))){
      return;
    }
    var elm = Event.element(event);
    if (!elm || !elm.fastUp) { return; }
    var elm = elm.fastUp(['A', 'BUTTON', 'INPUT', 'TEXTAREA', 'SELECT'], null, true, 2);
    if (!elm) { return; }
    
    // Call Init
    if(!elm._init){
      elm._init = true;
      document.fire('jcms:init', { elmId: elm.identify() });
    }
    
    // Call CallBack bind to each classname
    var classNames = $w(elm.className);
    classNames.each(function(className,idx) {
      var cb = Util._classToCallBack.get(className);
      if (Object.isFunction(cb)) {
        cb(event, elm, className);
      }
    }.bind(this));
  },
  
  /**
   * Register a new listener to be invoked on a click event happening on
   * A, BUTTON, INPUT, TEXTAREA, SELECT element having the specified classname.
   * The event listener callback will be invoked with the following parameters : 
   * 1 event
   * 2 the element on which the mathing classname was found
   * 3 the matching classname
   */
  observeClass: function(className, callback) {
    Util._classToCallBack.set(className, callback);
  },
  
  observeFocusClass: function(className, callback) {
    Util._focusToCallBack.set(className, callback);
  }
  
});

Event.observe(window,   "load",           Util._onLoadCB);
Event.observe(window,   "load",           function(){ Util._initIframe.defer(); }, false);
Event.observe(document, "refresh:after",  Util._initIframe    , false);

// ----------------------------------------------------
//  NOTIFIER
// ----------------------------------------------------

var Notifier = Class.create({
    
    _events: [[document, 'mousemove'], [document, 'keydown']],
    _timer: null,
    _idleTime: null,
    active: false,
    
    /**
     * Constructor of Notifier
     * 
     * @param time The idle time to wait
     * @param eventName the name of the event to throw (eg xxx:idle and xxx:active)
     * @param active true to fire active event (sometimes is not usefull)
     * @param className fire event only on element with given classname (this implementation do not fastUp())
     */
    initialize: function(time, eventName, active, className) {
      this.time = time;
      this.eventName = eventName || 'state';
      this.active = active || false;
      this.className = className;
        
      this.initObservers();
      this.setTimer();
    },
    
    initObservers: function() {
      this._events.each(function(e) {
          Event.observe(e[0], e[1], this.onInterrupt.bindAsEventListener(this));
      }.bind(this));
    },
    
    onInterrupt: function(event) {
      var target = Event.element(event);
      var eX     = Event.pointerX(event);
      var eY     = Event.pointerY(event);
      
      if (this.active){
        if (this._matchClassName(target)){
          document.fire(this.eventName+':active', { idleTime: new Date() - this._idleTime, target: target, eX: eX, eY: eY });
        }
      }
      this.setTimer(target, eX, eY);
    },
    
    setTimer: function(target, eX, eY) {
      
      clearTimeout(this._timer);
      
      if (!this._matchClassName(target)){
        return;
      }
      
      this._idleTime = new Date();
      this._timer = setTimeout(function() {
          document.fire(this.eventName+':idle', { target: target, eX: eX, eY: eY});
      }.bind(this), this.time)
    },
    
    _matchClassName: function(target){
      return !this.className || 
              !target || 
              (target.className && target.className.indexOf(this.className) >= 0);
    }
})

// ----------------------------------------------------
//  LOGGER
// ----------------------------------------------------

var JcmsLogger = {
  
  // Debug levels used by info(), debug(), warn()
  LEVEL_INFO:  "info",
  LEVEL_DEBUG: "debug",
  LEVEL_WARN:  "warn",
  LEVEL_ERROR: "error",
  
  // Debug status for each scope
  isDebug: true && window.console && window.console["debug"],
  
  // Component debug status
  Ajax:                 false,
  CtxMenuManager:       false,
  CtxMenu:              false,
  CtxMenuTrace:         false,
  WikiToolbar:          false,
  InputUtil:            false,
  Autochooser:          false,
  JcmsJsContext:        false,
  Table:                false,
  TinyMCE_JcmsPlugin:   false,
  TinyMCE_JcmsPluginCB: false,
  DocChooser:           false,
  TreeCat:              false,
  Modal:                false,
  JcmsAjaxRequest:      false,
  AjaxRefresh:          false,
  DHTMLHistory:         false,
  ShakeIE:              true,
  
  // --------------------
  //  INTERNAL
  // --------------------
  
  /**
   * A generic function to log message in Firebug console
   * http://www.joehewitt.com/software/firebug/docs.php
   * 
   * @param level the message level (default is DEBUG)
   */
  _log: function(level,args){
    
    var scope = args[0];
    var msg   = args[1];
    
    if (!window.console && document.URL.indexOf('debug=true') > 0  && level == JcmsLogger.LEVEL_INFO){ alert($A(args).inspect()); }
    
    if (!JcmsLogger._checkScope(level, scope, msg))
      return;
    
    // Default on DEBUG level
    level = (level == undefined) ? JcmsLogger.LEVEL_DEBUG : level;    
    
    // The message to log
    args[1]  = "["+level+"]["+scope+"] "+msg;
    args = $A(args).slice(1,args.length);
    
    // The function call
    if (window.console && window.console[level])
      window.console[level].apply(window.console,args);
  },
  _checkScope: function(level, scope, msg){
    // General check scope
    if ((!msg) || !JcmsLogger.isDebug)
      return false;
    
    // Default on DEBUG level
    level = (level == undefined) ? JcmsLogger.LEVEL_DEBUG : level;
    
    // Check scope
    if ((!scope) || (!JcmsLogger[scope] && level==JcmsLogger.LEVEL_DEBUG))
      return false;
      
    return true;
  },
  
  // --------------------
  //  FUNCTIONS
  // --------------------

  info: function(scope, msg){
    JcmsLogger._log(JcmsLogger.LEVEL_INFO,arguments);
  },
  debug: function(scope, msg){
    JcmsLogger._log(JcmsLogger.LEVEL_DEBUG,arguments);
  },
  warn: function(scope, msg){
    JcmsLogger._log(JcmsLogger.LEVEL_WARN,arguments);
  },
  error: function(scope, msg){
    JcmsLogger._log(JcmsLogger.LEVEL_ERROR,arguments);
  }
};



// ----------------------------------------------------
//  POPUP OBJECT
//  see also: 
//    - http://www.quirksmode.org/js/popup.html
//    - http://www.w3schools.com/htmldom/met_win_open.asp
//  Note: Title must not contains spaces !
// ----------------------------------------------------

var Popup = {
  encode: function(parameter){
    return encodeURIComponent(parameter);
  },
  
  callback: function(){ /* Warning ugly hack CnP in calendarPopup.jsp */
  
    if (!window.opener){
      return false;
    }
    
    if (window.opener.callback){
      window.opener.callback(arguments);
      return true;
    }
    
    if (!window.opener.callbackLinkId){
      return false;
    }
    
    window.opener.document.fire("popup:callback", { 
      linkId: window.opener.callbackLinkId,
      args: arguments
    });
    
    return true;
  },
  
  popupEvent: function(scope){
    var args = $A(arguments); args.shift();
    return Popup.popupWindow.apply(scope,args);
  },
  
  popupWindow: function(url, title, w, h, status, resizable, scrollbars, reuse, winOpener, callback){ 
    
    if (!status) status="no";
    if (!w) w=320;
    if (!h) h=260;
   
    resizable = "resizable="   + (Util.toBoolean(resizable,  true) ? "yes" : "no");
    scrollbars = "scrollbars=" + (Util.toBoolean(scrollbars, true) ? "yes" : "no");
   
    if (reuse == undefined) { 
      reuse = true; 
    }
   
    if (!navigator.jalios) {
      navigator.jalios = new Object();
    }
    
    var pWnd = navigator.jalios.popupWindow; // shorter convenient var
    
    // Set window opener
    if (!winOpener) {
      winOpener = window;
    }
    
    // With IE, title must not contains special characters and must not be longer than 48 characters... 
    if (title) {
      title = title.replace(new RegExp('[\s\:\/\.\-]+', 'img'), '_');
      title = title.substr(0, 48);
    }
    
    // Set a title
    if (!title && !winOpener.opener) {
      title = '_blank';
    }
    
    // Update title
    else if (!reuse && pWnd){
      navigator.jalios.popupCounter = navigator.jalios.popupCounter ? navigator.jalios.popupCounter+1 : 1;
      title = title + "_"+ navigator.jalios.popupCounter;
    }
    
    // close previous popup if needed
    if (reuse && pWnd && pWnd.close) {
      pWnd.close();
    }
    
    // Check popup blocker
    try {
      var params = 'status=' + status + ',width=' + w+ ',height=' + h + ',menubar=no,'+ resizable + ',' + scrollbars;
      navigator.jalios.popupWindow = winOpener.open(url, title, params);

      JcmsLogger.debug('Popup','popupWindow',url,params);

      pWnd = navigator.jalios.popupWindow;
      if (!pWnd){
        alert(I18N.glp('warn.popup.blocker'));
      }
    }
    catch(ex){
      alert(I18N.glp('warn.popup.blocker'));
    }
    
    // Set a callback (Wrap into a function to allow arguements parameters)
    winOpener.callback = callback ? function(args){ callback.apply(this, $A(args)); } : false;
    
    
    // Set a callbackId if 'this' is an Element (see Popup.popupEvent())
    winOpener.callbackLinkId = this.tagName ? $(this).identify() : false;
    
    // Set the focus if opener have the focus
    if (winOpener.focus && pWnd){
      pWnd.focus();
    }
    
    return false;
  },
  
  /**
   * Resize the current window to the size of the given div.
   * 
   * @param divID the div of which to retrieve dimension and to use as 
   * a reference for the new window size
   * @param offsetHeight an integer value that will be added to the new window height, 
   * use this value when you want to add some margin to the div height (default is 55 if not given)
   * @param minimumHeight an integer value indicating the minimum height to used
            after resize (default is 50).
   */
  autoResize: function(divID, offsetHeight, minimumHeight) {

    if (!offsetHeight) {
      offsetHeight = 55;
    }
    if (!minimumHeight) {
      minimumHeight = 50;
    }
 
    //var elementDim = $(document.body).getDimensions();
    var elementDim = $(divID).getDimensions();
    var vpBounds = Util.getViewportBounds(); // { x, y, width, height }
    
    // Compute the new height
    var newWinHeight = elementDim.height + offsetHeight;
    newWinHeight = Math.min(newWinHeight, self.screen.availHeight);
    
    // Make sure the window is not too high
    if (vpBounds.y && (newWinHeight + vpBounds.y > self.screen.availHeight) ) {
      newWinHeight = self.screen.availHeight - vpBounds.y;
    }
    
    // Resize the window
    window.resizeTo(vpBounds.width, Math.max(minimumHeight, newWinHeight));
    
    // FIXME: Should we use it ?
    Util.shakeIE();
  },
  
  /**
   * Check if the current window is a popup
   */
  isInPopup: function() {
    return !(opener == undefined);
  }
}

// ----------------------------------------------------
//  REALLY SIMPLE HISTORY
// ----------------------------------------------------

'JCMS.History'.namespace({
  init : function() {
    dhtmlHistory.initialize();
    dhtmlHistory.addListener(JCMS.History._dhtmlHistoryListener);
  },
  
  _dhtmlHistoryListener : function(newLocation, historyData) {
    var memo = {
      newLocation : newLocation,
      historyData : historyData
    };

    var fireEvent= function() {
      document.fire('history:change', memo);
    }
    fireEvent.defer();
  },
  
  /**
   * Register a listener to be invoked on history change
   */
  observe : function(callback) {
    document.observe('history:change', function(event) { 
      callback(event.memo.newLocation, event.memo.historyData);
    });
  },
  
  /**
   * Add new new history state
   */
  add : function(newLocation, historyData) {
    if (!dhtmlHistory){ return; }
    dhtmlHistory.add(newLocation, historyData);
  }
  
});

if (window.dhtmlHistory) {
  var t0 = new Date().getTime();
  
  // Force the RSH api to use Prototype.js for JSON
	window.dhtmlHistory.create({
	   toJSON:   function(o) { return Object.toJSON(o); },
	   fromJSON: function(s) { return s.evalJSON();     }
	});
	
	// initialize the RSH API
	Event.observe(window, 'load', function(){ JCMS.History.init.defer(); } );
	
	var t1 = new Date().getTime();
  JcmsLogger.info('DHTMLHistory', 'Init DHTML History', ' in '+(t1-t0)+' ms');
}


// ----------------------------------------------------
//  CONVENIENT
// ----------------------------------------------------

document.getElementsBySelector = function(selector) {
  return $$(selector);
}



