// ----------------------------------------------------
//  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);
}

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}".
   */
  getJcmsIds : function(element) {
    var ids = $w(element.className).findAll(function(str){ 
      return str.startsWith('ID_');
    }).collect(function(str){
      return str.substring(3);
    });
    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 (tags && tags.indexOf(elm.tagName) == -1) {
        continue;
      }
      if (className && !elm.hasClassName(className)) {
        continue;
      }
      return elm;
    }
    return;
  }
});

// ----------------------------------------------------
//  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
 */

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.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 ?
    this._periodexec = new PeriodicalExecuter(
       (function(){ 
         if (this.isDone)
           return;
         this.asyncJsonCall(); }).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(){
    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;
  }
};

JcmsJsonRequest = Class.create();
Object.extend(JcmsJsonRequest.prototype, JcmsAjaxRequest.prototype);


// ----------------------------------------------------
//  AJAX COMMON PACKAGE
// ----------------------------------------------------

if (!window.Ajax) {
  var Ajax = new Object();
}

Object.extend(Ajax,{
    version: '$Revision: 1.1 $',
    
    // ----------------------------
    //  Common usefull AJAX Method
    // ----------------------------
    
    setWaitState: function(wait, elm){
      
      var wnd  = parent ? parent : window;
      var doc  = parent ? parent.document : document;
      var body = doc.getElementsByTagName('body')[0];
      
      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 wait cursor
      if (wait){
        if (elm) elm.style.cursor  = 'wait';
        Ajax.waitDiv.style.display = 'block';
        body.style.cursor = 'wait';
        wnd.status = I18N.glp('info.msg.loading');
      }
      else{ // Reset cursor
        if (elm) elm.style.cursor  =  '';
        Ajax.waitDiv.style.display = 'none';
        body.style.cursor = '';
        wnd.status='';
      }
    },
    
    /**
     * 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,{
  
  /**
   * 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
        var evt = document.createEvent('KeyEvents'); 
        evt.initKeyEvent('keypress',true,true,window,false ,false,false,false,27,null);
        selection.input.dispatchEvent(evt);
      }
      
      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;
  }
});

// ----------------------------------------------------
//  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');
  }
});

// ----------------------------------------------------
//  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,{

  /**
   * 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 parentElm the root element to work with
   * @param deep should this method called recursively
   */
  cleanDOMElements: function(parentElm, deep){
    
    var children = parentElm.childNodes;
    
    if (!children){
      return;
    }
    
    $A(children).each(function(elm, idx){
      if (deep){
        Util.cleanDOMElements(elm);
      }
      parentElm.onclick = null;
      parentElm.removeChild(elm);
    });
  },
  
  /**
   * 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 };
  },
  
  /**
   * 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){
      Event.observe(document, eventName , callback); // InternetExplorer
    } else {
      Event.observe(window,   eventName , callback); // FireFox
    }
  },

  _classToCallBack : $H(), // classname => function
  
  _onLoadCB: function() {
    var method = Util._onClickCB.bindAsEventListener(this);
    Util.observeDocument('click', method);
    Util.observeDocument('jcms:click', method);
  },
  
  _onClickCB: function(event) {
    if (!(event.eventName === 'jcms:click' || Util.isLeftClick(event))){
      return;
    }
    var elm = Event.element(event);
    if (!elm) { return; }
    var elm = elm.fastUp(['A', 'BUTTON', 'INPUT'], null, true, 2);
    if (!elm) { return; }
    
    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 or INPUT 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);
  }
  
});

Event.observe(window, 'load', Util._onLoadCB);
    
// ----------------------------------------------------
//  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.hasClassName && target.hasClassName(this.className));
    }
})

// ----------------------------------------------------
//  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,
  JcmsAjaxRequest:    false,
  AjaxRefresh:        false,
  
  // --------------------
  //  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 (!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 = {
  popupWindow: function(url, title, w, h, status, resizable, scrollbars, reuse, winOpener){

    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 == undefined) {
      winOpener = window;
    }
    
    // Title must not contains white space
    if (title){
      title = title.replace(/\s+/, '_');
    }
    
    // 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);

      pWnd = navigator.jalios.popupWindow;
      if (!pWnd){
        alert(I18N.glp('warn.popup.blocker'));
      }
    }
    catch(ex){
      alert(I18N.glp('warn.popup.blocker'));
    }
    
    // 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));
  }
}

// ----------------------------------------------------
//  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) {
  // 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', JCMS.History.init);
}


// ----------------------------------------------------
//  CONVENIENT
// ----------------------------------------------------

document.getElementsBySelector = function(selector) {
  return $$(selector);
}


