/**
 * Saltation native.js
 * 
 * All purpose small Datatypes and Extesions to native Datatypes
 * 
 * @version 1.2.1 Build 230
 */

/**
 * Extension to the generic Number Object.
 */
Object.extend(Number.prototype, {
  /**
   * HR_SUFFIXES are the suffixes for human readable number output
   */
  HR_SUFFIXES: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'],
  
  /**
   * @return Number the maximum of this number an the given arguments
   */
  max: function() {
    return Math.max.apply(this, $A(arguments).concat(this));
  },
  
  /**
   * @return Number the minimum of this number and the given arguments
   */
  min: function() {
    return Math.min.apply(this, $A(arguments).concat(this));
  },
  /**
   * @return the number ensuring it to be within [min,max] bounds
   */
  inside: function(min, max) {
    var i = $I(min, max);
    if (i == null) {
      return this;
    }
    return i.snap(this);
  },
  /**
   * @return boolean true when this Number is not greater than max and not smaller than min
   */
  isWithin: function(min, max) {
    var i = $I(min, max);
    if (i == null) {
      return false;
    }
    return i.contains(this);
    //return this >= min && this <= max;
  },
  /**
   * @return boolean true when this Number is smaller than min or greater than max
   */
  isOutside: function(min, max) {
    var i = $I(min, max);
    if (i == null) {
      return true;
    }
    return !i.contains(this);
    //return this < min || this > max;
  },
  /**
   * Simple power function
   * @return Math.pow(this, power)
   */
  pow: function(power) {
    return Math.pow(this, power);
  },
  /**
   * Was here before I knew that toFixed(digits) existed
   */
  fix: function(digits) {
    /*
    var p = (10).pow(digits);
    return parseInt(this * p) / p;
    */
    return this.toFixed(digits);
  },
  /**
   * @return Number random number from 0 to this
   */
  random: function() {
    return Math.random() * this;
  },
  /**
   * @return int random number form 0 to this, rounded
   */
  ran: function() {
    return this.random().round();
  },
  /**
   * @return String the HEX Value of this Number
   */
  toHex: function() {
    return this.toString(16).toUpperCase();
  },
  /**
   * @return String the Binary value of this Number
   */
  toBinary: function() {
    return this.toString(2);
  },
  /**
   * @return int the number of positive digits in this Number
   */
  digits: function() {
    var re = 1;
    var tmp = this;
    while ((tmp /= 10) > 1) {
      re++;
    }
    return re;
  },
  /**
   * Returns a human readable form of a number.
   * If no arguments are supplied it uses 2 digits base 1024.
   * 
   * @param digits    the number of postpoint-digits
   * @param base      the base
   * @param tolerance 
   * 
   * @return String the human readable form of the number
   */
  hr: function(digits, base, tolerance) {
    if (Object.isUndefined(digits)) {
      digits = 2;
    }
    if (Object.isUndefined(base)) {
      base = 1024;
    }
    var t = base * (Object.isUndefined(tolerance) ? 0.5 : tolerance);
    var v = this;
    var n = 0;
    while (n < Number.prototype.HR_SUFFIXES.length && v > t) {
      v /= base;
      n++;
    }
    
    return v.fix(digits) + Number.prototype.HR_SUFFIXES[n];
  },
  /**
   * @return boolean true when the Number is an even number
   */
  isEven : function() {
    return this % 2 == 0;
  },
  /**
   * @return boolean true when the Number is an odd number
   */
  isOdd : function() {
    return this % 2 == 1;
  },
  /**
   * Transforms a Number to a Time.
   * Example: 13.5 -> 13:30:00 
   * 
   * @return String a time string
   */
  toTime: function(glue, base, snaps) {
    if (Object.isUndefined(base) || base == null) {
      base = 60;
    }
    if (Object.isUndefined(glue) || glue == null) {
      glue = ':';
    }
    if (Object.isUndefined(snaps) || snaps == null) {
      snaps = 3;
    }
    
    var re = [];
    var d = this;
    var i = 0;
    while (i < snaps) {
      var n = (i == snaps - 1) ? d.round() : d.floor();
      re.push(n.toPaddedString(2));
      d = (d - n) * base;
      i++;
    }
    
    return re.join(glue);
  },
  /**
   * Just as toTime, but does not return seconds
   */
  toShortTime: function() {
    var t = this.toTime();
    if (t.startsWith('0')) {
      t = t.substring(1);
    }
    while (t.endsWith(':00')) {
      t = t.cutoff(3);
    }
    return t;
  }
});

/**
 * Extension to the generic String Object.
 */
