/*
  $Id: neodom.js,v 1.104 2007/04/15 17:21:28 altblue Exp $
 
  NeoDOM: Prototype extensions package

  (c) 2006-2007 Marius Feraru <altblue@n0i.net>

  This module is free software; you can redistribute it
  and/or modify it under the same terms as Perl itself.
*/

Element.addMethods({
  hidden: function (el) {
    return !$(el).visible();
  },

  getInnerText: function (el) {
    el = $(el); if (!el) return;
    return ( el.innerText && !window.opera
      ? el.innerText : el.innerHTML.stripScripts().unescapeHTML()
    ).strip();
  },

  // IE fucks up completely, we cannot dig through el.attributes :(
  stringify: function (el) {
    var str = '<' + el.tagName.lc();
    if (el.id)        str += ' id="'    + el.id + '"';
    if (el.className) str += ' class="' + el.className + '"';
    if (el.rel)       str += ' rel="'   + el.rel + '"';
    str += '>';
    return str;
  },

  toggleClassName: function(el) {
    if (!(el = $(el))) return;
    $A(arguments).slice(1).each(
      function (cn) {
        this[(this.hasClassName(cn) ? 'remove' : 'add') + 'ClassName'](cn);
      }.bind(el)
    );
    return el;
  },

  addClassName: function (el) {
    if (!(el = $(el))) return;
    $A(arguments).slice(1).each(
      function (cn) { Element.classNames(this).add(cn) }.bind(el)
    );
    return el;
  },

  removeClassName: function (el) {
    if (!(el = $(el))) return;
    $A(arguments).slice(1).each(
      function (cn) { Element.classNames(this).remove(cn) }.bind(el)
    );
    return el;
  },

  removeListeners: function (el, evName) {
    if (!(el = $(el))) return;
    for (var i = 0, length = Event.observers.length; i < length; i++) {
      var item = Event.observers[i];
      if (item[0] === el && item[1] === evName) {
        Event.stopObserving.apply(Event, item);
        item[0] = null;
      }
    }
    return el;
  },

  loading: function (el, tag, text) {
    if (!(el = $(el))) return;
    tag = tag || 'div';
    el.update('<' + tag + ' class="loading">' + (text || 'loading...') + '</' + tag + '>');
    return el;
  },

  processing: function (el, bool) {
    if (!(el = $(el))) return;
    el.disabled = bool;
    el[bool ? 'addClassName' : 'removeClassName']('loading');
    return el;
  },

  findParent: function(el, tagName, className) {
    tagName = tagName.toLowerCase();
    el = $(el);
    while (el) {
      if (el && el.tagName
          && (!tagName || tagName == '*' || el.tagName.toLowerCase() == tagName)
          && (!className || Element.hasClassName(el, className))
        ) break;
      el = el.parentNode;
    }
    return $(el);
  },

  findRow: function(id, parent, opt) {
    var rows = [];
    if (!opt) {
      opt = {};
    }
    var REX = new RegExp('(?:^|:)' + (opt.name||'id') + '-' + id + '(?:$|:)');
    var trs = ($(parent) || document.body).getElementsByTagName(opt.tag||'tr');
    for (var j = 0; j < trs.length; j++) {
      var row = trs[j];
      if (row.id && REX.test(row.id)) {
        if (!opt.all) {
          return $(row);
        }
        rows.push($(row));
      }
    }
    if (!opt.all) {
      return;
    }
    return rows;
  },

  parseID: function(el) {
    if (!el) {
      return null;
    }
    var id = el.getAttribute('id');
    if (!id) {
      return {};
    }
    var pairs = id.split(':');
    return pairs.inject({}, function(params, pairString) {
      var pair = pairString.split('-');
      params[pair[0]] = pair[1];
      return params;
    });
  },

  parseRowID: function(el, tagName, className) {
    el = $(el);
    var parent = el.findParent(tagName || 'tr', className);
    if (!parent) {
      return null;
    }
    return el.parseID(parent);
  },

  create: function(type, parent, before, attrs) {
    var el = document.createElement(type);

    /* allow call "shortcuts" like (type, attrs) */
    if (typeof parent == 'string') {
      parent = $(parent);
    }
    if (parent && typeof parent == 'object') {
      if (typeof parent.childNodes == 'undefined') {
        attrs = parent;
        parent = before = null;
      } else {
        if (typeof before == 'string') {
          before = $(before);
        }
        if (before && typeof before == 'object' && typeof before.childNodes == 'undefined') {
          attrs = before;
          before = null;
        }
      }
    } else {
      parent = null;
    }

    /* add attributes in graceful DOM way */
    if (attrs) {
      for (var prop in attrs) {
        try {
          el[prop] = attrs[prop];
        } catch (e) {
          el.setAttribute(prop, attrs[prop]);
        }
      }
    }

    /* add element to current document? */
    if (parent) {
      try { /* IE croaks on null before */
        parent.insertBefore(el, before);
      } catch (e) {
        parent.appendChild(el);
      }
    }

    /* give it back */
    return $(el);
  },

  textNode: function(str) {
    return document.createTextNode(str);
  },

  getChildren: function(parent, tagName, className) {
    var children = ($(parent) || document.body).childNodes;
    var RE = new RegExp( "(^|\\s)" + className + "(\\s|$)" );
    return $A(children).inject([], function(elements, child) {
      if (
        ( !tagName || tagName == '*'
          || (child.tagName && child.tagName.toLowerCase() == tagName.toLowerCase() )
        ) && (
          !className || className == '*' || Element.hasClassName(child, className)
        )
      ) {
        elements.push( $(child) );
      }
      return elements;
    });
  },

  /* DEPRECATED: drop this! */
  addHandler: function(selector, eventName, handler) {
    $$(selector).each(function (el) { el.observe(eventName, handler) });
  },

  getCaret: function(ctrl) {
    var CaretPos = { start: 0, end: 0 };
    if (document.selection) { // IE
      ctrl.focus();
      var sel = document.selection.createRange();
      var len = sel.text.length;
      sel.moveStart ('character', -ctrl.value.length);
      CaretPos = {
        start: sel.text.length - len,
        end:   sel.text.length
      };
    } else {
      if (ctrl.selectionStart || ctrl.selectionStart == '0') {
        CaretPos = {
          start: ctrl.selectionStart,
          end:   ctrl.selectionEnd
        };
      }
    }
    return CaretPos;
  },

  setCaret: function(ctrl, caret) {
    if (typeof caret == 'number') {
      caret = { start: caret, end: caret };
    }
    if (ctrl.setSelectionRange) {
      ctrl.setSelectionRange(caret.start, caret.end);
      ctrl.focus();
    } else {
      if (ctrl.createTextRange) {
        var range = ctrl.createTextRange();
        range.collapse(true);
        range.moveEnd('character', caret.end);
        range.moveStart('character', caret.start);
        range.scrollIntoView();
        range.select();
      }
    }
  }

});
Object.extend(Element, Element.Methods);

Object.extend(Hash, {
  toQueryString: function(obj, separator) {
    var parts = [];
    this.prototype._each.call(obj, function(pair) {
      if (!pair.key) return;
      if (pair.value && pair.value.constructor == Array) {
        var values = pair.value.compact();
        if (values.length < 2)
          pair.value = values.reduce();
        else {
          key = encodeURIComponent(pair.key);
          values.each(function(value) {
            value = value != undefined ? encodeURIComponent(value) : '';
            parts.push(key + '=' + encodeURIComponent(value));
          });
          return;
        }
      }
      if (pair.value == undefined) pair[1] = '';
      parts.push(pair.map(encodeURIComponent).join('='));
    });
    return parts.join(separator || '&');
  }
});

Object.extend(Hash.prototype, {
  toQueryString: function(separator) {
    return Hash.toQueryString(this,separator);
  },
  extract: function (property) {
    var results = [];
    this.values().each( function(v) { results.push( v[property] ) } );
    return results;
  }
});

var $T = Element.create;
var $TT = Element.textNode;

Object.extend( Form.Methods, {
  resetErrors: function (form) {
    form.select('.error').invoke('removeClassName', 'error');
    form.select('.row-error').invoke('removeClassName', 'row-error');
    return form;
  },

  raiseErrors: function (form, errors) {
    if (!errors) return;
    for (var field in errors) {
      var element = $( form[field] );
      if (element) 
        element.raiseError(errors[field]);
    }
    return form;
  },

  setProcessing: function (form, bool) {
    form.getInputs('submit').invoke('processing', bool);
    return form;
  }
});
Object.extend(Form, Form.Methods);