Object.extend(String.prototype, {
  contains: function(needle) {
    return this.indexOf(needle) != -1;
  },
  /**
   * @return int occurrences of needle within this String
   */
  count: function(needle) {
    var n = 0;
    var pos = -1;
    
    while ((pos = this.indexOf(needle, pos + 1)) != -1) {
      n++;
    } 
    
    return n;
  },
  /**
   * Cuts off n characters from the end of the String
   */
  cutoff: function(length) {
    return this.substring(0, this.length - length);
  },
  
  /**
   * Enlarges a string to match a desired size.
   * Appends suffix until at least the desired size is reached.
   * 
   * @return String
   */
  lengthen: function(len, suffix) {
    if (this.length >= len) {
      return this;
    }
    
    if (Object.isUndefined(suffix)) {
      suffix = ' ';
    }
    var re = this;
    while (re.length < len) {
      re += suffix;
    }
    if (re.length > len) {
      re = re.substring(0, len);
    }
    
    return re;
  },
  
  /**
   * dunno
   */
  parseObject: function(evalValues, separator, delimiter, escapeChar) {
    var bEval = (evalValues === true);
    var separ = separator || ",";
    var delim = delimiter || ":";
    var escha = escapeChar || "'";
    
    var re = new Object();
    
    var objects = this.split(separ);
    var phrase = null;
    
    for (var i = 0; i < objects.length; i++) {
      var sub = objects[i];
      if (phrase != null) {
        sub = phrase + separ + sub;
      }
      
      if (sub.count(escha).isOdd()) {
        phrase = sub;
      } else if (sub.length > 0) {
        phrase = null;
        
        var kvp = sub.strip().split(delim);
        var key = kvp.shift().strip();
        var value = kvp.join(delim).strip();
        
        if (bEval) {
          try {
            re[key] = eval(value);
          } catch (ex) {
            re[key] = key + ":" + ex.message;
          }
        } else {
          if (value.startsWith(escha) && value.endsWith(escha)) {
            value = value.substr(1, value.length - 2);
          }
          re[key] = value.gsub(escha + escha, escha);
        }
      }     
    }
    
    return re;
  },
  /**
   * Returns the integer-value this String contains
   * @return int
   */
  intval: function() {
    var s = 0; 
    
    while (this.charAt(s) == '0' && s < this.length - 1) {
      s++;
    };
    
    return parseInt(this.substring(s));
  },
  TIME_SPLIT: /(\d{2})(?:\:(\d{2})(?:\:(\d{2}))?)?/,
  /**
   * Returns the Number value for a time-string this String contains
   * @see Number.toTime
   * @return Number
   */
  floatime: function() {
    var m = this.TIME_SPLIT.exec(this);
    if (m == null) {
      return parseFloat(this);
    }
    
    var hours = m[1].intval();
    var minutes = (m[2] || "0").intval() || 0;
    var seconds = (m[3] || "0").intval() || 0;
    
    return hours 
      + minutes / 60 
      + seconds / 3600;
  },
  /**
   * Creates a XML-Tag from the current String
   * 
   * @return String
   */
  tag: function(attributes) {
    if (Object.isUndefined(attributes)) {
      attributes = { };
    } else if (Object.isString(attributes)) {
      attributes = {
        'class': attributes
      };
    }
    var re = [];
    
    re.push('<' + this);
    
    for (var e in attributes) {
      var ae = attributes[e];
      if (Object.isArray(ae)) {
        re.push(' ' + e + '="' + ae.join(' ') + '"');
      } else {
        re.push(' ' + e + '="' + ae + '"');
      }
    }
    
    re.push('>');
    
    return re.glue();
  },
  
  /**
   * Wraps the current String in a XML-Tag
   * 
   * &lt;tag attributes...&gt;this&lt;/tag&gt;
   * 
   * @see String.tag
   * @return String
   */
  wrap: function(tag, attributes) {
    if (Object.isUndefined(tag)) {
      tag = 'strong';
    }
    if (Object.isUndefined(attributes)) {
      attributes = { };
    }
    var re = [];
    
    re.push(tag.tag(attributes));
    re.push(this);
    re.push('</' + tag + '>');
    
    return re.glue();
  },
  /**
   * @return String this String plus a HTML line break
   */
  br: function() {
    return this + '<br />';
  },
  /**
   * @return String this String plus a HTML HR
   */
  hr: function() {
    return this + '<hr />';
  },
  /**
   * @return String a cliky from this String
   */
  clicky: function(onclick, options) {
    if (Object.isUndefined(options)) {
      options = {};
    }
    options.onclick = onclick;
    return this.wrap('a', options);
  }
});

/**
 * Extension to the generic Array Object.
 */
Object.extend(Array.prototype, {
  /**
   * String Function shortcut for this.join("");
   */
  glue: function() {
    return this.join("");
  },
  
  /**
   * contains test whether a specified needle element is part of this array
   * 
   * @param needle any object that is suspected to be within the array
   * @return true if the needle is part of the array, false otherwise
   */
  contains: function(needle, regex, part) {
    if (Object.isUndefined(regex)) {
      regex = false;
    }
    
    for (var i = 0; i < this.length; i++) {
      if (regex) {
        var match = needle.exec(this[i]);
        if (match != null) {
          if (Object.isUndefined(part)) {
            return this[i];
          } else {
            return match[part];
          }
        }
      } else {
        if (this[i] == needle) {
          return true;
        }
      }
    }
    
    return false;
  },
  getKeys: function(expr) {
    var re = [];
    
    if (Object.isUndefined(expr)) {
      for (var e in this) {
        re.push(e);
      }
    } else {
      for (var e in this) {
        if (e.match(expr) != null) {
          re.push(e);
        }
      }
    }
    
    return re;
  },
  /**
   * @return array containing the removed values;
   */
  removeValue: function(value, regex) {
    if (Object.isUndefined(regex)) {
      regex = false;
    }
    
    var re = [];
    
    for (var i = 0; i < this.length; i++) {
      var v = this[i];
      if (regex) {
        if (Object.isString(v) && value.exec(v) != null) {
          re.push(this.splice(i, 1));
          i--;
        }
      } else {
        if (v == value) {
          re.push(this.splice(i, 1));
          i--;
        }
      }
    }
    
    return re;
  },
  /**
   * wrap, just as its subfunctions prefix and suffix, modifies the values of an array.
   * 
   * A wrapped array will only contain string values.
   * Each element of the array will look like (prefix + value + suffix).
   * 
   * Example:
   * $A($R(1,4)).prefix("id_") will result in ["id_1", "id_2", "id_3", "id_4"]
   * 
   * @param string prefix the string to be put in front of the value
   * @param string suffix the string to be put behind the value
   * @return the modified Array  
   */
  wrap: function(prefix, suffix) {
    var p = prefix || '';
    var s = suffix || '';
    
    for (var i = 0; i < this.length; i++) {
      this[i] = p + this[i] + s;
    }
    
    return this;
  },
  
  prefix: function(str) {
    return this.wrap(str);
  },
  
  suffix: function(str) {
    return this.wrap('', str);
  },
  
  //
  // arithemetical functions for Arrays
  //
  
  intval: function() {
    var re = [];
    
    for (var i = 0; i < this.length; i++) {
      re.push(parseInt(Number(this[i])));
    }
    
    return re;
  },
  
  floatval: function() {
    var re = [];
    
    for (var i = 0; i < this.length; i++) {
      re.push(Number(this[i]));
    }
    
    return re;
  },
  
  sum: function() {
    var re = 0;
    
    for (var i = 0; i < this.length; i++) {
      re += this[i];
    }
    
    return re;
  },
  
  multiply: function() {
    if (this.length == 0) {
      return 0;
    }
    
    var re = this[0];
    for (var i = 1; i < this.length; i++) {
      re *= this[i];
    }
    return re;
  },
  
  getMinimum: function() {
    if (this.length == 0) {
      return null;
    }
    
    var re = this[0]; 
    
    for (var i = 1; i < this.length; i++) {
      re = Math.min(re, this[i]); 
    }
    
    return re;
  },
  
  getMaximum: function() {
    if (this.length == 0) {
      return null;
    }
    
    var re = this[0]; 
    
    for (var i = 1; i < this.length; i++) {
      re = Math.max(re, this[i]); 
    }
    
    return re;
  },
  
  getAverage: function() {
    return this.sum() / this.length;
  },
  
  getMedian: function(sortFunction) {
    if (this.length == 0) {
      return null;
    }
    if (this.length == 1) {
      return this[0];
    }
    
    var sl;
    if (!Object.isUndefined(sortFunction)) {
      sl = this.sort(sortFunction);
    } else {
      sl = this.sort();
    }
    
    return sl[(sl.length / 2).floor()];
  },
  
  getDifferentValues: function() {
    var re = 0;
    var last = null;
    var sorted = this.sort();
    
    for (var i = 0; i < this.length; ++i) {
      if (this[i] != last) {
        last = this[i];
        ++re;
      }
    }
    
    return re;
  },
  
  getStatistics: function() {
    if (this.length == 0) {
      return null;
    }
    
    return {
      minimum: this.getMinimum(),
      maximum: this.getMaximum(),
      average: this.getAverage(),
      median:  this.getMedian(),
      values:  this.getDifferentValues()
    }
  },

  /**
   * Checks whether all elements of this array are the same as a compare value.
   * If the compare value is a function the values of the array will be passed as the first argument (like in Array.each()).
   * 
   * @param compare may be either a value to check agains or an unary boolean return function
   * @return boolean
   */
  isAll: function(compare) {
    if (Object.isFunction(compare)) {
      for (var i = 0; i < this.length; i++) {
        if (!compare(this[i])) {
          return false;
        }
      }
      return true;
    } else {
      for (var i = 0; i < this.length; i++) {
        if (this[i] != compare) {
          return false;
        }
      }
      return true;
    }
  },
  
  /**
   * Nearly the same as Array.isAll, but stops as soon as the first match is found.
   * 
   * @param compare may be either a value to check against or an unary boolean return function
   * @return boolean
   */
  hasAny: function(compare) {
    if (Object.isFunction(compare)) {
      for (var i = 0; i < this.length; i++) {
        if (compare(this[i])) {
          return true;
        }
      }
      return false;
    } else {
      for (var i = 0; i < this.length; i++) {
        if (this[i] == compare) {
          return true;
        }
      }
      return false;
    }
  }
});

/**
 * Extension to the generic Function
 */
Object.extend(Function.prototype, {
  times: function() {
    var __method = this;
    var args = $A(arguments);
    var count = args.shift();
    
    var re = [];
    
    for (var i = 0; i < count; i++) {
      re.push(__method.apply(__method, args));
    }
    
    return re;
  }
});

/**
 * Extension to the Element.Methods
 */
Object.extend(Element.Methods, {
  /**
   * Moves an Element to a specified (x,y) coordinate
   * 
   * @return Element the Element itself
   */
  moveTo: function(element, x, y) {
    var s = {};
    if (Object.isElement(x)) {
      var pos = x.positionedOffset();
      var s = {
        'top':  pos[1],
        'left': pos[0]
      };
    } else {
      var pt = $P(x, y);
      s = {
        'top':  pt.y,
        'left': pt.x
      };
    }
    
    element.style.left = s.left + 'px';
    element.style.top  = s.top  + 'px';
    
    return element;
  },
  /**
   * Moves an element right below the lower edge of another Element
   * 
   * @return Element the Element itself
   */
  moveBelow: function(element, target) {
    var ptTarget = $P(target);
    var dTarget = $D(target); 
    element.style.top = (ptTarget.x + dTarget.height) + 'px';
    
    return element;
  },
  /**
   * Returns the CenterPoint of an Element
   * 
   * @return Point
   */
  getCenter: function(element) {
    return $P(element).moveBy($D(element).getCenter());
  },
  /**
   * Moves an Element so that it will be right above the center of its Parental Layout Element
   */
  centerOnParent: function(element) {
    var parent = element.getOffsetParent();
    var pd = $D(parent);
    var ed = $D(element);
    var nc = pd.subtract(ed).divide(2);
    
    return element.moveTo(nc);
  },
  /**
   * Returns true when a Point is within the absolute range of this Element
   * 
   * @return boolean
   */
  containsPoint: function(element, x, y) {
    var pt = element.getRelativePosition(x, y);
    var dim = $D(element);

    return pt.x >= 0 && pt.x <= dim.getWidth()
        && pt.y >= 0 && pt.y <= dim.getHeight();
  },
  /**
   * 
   */
  getRelativePosition: function(element, x, y) {
    return $P(x, y).subtract(element); 
  },
  
  /**
   * lets the first element mimic the style of the second
   * 
   * @return Element the Element itself
   */
  fit: function(element, target) {
    element.setStyle(target.getStyles());
    
    return element;
  },
  
  /**
   * enables/disables an element
   * 
   * @return Element the Element itself
   */
  setEnabled: function(element, enabled) {
    if (enabled !== false) {
      element.removeAttribute("disabled");
    } else {
      element.setAttribute("disabled", "disabled");
    }
    
    return element;
  },
  
  /**
   * property returning the enabled status of an Element
   * @return boolean
   */
  isEnabled: function(element) {
    return element.hasAttribute("disabled") == false;
  },
  
  /**
   * just the same as isEnabled
   * @return boolean
   */
  enabled: function(element) {
    return element.isEnabled();
  }
});