Object.extend( Form.Element.Methods, {
  raiseError: function (element, message) {
    element.addClassName('error');
    var row = element.findParent('div', 'row');
    if (row) {
      row.addClassName('row-error');
      try {
        new top.Effect.Shake(row, {duration: 0.4});
      } catch (e) {};
    }
    if (message) {
      top.Message.error(message, {
        onClose: function () { this.activate() }.bind(element)
      });
    } else {
      element.activate();
    }
    return element;
  },

  resetError: function (element) {
    element.removeClassName('error');
    var row = element.findParent('div', 'row');
    if (row) row.removeClassName('row-error');
    return element;
  },

  setProcessing: function (element, bool) {
    element[bool ? 'disable' : 'enable' ]();
    element[ (bool ? 'add' : 'remove') + 'ClassName' ]('loading');
    return element;
  }
});
Object.extend(Form.Element, Form.Element.Methods);

/*
  Yes, redo :(
  = needed by 1.5.1_pre0
*/
Object.extend(Element.Methods.ByTag, {
  "FORM":     Object.clone(Form.Methods),
  "INPUT":    Object.clone(Form.Element.Methods),
  "SELECT":   Object.clone(Form.Element.Methods),
  "TEXTAREA": Object.clone(Form.Element.Methods)
});


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