/**
 * PHP time function copy
 * @return Number the number of seconds passed since 1970-01-01 00:00:00
 */
function time() { return Math.floor((new Date()).getTime() / 1000) };
Object.extend(Date.prototype, {
  sqlDateMatch: /(\d{2,4})-(\d{2})-(\d{2})(?: (\d{2})\:(\d{2})\:(\d{2}))?/,
  fromSQL: function(date) {
    var m = this.sqlDateMatch.exec(date);
    if (m == null) {
      return this;
    }
    
    var year = m[1].intval();
    var month = m[2].intval();
    var day = m[3].intval();
    var hours = m[4] || 0;
    var minutes = m[5] || 0;
    var seconds = m[6] || 0;
    
    return new Date(year, month - 1, day, hours, minutes, seconds);
  },
  toSQL: function() {
    return this.toSQLDate() + ' ' + this.toSQLTime();
      
  },
  toSQLTime: function() {
    return this.getHours().toPaddedString(2) + ':' + 
      this.getMinutes().toPaddedString(2) + ':' +
      this.getSeconds().toPaddedString(2);
  },
  toSQLDate: function() {
    return this.getFullYear().toPaddedString(4) +
      '-' + (this.getMonth() + 1).toPaddedString(2) +
      '-' + this.getDate().toPaddedString(2);
  },
  moveByDays: function(days) {
    this.setDate(this.getDate() + days);
    return this;
  },
  moveByWeeks: function(weeks) {
    this.setDate(this.getDate() + weeks * 7);
    return this;
  },
  moveByMonths: function(months) {
    this.setMonth(this.getMonth() + months);
    return this;
  },
  getNextDay: function() {
    return (new Date(this)).moveByDays(1);
  },
  getPreviousDay: function() {
    return (new Date(this)).moveByDays(-1);
  },
  getRelativeDate: function(months, weeks, days) {
    var re = new Date(this);
    re.moveByMonths(months || 0);
    re.moveByWeeks(weeks || 0);
    re.moveByDays(days || 0);
    
    return re;
  },
  /**
   * @deprecated use equalsDate instead
   */
  sameDay: function(date) {
    return this.equalsDate(date);
  },
  equalsDate: function(date) {
    if (date instanceof Date) {
      return this.getFullYear() == date.getFullYear() 
        && this.getMonth() == date.getMonth()
        && this.getDate() == date.getDate();
    }
    return false;
  },
  /**
   * equalizes the dates of two 'Date' Objects, but keeps the time.
   */
  setSameDay: function(date) {
    if (date instanceof Date) {
      this.setFullYear(date.getFullYear());
      this.setMonth(date.getMonth());
      this.setDate(date.getDate());
    }
    
    return this;
  },
  isMonday: function() {
    return this.getDay() == 1;
  },
  isTuesday: function() {
    return this.getDay() == 2;
  },
  isWednesday: function() {
    return this.getDay() == 3;
  },
  isThursday: function() {
    return this.getDay() == 4;
  },
  isFriday: function() {
    return this.getDay() == 5;
  },
  isSaturday: function() {
    return this.getDay() == 6;
  },
  isSunday: function() {
    return this.getDay() == 0;
  }
});
/**
 * Converts the parameter date to a Date.
 * If it is a Date already then exactly that Date will be returned. 
 */
function $DT(date) {
  if (Object.isUndefined(date)) {
    return new Date();
  } else if (date instanceof Date) {
    return date;
  } else if (Object.isString(date)) {
    return (new Date()).fromSQL(date);
  }
  return new Date(date);
}

/**
 * Numeric Interval
 */
Interval = Class.create();
Object.extend(Interval.prototype, {
  pattern: /(\[|\])?(\d+(?:\.\d+)?)(?:,|;|-)(\d+(?:\.\d+)?)(\[|\])?/,
  initialize: function(start, end) {
    if (Object.isArray(start) && start.length == 2) {
      end = start[1];
      start = start[0];
    } 
    
    if (Object.isString(start)) {
      this.unserialize(start);
    } else {
      if (Object.isUndefined(end)) {
        end = start;
        start = 0;
      }
      
      this.setStart(start);
      this.setEnd(end);
      this.includeStart = true;
      this.includeEnd = true;
    }
  },
  getStart: function() {
    return this.start;
  },
  setStart: function(start) {
    this.start = start;
  },
  getEnd: function() {
    return this.end;
  },
  setEnd: function(end) {
    this.end = end;
  },
  getLength: function() {
    return this.end - this.start;
  },
  moveBy: function(delta) {
    this.start += delta;
    this.end   += delta;
    return this;
  },
  multipy: function(m) {
    this.start *= m;
    this.end   *= m;
    return this;
  },
  contains: function(v) {
    var start = this.getStart();
    var end = this.getEnd();

    var testStart = true;
    var testEnd = true;

    var sv = v;
    var ev = v;
    
    if (v instanceof Interval) {
      sv = v.getStart();
      if (sv == start && v.includeStart == this.includeStart) {
        testStart = false;
      }

      ev = v.getEnd();
      if (ev == end && v.includeEnd == this.includeEnd) {
        testEnd = false;
      }
    }
    
    if (testStart && ((this.includeStart && start > sv) || (!this.includeStart && start >= sv))) {
      return false;
    }
    if (testEnd && ((this.includeEnd && end < ev) || (!this.includeEnd && end <= ev))) {
      return false;
    }
    
    return true;
  },
  combine: function(min, max) {
    var interval = $I(min, max);
    return new Interval(this.getStart().min(interval.getStart()), this.getEnd().max(interval.getEnd()));
  },
  intersect: function(min, max) {
    var interval = $I(min, max);
    return new Interval(this.getStart().max(interval.getStart()), this.getEnd().min(interval.getEnd()))
  },
  serialize: function() {
    var re =  
      (this.includeStart ? '[' : ']') +
      this.getStart() + 
      ',' +
      this.getEnd() +
      (this.includeEnd ? ']' : '[')
    ;
    
    return re;
  },
  random: function() {
    return Math.random() * (this.getEnd() - this.getStart()) + this.getStart();
  },
  ran: function() {
    return this.random().round();
  },
  snap: function(n) {
    return n.max(this.getStart()).min(this.getEnd());
  },
  unserialize: function(interval) {
    var m = this.pattern.exec(interval);
    if (m == null) {
      return false;
    }
    
    this.start = parseFloat(m[2]);
    this.end = parseFloat(m[3]);
    
    this.includeStart = m[1] != ']';
    this.includeEnd = m[4] != '[';
  },
  toString: function() {
    return "Interval(" + this.serialize() + ")";
  }
});

function $I(start, end) {
  if (Object.isUndefined(start) || start == null) {
    return null;
  } else if (start instanceof Interval) {
    return start;
  } else {
    return new Interval(start, end);
  }
}

/**
 * Stores a Time-Interval in Hours
 * 
 * this.start and this.duration contain hour values
 */
TimeInterval = Class.create();
Object.extend(TimeInterval.prototype, {
  initialize: function(start, duration) {
    if (Object.isString(start)) {
      this.unserialize(start);
    } else {
      this.setStartTime(start);
      this.setDuration(duration);
    }
  },
  
  setStartTime: function(start) {
    if (start instanceof Date) {
      this.start = start.getHours() 
        + start.getMinutes() / 60 
        + start.getSeconds() / 3600
    } else if (Object.isString(start)) {
      this.start = start.floatime();
    } else {
      this.start = start;
    }
  },
  
  getStartTime: function() {
    return this.start;
  },
  
  getEndTime: function() {
    return this.start + this.duration;
  },

  getDuration: function() {
    return this.duration;
  },
  
  setDuration: function(duration) {
    this.duration = duration;
  },
  
  getDurationMinutes: function() {
    return this.duration * 60;
  },
  
  setDurationMinutes: function(minutes) {
    this.duration = minutes / 60;
  },
  
  toTime: function(glue) {
    if (this.duration == 0) {
      return '';
    }
    
    var re = [this.getStartTime().toTime(null, null, 2)];
    if (this.duration > 0) {
      re.push(this.getEndTime().toTime(null, null, 2));
    }
    return re.join(Object.isUndefined(glue) ? ' - ' : glue);
  },
  
  serialize: function() {
    return this.getStartTime().toShortTime() + '-' + this.getEndTime().toShortTime();
  },
  
  unserialize: function(description) {
    if (Object.isUndefined(description) || description == null || description.strip().length == 0) {
      this.start = 0;
      this.duration = 0;
      return;
    }
    var split = description.split('-');
    this.start = split[0].floatime();
    var to = split[1].floatime();
    this.duration = to - this.start;
  },
  
  equals: function(otherInterval) {
    if (!(otherInterval instanceof TimeInterval)) {
      return false;
    }
    
    return this.start == otherInterval.start 
      && this.duration == otherInterval.duration;
  },
  
  toInterval: function() {
    return new Interval(this.start, this.start + this.duration);
  },
  
  toString: function() {
    return 'TimeInterval(' + this.serialize() + ')';
  }
});

/**
 * A ValueBag supports param, iparam, bparam commands
 */
var ValueBag = Class.create(Hash, {});
Object.extend(ValueBag.prototype, {
  /**
   * Default Parameter Getter Mehtod.
   * 
   * @param name the name of the requested object
   * @param defaultValue a default value, null if omitted
   * @return the value of name, null if name could not be found 
   */
  param: function(name, defaultValue) {
    if (Object.isUndefined(this.get(name))) {
      return defaultValue || null;
    }
    
    return this._object[name];
  },
  
  /**
   * Returns an integer value with 0 as fallback
   */
  iparam: function(name, defaultValue) {
    return parseInt(this.param(name, defaultValue)) || 0;
  },
  
  /**
   * Returns a boolean value with false as fallback
   */
  bparam: function(name, defaultValue) {
    return this.param(name, defaultValue) != false;
  },
  
  /**
   * Returns an array value with an empty array as fallback 
   */
  aparam: function(name, defaultValue) {
    return this.param(name, defaultValue) || [];
  },
  
  /**
   * Returns a function value, with Prototype.emptyFunction as fallback
   */
  fparam: function(name, defaultValue) {
    return this.param(name, defaultValue) || Prototype.emptyFunction;
  },
  
  /**
   * Returns a Prototype Hash value with an empty Hash as fallback
   */
  hparam: function(name, defaultValue) {
    return this.param(name, defaultValue) || $H();
  },
  
  /**
   * Overloads all Function in an object with the functions from this data
   */
  overload: function(o) {
    for (var m in this._object) {
      if (Object.isFunction(this._object[m])) {
        o[m] = this._object[m];
      }
    }
    
    return o;
  },
  
  /**
   * Returns another ValueBag for a desired child object
   */
  bag: function(name) {
    return $V(this.get(name));
  }
});

function $V(object) {
  if (object instanceof ValueBag) {
    return object;
  }
  return new ValueBag(object);
}