Object.extend(Neo, {
  initialize: function(options) {
	Object.extend(this, options || {});
  },

  evalJSON: function(text) {
    var json = null;
    try {
      eval('json = ' + text);
    } catch (e) {}; // { console.debug('cannot eval JSON: ' + e.name + ' - ' + e.message + '\nJSON was: "' + text + '"') };
    return json;
  },

  _eRE: new RegExp('(\\'
    + '/ . * + ? | ( ) [ ] { } \\'.split(' ').join('|\\')
    + ')', 'g'),
  escapeRE: function(text) {
    return text.replace(this._eRE, '\\$1');
  },

  parseURL: function(url) {
    var p = (url || window.location.href).match(
              /^(?:([^:\/?#]+):)?(?:\/\/([^\/?#:]*))?(?::(\d+))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/
            );
    if (!p) return {};
    return {
      scheme:   p[1],
      host:     p[2],
      port:     p[3],
      path:     p[4],
      args:     this.parseQueryArgs(p[5]),
      fragment: p[6]
    };
  },

  parseQueryArgs: function(query) {
    var match = (query || '').strip().match(/([^?#]*)(#.*)?$/);
    if (!match) return {};

    return match[1].split(/[&;]/).inject({}, function(hash, pair) {
      if ((pair = pair.split('='))[0]) {
        var name = decodeURIComponent(pair[0]);
        var value = pair[1] ? decodeURIComponent(pair[1].replace(/\+/g, ' ')) : undefined;

        if (hash[name] !== undefined) {
          if (hash[name].constructor != Array)
            hash[name] = [hash[name]];
          if (value) hash[name].push(value);
        }
        else hash[name] = value;
      }
      return hash;
    });
  },

  box: function (id, url, onTopOf) {
    var win = $(id);
    if (win) return false;
    var up = $(onTopOf || 'content') || document.body;
    win = $T('div', up, up.firstChild, {id: id, className: 'neobox-wrapper'});
    win.box = $T( 'div', win, {className: 'neobox neobox-clear'});
    win.content = $T('div', win.box, {className: 'neobox-content'});
    var clear = $T('div', win, {className: 'clear'});
    win.content.loading();
    this.addCloseButton(win);

    new Ajax.Request(url, {
      method: 'get',
      evalScripts: true,
      onFailure: function () {
          Message.add('request failed', {level: 'error'});
          this.remove();
      }.bind(win),
      onSuccess: function (req, json) {
        if (json && json.error) {
          Message.add(json.error, {level: 'error'});
          this.remove();
          return false;
        }
        this.content.update(req.responseText);
      }.bind(win)
    });

    return win;
  },

  popup: function (opt) {
    opt || ( opt = {} );
    var win = $T('div', document.body, {className: 'neobox-wrapper'});
    win.clear = $T('div', win, {className: 'clear'});
    win.box = $T('div', win, {className: 'neobox neobox-clear'});
    win.content = $T('div', win.box, {className: 'neobox-content'});
    this.addCloseButton(win);

    var posW = Position.cumulativeOffset( opt.parent  || $('main') );
    var posE = Position.cumulativeOffset( opt.trigger || $('main') );
    win.setStyle({
      'position'  : 'absolute',
      'width'     : (parseInt(Element.getStyle( opt.parent || $('main'), 'width')) - 60) + 'px',
      'left'      : (parseInt(posW[0])+30) + 'px',
      'top'       : posE[1] + 'px',
      'text-align': 'left',
      'z-index'   : '1000',
      'color'     : 'black',
      'background-color' : 'white'
    });
    
    if (opt.style) win.setStyle(opt.style);
    if (opt.innerHTML) {
      win.content.innerHTML = opt.innerHTML;
    } else {
      win.content.loading();
    }
    return win;
  },

  addCloseButton: function (parent, closeWhat) {
    var a = $T('a', parent, {href:'#',title:'close',className:'close',innerHTML:'close'});
    a.onclick = function(ev) {
      Event.stop(ev); this.remove(); return false;
    }.bindAsEventListener($(closeWhat) || $(parent));
    return a;
  },

  addCancelButton: function (parent, undoMethod) {
    var a = $T('a', parent, {href:'#',title:'cancel',className:'cancel',innerHTML:'cancel'});
    Event.observe(a, 'click', undoMethod || function (ev) {
        Event.stop(ev); this.remove(); return false;
      }.bindAsEventListener($(parent))
    );
    return a;
  },

  closePopups: function (base) {
    $(base || document.body).select('.popup').each(Element.remove);
  },

  /*
   * getPageSize()
   * Returns object with { width, height, viewport: { width, height} }
   * Core code from - quirksmode.org
   * Edit for Firefox by pHaez
  */
  pageSize: function() {
  	var xScroll, yScroll;

  	if (window.innerHeight && window.scrollMaxY) {	
  		xScroll = document.body.scrollWidth;
  		yScroll = window.innerHeight + window.scrollMaxY;
  	} else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac
  		xScroll = document.body.scrollWidth;
  		yScroll = document.body.scrollHeight;
  	} else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari
  		xScroll = document.body.offsetWidth;
  		yScroll = document.body.offsetHeight;
  	}

  	var windowWidth, windowHeight;

  	if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
  		windowWidth = document.documentElement.clientWidth;
  		windowHeight = document.documentElement.clientHeight;
  	} else if (self.innerHeight) {	// all except Explorer
  		windowWidth = self.innerWidth;
  		windowHeight = self.innerHeight;
  	} else if (document.body) { // other Explorers
  		windowWidth = document.body.clientWidth;
  		windowHeight = document.body.clientHeight;
  	}	
  	var pageHeight, pageWidth;

  	// for small pages with total height less then height of the viewport
  	if(yScroll < windowHeight){
  		pageHeight = windowHeight;
  	} else { 
  		pageHeight = yScroll;
  	}

  	// for small pages with total width less then width of the viewport
  	if(xScroll < windowWidth){
  		pageWidth = windowWidth;
  	} else {
  		pageWidth = xScroll;
  	}

  	return {
  	  width:    pageWidth,
  	  height:   pageHeight,
  	  viewport: {
  	    width:  windowWidth,
  	    height: windowHeight
      }
    };
  },

  uploadProgressCache: {},

  endUploadProgress: function (uploadID) {
    var pr = this.uploadProgressCache[uploadID];
    if (!pr) return;
    try {
      delete this.uploadProgressCache[this.uploadID];
      if (pr.timer) clearTimeout(pr.timer);
      Element.remove(pr.parentNode);
    } catch (e) {};
  },

  uploadProgress: function (fieldName, uploadID, message) {
    if (!fieldName || !uploadID || this.uploadProgressCache[uploadID]) return false;
    var pr = $T('div', {className: 'progress-wrapper', id: 'msg-pr-' + uploadID, fieldName: fieldName, uploadID: uploadID, delay: 100, zeros: 0});
    this.uploadProgressCache[uploadID] = pr;
    pr.graph = $T('div', pr, {className: 'progress-graph', id: 'msg-pr-' + uploadID + '-graph'});
    pr.done = $T('div', pr.graph, {className: 'progress-done', id: 'msg-pr-' + uploadID + '-done'});
    pr.meter = $T('div', pr, {className: 'progress-meter', id: 'msg-pr-' + uploadID + '-meter', innerHTML: '0%'});
    pr.label = $T('div', pr, {className: 'progress-label', innerHTML: ' ' + (message || 'uploading') + '… '});
    Message.add(pr, {level: 'progress', ma: 1, noClose: 1, timeout: 0});
    pr.timer = setTimeout(Neo._uploadProgressMonitor.bind(pr), pr.delay);
    return false;
  },

  /* "this" should be the PR */
  _uploadProgressMonitor: function () {
    if (this.timer) clearTimeout(this.timer);
    new Ajax.Request('/ws/upload_progress', {
      method: 'get',
      parameters: 'name=' + this.fieldName + ';uploadID=' + this.uploadID,
      onFailure: function (req) {
        Message.add('upload tracking failed :(', {level: 'error'});
        Element.remove(this.parentNode);
        if (this.timer) clearTimeout(this.timer);
        delete Neo.uploadProgressCache[this.uploadID];
      }.bind(this),
      onSuccess: Neo._uploadProgress.bind(this)
    });

  },

  _uploadProgress: function (req, json) {
    json = NeoForm.evalReq(req);
    if (!json) {
      Message.add('upload tracking failed', {level: 'error'});
      Element.remove(this.parentNode);
      delete Neo.uploadProgressCache[this.uploadID];
      return false;
    }
    var current_proc = parseInt(this.meter.innerHTML);
    if (json[2] == 0) {
      this.zeros++;
    }
    if (current_proc < json[2] && json[2] <= 100) {
      this.done.style.width = this.meter.innerHTML = json[2] + '%';
      if (this.delay >= 150) this.delay -= 50;
    } else {
      if (this.delay <= 950) this.delay += 50;
    }
    if ( this.zeros > 2 || json[2] >= 100
         || (current_proc > json[2] && json[0] == json[1])
       ) {
      this.done.style.width = this.meter.innerHTML = '100%';
      this.label.innerHTML = this.label.innerHTML + ' <strong>DONE!</strong>';
      var who = this.parentNode;
      new Effect.Pulsate(who, {duration: 1, from: 0.5});
      delete Neo.uploadProgressCache[this.uploadID];
      setTimeout(function(){Element.remove(this)}.bind(who), 2500);
    } else {
      this.timer = setTimeout(Neo._uploadProgressMonitor.bind(this), this.delay);
    }
  },

  onPageLoad: function(method) {
    if (document.addEventListener) {
      document.addEventListener("DOMContentLoaded", method, false);
    } else {
      Event.observe(window, 'load', method, false);
    }
  },

  accessKeys: function (ev, show) {
    if (!ev || ev.keyCode != 18) return false;
    var todo = show ? 'addClassName' : 'removeClassName';
    $(document.body).select('.accesskey').each(function (el) {
      el[todo]('highlight');
    });
  },
  visualAccessKeys: function() {
    Event.observe(window, 'load', function () {
      Event.observe(window, 'keydown', this.accessKeys.bindAsEventListener(this, 1));
      Event.observe(window, 'keyup', this.accessKeys.bindAsEventListener(this, 0));
    }.bind(this));
  }

});

parseURL = Neo.parseURL.bind(Neo);
escapeRE = Neo.escapeRE.bind(Neo);
pageSize = Neo.pageSize.bind(Neo);

Neo.Universal = {
  maximizeHeight: function() {
    var s = $('start'), f = $('footer'), b = $('body'), D = $(document.body);
    if (!s) return false;

    var h = Neo.pageSize().viewport.height;
    if (f) h -= f.getDimensions().height;

    var bs = this.baseFontSize(), em = bs[1] === 'px' ? bs[0] : 1;

    ['margin-top', 'margin-bottom', 'padding-top', 'padding-bottom'].each(
      function(prop) {
        [b,D].each(
          function(E) {
            if (E) h -= this.toPx( E.getStyle(prop), em );
          }.bind(this)
        );
      }.bind(this)
    );
    s.setStyle({ 'min-height' : h + 'px' });
  },

  toPx: function (size, em) {
    var sz = size.lc().match(/^(.+?)([^0-9]+)$/);
    sz.shift();
    switch (sz[1]) {
      case 'pt': sz[0] *= 1.3333; break;
      case 'em': sz[0] *= em; break;
    };
    return sz[0];
  },

  // Really stupid way for converting "everything" to PX...
  // Anyway, it's for IE, so "stupid" it's legal :(
  baseFontSize: function() {
    var sz = $(document.body).getStyle('font-size').lc().match(/^(.+?)([^0-9]+)$/);
    if (!sz || sz.length != 3) return sz;
    sz.shift();
    switch (sz[1]) {
      case 'pt': sz[1] = 'px', sz[0] *= 1.3333; break;
      case 'em': sz[1] = 'px', sz[0] *= 12*1.3333; break;
    };
    return sz;
  }
};

/* make Prototype's evalJSON handle "unicode bytes" gracefully */
Ajax.Request.prototype.evalJSON = function() {
  var json = this.getHeader('X-JSON');
  if (!json) return null;
  try {
    var jutf = decodeURIComponent(escape(json));
    if (jutf.length < json.length) {
      json = jutf;
    }
  } catch (e) {};
  try {
    return eval('(' + json + ')');
  } catch (e) {
    return null;
  }
};

/* allow multiple expressions (separated by comma) */
/*
  or: $A(arguments).inject([], function(args, expr){ return args.concat( expr.split(',') ) })
  or: $A(arguments).map(function(el){ return el.split(',') }).flatten()
*/
function $$() {
  return Selector.findChildElements(document,
    $A(arguments).invoke('split',',').flatten()
  );
};

/* FIXME: Who's the original author?! */
function sprintf() {
  if (!arguments || arguments.length < 1 || !RegExp) {
    return;
  }
  var str = arguments[0];
  var retstr = '';
  var re = /([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X)(.*)/;
  var a = b = [], numSubstitutions = 0, numMatches = 0;
  while ( a = re.exec(str) ) {
    var leftpart = a[1], pPad = a[2], pJustify = a[3], pMinLength = a[4];
    var pPrecision = a[5], pType = a[6], rightPart = a[7];
    numMatches++;
    if (pType == '%') {
      subst = '%';
    } else {
      numSubstitutions++;
      if (numSubstitutions >= arguments.length) {
        throw 'sprintf error: Not enough function arguments ('
              + (arguments.length - 1)
              + ', excluding the string)\r\n'
              + 'for the number of substitution parameters in string ('
              + numSubstitutions + ' so far).';
      }
      var param = arguments[numSubstitutions];

      var pad = '';
      if (pPad && pPad.substr(0,1) == "'") {
        pad = leftpart.substr(1,1);
      } else {
        if (pPad) {
          pad = pPad;
        }
      }

      var justifyRight = true;
      if (pJustify && pJustify === "-") {
        justifyRight = false;
      }

      var minLength = -1;
      if (pMinLength) {
        minLength = parseInt(pMinLength);
      }

      var precision = -1;
      if (pPrecision && pType == 'f') {
        precision = parseInt( pPrecision.substring(1) );
      }
      var subst = param;
      switch (pType) {
        case 'b': subst = parseInt(param).toString(2); break;
        case 'c': subst = String.fromCharCode(parseInt(param)); break;
        case 'd': subst = parseInt(param) ? parseInt(param) : 0; break;
        case 'u': subst = Math.abs(param); break;
        case 'o': subst = parseInt(param).toString(8); break;
        case 's': subst = param; break;
        case 'x': subst = ('' + parseInt(param).toString(16)).toLowerCase(); break;
        case 'X': subst = ('' + parseInt(param).toString(16)).toUpperCase(); break;
        case 'f': if (precision > -1) {
                    subst = Math.round( parseFloat(param) * Math.pow(10, precision) )
                            / Math.pow(10, precision);
                    var dec = subst.toString().replace(/^\d+\./, '');
                    var padRight = precision - dec.length;
                    if (padRight > 0 ) {
                      var arrTmp = new Array(padRight + 1);
                      subst = subst.toString() + arrTmp.join('0');
                    }
                  } else {
                    subst = parseFloat(param);
                  }
                  break;
      };
      var padLeft = minLength - subst.toString().length;
      if (padLeft > 0) {
        var arrTmp = new Array(padLeft+1);
        var padding = arrTmp.join(pad?pad:" ");
      } else {
        var padding = "";
      }
    }
    str = rightPart;
    retstr += leftpart + padding + subst;
  }
  return retstr + str;
};

Object.extend(Number.prototype, {
  sprintf: function(format) {
    return sprintf(format, this);
  },
  range: function(end, inc) {
    var start = this.valueOf();
    inc || (inc = 1);
    var reverse = false;
    if (start > end) {
      reverse = true;
      var sw = start;
      start = end;
      end = sw;
    }
    var ret = [];
    for (var i = start; i <= end; i += inc)
      if (reverse) ret.unshift(i);
      else ret.push(i);
    return ret;
  }
});

Object.extend(String.prototype, {
  uc: String.prototype.toUpperCase,
  lc: String.prototype.toLowerCase,
  clean: function() {
    return this.strip().replace(/[\r\n\s]+/g, ' ');
  },
  localURL: function() {
    return this.replace(/^(?:[^:\/?#]+:)?(?:\/\/[^\/?#:]*)?(?::\d+)?/, '') || '/';
  },
  qw: function() {
    var s = this.strip();
    return s ? s.split(/\s+/) : [];
  },
  sprintf: function(format) {
    return sprintf(format, this);
  },
  times: function(count) {
    if (this == '' || count < 1) return '';
    var txt = this, bin = count.toString(2);
    for (var i = 1; i < bin.length; i++) {
      txt += txt;
      if (bin.charAt(i) === '1') txt = txt + this;
    }
    return txt;
  },
  toQueryParams: function() {
    return Neo.parseQueryArgs(this);
  },
  toTags: function() {
    return this.strip().replace(/(?:\r?\n)+/g, ',').replace(/^[,\s]+/, ''
      ).replace(/[,\s]+$/, '').replace(/(?:,\s*)+/g, ',').replace(/\s+/g, ' '
      ).split(/\s*,\s*/).without('').uniq();
  }
});

function qw(str) { return str.qw() };

Function.prototype.delay = function(ms, bind) {
  return setTimeout(this.bind(bind || this), ms);
};
Function.prototype.periodical = function(ms, bind) {
  return setInterval(this.bind(bind || this), ms);
};

/* Textareas which are automatically scaled ;-) */
function ElasticTextarea(el, opt) {
  this.dimension = function() {
    var rows = this.element.value.split('\n');
    var cols = 1;
    for (var i=0, len = rows.length; i < len; i++)
      if (cols < rows[i].length)
        cols = rows[i].length;
    return [cols, rows.length];
  };
  this.resize = function() {
    var d = this.dimension();
    this.element.setStyle({'overflow-x': d[0] > this.maxX ? 'scroll' : 'hidden'});
    this.element.setStyle({'overflow-y': d[1] > this.maxY ? 'scroll' : 'hidden'});
    this.element.cols = d[0] < this.minX ? this.minX : d[0] > this.maxX ? this.maxX : d[0];
    this.element.rows = d[1] < this.minY ? this.minY : d[1] > this.maxY ? this.maxY : d[1];
  };
  this.monitor = function(ev) {
    setTimeout(this.resize.bind(this), 10);
    return true;
  };

  el.setAttribute('wrap', 'off');
  this.element = el;
  opt = opt || {};
  var dim = this.dimension();
  this.minX = opt.minX || el.cols || dim[0];
  this.minY = opt.minY || el.rows || dim[1];
  if (opt.maxX) {
    this.maxX = opt.maxX;
  } else {
    var x = Neo.pageSize().viewport.width - Position.cumulativeOffset(el)[0];
    var r = 9.5; // FIXME: find a way to compute px_width_per_char
    this.maxX = parseInt( x / r );
  }
  if (opt.maxY) {
    this.maxY = opt.maxY;
  } else {
    var x = Neo.pageSize().viewport.height - Position.cumulativeOffset(el)[1];
    var r = 9.5; // FIXME: find a way to compute px_width_per_char
    this.maxY = parseInt( x / r );
  }

  if (this.minX > this.maxX) this.minX = this.maxX;
  if (this.minY > this.maxY) this.minY = this.maxY;

  this.element.observe('keypress', this.monitor.bindAsEventListener(this));
  this.resize();
  return this;
};

/* Input[type="text"] which are automatically scaled ;-) */
function ElasticInput(el, opt) {
  this.resize = function() {
    var x = this.element.value.toString().length + 1;
    this.element.size = x < this.minX ? this.minX : x > this.maxX ? this.maxX : x;
  };
  this.monitor = function(ev) {
    setTimeout(this.resize.bind(this), 10);
    return true;
  };

  this.element = el;
  opt = opt || {};
  this.minX = opt.minX || el.size || (el.value.length + 1);
  if (opt.maxX) {
    this.maxX = opt.maxX;
  } else {
    if (el.maxLength && el.maxLength > 0) {
      this.maxX = el.maxLength;
    } else {
      var x = Neo.pageSize().viewport.width - Position.cumulativeOffset(el)[0];
      var r = 9.5; // FIXME: find a way to compute px_width_per_char
      this.maxX = parseInt( x / r );
    }
  }
  if (this.minX > this.maxX) {
    this.minX = this.maxX;
  }
  this.element.observe('keypress', this.monitor.bindAsEventListener(this));
  this.resize();
  return this;
};

/* MouseMove goes BEFORE MouseOver in Opera 9.1 and IE 7 :( */
var Rating = function(el, opt) {
  opt = opt || {};
  el = $(el);
  if (!el) {
    throw "Cannot initialize Rating: invalid base element!";
  }
  if (!opt.rating) {
    opt.rating = el.className.replace(/^.*?rating-(\d+).*$/, '$1') || 0;
  }
  this.rating  = opt.rating;
  this.afterClick = opt.afterClick || function(){return true};
  this.build(el, opt);

  this.onMouseMoveListener = this.onMouseMove.bindAsEventListener(this);
  this.onMouseOverListener = this.onMouseOver.bindAsEventListener(this);
  this.onMouseOutListener  = this.onMouseOut.bindAsEventListener(this);
  this.onClickListener     = this.onClick.bindAsEventListener(this);
  this.selector.observe('mousemove', this.onMouseMoveListener);
  this.selector.observe('mouseover', this.onMouseOverListener);
  this.selector.observe('mouseout',  this.onMouseOutListener);
  this.selector.observe('click',     this.onClickListener);

  return this;
};

Object.extend(Rating.prototype, {
  build: function (el, opt) {
    this.element = $T('em', el.up(), el, {className: 'rating rating-' + this.rating});
    el.remove();
    this.label = $T('span', this.element, {className: 'label', innerHTML: (opt.label || 'Rating:') + ' '});
    this.selector = $T('span', this.element, {className: 'selector', innerHTML: '&#8203;'});
    this.rate = $T('span', this.element, {className: 'rating', innerHTML: this.rating});
  },

  getClickRating: function(ev) {
    return Math.ceil(100 * (ev.pointerX() - this.offset) / this.width / 20);
  },

  update: function (rating) {
    this.rating = rating;
    this.rate.innerHTML = rating;
    this.element.className = 'rating rating-' + rating;
  },

  onMouseOver: function (ev) {
    if (this._over) return;  // for Opera and IE :(
    this._over = true;
    this.originalRating = this.rating;
    this.width   = parseInt( this.selector.getDimensions().width );
    this.offset  = this.selector.cumulativeOffset().left;
  },

  onMouseOut: function (ev) {
    this._over = false;
    this.update(this.originalRating);
  },

  onMouseMove: function (ev) {
    if (!this._over) this.onMouseOver(ev);  // for Opera :(
    var rating = this.getClickRating(ev);
    if (this.rating !== rating) this.update(rating);
  },

  onClick: function (ev) {
    var rating = this.getClickRating(ev);
    if ( this.afterClick(rating, this) ) {
      this.originalRating = rating;
      this.update(rating);
    }
  }
});

/* a very basic "tree" widget */
Tree = Class.create();
Tree.prototype = {
  initialize: function (root, o) {
    o = o || {};
    this.branchTag  = o.branchTag  || 'ul';
    this.leafTag    = o.leafTag    || 'li';
    this.onCollapse = o.onCollapse || function() {};
    this.onExpand   = o.onExpand   || function() {};
    this.root = $(root);
    if (!this.root || this.root.tagName.lc() !== this.branchTag)
      return false;
    this.setup(this.root);
    this.root.observe('click', this.clickHandler.bindAsEventListener(this));
  },
  setup: function (ul) {
    var li = $(ul).down(this.leafTag);
    while (li) {
      var branch = $(li).down(this.branchTag);
      li.addClassName('tree-' + (branch ? 'branch' : 'leaf'));
      if (branch)
        this.setup(branch);
      li = li.next(this.leafTag);
    }
  },
  clickHandler: function (ev) {
    var li = Event.element(ev);
    if (li.tagName.lc() !== this.leafTag || !li.hasClassName('tree-branch'))
      return true;
    if (li.hasClassName('tree-collapsed')) {
      li.removeClassName('tree-collapsed');
      this.onExpand(li);
    } else {
      li.addClassName('tree-collapsed');
      this.onCollapse(li);
    }
  }
};


/* keep it last: refreshes Prototype's hacks */
Element.addMethods();