Element.addMethods({
  setEnabled: function(element, enabled) {
    if (enabled !== false) {
      element.removeAttribute('disabled');
    } else {
      element.setAttribute('disabled', 'disabled');
    }
  },
  
  isEnabled: function(element) {
    return element.hasAttribute('disabled') == false;
  },
  
  enabled: function(element) {
    return element.isEnabled();
  }
});

var Point = Class.create();
Object.extend(Point.prototype, {
  initialize: function(x,y) {
    if (Object.isUndefined(x)) {
      this.x = 0;
      this.y = 0;
    } else if (Object.isArray(x)) {
      this.x = x[0];
      this.y = x[1];
    } else if ((x instanceof Point) || (typeof(x) == 'object')) {
      this.x = x.x;
      this.y = x.y;
    } else {
      this.x = x;
      if (Object.isUndefined(y)) {
        this.y = x;
      } else {
        this.y = y;
      }
    }
  },
  
  getX: function() {
    return this.x || 0;
  },
  
  setX: function(x) {
    this.x = x;
    return this;
  },
  
  getY: function() {
    return this.y || 0;
  },
  
  setY: function(y) {
    this.y = y;
    return this;
  },
  
  clone: function() {
    return new Point(this.getX(), this.getY());
  },
  
  map: function(method, x, y) {
    if (Object.isUndefined(x, y)) {
      this.x = method(this.x);
      this.y = method(this.y);
    } else {
      var pt = $(x, y);
      this.x = method(this.x, pt.x);
      this.y = method(this.y, pt.y);
    }
    
    return this;
  },
  
  moveTo: function(x, y) {
    var pt = $P(x, y);
    this.x = pt.x;
    this.y = pt.y;
    return this;
  },
  
  /**
   * A destructive add
   * 
   * @return Point this point
   */
  moveBy: function(x, y) {
    var pt = $P(x, y);
    this.x += pt.x;
    this.y += pt.y;
    return this;
  },
  
  invert: function(destructive) {
    if (Object.isUndefined(destructive)) {
      destructive = false;
    }
    if (destructive) {
      this.x *= -1;
      this.y *= -1;
      return this;
    }
    return this.clone().invert(true);
  },
  
  transpose: function(destructive) {
    if (Object.isUndefined(destructive)) {
      destructive = false;
    }
    if (destructive) {
      this.x = 1 / this.x;
      this.y = 1 / this.y;
      return this;
    }
    return this.clone().transpose(true);
  },
  
  swap: function() {
    var t = this.x;
    this.x = this.y;
    this.y = t;
    
    return this;
  },
  
  floor: function() {
    return this.clone().map(Math.floor);
  },
  
  round: function() {
    return this.clone().map(Math.round);
  },
  
  ceil: function() {
    return this.clone().map(Math.ceil);
  },

  add: function(x, y) {
    var pt = $P(x, y);
    return new Point(this.x + pt.x, this.y + pt.y);
  },
  
  subtract: function(x, y) {
    return this.add($P(x, y).invert());
  },
  
  multiply: function(x, y) {
    var pt = $P(x, y);
    return this.clone.moveTo(this.getX() * pt.getX(), this.getY() * pt.getY());
  },
  
  divide: function(x, y) {
    var pt = $P(x, y);
    return this.clone().moveTo(this.getX() / pt.getX(), this.getY() / pt.getY());
  },
  
  normalize: function(width, height) {
    var dim = $D(width, height);
    if (dim.getArea() > 0) {
      return new Point(
        this.x / dim.getWidth(), 
        this.y / dim.getHeight()
      );
    }
    return this;
  },
  
  min: function(x, y) {
    return this.clone().map(Math.min, x, y);
  },
  
  max: function(x, y) {
    return this.clone().map(Math.max, x, y);
  },
  
  distanceTo: function(x, y) {
    return this.subtract(x, y).getLength();
  },
  
  getLength: function() {
    return Math.sqrt(this.x.pow(2) + this.y.pow(2));
  },
  
  toString: function() {
    return 'Point(' + this.x + ', ' + this.y + ')';
  }
});

function $P(x, y) {
  if (Object.isElement(x) || (Object.isString(x) && !Object.isUndefined($(x)))) {
    return new Point($(x).cumulativeOffset());
  }
  if (typeof(x) == 'object' && x['pointer']) {
    return new Point(x.pointer());
  }
  if (x instanceof Point) {
    return x;
  }
  return new Point(x, y);
}

var Dimensions = Class.create(Point, {
  initialize: function($super, width, height) {
    if (typeof(width) == 'object') {
      try {
        this.moveTo(width.width, width.height);
      } catch (ex) {
        this.moveTo(width, height);
      }
    } else {
      this.moveTo(width, height);
    }
  }
});
Object.extend(Dimensions.prototype, {
  clone: function() {
    return new Dimensions(this.getWidth(), this.getHeight());
  },
  setWidth: function(width) {
    this.x = width;
    return this;
  },
  
  setHeight: function(height) {
    this.y = height;
    return this;
  },
  
  getWidth: function() {
    return this.x;
  },
  
  getHeight: function() {
    return this.y;
  },
  
  getArea: function() {
    return this.getWidth() * this.getHeight();
  },
  
  getCenter: function() {
    return this.divide(2);
  },
  
  toString: function() {
    return 'Dimensions(' + this.getWidth() + ', ' + this.getHeight() + ')';
  }
});

function $D(w, h) {
  if (Object.isElement(w) || (Object.isString(w) && !Object.isUndefined($(w)))) {
    return new Dimensions($(w).getDimensions());
  }
  if (w instanceof Dimensions) {
    return w;
  }
  return new Dimensions(w, h);
}

/**
 * What is the Matrix?
 * 
 * a default two dimension table
 */
var Matrix = Class.create();
Object.extend(Matrix.prototype, {
  OPTION_NO_FILL: 0x10,
  
  //
  // init
  //
  initialize: function(width, height, value, options) {
    if (width instanceof Matrix) {
      this.width = width.width;
      this.height = width.height;
      this.data = [];
      for (var i = 0; i < width.data.length; i++) {
        this.data.push(width.data[i]);
      }
      return this;
    }
    
    // just expect width and height to be int values for now
    this.width = width;
    this.height = height;
    
    // initialize the data array (one dimension long array)
    this.data = [];
    
    if (Object.isUndefined(options)) {
      options = 0;
    }
    if (!(options & this.OPTION_NO_FILL)) {
      if (Object.isFunction(value)) {
        this.fill(null);
        this.map(value, true);
      } else {
        this.fill(value);
      }
    }
  },
  
  clone: function() {
    return new Matrix(this);
  },
  
  toArray: function() {
    var re = [];
    var n = 0;
    for (var j = 0; j < this.height; j++) {
      var row = [];
      for (var i = 0; i < this.width; i++, n++) {
        row.push(this.data[n]);
      }
      re.push(row);
    }
    
    return re;
  },
  
  getWidth: function() {
    return this.width;
  },
  
  getHeight: function() {
    return this.height;
  },
  
  getSize: function() {
    return this.width * this.height;
  },
  
  set: function(x, y, v) {
    if (x > this.widht || y > this.height) {
      throw new Error('MatrixIndexOutOfBounds', 'Matrix.set(' + x + ', ' + y + ') is not in (' + this.width + ', ' + this.height + ')');
    }
    var pos = this.width * y + x;
    var old = this.data[pos];
    this.data[pos] = v;
    return old;
  },
  
  // no-check internal set (should be faster)
  _set: function(x, y, v) {
    this.data[this.width * y + x] = v;
  },
  
  get: function(x, y) {
    var pos = this.width * y + x;
    if (pos < 0 || pos > this.data.length) {
      throw new Error('MatrixIndexOutOfBounds', 'nMatrix.get(' + x + ', ' + y + ') is not in (' + this.width + ', ' + this.height + ')');
    }
    return this.data[pos];
  },

  // no-check internal get (should be faster)
  _get: function(x, y) {
    return this.data[this.width * y + x];
  },
  
  fill: function(value) {
    if (Object.isUndefined(value)) {
      value = null;
    }
    var size = this.getSize();
    // initialize the maxrix
    for (var i = 0; i < size; i++) {
      this.data[i] = value;
    }
  },
  
  getColumn: function(column) {
    var re = [];
    for (var i = 0; i < this.height; i++) {
      re.push(this._get(column, i));
    }
    
    return re;
  },
  
  getRow: function(row) {
    var re = [];
    for (var i = 0; i < this.width; i++) {
      re.push(this._get(i, row));
    }
    
    return re;
  },
  
  setBlock: function(x, y, width, height, value) {
    for (var i = 0; i < width; i++) {
      for (var j = 0; j < height; j++) {
        this._set(x + i, y + j, value);
      }
    }
  },
  
  getBlock: function(x, y, width, height) {
    var m = new Matrix(width, height);
    for (var i = 0; i < width; i++) {
      for (var j = 0; j < height; j++) {
        m._set(i, j, this.get(i + x, j + y));
      }
    }
    
    return m;
  },
  
  getAllObjects: function(type) {
    if (Object.isUndefined(type)) {
      typeFunction = function(o) { return typeof(o) == 'object'; };
    } else if (Object.isFunction(type)) {
      typeFunction = type;
    } else if (Object.isString(type)) {
      typeFunction = function(o) { return o instanceof eval(type); };
    } else {
      typeFunction = function(o) { return o instanceof type; };
    }

    var re = [];
    for (var i = 0; i < this.data.length; i++) {
      if (typeFunction(this.data[i])) {
        re.push(this.data[i]);
      }
    }
    
    return re;
  },
  
  indexOf: function(value, x, y) {
    var start = 0;
    if (!Object.isUndefined(x)) {
      var pt = $P(x, y);
      start = start.max(pt.y * this.width + pt.x);
    }
    for (var i = start; i < this.data.length; i++) {
      if (this.data[i] == value) {
        return [i % this.width, (i / this.width).floor()];
      }
    }
    return -1;
  },
  
  lastIndexOf: function(value, x, y) {
    var start = this.data.length - 1;
    if (!Object.isUndefined(x)) {
      var pt = $P(x, y);
      start = start.min(pt.y * this.width + pt.x);
    }
    for (var i = start; i >= 0; i--) {
      if (this.data[i] == value) {
        return [i % this.width, (i / this.width).floor()];
      }
    }
    return -1;
  },
  
  /**
   * overrides a subset of this matrix with another (matrix overlay)
   */
  insert: function(x, y, matrix) {
    // expect the matrix to fit this matrix
    var mw = matrix.getWidth();
    var mh = matrix.getHeight();
    
    for (var j = 0; j < hm; j++) {
      for (var i = 0; i < mw; i++) {
        // slow but simple deep copy
        // could be made faster betimes
        this._set(x + i, y + j, matrix._get(i, j));
      }
    }
    
    return this;
  },
  
  /**
   * a subset matrix merge, non-destructive by default
   */
  merge: function(x, y, matrix, method, destructive) {
    if (Object.isUndefined(matrix) || matrix == null) {
      matrix = this.clone();
    }
    
    if (Object.isUndefined(destructive)) {
      destructive = false;
    }
    
    var m;
    if (destructive) {
      m = this;
    } else {
      m = new Matrix(this.width, this.height, null, this.OPTIONS_NO_FILL);
    }
    
    // expect the matrix to fit this matrix
    for (var j = 0; j < this.height; j++) {
      for (var i = 0; i < this.width; i++) {
        // slow but simple deep copy
        // could be made faster betimes
        var one = this._get(x + i, y + j);
        var two = matrix._get(i, j);
        m._set(x + i, y + j, method(one, two));
      }
    }
    
    return m;
  },
  
  /**
   * a functional mapping for all values in this matrix,
   * the arguments passed to the method are (sourceValue, x, y)
   */
  map: function(method, destructive) {
    if (Object.isUndefined(destructive)) {
      destructive = false;
    }
    
    var m;
    if (destructive) {
      m = this;
    } else {
      m = new Matrix(this.width, this.height, null, this.OPTIONS_NO_FILL);
    }
    
    var size = this.getSize();
    var x = -1; 
    var y = -1;
    for (var i = 0; i < size; i++) {
      if (i % this.width == 0) {
        x = 0;
        y++;
      } else {
        x++;
      }
      m.data[i] = method(this.data[i], x, y); 
    }
    
    return m;
  },
  
  //
  // convenience merge methods
  //
  
  getAndMatrix: function(matrix, destructive) {
    return this.merge(0, 0, matrix, Matrix.Methods.and, destructive);
  },
  
  getOrMatrix: function(matrix, destructive) {
    return this.merge(0, 0, matrix, Matrix.Methods.or, destructive);
  },
  
  getXorMatrix: function(matrix, destructive) {
    return this.merge(0, 0, matrix, Matrix.Methods.xor, destructive);
  },
  
  getNorMatrix: function(matrix, destructive) {
    return this.merge(0, 0, matrix, Matrix.Methods.nor, destructive);
  },

  getNandMatrix: function(matrix, destructive) {
    return this.merge(0, 0, matrix, Matrix.Methods.nand, destructive);
  },
  
  getNotMatrix: function(destructive) {
    return this.map(Matrix.Methods.not, destructive);
  },
  
  //
  // data status methods
  //
  isEmpty: function(emptyCompareFunction) {
    if (Object.isUndefined(emptyCompareFunction)) {
      emptyCompareFunction = Matrix.Methods.empty;
    }
    return this.isAll(emptyCompareFunction);
  },
  
  isAll: function(compare) {
    /*
    if (Object.isFunction(compare)) {
      for (var i = 0; i < this.data.length; i++) {
        if (!compare(this.data[i])) {
          return false;
        }
      }
      return true;
    } else {
      for (var i = 0; i < this.data.length; i++) {
        if (this.data[i] != compare) {
          return false;
        }
      }
      return true;
    }
    */
    return this.data.isAll(compare);
  },
  
  /**
   * Checks whether there is any data within a defined block
   * 
   * @return boolean
   */
  hasDataAt: function(x, y, width, height, compareFunction) {
    if (Object.isUndefined(compareFunction)) {
      compareFunction = Matrix.Methods.notEmpty;
    }
    var args = $A(arguments);
    x = x || 0;
    y = y || 0;
    
    if (x + width > this.width || y + height > this.height) {
      throw new Error('MatrixIndexOutOfBounds', 'Matrix.hasDataAt(' + args.join(', ') + ') is not in (' + this.width + ', ' + this.height + ')');
    }
     
    if (!Object.isNumber(x) || !Object.isNumber(y)) {
      return false;
    }
    
    for (var i = 0; i < width; i++) {
      for (var j = 0; j < height; j++) {
        if (compareFunction(this._get(x + i, y + j))) {
          return true;
        }
      }
    }
    
    return false;
  },
  
  countValuesInRow: function(row, compareFunction) {
    if (Object.isUndefined(compareFunction)) {
      compareFunction = Matrix.Methods.notEmpty;
    }
    var re = 0;
    for (var i = 0; i < this.width; i++) {
      if (compareFunction(this._get(i, row))) {
        re++;
      }
    }
    
    return re;
  },
  
  countValuesInColumn: function(column, compareFunction) {
    if (Object.isUndefined(compareFunction)) {
      compareFunction = Matrix.Methods.notEmpty;
    }
    var re = 0;
    for (var i = 0; i < this.height; i++) {
      if (compareFunction(this._get(column, i))) {
        re++;
      }
    }
    
    return re;
  },
  
  countMaxValuesInRows: function(startRow, rows) {
    var maxVal = 0;
    for (var i = startRow; i < startRow + rows; i++) {
      maxVal = maxVal.max(this.countValuesInRow(i));
    }
    return maxVal;
  },
  
  toBinaryOut: function() {
    var f = function(v) { return v ? '+' : '-' };
    return this.map(f).toArray().join('');
  },
  
  toString: function() {
    return 'Matrix(' + this.width + ' x ' + this.height + ')';
  }
});

Matrix.Methods = {
  and: function(one, two) {
    return one & two;
  },
  
  or: function(one, two) {
    return one | two;
  },
  
  xor: function(one, two) {
    return one ^ two;
  },
  
  nor: function(one, two) {
    return !(one | two);
  },
  
  nand: function(one, two) {
    return !(one & two);
  },
  
  not: function(value) {
    return !value;
  },
  
  empty: function(value) {
    if (Object.isUndefined(value) || value == null) {
      return true;
    }
    if (Object.isString(value) && value.strip().length == 0) {
      return true;
    }
    if (Object.isNumber(value) && parseInt(value) == 0) {
      return true;
    }
    if (Object.isArray() && value.length == 0) {
      return true;
    }
    return false;
  },
  
  notEmpty: function(value) {
    return !Matrix.Methods.empty(value);
  }
};

$M = function() {
  var args = $(arguments);
  if (args[0] instanceof Matrix) {
    return args[0];
  } else {
    return new Matrix.apply(this, args);
  }
}

function passClick(e, method) {
  if (Object.isUndefined(method)) {
    method = 'onclick';
  }
  var activators = this.select('a,button');
  if (activators.length > 0) {
    activators[0][method]();
  }
  
  e.stop();
  
  return false;
}

