/*  Prototype JavaScript framework, version 1.6.1
 *  (c) 2005-2009 Sam Stephenson
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://www.prototypejs.org/
 *
 *--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.6.1',

  Browser: (function(){
    var ua = navigator.userAgent;
    var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]';
    return {
      IE:             !!window.attachEvent && !isOpera,
      Opera:          isOpera,
      WebKit:         ua.indexOf('AppleWebKit/') > -1,
      Gecko:          ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1,
      MobileSafari:   /Apple.*Mobile.*Safari/.test(ua)
    }
  })(),

  BrowserFeatures: {
    XPath: !!document.evaluate,
    SelectorsAPI: !!document.querySelector,
    ElementExtensions: (function() {
      var constructor = window.Element || window.HTMLElement;
      return !!(constructor && constructor.prototype);
    })(),
    SpecificElementExtensions: (function() {
      if (typeof window.HTMLDivElement !== 'undefined')
        return true;

      var div = document.createElement('div');
      var form = document.createElement('form');
      var isSupported = false;

      if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) {
        isSupported = true;
      }

      div = form = null;

      return isSupported;
    })()
  },

  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',

  emptyFunction: function() { },
  K: function(x) { return x }
};

if (Prototype.Browser.MobileSafari)
  Prototype.BrowserFeatures.SpecificElementExtensions = false;


var Abstract = { };


var Try = {
  these: function() {
    var returnValue;

    for (var i = 0, length = arguments.length; i < length; i++) {
      var lambda = arguments[i];
      try {
        returnValue = lambda();
        break;
      } catch (e) { }
    }

    return returnValue;
  }
};

/* Based on Alex Arnell's inheritance implementation. */

var Class = (function() {
  function subclass() {};
  function create() {
    var parent = null, properties = $A(arguments);
    if (Object.isFunction(properties[0]))
      parent = properties.shift();

    function klass() {
      this.initialize.apply(this, arguments);
    }

    Object.extend(klass, Class.Methods);
    klass.superclass = parent;
    klass.subclasses = [];

    if (parent) {
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);

    if (!klass.prototype.initialize)
      klass.prototype.initialize = Prototype.emptyFunction;

    klass.prototype.constructor = klass;
    return klass;
  }

  function addMethods(source) {
    var ancestor   = this.superclass && this.superclass.prototype;
    var properties = Object.keys(source);

    if (!Object.keys({ toString: true }).length) {
      if (source.toString != Object.prototype.toString)
        properties.push("toString");
      if (source.valueOf != Object.prototype.valueOf)
        properties.push("valueOf");
    }

    for (var i = 0, length = properties.length; i < length; i++) {
      var property = properties[i], value = source[property];
      if (ancestor && Object.isFunction(value) &&
          value.argumentNames().first() == "$super") {
        var method = value;
        value = (function(m) {
          return function() { return ancestor[m].apply(this, arguments); };
        })(property).wrap(method);

        value.valueOf = method.valueOf.bind(method);
        value.toString = method.toString.bind(method);
      }
      this.prototype[property] = value;
    }

    return this;
  }

  return {
    create: create,
    Methods: {
      addMethods: addMethods
    }
  };
})();
(function() {

  var _toString = Object.prototype.toString;

  function extend(destination, source) {
    for (var property in source)
      destination[property] = source[property];
    return destination;
  }

  function inspect(object) {
    try {
      if (isUndefined(object)) return 'undefined';
      if (object === null) return 'null';
      return object.inspect ? object.inspect() : String(object);
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  }

  function toQueryString(object) {
    return $H(object).toQueryString();
  }

  function toHTML(object) {
    return object && object.toHTML ? object.toHTML() : String.interpret(object);
  }

  function keys(object) {
    var results = [];
    for (var property in object)
      results.push(property);
    return results;
  }

  function values(object) {
    var results = [];
    for (var property in object)
      results.push(object[property]);
    return results;
  }

  function clone(object) {
    return extend({ }, object);
  }

  function isElement(object) {
    return !!(object && object.nodeType == 1);
  }

  function isArray(object) {
    return _toString.call(object) == "[object Array]";
  }


  function isHash(object) {
    return object instanceof Hash;
  }

  function isFunction(object) {
    return typeof object === "function";
  }

  function isString(object) {
    return _toString.call(object) == "[object String]";
  }

  function isNumber(object) {
    return _toString.call(object) == "[object Number]";
  }

  function isUndefined(object) {
    return typeof object === "undefined";
  }

  extend(Object, {
    extend:        extend,
    inspect:       inspect,
    toQueryString: toQueryString,
    toHTML:        toHTML,
    keys:          keys,
    values:        values,
    clone:         clone,
    isElement:     isElement,
    isArray:       isArray,
    isHash:        isHash,
    isFunction:    isFunction,
    isString:      isString,
    isNumber:      isNumber,
    isUndefined:   isUndefined
  });
})();
Object.extend(Function.prototype, (function() {
  var slice = Array.prototype.slice;

  function update(array, args) {
    var arrayLength = array.length, length = args.length;
    while (length--) array[arrayLength + length] = args[length];
    return array;
  }

  function merge(array, args) {
    array = slice.call(array, 0);
    return update(array, args);
  }

  function argumentNames() {
    var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1]
      .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '')
      .replace(/\s+/g, '').split(',');
    return names.length == 1 && !names[0] ? [] : names;
  }

  function bind(context) {
    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
    var __method = this, args = slice.call(arguments, 1);
    return function() {
      var a = merge(args, arguments);
      return __method.apply(context, a);
    }
  }

  function bindAsEventListener(context) {
    var __method = this, args = slice.call(arguments, 1);
    return function(event) {
      var a = update([event || window.event], args);
      return __method.apply(context, a);
    }
  }

  function curry() {
    if (!arguments.length) return this;
    var __method = this, args = slice.call(arguments, 0);
    return function() {
      var a = merge(args, arguments);
      return __method.apply(this, a);
    }
  }

  function delay(timeout) {
    var __method = this, args = slice.call(arguments, 1);
    timeout = timeout * 1000
    return window.setTimeout(function() {
      return __method.apply(__method, args);
    }, timeout);
  }

  function defer() {
    var args = update([0.01], arguments);
    return this.delay.apply(this, args);
  }

  function wrap(wrapper) {
    var __method = this;
    return function() {
      var a = update([__method.bind(this)], arguments);
      return wrapper.apply(this, a);
    }
  }

  function methodize() {
    if (this._methodized) return this._methodized;
    var __method = this;
    return this._methodized = function() {
      var a = update([this], arguments);
      return __method.apply(null, a);
    };
  }

  return {
    argumentNames:       argumentNames,
    bind:                bind,
    bindAsEventListener: bindAsEventListener,
    curry:               curry,
    delay:               delay,
    defer:               defer,
    wrap:                wrap,
    methodize:           methodize
  }
})());

RegExp.prototype.match = RegExp.prototype.test;

RegExp.escape = function(str) {
  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};
var PeriodicalExecuter = Class.create({
  initialize: function(callback, frequency) {
    this.callback = callback;
    this.frequency = frequency;
    this.currentlyExecuting = false;

    this.registerCallback();
  },

  registerCallback: function() {
    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
  },

  execute: function() {
    this.callback(this);
  },

  stop: function() {
    if (!this.timer) return;
    clearInterval(this.timer);
    this.timer = null;
  },

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.execute();
        this.currentlyExecuting = false;
      } catch(e) {
        this.currentlyExecuting = false;
        throw e;
      }
    }
  }
});
Object.extend(String, {
  interpret: function(value) {
    return value == null ? '' : String(value);
  },
  specialChar: {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '\\': '\\\\'
  }
});

Object.extend(String.prototype, (function() {

  function prepareReplacement(replacement) {
    if (Object.isFunction(replacement)) return replacement;
    var template = new Template(replacement);
    return function(match) { return template.evaluate(match) };
  }

  function gsub(pattern, replacement) {
    var result = '', source = this, match;
    replacement = prepareReplacement(replacement);

    if (Object.isString(pattern))
      pattern = RegExp.escape(pattern);

    if (!(pattern.length || pattern.source)) {
      replacement = replacement('');
      return replacement + source.split('').join(replacement) + replacement;
    }

    while (source.length > 0) {
      if (match = source.match(pattern)) {
        result += source.slice(0, match.index);
        result += String.interpret(replacement(match));
        source  = source.slice(match.index + match[0].length);
      } else {
        result += source, source = '';
      }
    }
    return result;
  }

  function sub(pattern, replacement, count) {
    replacement = prepareReplacement(replacement);
    count = Object.isUndefined(count) ? 1 : count;

    return this.gsub(pattern, function(match) {
      if (--count < 0) return match[0];
      return replacement(match);
    });
  }

  function scan(pattern, iterator) {
    this.gsub(pattern, iterator);
    return String(this);
  }

  function truncate(length, truncation) {
    length = length || 30;
    truncation = Object.isUndefined(truncation) ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : String(this);
  }

  function strip() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
  }

  function stripTags() {
    return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, '');
  }

  function stripScripts() {
    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
  }

  function extractScripts() {
    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
    return (this.match(matchAll) || []).map(function(scriptTag) {
      return (scriptTag.match(matchOne) || ['', ''])[1];
    });
  }

  function evalScripts() {
    return this.extractScripts().map(function(script) { return eval(script) });
  }

  function escapeHTML() {
    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }

  function unescapeHTML() {
    return this.stripTags().replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&');
  }


  function toQueryParams(separator) {
    var match = this.strip().match(/([^?#]*)(#.*)?$/);
    if (!match) return { };

    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
      if ((pair = pair.split('='))[0]) {
        var key = decodeURIComponent(pair.shift());
        var value = pair.length > 1 ? pair.join('=') : pair[0];
        if (value != undefined) value = decodeURIComponent(value);

        if (key in hash) {
          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
          hash[key].push(value);
        }
        else hash[key] = value;
      }
      return hash;
    });
  }

  function toArray() {
    return this.split('');
  }

  function succ() {
    return this.slice(0, this.length - 1) +
      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
  }

  function times(count) {
    return count < 1 ? '' : new Array(count + 1).join(this);
  }

  function camelize() {
    var parts = this.split('-'), len = parts.length;
    if (len == 1) return parts[0];

    var camelized = this.charAt(0) == '-'
      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
      : parts[0];

    for (var i = 1; i < len; i++)
      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

    return camelized;
  }

  function capitalize() {
    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
  }

  function underscore() {
    return this.replace(/::/g, '/')
               .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
               .replace(/([a-z\d])([A-Z])/g, '$1_$2')
               .replace(/-/g, '_')
               .toLowerCase();
  }

  function dasherize() {
    return this.replace(/_/g, '-');
  }

  function inspect(useDoubleQuotes) {
    var escapedString = this.replace(/[\x00-\x1f\\]/g, function(character) {
      if (character in String.specialChar) {
        return String.specialChar[character];
      }
      return '\\u00' + character.charCodeAt().toPaddedString(2, 16);
    });
    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  }

  function include(pattern) {
    return this.indexOf(pattern) > -1;
  }

  function startsWith(pattern) {
    return this.indexOf(pattern) === 0;
  }

  function endsWith(pattern) {
    var d = this.length - pattern.length;
    return d >= 0 && this.lastIndexOf(pattern) === d;
  }

  function empty() {
    return this == '';
  }

  function blank() {
    return /^\s*$/.test(this);
  }

  function interpolate(object, pattern) {
    return new Template(this, pattern).evaluate(object);
  }

  return {
    gsub:           gsub,
    sub:            sub,
    scan:           scan,
    truncate:       truncate,
    strip:          String.prototype.trim ? String.prototype.trim : strip,
    stripTags:      stripTags,
    stripScripts:   stripScripts,
    extractScripts: extractScripts,
    evalScripts:    evalScripts,
    escapeHTML:     escapeHTML,
    unescapeHTML:   unescapeHTML,
    toQueryParams:  toQueryParams,
    parseQuery:     toQueryParams,
    toArray:        toArray,
    succ:           succ,
    times:          times,
    camelize:       camelize,
    capitalize:     capitalize,
    underscore:     underscore,
    dasherize:      dasherize,
    inspect:        inspect,
    include:        include,
    startsWith:     startsWith,
    endsWith:       endsWith,
    empty:          empty,
    blank:          blank,
    interpolate:    interpolate
  };
})());

var Template = Class.create({
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    if (object && Object.isFunction(object.toTemplateReplacements))
      object = object.toTemplateReplacements();

    return this.template.gsub(this.pattern, function(match) {
      if (object == null) return (match[1] + '');

      var before = match[1] || '';
      if (before == '\\') return match[2];

      var ctx = object, expr = match[3];
      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
      match = pattern.exec(expr);
      if (match == null) return before;

      while (match != null) {
        var comp = match[1].startsWith('[') ? match[2].replace(/\\\\]/g, ']') : match[1];
        ctx = ctx[comp];
        if (null == ctx || '' == match[3]) break;
        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
        match = pattern.exec(expr);
      }

      return before + String.interpret(ctx);
    });
  }
});
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;

var $break = { };

var Enumerable = (function() {
  function each(iterator, context) {
    var index = 0;
    try {
      this._each(function(value) {
        iterator.call(context, value, index++);
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  }

  function eachSlice(number, iterator, context) {
    var index = -number, slices = [], array = this.toArray();
    if (number < 1) return array;
    while ((index += number) < array.length)
      slices.push(array.slice(index, index+number));
    return slices.collect(iterator, context);
  }

  function all(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = true;
    this.each(function(value, index) {
      result = result && !!iterator.call(context, value, index);
      if (!result) throw $break;
    });
    return result;
  }

  function any(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = false;
    this.each(function(value, index) {
      if (result = !!iterator.call(context, value, index))
        throw $break;
    });
    return result;
  }

  function collect(iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];
    this.each(function(value, index) {
      results.push(iterator.call(context, value, index));
    });
    return results;
  }

  function detect(iterator, context) {
    var result;
    this.each(function(value, index) {
      if (iterator.call(context, value, index)) {
        result = value;
        throw $break;
      }
    });
    return result;
  }

  function findAll(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  }

  function grep(filter, iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];

    if (Object.isString(filter))
      filter = new RegExp(RegExp.escape(filter));

    this.each(function(value, index) {
      if (filter.match(value))
        results.push(iterator.call(context, value, index));
    });
    return results;
  }

  function include(object) {
    if (Object.isFunction(this.indexOf))
      if (this.indexOf(object) != -1) return true;

    var found = false;
    this.each(function(value) {
      if (value == object) {
        found = true;
        throw $break;
      }
    });
    return found;
  }

  function inGroupsOf(number, fillWith) {
    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
    return this.eachSlice(number, function(slice) {
      while(slice.length < number) slice.push(fillWith);
      return slice;
    });
  }

  function inject(memo, iterator, context) {
    this.each(function(value, index) {
      memo = iterator.call(context, memo, value, index);
    });
    return memo;
  }

  function invoke(method) {
    var args = $A(arguments).slice(1);
    return this.map(function(value) {
      return value[method].apply(value, args);
    });
  }

  function max(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value >= result)
        result = value;
    });
    return result;
  }

  function min(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value < result)
        result = value;
    });
    return result;
  }

  function partition(iterator, context) {
    iterator = iterator || Prototype.K;
    var trues = [], falses = [];
    this.each(function(value, index) {
      (iterator.call(context, value, index) ?
        trues : falses).push(value);
    });
    return [trues, falses];
  }

  function pluck(property) {
    var results = [];
    this.each(function(value) {
      results.push(value[property]);
    });
    return results;
  }

  function reject(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (!iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  }

  function sortBy(iterator, context) {
    return this.map(function(value, index) {
      return {
        value: value,
        criteria: iterator.call(context, value, index)
      };
    }).sort(function(left, right) {
      var a = left.criteria, b = right.criteria;
      return a < b ? -1 : a > b ? 1 : 0;
    }).pluck('value');
  }

  function toArray() {
    return this.map();
  }

  function zip() {
    var iterator = Prototype.K, args = $A(arguments);
    if (Object.isFunction(args.last()))
      iterator = args.pop();

    var collections = [this].concat(args).map($A);
    return this.map(function(value, index) {
      return iterator(collections.pluck(index));
    });
  }

  function size() {
    return this.toArray().length;
  }

  function inspect() {
    return '#<Enumerable:' + this.toArray().inspect() + '>';
  }









  return {
    each:       each,
    eachSlice:  eachSlice,
    all:        all,
    every:      all,
    any:        any,
    some:       any,
    collect:    collect,
    map:        collect,
    detect:     detect,
    findAll:    findAll,
    select:     findAll,
    filter:     findAll,
    grep:       grep,
    include:    include,
    member:     include,
    inGroupsOf: inGroupsOf,
    inject:     inject,
    invoke:     invoke,
    max:        max,
    min:        min,
    partition:  partition,
    pluck:      pluck,
    reject:     reject,
    sortBy:     sortBy,
    toArray:    toArray,
    entries:    toArray,
    zip:        zip,
    size:       size,
    inspect:    inspect,
    find:       detect
  };
})();
function $A(iterable) {
  if (!iterable) return [];
  if ('toArray' in Object(iterable)) return iterable.toArray();
  var length = iterable.length || 0, results = new Array(length);
  while (length--) results[length] = iterable[length];
  return results;
}

function $w(string) {
  if (!Object.isString(string)) return [];
  string = string.strip();
  return string ? string.split(/\s+/) : [];
}

Array.from = $A;


(function() {
  var arrayProto = Array.prototype,
      slice = arrayProto.slice,
      _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available

  function each(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  }
  if (!_each) _each = each;

  function clear() {
    this.length = 0;
    return this;
  }

  function first() {
    return this[0];
  }

  function last() {
    return this[this.length - 1];
  }

  function compact() {
    return this.select(function(value) {
      return value != null;
    });
  }

  function flatten() {
    return this.inject([], function(array, value) {
      if (Object.isArray(value))
        return array.concat(value.flatten());
      array.push(value);
      return array;
    });
  }

  function without() {
    var values = slice.call(arguments, 0);
    return this.select(function(value) {
      return !values.include(value);
    });
  }

  function reverse(inline) {
    return (inline !== false ? this : this.toArray())._reverse();
  }

  function uniq(sorted) {
    return this.inject([], function(array, value, index) {
      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
        array.push(value);
      return array;
    });
  }

  function intersect(array) {
    return this.uniq().findAll(function(item) {
      return array.detect(function(value) { return item === value });
    });
  }


  function clone() {
    return slice.call(this, 0);
  }

  function size() {
    return this.length;
  }

  function inspect() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  }

  function indexOf(item, i) {
    i || (i = 0);
    var length = this.length;
    if (i < 0) i = length + i;
    for (; i < length; i++)
      if (this[i] === item) return i;
    return -1;
  }

  function lastIndexOf(item, i) {
    i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
    var n = this.slice(0, i).reverse().indexOf(item);
    return (n < 0) ? n : i - n - 1;
  }

  function concat() {
    var array = slice.call(this, 0), item;
    for (var i = 0, length = arguments.length; i < length; i++) {
      item = arguments[i];
      if (Object.isArray(item) && !('callee' in item)) {
        for (var j = 0, arrayLength = item.length; j < arrayLength; j++)
          array.push(item[j]);
      } else {
        array.push(item);
      }
    }
    return array;
  }

  Object.extend(arrayProto, Enumerable);

  if (!arrayProto._reverse)
    arrayProto._reverse = arrayProto.reverse;

  Object.extend(arrayProto, {
    _each:     _each,
    clear:     clear,
    first:     first,
    last:      last,
    compact:   compact,
    flatten:   flatten,
    without:   without,
    reverse:   reverse,
    uniq:      uniq,
    intersect: intersect,
    clone:     clone,
    toArray:   clone,
    size:      size,
    inspect:   inspect
  });

  var CONCAT_ARGUMENTS_BUGGY = (function() {
    return [].concat(arguments)[0][0] !== 1;
  })(1,2)

  if (CONCAT_ARGUMENTS_BUGGY) arrayProto.concat = concat;

  if (!arrayProto.indexOf) arrayProto.indexOf = indexOf;
  if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf;
})();
function $H(object) {
  return new Hash(object);
};

var Hash = Class.create(Enumerable, (function() {
  function initialize(object) {
    this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
  }

  function _each(iterator) {
    for (var key in this._object) {
      var value = this._object[key], pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  }

  function set(key, value) {
    return this._object[key] = value;
  }

  function get(key) {
    if (this._object[key] !== Object.prototype[key])
      return this._object[key];
  }

  function unset(key) {
    var value = this._object[key];
    delete this._object[key];
    return value;
  }

  function toObject() {
    return Object.clone(this._object);
  }

  function keys() {
    return this.pluck('key');
  }

  function values() {
    return this.pluck('value');
  }

  function index(value) {
    var match = this.detect(function(pair) {
      return pair.value === value;
    });
    return match && match.key;
  }

  function merge(object) {
    return this.clone().update(object);
  }

  function update(object) {
    return new Hash(object).inject(this, function(result, pair) {
      result.set(pair.key, pair.value);
      return result;
    });
  }

  function toQueryPair(key, value) {
    if (Object.isUndefined(value)) return key;
    return key + '=' + encodeURIComponent(String.interpret(value));
  }

  function toQueryString() {
    return this.inject([], function(results, pair) {
      var key = encodeURIComponent(pair.key), values = pair.value;

      if (values && typeof values == 'object') {
        if (Object.isArray(values))
          return results.concat(values.map(toQueryPair.curry(key)));
      } else results.push(toQueryPair(key, values));
      return results;
    }).join('&');
  }

  function inspect() {
    return '#<Hash:{' + this.map(function(pair) {
      return pair.map(Object.inspect).join(': ');
    }).join(', ') + '}>';
  }

  function clone() {
    return new Hash(this);
  }

  return {
    initialize:             initialize,
    _each:                  _each,
    set:                    set,
    get:                    get,
    unset:                  unset,
    toObject:               toObject,
    toTemplateReplacements: toObject,
    keys:                   keys,
    values:                 values,
    index:                  index,
    merge:                  merge,
    update:                 update,
    toQueryString:          toQueryString,
    inspect:                inspect,
    clone:                  clone
  };
})());

Hash.from = $H;
Object.extend(Number.prototype, (function() {
  function toColorPart() {
    return this.toPaddedString(2, 16);
  }

  function succ() {
    return this + 1;
  }

  function times(iterator, context) {
    $R(0, this, true).each(iterator, context);
    return this;
  }

  function toPaddedString(length, radix) {
    var string = this.toString(radix || 10);
    return '0'.times(length - string.length) + string;
  }

  function abs() {
    return Math.abs(this);
  }

  function round() {
    return Math.round(this);
  }

  function ceil() {
    return Math.ceil(this);
  }

  function floor() {
    return Math.floor(this);
  }

  return {
    toColorPart:    toColorPart,
    succ:           succ,
    times:          times,
    toPaddedString: toPaddedString,
    abs:            abs,
    round:          round,
    ceil:           ceil,
    floor:          floor
  };
})());

function $R(start, end, exclusive) {
  return new ObjectRange(start, end, exclusive);
}

var ObjectRange = Class.create(Enumerable, (function() {
  function initialize(start, end, exclusive) {
    this.start = start;
    this.end = end;
    this.exclusive = exclusive;
  }

  function _each(iterator) {
    var value = this.start;
    while (this.include(value)) {
      iterator(value);
      value = value.succ();
    }
  }

  function include(value) {
    if (value < this.start)
      return false;
    if (this.exclusive)
      return value < this.end;
    return value <= this.end;
  }

  return {
    initialize: initialize,
    _each:      _each,
    include:    include
  };
})());



var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
};

Ajax.Responders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (Object.isFunction(responder[callback])) {
        try {
          responder[callback].apply(responder, [request, transport, json]);
        } catch (e) { }
      }
    });
  }
};

Object.extend(Ajax.Responders, Enumerable);

Ajax.Responders.register({
  onCreate:   function() { Ajax.activeRequestCount++ },
  onComplete: function() { Ajax.activeRequestCount-- }
});
Ajax.Base = Class.create({
  initialize: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      encoding:     'UTF-8',
      parameters:   '',
      evalJSON:     true,
      evalJS:       true
    };
    Object.extend(this.options, options || { });

    this.options.method = this.options.method.toLowerCase();

    if (Object.isString(this.options.parameters))
      this.options.parameters = this.options.parameters.toQueryParams();
    else if (Object.isHash(this.options.parameters))
      this.options.parameters = this.options.parameters.toObject();
  }
});
Ajax.Request = Class.create(Ajax.Base, {
  _complete: false,

  initialize: function($super, url, options) {
    $super(options);
    this.transport = Ajax.getTransport();
    this.request(url);
  },

  request: function(url) {
    this.url = url;
    this.method = this.options.method;
    var params = Object.clone(this.options.parameters);

    if (!['get', 'post'].include(this.method)) {
      params['_method'] = this.method;
      this.method = 'post';
    }

    this.parameters = params;

    if (params = Object.toQueryString(params)) {
      if (this.method == 'get')
        this.url += (this.url.include('?') ? '&' : '?') + params;
      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
        params += '&_=';
    }

    try {
      var response = new Ajax.Response(this);
      if (this.options.onCreate) this.options.onCreate(response);
      Ajax.Responders.dispatch('onCreate', this, response);

      this.transport.open(this.method.toUpperCase(), this.url,
        this.options.asynchronous);

      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);

      this.transport.onreadystatechange = this.onStateChange.bind(this);
      this.setRequestHeaders();

      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
      this.transport.send(this.body);

      /* Force Firefox to handle ready state 4 for synchronous requests */
      if (!this.options.asynchronous && this.transport.overrideMimeType)
        this.onStateChange();

    }
    catch (e) {
      this.dispatchException(e);
    }
  },

  onStateChange: function() {
    var readyState = this.transport.readyState;
    if (readyState > 1 && !((readyState == 4) && this._complete))
      this.respondToReadyState(this.transport.readyState);
  },

  setRequestHeaders: function() {
    var headers = {
      'X-Requested-With': 'XMLHttpRequest'
    };

    if (this.method == 'post') {
      /* Force "Connection: close" for older Mozilla browsers to work
       * around a bug where XMLHttpRequest sends an incorrect
       * Content-length header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType &&
          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
            headers['Connection'] = 'close';
    }

    if (typeof this.options.requestHeaders == 'object') {
      var extras = this.options.requestHeaders;

      if (Object.isFunction(extras.push))
        for (var i = 0, length = extras.length; i < length; i += 2)
          headers[extras[i]] = extras[i+1];
      else
        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
    }

    for (var name in headers)
      this.transport.setRequestHeader(name, headers[name]);
  },

  success: function() {
    var status = this.getStatus();
    return !status || (status >= 200 && status < 300);
  },

  getStatus: function() {
    try {
      return this.transport.status || 0;
    } catch (e) { return 0 }
  },

  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);

    if (state == 'Complete') {
      try {
        this._complete = true;
        (this.options['on' + response.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(response, response.headerJSON);
      } catch (e) {
        this.dispatchException(e);
      }

      var contentType = response.getHeader('Content-type');
      if (this.options.evalJS == 'force'
          || (this.options.evalJS && this.isSameOrigin() && contentType
          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
        this.evalResponse();
    }

    try {
      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
    } catch (e) {
      this.dispatchException(e);
    }

    if (state == 'Complete') {
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },

  isSameOrigin: function() {
    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
      protocol: location.protocol,
      domain: document.domain,
      port: location.port ? ':' + location.port : ''
    }));
  },

  getHeader: function(name) {
    try {
      return this.transport.getResponseHeader(name) || null;
    } catch (e) { return null; }
  },

  evalResponse: function() {
    try {
      return JSON.parse(this.transport.responseText);
    } catch (e) {
      this.dispatchException(e);
    }
  },

  dispatchException: function(exception) {
    (this.options.onException || Prototype.emptyFunction)(this, exception);
    Ajax.Responders.dispatch('onException', this, exception);
  }
});

Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];








Ajax.Response = Class.create({
  initialize: function(request){
    this.request = request;
    var transport  = this.transport  = request.transport,
        readyState = this.readyState = transport.readyState;

    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
      this.status       = this.getStatus();
      this.statusText   = this.getStatusText();
      this.responseText = String.interpret(transport.responseText);
      this.headerJSON   = this._getHeaderJSON();
    }

    if(readyState == 4) {
      var xml = transport.responseXML;
      this.responseXML  = Object.isUndefined(xml) ? null : xml;
      this.responseJSON = this._getResponseJSON();
    }
  },

  status:      0,

  statusText: '',

  getStatus: Ajax.Request.prototype.getStatus,

  getStatusText: function() {
    try {
      return this.transport.statusText || '';
    } catch (e) { return '' }
  },

  getHeader: Ajax.Request.prototype.getHeader,

  getAllHeaders: function() {
    try {
      return this.getAllResponseHeaders();
    } catch (e) { return null }
  },

  getResponseHeader: function(name) {
    return this.transport.getResponseHeader(name);
  },

  getAllResponseHeaders: function() {
    return this.transport.getAllResponseHeaders();
  },

  _getHeaderJSON: function() {
    var json = this.getHeader('X-JSON');
    if (!json) return null;
    json = decodeURIComponent(escape(json));
    try {
      return JSON.parse(json);
    } catch (e) {
      this.request.dispatchException(e);
    }
  },

  _getResponseJSON: function() {
    var options = this.request.options;
    if (!options.evalJSON || (options.evalJSON != 'force' &&
      !(this.getHeader('Content-type') || '').include('application/json')) ||
        this.responseText.blank())
          return null;
    try {
      return JSON.parse(this.responseText);
    } catch (e) {
      this.request.dispatchException(e);
    }
  }
});

Ajax.Updater = Class.create(Ajax.Request, {
  initialize: function($super, container, url, options) {
    this.container = {
      success: (container.success || container),
      failure: (container.failure || (container.success ? null : container))
    };

    options = Object.clone(options);
    var onComplete = options.onComplete;
    options.onComplete = (function(response, json) {
      this.updateContent(response.responseText);
      if (Object.isFunction(onComplete)) onComplete(response, json);
    }).bind(this);

    $super(url, options);
  },

  updateContent: function(responseText) {
    var receiver = this.container[this.success() ? 'success' : 'failure'],
        options = this.options;

    if (!options.evalScripts) responseText = responseText.stripScripts();

    if (receiver = $(receiver)) {
      if (options.insertion) {
        if (Object.isString(options.insertion)) {
          var insertion = { }; insertion[options.insertion] = responseText;
          receiver.insert(insertion);
        }
        else options.insertion(receiver, responseText);
      }
      else receiver.update(responseText);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
  initialize: function($super, container, url, options) {
    $super(options);
    this.onComplete = this.options.onComplete;

    this.frequency = (this.options.frequency || 2);
    this.decay = (this.options.decay || 1);

    this.updater = { };
    this.container = container;
    this.url = url;

    this.start();
  },

  start: function() {
    this.options.onComplete = this.updateComplete.bind(this);
    this.onTimerEvent();
  },

  stop: function() {
    this.updater.options.onComplete = undefined;
    clearTimeout(this.timer);
    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
  },

  updateComplete: function(response) {
    if (this.options.decay) {
      this.decay = (response.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = response.responseText;
    }
    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
  },

  onTimerEvent: function() {
    this.updater = new Ajax.Updater(this.container, this.url, this.options);
  }
});



function $(element) {
  if (arguments.length > 1) {
    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
      elements.push($(arguments[i]));
    return elements;
  }
  if (Object.isString(element))
    element = document.getElementById(element);
  return Element.extend(element);
}

if (Prototype.BrowserFeatures.XPath) {
  document._getElementsByXPath = function(expression, parentElement) {
    var results = [];
    var query = document.evaluate(expression, $(parentElement) || document,
      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (var i = 0, length = query.snapshotLength; i < length; i++)
      results.push(Element.extend(query.snapshotItem(i)));
    return results;
  };
}

/*--------------------------------------------------------------------------*/

if (!window.Node) var Node = { };

if (!Node.ELEMENT_NODE) {
  Object.extend(Node, {
    ELEMENT_NODE: 1,
    ATTRIBUTE_NODE: 2,
    TEXT_NODE: 3,
    CDATA_SECTION_NODE: 4,
    ENTITY_REFERENCE_NODE: 5,
    ENTITY_NODE: 6,
    PROCESSING_INSTRUCTION_NODE: 7,
    COMMENT_NODE: 8,
    DOCUMENT_NODE: 9,
    DOCUMENT_TYPE_NODE: 10,
    DOCUMENT_FRAGMENT_NODE: 11,
    NOTATION_NODE: 12
  });
}


(function(global) {

  var SETATTRIBUTE_IGNORES_NAME = (function(){
    var elForm = document.createElement("form");
    var elInput = document.createElement("input");
    var root = document.documentElement;
    elInput.setAttribute("name", "test");
    elForm.appendChild(elInput);
    root.appendChild(elForm);
    var isBuggy = elForm.elements
      ? (typeof elForm.elements.test == "undefined")
      : null;
    root.removeChild(elForm);
    elForm = elInput = null;
    return isBuggy;
  })();

  var element = global.Element;
  global.Element = function(tagName, attributes) {
    attributes = attributes || { };
    tagName = tagName.toLowerCase();
    var cache = Element.cache;
    if (SETATTRIBUTE_IGNORES_NAME && attributes.name) {
      tagName = '<' + tagName + ' name="' + attributes.name + '">';
      delete attributes.name;
      return Element.writeAttribute(document.createElement(tagName), attributes);
    }
    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
  };
  Object.extend(global.Element, element || { });
  if (element) global.Element.prototype = element.prototype;
})(this);

Element.cache = { };
Element.idCounter = 1;

Element.Methods = {
  visible: function(element) {
    return $(element).style.display != 'none';
  },

  toggle: function(element) {
    element = $(element);
    Element[Element.visible(element) ? 'hide' : 'show'](element);
    return element;
  },


  hide: function(element) {
    element = $(element);
    element.style.display = 'none';
    return element;
  },

  show: function(element) {
    element = $(element);
    element.style.display = '';
    return element;
  },

  remove: function(element) {
    element = $(element);
    element.parentNode.removeChild(element);
    return element;
  },

  update: (function(){

    var SELECT_ELEMENT_INNERHTML_BUGGY = (function(){
      var el = document.createElement("select"),
          isBuggy = true;
      el.innerHTML = "<option value=\"test\">test</option>";
      if (el.options && el.options[0]) {
        isBuggy = el.options[0].nodeName.toUpperCase() !== "OPTION";
      }
      el = null;
      return isBuggy;
    })();

    var TABLE_ELEMENT_INNERHTML_BUGGY = (function(){
      try {
        var el = document.createElement("table");
        if (el && el.tBodies) {
          el.innerHTML = "<tbody><tr><td>test</td></tr></tbody>";
          var isBuggy = typeof el.tBodies[0] == "undefined";
          el = null;
          return isBuggy;
        }
      } catch (e) {
        return true;
      }
    })();

    var SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING = (function () {
      var s = document.createElement("script"),
          isBuggy = false;
      try {
        s.appendChild(document.createTextNode(""));
        isBuggy = !s.firstChild ||
          s.firstChild && s.firstChild.nodeType !== 3;
      } catch (e) {
        isBuggy = true;
      }
      s = null;
      return isBuggy;
    })();

    function update(element, content) {
      element = $(element);

      if (content && content.toElement)
        content = content.toElement();

      if (Object.isElement(content))
        return element.update().insert(content);

      content = Object.toHTML(content);

      var tagName = element.tagName.toUpperCase();

      if (tagName === 'SCRIPT' && SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING) {
        element.text = content;
        return element;
      }

      if (SELECT_ELEMENT_INNERHTML_BUGGY || TABLE_ELEMENT_INNERHTML_BUGGY) {
        if (tagName in Element._insertionTranslations.tags) {
          while (element.firstChild) {
            element.removeChild(element.firstChild);
          }
          Element._getContentFromAnonymousElement(tagName, content.stripScripts())
            .each(function(node) {
              element.appendChild(node)
            });
        }
        else {
          element.innerHTML = content.stripScripts();
        }
      }
      else {
        element.innerHTML = content.stripScripts();
      }

      content.evalScripts.bind(content).defer();
      return element;
    }

    return update;
  })(),

  replace: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    else if (!Object.isElement(content)) {
      content = Object.toHTML(content);
      var range = element.ownerDocument.createRange();
      range.selectNode(element);
      content.evalScripts.bind(content).defer();
      content = range.createContextualFragment(content.stripScripts());
    }
    element.parentNode.replaceChild(content, element);
    return element;
  },

  insert: function(element, insertions) {
    element = $(element);

    if (Object.isString(insertions) || Object.isNumber(insertions) ||
        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
          insertions = {bottom:insertions};

    var content, insert, tagName, childNodes;

    for (var position in insertions) {
      content  = insertions[position];
      position = position.toLowerCase();
      insert = Element._insertionTranslations[position];

      if (content && content.toElement) content = content.toElement();
      if (Object.isElement(content)) {
        insert(element, content);
        continue;
      }

      content = Object.toHTML(content);

      tagName = ((position == 'before' || position == 'after')
        ? element.parentNode : element).tagName.toUpperCase();

      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

      if (position == 'top' || position == 'after') childNodes.reverse();
      childNodes.each(insert.curry(element));

      content.evalScripts.bind(content).defer();
    }

    return element;
  },

  wrap: function(element, wrapper, attributes) {
    element = $(element);
    if (Object.isElement(wrapper))
      $(wrapper).writeAttribute(attributes || { });
    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
    else wrapper = new Element('div', wrapper);
    if (element.parentNode)
      element.parentNode.replaceChild(wrapper, element);
    wrapper.appendChild(element);
    return wrapper;
  },

  inspect: function(element) {
    element = $(element);
    var result = '<' + element.tagName.toLowerCase();
    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
      var property = pair.first(), attribute = pair.last();
      var value = (element[property] || '').toString();
      if (value) result += ' ' + attribute + '=' + value.inspect(true);
    });
    return result + '>';
  },

  recursivelyCollect: function(element, property) {
    element = $(element);
    var elements = [];
    while (element = element[property])
      if (element.nodeType == 1)
        elements.push(Element.extend(element));
    return elements;
  },

  ancestors: function(element) {
    return Element.recursivelyCollect(element, 'parentNode');
  },

  descendants: function(element) {
    return Element.select(element, "*");
  },

  firstDescendant: function(element) {
    element = $(element).firstChild;
    while (element && element.nodeType != 1) element = element.nextSibling;
    return $(element);
  },

  immediateDescendants: function(element) {
    if (!(element = $(element).firstChild)) return [];
    while (element && element.nodeType != 1) element = element.nextSibling;
    if (element) return [element].concat($(element).nextSiblings());
    return [];
  },

  previousSiblings: function(element) {
    return Element.recursivelyCollect(element, 'previousSibling');
  },

  nextSiblings: function(element) {
    return Element.recursivelyCollect(element, 'nextSibling');
  },

  siblings: function(element) {
    element = $(element);
    return Element.previousSiblings(element).reverse()
      .concat(Element.nextSiblings(element));
  },

  match: function(element, selector) {
    if (Object.isString(selector))
      selector = new Selector(selector);
    return selector.match($(element));
  },

  up: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(element.parentNode);
    var ancestors = Element.ancestors(element);
    return Object.isNumber(expression) ? ancestors[expression] :
      Selector.findElement(ancestors, expression, index);
  },

  down: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return Element.firstDescendant(element);
    return Object.isNumber(expression) ? Element.descendants(element)[expression] :
      Element.select(element, expression)[index || 0];
  },

  previous: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
    var previousSiblings = Element.previousSiblings(element);
    return Object.isNumber(expression) ? previousSiblings[expression] :
      Selector.findElement(previousSiblings, expression, index);
  },

  next: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
    var nextSiblings = Element.nextSiblings(element);
    return Object.isNumber(expression) ? nextSiblings[expression] :
      Selector.findElement(nextSiblings, expression, index);
  },


  select: function(element) {
    var args = Array.prototype.slice.call(arguments, 1);
    return Selector.findChildElements(element, args);
  },

  adjacent: function(element) {
    var args = Array.prototype.slice.call(arguments, 1);
    return Selector.findChildElements(element.parentNode, args).without(element);
  },

  identify: function(element) {
    element = $(element);
    var id = Element.readAttribute(element, 'id');
    if (id) return id;
    do { id = 'anonymous_element_' + Element.idCounter++ } while ($(id));
    Element.writeAttribute(element, 'id', id);
    return id;
  },

  readAttribute: function(element, name) {
    element = $(element);
    if (Prototype.Browser.IE) {
      var t = Element._attributeTranslations.read;
      if (t.values[name]) return t.values[name](element, name);
      if (t.names[name]) name = t.names[name];
      if (name.include(':')) {
        return (!element.attributes || !element.attributes[name]) ? null :
         element.attributes[name].value;
      }
    }
    return element.getAttribute(name);
  },

  writeAttribute: function(element, name, value) {
    element = $(element);
    var attributes = { }, t = Element._attributeTranslations.write;

    if (typeof name == 'object') attributes = name;
    else attributes[name] = Object.isUndefined(value) ? true : value;

    for (var attr in attributes) {
      name = t.names[attr] || attr;
      value = attributes[attr];
      if (t.values[attr]) name = t.values[attr](element, value);
      if (value === false || value === null)
        element.removeAttribute(name);
      else if (value === true)
        element.setAttribute(name, name);
      else element.setAttribute(name, value);
    }
    return element;
  },

  getHeight: function(element) {
    return Element.getDimensions(element).height;
  },

  getWidth: function(element) {
    return Element.getDimensions(element).width;
  },

  classNames: function(element) {
    return new Element.ClassNames(element);
  },

  hasClassName: function(element, className) {
    if (!(element = $(element))) return;
    var elementClassName = element.className;
    return (elementClassName.length > 0 && (elementClassName == className ||
      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    if (!Element.hasClassName(element, className))
      element.className += (element.className ? ' ' : '') + className;
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    element.className = element.className.replace(
      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
    return element;
  },

  toggleClassName: function(element, className) {
    if (!(element = $(element))) return;
    return Element[Element.hasClassName(element, className) ?
      'removeClassName' : 'addClassName'](element, className);
  },

  cleanWhitespace: function(element) {
    element = $(element);
    var node = element.firstChild;
    while (node) {
      var nextNode = node.nextSibling;
      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
        element.removeChild(node);
      node = nextNode;
    }
    return element;
  },

  empty: function(element) {
    return $(element).innerHTML.blank();
  },

  descendantOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);

    if (element.compareDocumentPosition)
      return (element.compareDocumentPosition(ancestor) & 8) === 8;

    if (ancestor.contains)
      return ancestor.contains(element) && ancestor !== element;

    while (element = element.parentNode)
      if (element == ancestor) return true;

    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var pos = Element.cumulativeOffset(element);
    window.scrollTo(pos[0], pos[1]);
    return element;
  },

  getStyle: function(element, style) {
    element = $(element);
    style = style == 'float' ? 'cssFloat' : style.camelize();
    var value = element.style[style];
    if (!value || value == 'auto') {
      var css = document.defaultView.getComputedStyle(element, null);
      value = css ? css[style] : null;
    }
    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
    return value == 'auto' ? null : value;
  },

  getOpacity: function(element) {
    return $(element).getStyle('opacity');
  },

  setStyle: function(element, styles) {
    element = $(element);
    var elementStyle = element.style, match;
    if (Object.isString(styles)) {
      element.style.cssText += ';' + styles;
      return styles.include('opacity') ?
        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
    }
    for (var property in styles)
      if (property == 'opacity') element.setOpacity(styles[property]);
      else
        elementStyle[(property == 'float' || property == 'cssFloat') ?
          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
            property] = styles[property];

    return element;
  },

  setOpacity: function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;
    return element;
  },

  getDimensions: function(element) {
    element = $(element);
    var display = Element.getStyle(element, 'display');
    if (display != 'none' && display != null) // Safari bug
      return {width: element.offsetWidth, height: element.offsetHeight};

    var els = element.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    var originalDisplay = els.display;
    els.visibility = 'hidden';
    if (originalPosition != 'fixed') // Switching fixed to absolute causes issues in Safari
      els.position = 'absolute';
    els.display = 'block';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = originalDisplay;
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return {width: originalWidth, height: originalHeight};
  },

  makePositioned: function(element) {
    element = $(element);
    var pos = Element.getStyle(element, 'position');
    if (pos == 'static' || !pos) {
      element._madePositioned = true;
      element.style.position = 'relative';
      if (Prototype.Browser.Opera) {
        element.style.top = 0;
        element.style.left = 0;
      }
    }
    return element;
  },

  undoPositioned: function(element) {
    element = $(element);
    if (element._madePositioned) {
      element._madePositioned = undefined;
      element.style.position =
        element.style.top =
        element.style.left =
        element.style.bottom =
        element.style.right = '';
    }
    return element;
  },

  makeClipping: function(element) {
    element = $(element);
    if (element._overflow) return element;
    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
    if (element._overflow !== 'hidden')
      element.style.overflow = 'hidden';
    return element;
  },

  undoClipping: function(element) {
    element = $(element);
    if (!element._overflow) return element;
    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
    element._overflow = null;
    return element;
  },

  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
      if (element) {
        if (element.tagName.toUpperCase() == 'BODY') break;
        var p = Element.getStyle(element, 'position');
        if (p !== 'static') break;
      }
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  absolutize: function(element) {
    element = $(element);
    if (Element.getStyle(element, 'position') == 'absolute') return element;

    var offsets = Element.positionedOffset(element);
    var top     = offsets[1];
    var left    = offsets[0];
    var width   = element.clientWidth;
    var height  = element.clientHeight;

    element._originalLeft   = left - parseFloat(element.style.left  || 0);
    element._originalTop    = top  - parseFloat(element.style.top || 0);
    element._originalWidth  = element.style.width;
    element._originalHeight = element.style.height;

    element.style.position = 'absolute';
    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.width  = width + 'px';
    element.style.height = height + 'px';
    return element;
  },

  relativize: function(element) {
    element = $(element);
    if (Element.getStyle(element, 'position') == 'relative') return element;

    element.style.position = 'relative';
    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);

    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.height = element._originalHeight;
    element.style.width  = element._originalWidth;
    return element;
  },

  cumulativeScrollOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  getOffsetParent: function(element) {
    if (element.offsetParent) return $(element.offsetParent);
    if (element == document.body) return $(element);

    while ((element = element.parentNode) && element != document.body)
      if (Element.getStyle(element, 'position') != 'static')
        return $(element);

    return $(document.body);
  },

  viewportOffset: function(forElement) {
    var valueT = 0, valueL = 0;

    var element = forElement;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;

      if (element.offsetParent == document.body &&
        Element.getStyle(element, 'position') == 'absolute') break;

    } while (element = element.offsetParent);

    element = forElement;
    do {
      if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
        valueT -= element.scrollTop  || 0;
        valueL -= element.scrollLeft || 0;
      }
    } while (element = element.parentNode);

    return Element._returnOffset(valueL, valueT);
  },

  clonePosition: function(element, source) {
    var options = Object.extend({
      setLeft:    true,
      setTop:     true,
      setWidth:   true,
      setHeight:  true,
      offsetTop:  0,
      offsetLeft: 0
    }, arguments[2] || { });

    source = $(source);
    var p = Element.viewportOffset(source);

    element = $(element);
    var delta = [0, 0];
    var parent = null;
    if (Element.getStyle(element, 'position') == 'absolute') {
      parent = Element.getOffsetParent(element);
      delta = Element.viewportOffset(parent);
    }

    if (parent == document.body) {
      delta[0] -= document.body.offsetLeft;
      delta[1] -= document.body.offsetTop;
    }

    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
    return element;
  }
};

Object.extend(Element.Methods, {
  getElementsBySelector: Element.Methods.select,

  childElements: Element.Methods.immediateDescendants
});

Element._attributeTranslations = {
  write: {
    names: {
      className: 'class',
      htmlFor:   'for'
    },
    values: { }
  }
};

if (Prototype.Browser.Opera) {
  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
    function(proceed, element, style) {
      switch (style) {
        case 'left': case 'top': case 'right': case 'bottom':
          if (proceed(element, 'position') === 'static') return null;
        case 'height': case 'width':
          if (!Element.visible(element)) return null;

          var dim = parseInt(proceed(element, style), 10);

          if (dim !== element['offset' + style.capitalize()])
            return dim + 'px';

          var properties;
          if (style === 'height') {
            properties = ['border-top-width', 'padding-top',
             'padding-bottom', 'border-bottom-width'];
          }
          else {
            properties = ['border-left-width', 'padding-left',
             'padding-right', 'border-right-width'];
          }
          return properties.inject(dim, function(memo, property) {
            var val = proceed(element, property);
            return val === null ? memo : memo - parseInt(val, 10);
          }) + 'px';
        default: return proceed(element, style);
      }
    }
  );

  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
    function(proceed, element, attribute) {
      if (attribute === 'title') return element.title;
      return proceed(element, attribute);
    }
  );
}

else if (Prototype.Browser.IE) {
  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
    function(proceed, element) {
      element = $(element);
      try { element.offsetParent }
      catch(e) { return $(document.body) }
      var position = element.getStyle('position');
      if (position !== 'static') return proceed(element);
      element.setStyle({ position: 'relative' });
      var value = proceed(element);
      element.setStyle({ position: position });
      return value;
    }
  );

  $w('positionedOffset viewportOffset').each(function(method) {
    Element.Methods[method] = Element.Methods[method].wrap(
      function(proceed, element) {
        element = $(element);
        try { element.offsetParent }
        catch(e) { return Element._returnOffset(0,0) }
        var position = element.getStyle('position');
        if (position !== 'static') return proceed(element);
        var offsetParent = element.getOffsetParent();
        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
          offsetParent.setStyle({ zoom: 1 });
        element.setStyle({ position: 'relative' });
        var value = proceed(element);
        element.setStyle({ position: position });
        return value;
      }
    );
  });

  Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap(
    function(proceed, element) {
      try { element.offsetParent }
      catch(e) { return Element._returnOffset(0,0) }
      return proceed(element);
    }
  );

  Element.Methods.getStyle = function(element, style) {
    element = $(element);
    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
    var value = element.style[style];
    if (!value && element.currentStyle) value = element.currentStyle[style];

    if (style == 'opacity') {
      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
        if (value[1]) return parseFloat(value[1]) / 100;
      return 1.0;
    }

    if (value == 'auto') {
      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
        return element['offset' + style.capitalize()] + 'px';
      return null;
    }
    return value;
  };

  Element.Methods.setOpacity = function(element, value) {
    function stripAlpha(filter){
      return filter.replace(/alpha\([^\)]*\)/gi,'');
    }
    element = $(element);
    var currentStyle = element.currentStyle;
    if ((currentStyle && !currentStyle.hasLayout) ||
      (!currentStyle && element.style.zoom == 'normal'))
        element.style.zoom = 1;

    var filter = element.getStyle('filter'), style = element.style;
    if (value == 1 || value === '') {
      (filter = stripAlpha(filter)) ?
        style.filter = filter : style.removeAttribute('filter');
      return element;
    } else if (value < 0.00001) value = 0;
    style.filter = stripAlpha(filter) +
      'alpha(opacity=' + (value * 100) + ')';
    return element;
  };

  Element._attributeTranslations = (function(){

    var classProp = 'className';
    var forProp = 'for';

    var el = document.createElement('div');

    el.setAttribute(classProp, 'x');

    if (el.className !== 'x') {
      el.setAttribute('class', 'x');
      if (el.className === 'x') {
        classProp = 'class';
      }
    }
    el = null;

    el = document.createElement('label');
    el.setAttribute(forProp, 'x');
    if (el.htmlFor !== 'x') {
      el.setAttribute('htmlFor', 'x');
      if (el.htmlFor === 'x') {
        forProp = 'htmlFor';
      }
    }
    el = null;

    return {
      read: {
        names: {
          'class':      classProp,
          'className':  classProp,
          'for':        forProp,
          'htmlFor':    forProp
        },
        values: {
          _getAttr: function(element, attribute) {
            return element.getAttribute(attribute);
          },
          _getAttr2: function(element, attribute) {
            return element.getAttribute(attribute, 2);
          },
          _getAttrNode: function(element, attribute) {
            var node = element.getAttributeNode(attribute);
            return node ? node.value : "";
          },
          _getEv: (function(){

            var el = document.createElement('div');
            el.onclick = Prototype.emptyFunction;
            var value = el.getAttribute('onclick');
            var f;

            if (String(value).indexOf('{') > -1) {
              f = function(element, attribute) {
                attribute = element.getAttribute(attribute);
                if (!attribute) return null;
                attribute = attribute.toString();
                attribute = attribute.split('{')[1];
                attribute = attribute.split('}')[0];
                return attribute.strip();
              };
            }
            else if (value === '') {
              f = function(element, attribute) {
                attribute = element.getAttribute(attribute);
                if (!attribute) return null;
                return attribute.strip();
              };
            }
            el = null;
            return f;
          })(),
          _flag: function(element, attribute) {
            return $(element).hasAttribute(attribute) ? attribute : null;
          },
          style: function(element) {
            return element.style.cssText.toLowerCase();
          },
          title: function(element) {
            return element.title;
          }
        }
      }
    }
  })();

  Element._attributeTranslations.write = {
    names: Object.extend({
      cellpadding: 'cellPadding',
      cellspacing: 'cellSpacing'
    }, Element._attributeTranslations.read.names),
    values: {
      checked: function(element, value) {
        element.checked = !!value;
      },

      style: function(element, value) {
        element.style.cssText = value ? value : '';
      }
    }
  };

  Element._attributeTranslations.has = {};

  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
      'encType maxLength readOnly longDesc frameBorder').each(function(attr) {
    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
  });

  (function(v) {
    Object.extend(v, {
      href:        v._getAttr2,
      src:         v._getAttr2,
      type:        v._getAttr,
      action:      v._getAttrNode,
      disabled:    v._flag,
      checked:     v._flag,
      readonly:    v._flag,
      multiple:    v._flag,
      onload:      v._getEv,
      onunload:    v._getEv,
      onclick:     v._getEv,
      ondblclick:  v._getEv,
      onmousedown: v._getEv,
      onmouseup:   v._getEv,
      onmouseover: v._getEv,
      onmousemove: v._getEv,
      onmouseout:  v._getEv,
      onfocus:     v._getEv,
      onblur:      v._getEv,
      onkeypress:  v._getEv,
      onkeydown:   v._getEv,
      onkeyup:     v._getEv,
      onsubmit:    v._getEv,
      onreset:     v._getEv,
      onselect:    v._getEv,
      onchange:    v._getEv
    });
  })(Element._attributeTranslations.read.values);

  if (Prototype.BrowserFeatures.ElementExtensions) {
    (function() {
      function _descendants(element) {
        var nodes = element.getElementsByTagName('*'), results = [];
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.tagName !== "!") // Filter out comment nodes.
            results.push(node);
        return results;
      }

      Element.Methods.down = function(element, expression, index) {
        element = $(element);
        if (arguments.length == 1) return element.firstDescendant();
        return Object.isNumber(expression) ? _descendants(element)[expression] :
          Element.select(element, expression)[index || 0];
      }
    })();
  }

}

else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1) ? 0.999999 :
      (value === '') ? '' : (value < 0.00001) ? 0 : value;
    return element;
  };
}

else if (Prototype.Browser.WebKit) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;

    if (value == 1)
      if(element.tagName.toUpperCase() == 'IMG' && element.width) {
        element.width++; element.width--;
      } else try {
        var n = document.createTextNode(' ');
        element.appendChild(n);
        element.removeChild(n);
      } catch (e) { }

    return element;
  };

  Element.Methods.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      if (element.offsetParent == document.body)
        if (Element.getStyle(element, 'position') == 'absolute') break;

      element = element.offsetParent;
    } while (element);

    return Element._returnOffset(valueL, valueT);
  };
}

if ('outerHTML' in document.documentElement) {
  Element.Methods.replace = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) {
      element.parentNode.replaceChild(content, element);
      return element;
    }

    content = Object.toHTML(content);
    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();

    if (Element._insertionTranslations.tags[tagName]) {
      var nextSibling = element.next();
      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
      parent.removeChild(element);
      if (nextSibling)
        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
      else
        fragments.each(function(node) { parent.appendChild(node) });
    }
    else element.outerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

Element._returnOffset = function(l, t) {
  var result = [l, t];
  result.left = l;
  result.top = t;
  return result;
};

Element._getContentFromAnonymousElement = function(tagName, html) {
  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
  if (t) {
    div.innerHTML = t[0] + html + t[1];
    t[2].times(function() { div = div.firstChild });
  } else div.innerHTML = html;
  return $A(div.childNodes);
};

Element._insertionTranslations = {
  before: function(element, node) {
    element.parentNode.insertBefore(node, element);
  },
  top: function(element, node) {
    element.insertBefore(node, element.firstChild);
  },
  bottom: function(element, node) {
    element.appendChild(node);
  },
  after: function(element, node) {
    element.parentNode.insertBefore(node, element.nextSibling);
  },
  tags: {
    TABLE:  ['<table>',                '</table>',                   1],
    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
    SELECT: ['<select>',               '</select>',                  1]
  }
};

(function() {
  var tags = Element._insertionTranslations.tags;
  Object.extend(tags, {
    THEAD: tags.TBODY,
    TFOOT: tags.TBODY,
    TH:    tags.TD
  });
})();

Element.Methods.Simulated = {
  hasAttribute: function(element, attribute) {
    attribute = Element._attributeTranslations.has[attribute] || attribute;
    var node = $(element).getAttributeNode(attribute);
    return !!(node && node.specified);
  }
};

Element.Methods.ByTag = { };

Object.extend(Element, Element.Methods);

(function(div) {

  if (!Prototype.BrowserFeatures.ElementExtensions && div['__proto__']) {
    window.HTMLElement = { };
    window.HTMLElement.prototype = div['__proto__'];
    Prototype.BrowserFeatures.ElementExtensions = true;
  }

  div = null;

})(document.createElement('div'))

Element.extend = (function() {

  function checkDeficiency(tagName) {
    if (typeof window.Element != 'undefined') {
      var proto = window.Element.prototype;
      if (proto) {
        var id = '_' + (Math.random()+'').slice(2);
        var el = document.createElement(tagName);
        proto[id] = 'x';
        var isBuggy = (el[id] !== 'x');
        delete proto[id];
        el = null;
        return isBuggy;
      }
    }
    return false;
  }

  function extendElementWith(element, methods) {
    for (var property in methods) {
      var value = methods[property];
      if (Object.isFunction(value) && !(property in element))
        element[property] = value.methodize();
    }
  }

  var HTMLOBJECTELEMENT_PROTOTYPE_BUGGY = checkDeficiency('object');

  if (Prototype.BrowserFeatures.SpecificElementExtensions) {
    if (HTMLOBJECTELEMENT_PROTOTYPE_BUGGY) {
      return function(element) {
        if (element && typeof element._extendedByPrototype == 'undefined') {
          var t = element.tagName;
          if (t && (/^(?:object|applet|embed)$/i.test(t))) {
            extendElementWith(element, Element.Methods);
            extendElementWith(element, Element.Methods.Simulated);
            extendElementWith(element, Element.Methods.ByTag[t.toUpperCase()]);
          }
        }
        return element;
      }
    }
    return Prototype.K;
  }

  var Methods = { }, ByTag = Element.Methods.ByTag;

  var extend = Object.extend(function(element) {
    if (!element || typeof element._extendedByPrototype != 'undefined' ||
        element.nodeType != 1 || element == window) return element;

    var methods = Object.clone(Methods),
        tagName = element.tagName.toUpperCase();

    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);

    extendElementWith(element, methods);

    element._extendedByPrototype = Prototype.emptyFunction;
    return element;

  }, {
    refresh: function() {
      if (!Prototype.BrowserFeatures.ElementExtensions) {
        Object.extend(Methods, Element.Methods);
        Object.extend(Methods, Element.Methods.Simulated);
      }
    }
  });

  extend.refresh();
  return extend;
})();

Element.hasAttribute = function(element, attribute) {
  if (element.hasAttribute) return element.hasAttribute(attribute);
  return Element.Methods.Simulated.hasAttribute(element, attribute);
};

Element.addMethods = function(methods) {
  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;

  if (!methods) {
    Object.extend(Form, Form.Methods);
    Object.extend(Form.Element, Form.Element.Methods);
    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 (arguments.length == 2) {
    var tagName = methods;
    methods = arguments[1];
  }

  if (!tagName) Object.extend(Element.Methods, methods || { });
  else {
    if (Object.isArray(tagName)) tagName.each(extend);
    else extend(tagName);
  }

  function extend(tagName) {
    tagName = tagName.toUpperCase();
    if (!Element.Methods.ByTag[tagName])
      Element.Methods.ByTag[tagName] = { };
    Object.extend(Element.Methods.ByTag[tagName], methods);
  }

  function copy(methods, destination, onlyIfAbsent) {
    onlyIfAbsent = onlyIfAbsent || false;
    for (var property in methods) {
      var value = methods[property];
      if (!Object.isFunction(value)) continue;
      if (!onlyIfAbsent || !(property in destination))
        destination[property] = value.methodize();
    }
  }

  function findDOMClass(tagName) {
    var klass;
    var trans = {
      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
      "FrameSet", "IFRAME": "IFrame"
    };
    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName.capitalize() + 'Element';
    if (window[klass]) return window[klass];

    var element = document.createElement(tagName);
    var proto = element['__proto__'] || element.constructor.prototype;
    element = null;
    return proto;
  }

  var elementPrototype = window.HTMLElement ? HTMLElement.prototype :
   Element.prototype;

  if (F.ElementExtensions) {
    copy(Element.Methods, elementPrototype);
    copy(Element.Methods.Simulated, elementPrototype, true);
  }

  if (F.SpecificElementExtensions) {
    for (var tag in Element.Methods.ByTag) {
      var klass = findDOMClass(tag);
      if (Object.isUndefined(klass)) continue;
      copy(T[tag], klass.prototype);
    }
  }

  Object.extend(Element, Element.Methods);
  delete Element.ByTag;

  if (Element.extend.refresh) Element.extend.refresh();
  Element.cache = { };
};


document.viewport = {

  getDimensions: function() {
    return { width: this.getWidth(), height: this.getHeight() };
  },

  getScrollOffsets: function() {
    return Element._returnOffset(
      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
      window.pageYOffset || document.documentElement.scrollTop  || document.body.scrollTop);
  }
};

(function(viewport) {
  var B = Prototype.Browser, doc = document, element, property = {};

  function getRootElement() {
    if (B.WebKit && !doc.evaluate)
      return document;

    if (B.Opera && window.parseFloat(window.opera.version()) < 9.5)
      return document.body;

    return document.documentElement;
  }

  function define(D) {
    if (!element) element = getRootElement();

    property[D] = 'client' + D;

    viewport['get' + D] = function() { return element[property[D]] };
    return viewport['get' + D]();
  }

  viewport.getWidth  = define.curry('Width');

  viewport.getHeight = define.curry('Height');
})(document.viewport);


Element.Storage = {
  UID: 1
};

Element.addMethods({
  getStorage: function(element) {
    if (!(element = $(element))) return;

    var uid;
    if (element === window) {
      uid = 0;
    } else {
      if (typeof element._prototypeUID === "undefined")
        element._prototypeUID = [Element.Storage.UID++];
      uid = element._prototypeUID[0];
    }

    if (!Element.Storage[uid])
      Element.Storage[uid] = $H();

    return Element.Storage[uid];
  },

  store: function(element, key, value) {
    if (!(element = $(element))) return;

    if (arguments.length === 2) {
      Element.getStorage(element).update(key);
    } else {
      Element.getStorage(element).set(key, value);
    }

    return element;
  },

  retrieve: function(element, key, defaultValue) {
    if (!(element = $(element))) return;
    var hash = Element.getStorage(element), value = hash.get(key);

    if (Object.isUndefined(value)) {
      hash.set(key, defaultValue);
      value = defaultValue;
    }

    return value;
  },

  clone: function(element, deep) {
    if (!(element = $(element))) return;
    var clone = element.cloneNode(deep);
    clone._prototypeUID = void 0;
    if (deep) {
      var descendants = Element.select(clone, '*'),
          i = descendants.length;
      while (i--) {
        descendants[i]._prototypeUID = void 0;
      }
    }
    return Element.extend(clone);
  }
});
/* Portions of the Selector class are derived from Jack Slocum's DomQuery,
 * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
 * license.  Please see http://www.yui-ext.com/ for more information. */

var Selector = Class.create({
  initialize: function(expression) {
    this.expression = expression.strip();

    if (this.shouldUseSelectorsAPI()) {
      this.mode = 'selectorsAPI';
    } else if (this.shouldUseXPath()) {
      this.mode = 'xpath';
      this.compileXPathMatcher();
    } else {
      this.mode = "normal";
      this.compileMatcher();
    }

  },

  shouldUseXPath: (function() {

    var IS_DESCENDANT_SELECTOR_BUGGY = (function(){
      var isBuggy = false;
      if (document.evaluate && window.XPathResult) {
        var el = document.createElement('div');
        el.innerHTML = '<ul><li></li></ul><div><ul><li></li></ul></div>';

        var xpath = ".//*[local-name()='ul' or local-name()='UL']" +
          "//*[local-name()='li' or local-name()='LI']";

        var result = document.evaluate(xpath, el, null,
          XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

        isBuggy = (result.snapshotLength !== 2);
        el = null;
      }
      return isBuggy;
    })();

    return function() {
      if (!Prototype.BrowserFeatures.XPath) return false;

      var e = this.expression;

      if (Prototype.Browser.WebKit &&
       (e.include("-of-type") || e.include(":empty")))
        return false;

      if ((/(\[[\w-]*?:|:checked)/).test(e))
        return false;

      if (IS_DESCENDANT_SELECTOR_BUGGY) return false;

      return true;
    }

  })(),

  shouldUseSelectorsAPI: function() {
    if (!Prototype.BrowserFeatures.SelectorsAPI) return false;

    if (Selector.CASE_INSENSITIVE_CLASS_NAMES) return false;

    if (!Selector._div) Selector._div = new Element('div');

    try {
      Selector._div.querySelector(this.expression);
    } catch(e) {
      return false;
    }

    return true;
  },

  compileMatcher: function() {
    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
        c = Selector.criteria, le, p, m, len = ps.length, name;

    if (Selector._cache[e]) {
      this.matcher = Selector._cache[e];
      return;
    }

    this.matcher = ["this.matcher = function(root) {",
                    "var r = root, h = Selector.handlers, c = false, n;"];

    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i = 0; i<len; i++) {
        p = ps[i].re;
        name = ps[i].name;
        if (m = e.match(p)) {
          this.matcher.push(Object.isFunction(c[name]) ? c[name](m) :
            new Template(c[name]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.matcher.push("return h.unique(n);\n}");
    eval(this.matcher.join('\n'));
    Selector._cache[this.expression] = this.matcher;
  },

  compileXPathMatcher: function() {
    var e = this.expression, ps = Selector.patterns,
        x = Selector.xpath, le, m, len = ps.length, name;

    if (Selector._cache[e]) {
      this.xpath = Selector._cache[e]; return;
    }

    this.matcher = ['.//*'];
    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i = 0; i<len; i++) {
        name = ps[i].name;
        if (m = e.match(ps[i].re)) {
          this.matcher.push(Object.isFunction(x[name]) ? x[name](m) :
            new Template(x[name]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.xpath = this.matcher.join('');
    Selector._cache[this.expression] = this.xpath;
  },

  findElements: function(root) {
    root = root || document;
    var e = this.expression, results;

    switch (this.mode) {
      case 'selectorsAPI':
        if (root !== document) {
          var oldId = root.id, id = $(root).identify();
          id = id.replace(/([\.:])/g, "\\$1");
          e = "#" + id + " " + e;
        }

        results = $A(root.querySelectorAll(e)).map(Element.extend);
        root.id = oldId;

        return results;
      case 'xpath':
        return document._getElementsByXPath(this.xpath, root);
      default:
       return this.matcher(root);
    }
  },

  match: function(element) {
    this.tokens = [];

    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
    var le, p, m, len = ps.length, name;

    while (e && le !== e && (/\S/).test(e)) {
      le = e;
      for (var i = 0; i<len; i++) {
        p = ps[i].re;
        name = ps[i].name;
        if (m = e.match(p)) {
          if (as[name]) {
            this.tokens.push([name, Object.clone(m)]);
            e = e.replace(m[0], '');
          } else {
            return this.findElements(document).include(element);
          }
        }
      }
    }

    var match = true, name, matches;
    for (var i = 0, token; token = this.tokens[i]; i++) {
      name = token[0], matches = token[1];
      if (!Selector.assertions[name](element, matches)) {
        match = false; break;
      }
    }

    return match;
  },

  toString: function() {
    return this.expression;
  },

  inspect: function() {
    return "#<Selector:" + this.expression.inspect() + ">";
  }
});

if (Prototype.BrowserFeatures.SelectorsAPI &&
 document.compatMode === 'BackCompat') {
  Selector.CASE_INSENSITIVE_CLASS_NAMES = (function(){
    var div = document.createElement('div'),
     span = document.createElement('span');

    div.id = "prototype_test_id";
    span.className = 'Test';
    div.appendChild(span);
    var isIgnored = (div.querySelector('#prototype_test_id .test') !== null);
    div = span = null;
    return isIgnored;
  })();
}

Object.extend(Selector, {
  _cache: { },

  xpath: {
    descendant:   "//*",
    child:        "/*",
    adjacent:     "/following-sibling::*[1]",
    laterSibling: '/following-sibling::*',
    tagName:      function(m) {
      if (m[1] == '*') return '';
      return "[local-name()='" + m[1].toLowerCase() +
             "' or local-name()='" + m[1].toUpperCase() + "']";
    },
    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
    id:           "[@id='#{1}']",
    attrPresence: function(m) {
      m[1] = m[1].toLowerCase();
      return new Template("[@#{1}]").evaluate(m);
    },
    attr: function(m) {
      m[1] = m[1].toLowerCase();
      m[3] = m[5] || m[6];
      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
    },
    pseudo: function(m) {
      var h = Selector.xpath.pseudos[m[1]];
      if (!h) return '';
      if (Object.isFunction(h)) return h(m);
      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
    },
    operators: {
      '=':  "[@#{1}='#{3}']",
      '!=': "[@#{1}!='#{3}']",
      '^=': "[starts-with(@#{1}, '#{3}')]",
      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
      '*=': "[contains(@#{1}, '#{3}')]",
      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
    },
    pseudos: {
      'first-child': '[not(preceding-sibling::*)]',
      'last-child':  '[not(following-sibling::*)]',
      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
      'empty':       "[count(*) = 0 and (count(text()) = 0)]",
      'checked':     "[@checked]",
      'disabled':    "[(@disabled) and (@type!='hidden')]",
      'enabled':     "[not(@disabled) and (@type!='hidden')]",
      'not': function(m) {
        var e = m[6], p = Selector.patterns,
            x = Selector.xpath, le, v, len = p.length, name;

        var exclusion = [];
        while (e && le != e && (/\S/).test(e)) {
          le = e;
          for (var i = 0; i<len; i++) {
            name = p[i].name
            if (m = e.match(p[i].re)) {
              v = Object.isFunction(x[name]) ? x[name](m) : new Template(x[name]).evaluate(m);
              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
              e = e.replace(m[0], '');
              break;
            }
          }
        }
        return "[not(" + exclusion.join(" and ") + ")]";
      },
      'nth-child':      function(m) {
        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
      },
      'nth-last-child': function(m) {
        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
      },
      'nth-of-type':    function(m) {
        return Selector.xpath.pseudos.nth("position() ", m);
      },
      'nth-last-of-type': function(m) {
        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
      },
      'first-of-type':  function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
      },
      'last-of-type':   function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
      },
      'only-of-type':   function(m) {
        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
      },
      nth: function(fragment, m) {
        var mm, formula = m[6], predicate;
        if (formula == 'even') formula = '2n+0';
        if (formula == 'odd')  formula = '2n+1';
        if (mm = formula.match(/^(\d+)$/)) // digit only
          return '[' + fragment + "= " + mm[1] + ']';
        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
          if (mm[1] == "-") mm[1] = -1;
          var a = mm[1] ? Number(mm[1]) : 1;
          var b = mm[2] ? Number(mm[2]) : 0;
          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
          "((#{fragment} - #{b}) div #{a} >= 0)]";
          return new Template(predicate).evaluate({
            fragment: fragment, a: a, b: b });
        }
      }
    }
  },

  criteria: {
    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',
    className:    'n = h.className(n, r, "#{1}", c);    c = false;',
    id:           'n = h.id(n, r, "#{1}", c);           c = false;',
    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
    attr: function(m) {
      m[3] = (m[5] || m[6]);
      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
    },
    pseudo: function(m) {
      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
    },
    descendant:   'c = "descendant";',
    child:        'c = "child";',
    adjacent:     'c = "adjacent";',
    laterSibling: 'c = "laterSibling";'
  },

  patterns: [
    { name: 'laterSibling', re: /^\s*~\s*/ },
    { name: 'child',        re: /^\s*>\s*/ },
    { name: 'adjacent',     re: /^\s*\+\s*/ },
    { name: 'descendant',   re: /^\s/ },

    { name: 'tagName',      re: /^\s*(\*|[\w\-]+)(\b|$)?/ },
    { name: 'id',           re: /^#([\w\-\*]+)(\b|$)/ },
    { name: 'className',    re: /^\.([\w\-\*]+)(\b|$)/ },
    { name: 'pseudo',       re: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/ },
    { name: 'attrPresence', re: /^\[((?:[\w-]+:)?[\w-]+)\]/ },
    { name: 'attr',         re: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ }
  ],

  assertions: {
    tagName: function(element, matches) {
      return matches[1].toUpperCase() == element.tagName.toUpperCase();
    },

    className: function(element, matches) {
      return Element.hasClassName(element, matches[1]);
    },

    id: function(element, matches) {
      return element.id === matches[1];
    },

    attrPresence: function(element, matches) {
      return Element.hasAttribute(element, matches[1]);
    },

    attr: function(element, matches) {
      var nodeValue = Element.readAttribute(element, matches[1]);
      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
    }
  },

  handlers: {
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        a.push(node);
      return a;
    },

    mark: function(nodes) {
      var _true = Prototype.emptyFunction;
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = _true;
      return nodes;
    },

    unmark: (function(){

      var PROPERTIES_ATTRIBUTES_MAP = (function(){
        var el = document.createElement('div'),
            isBuggy = false,
            propName = '_countedByPrototype',
            value = 'x'
        el[propName] = value;
        isBuggy = (el.getAttribute(propName) === value);
        el = null;
        return isBuggy;
      })();

      return PROPERTIES_ATTRIBUTES_MAP ?
        function(nodes) {
          for (var i = 0, node; node = nodes[i]; i++)
            node.removeAttribute('_countedByPrototype');
          return nodes;
        } :
        function(nodes) {
          for (var i = 0, node; node = nodes[i]; i++)
            node._countedByPrototype = void 0;
          return nodes;
        }
    })(),

    index: function(parentNode, reverse, ofType) {
      parentNode._countedByPrototype = Prototype.emptyFunction;
      if (reverse) {
        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
          var node = nodes[i];
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
        }
      } else {
        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
      }
    },

    unique: function(nodes) {
      if (nodes.length == 0) return nodes;
      var results = [], n;
      for (var i = 0, l = nodes.length; i < l; i++)
        if (typeof (n = nodes[i])._countedByPrototype == 'undefined') {
          n._countedByPrototype = Prototype.emptyFunction;
          results.push(Element.extend(n));
        }
      return Selector.handlers.unmark(results);
    },

    descendant: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, node.getElementsByTagName('*'));
      return results;
    },

    child: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        for (var j = 0, child; child = node.childNodes[j]; j++)
          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
      }
      return results;
    },

    adjacent: function(nodes) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        var next = this.nextElementSibling(node);
        if (next) results.push(next);
      }
      return results;
    },

    laterSibling: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, Element.nextSiblings(node));
      return results;
    },

    nextElementSibling: function(node) {
      while (node = node.nextSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    previousElementSibling: function(node) {
      while (node = node.previousSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    tagName: function(nodes, root, tagName, combinator) {
      var uTagName = tagName.toUpperCase();
      var results = [], h = Selector.handlers;
      if (nodes) {
        if (combinator) {
          if (combinator == "descendant") {
            for (var i = 0, node; node = nodes[i]; i++)
              h.concat(results, node.getElementsByTagName(tagName));
            return results;
          } else nodes = this[combinator](nodes);
          if (tagName == "*") return nodes;
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.tagName.toUpperCase() === uTagName) results.push(node);
        return results;
      } else return root.getElementsByTagName(tagName);
    },

    id: function(nodes, root, id, combinator) {
      var targetNode = $(id), h = Selector.handlers;

      if (root == document) {
        if (!targetNode) return [];
        if (!nodes) return [targetNode];
      } else {
        if (!root.sourceIndex || root.sourceIndex < 1) {
          var nodes = root.getElementsByTagName('*');
          for (var j = 0, node; node = nodes[j]; j++) {
            if (node.id === id) return [node];
          }
        }
      }

      if (nodes) {
        if (combinator) {
          if (combinator == 'child') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (targetNode.parentNode == node) return [targetNode];
          } else if (combinator == 'descendant') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Element.descendantOf(targetNode, node)) return [targetNode];
          } else if (combinator == 'adjacent') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Selector.handlers.previousElementSibling(targetNode) == node)
                return [targetNode];
          } else nodes = h[combinator](nodes);
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node == targetNode) return [targetNode];
        return [];
      }
      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
    },

    className: function(nodes, root, className, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      return Selector.handlers.byClassName(nodes, root, className);
    },

    byClassName: function(nodes, root, className) {
      if (!nodes) nodes = Selector.handlers.descendant([root]);
      var needle = ' ' + className + ' ';
      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
        nodeClassName = node.className;
        if (nodeClassName.length == 0) continue;
        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
          results.push(node);
      }
      return results;
    },

    attrPresence: function(nodes, root, attr, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var results = [];
      for (var i = 0, node; node = nodes[i]; i++)
        if (Element.hasAttribute(node, attr)) results.push(node);
      return results;
    },

    attr: function(nodes, root, attr, value, operator, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var handler = Selector.operators[operator], results = [];
      for (var i = 0, node; node = nodes[i]; i++) {
        var nodeValue = Element.readAttribute(node, attr);
        if (nodeValue === null) continue;
        if (handler(nodeValue, value)) results.push(node);
      }
      return results;
    },

    pseudo: function(nodes, name, value, root, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      if (!nodes) nodes = root.getElementsByTagName("*");
      return Selector.pseudos[name](nodes, value, root);
    }
  },

  pseudos: {
    'first-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.previousElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'last-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.nextElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'only-child': function(nodes, value, root) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
          results.push(node);
      return results;
    },
    'nth-child':        function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root);
    },
    'nth-last-child':   function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true);
    },
    'nth-of-type':      function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, false, true);
    },
    'nth-last-of-type': function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true, true);
    },
    'first-of-type':    function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, false, true);
    },
    'last-of-type':     function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, true, true);
    },
    'only-of-type':     function(nodes, formula, root) {
      var p = Selector.pseudos;
      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
    },

    getIndices: function(a, b, total) {
      if (a == 0) return b > 0 ? [b] : [];
      return $R(1, total).inject([], function(memo, i) {
        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
        return memo;
      });
    },

    nth: function(nodes, formula, root, reverse, ofType) {
      if (nodes.length == 0) return [];
      if (formula == 'even') formula = '2n+0';
      if (formula == 'odd')  formula = '2n+1';
      var h = Selector.handlers, results = [], indexed = [], m;
      h.mark(nodes);
      for (var i = 0, node; node = nodes[i]; i++) {
        if (!node.parentNode._countedByPrototype) {
          h.index(node.parentNode, reverse, ofType);
          indexed.push(node.parentNode);
        }
      }
      if (formula.match(/^\d+$/)) { // just a number
        formula = Number(formula);
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.nodeIndex == formula) results.push(node);
      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
        if (m[1] == "-") m[1] = -1;
        var a = m[1] ? Number(m[1]) : 1;
        var b = m[2] ? Number(m[2]) : 0;
        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
          for (var j = 0; j < l; j++)
            if (node.nodeIndex == indices[j]) results.push(node);
        }
      }
      h.unmark(nodes);
      h.unmark(indexed);
      return results;
    },

    'empty': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (node.tagName == '!' || node.firstChild) continue;
        results.push(node);
      }
      return results;
    },

    'not': function(nodes, selector, root) {
      var h = Selector.handlers, selectorType, m;
      var exclusions = new Selector(selector).findElements(root);
      h.mark(exclusions);
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node._countedByPrototype) results.push(node);
      h.unmark(exclusions);
      return results;
    },

    'enabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node.disabled && (!node.type || node.type !== 'hidden'))
          results.push(node);
      return results;
    },

    'disabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.disabled) results.push(node);
      return results;
    },

    'checked': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.checked) results.push(node);
      return results;
    }
  },

  operators: {
    '=':  function(nv, v) { return nv == v; },
    '!=': function(nv, v) { return nv != v; },
    '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
    '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
    '*=': function(nv, v) { return nv == v || nv && nv.include(v); },
    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
    '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
     '-').include('-' + (v || "").toUpperCase() + '-'); }
  },

  split: function(expression) {
    var expressions = [];
    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
      expressions.push(m[1].strip());
    });
    return expressions;
  },

  matchElements: function(elements, expression) {
    var matches = $$(expression), h = Selector.handlers;
    h.mark(matches);
    for (var i = 0, results = [], element; element = elements[i]; i++)
      if (element._countedByPrototype) results.push(element);
    h.unmark(matches);
    return results;
  },

  findElement: function(elements, expression, index) {
    if (Object.isNumber(expression)) {
      index = expression; expression = false;
    }
    return Selector.matchElements(elements, expression || '*')[index || 0];
  },

  findChildElements: function(element, expressions) {
    expressions = Selector.split(expressions.join(','));
    var results = [], h = Selector.handlers;
    for (var i = 0, l = expressions.length, selector; i < l; i++) {
      selector = new Selector(expressions[i].strip());
      h.concat(results, selector.findElements(element));
    }
    return (l > 1) ? h.unique(results) : results;
  }
});

if (Prototype.Browser.IE) {
  Object.extend(Selector.handlers, {
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        if (node.tagName !== "!") a.push(node);
      return a;
    }
  });
}

function $$() {
  return Selector.findChildElements(document, $A(arguments));
}

var Form = {
  reset: function(form) {
    form = $(form);
    form.reset();
    return form;
  },

  serializeElements: function(elements, options) {
    if (typeof options != 'object') options = { hash: !!options };
    else if (Object.isUndefined(options.hash)) options.hash = true;
    var key, value, submitted = false, submit = options.submit;

    var data = elements.inject({ }, function(result, element) {
      if (!element.disabled && element.name) {
        key = element.name; value = $(element).getValue();
        if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
            submit !== false && (!submit || key == submit) && (submitted = true)))) {
          if (key in result) {
            if (!Object.isArray(result[key])) result[key] = [result[key]];
            result[key].push(value);
          }
          else result[key] = value;
        }
      }
      return result;
    });

    return options.hash ? data : Object.toQueryString(data);
  }
};

Form.Methods = {
  serialize: function(form, options) {
    return Form.serializeElements(Form.getElements(form), options);
  },

  getElements: function(form) {
    var elements = $(form).getElementsByTagName('*'),
        element,
        arr = [ ],
        serializers = Form.Element.Serializers;
    for (var i = 0; element = elements[i]; i++) {
      arr.push(element);
    }
    return arr.inject([], function(elements, child) {
      if (serializers[child.tagName.toLowerCase()])
        elements.push(Element.extend(child));
      return elements;
    })
  },

  getInputs: function(form, typeName, name) {
    form = $(form);
    var inputs = form.getElementsByTagName('input');

    if (!typeName && !name) return $A(inputs).map(Element.extend);

    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
      var input = inputs[i];
      if ((typeName && input.type != typeName) || (name && input.name != name))
        continue;
      matchingInputs.push(Element.extend(input));
    }

    return matchingInputs;
  },

  disable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('disable');
    return form;
  },

  enable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('enable');
    return form;
  },

  findFirstElement: function(form) {
    var elements = $(form).getElements().findAll(function(element) {
      return 'hidden' != element.type && !element.disabled;
    });
    var firstByIndex = elements.findAll(function(element) {
      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
    }).sortBy(function(element) { return element.tabIndex }).first();

    return firstByIndex ? firstByIndex : elements.find(function(element) {
      return /^(?:input|select|textarea)$/i.test(element.tagName);
    });
  },

  focusFirstElement: function(form) {
    form = $(form);
    form.findFirstElement().activate();
    return form;
  },

  request: function(form, options) {
    form = $(form), options = Object.clone(options || { });

    var params = options.parameters, action = form.readAttribute('action') || '';
    if (action.blank()) action = window.location.href;
    options.parameters = form.serialize(true);

    if (params) {
      if (Object.isString(params)) params = params.toQueryParams();
      Object.extend(options.parameters, params);
    }

    if (form.hasAttribute('method') && !options.method)
      options.method = form.method;

    return new Ajax.Request(action, options);
  }
};

/*--------------------------------------------------------------------------*/


Form.Element = {
  focus: function(element) {
    $(element).focus();
    return element;
  },

  select: function(element) {
    $(element).select();
    return element;
  }
};

Form.Element.Methods = {

  serialize: function(element) {
    element = $(element);
    if (!element.disabled && element.name) {
      var value = element.getValue();
      if (value != undefined) {
        var pair = { };
        pair[element.name] = value;
        return Object.toQueryString(pair);
      }
    }
    return '';
  },

  getValue: function(element) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    return Form.Element.Serializers[method](element);
  },

  setValue: function(element, value) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    Form.Element.Serializers[method](element, value);
    return element;
  },

  clear: function(element) {
    $(element).value = '';
    return element;
  },

  present: function(element) {
    return $(element).value != '';
  },

  activate: function(element) {
    element = $(element);
    try {
      element.focus();
      if (element.select && (element.tagName.toLowerCase() != 'input' ||
          !(/^(?:button|reset|submit)$/i.test(element.type))))
        element.select();
    } catch (e) { }
    return element;
  },

  disable: function(element) {
    element = $(element);
    element.disabled = true;
    return element;
  },

  enable: function(element) {
    element = $(element);
    element.disabled = false;
    return element;
  }
};

/*--------------------------------------------------------------------------*/

var Field = Form.Element;

var $F = Form.Element.Methods.getValue;

/*--------------------------------------------------------------------------*/

Form.Element.Serializers = {
  input: function(element, value) {
    switch (element.type.toLowerCase()) {
      case 'checkbox':
      case 'radio':
        return Form.Element.Serializers.inputSelector(element, value);
      default:
        return Form.Element.Serializers.textarea(element, value);
    }
  },

  inputSelector: function(element, value) {
    if (Object.isUndefined(value)) return element.checked ? element.value : null;
    else element.checked = !!value;
  },

  textarea: function(element, value) {
    if (Object.isUndefined(value)) return element.value;
    else element.value = value;
  },

  select: function(element, value) {
    if (Object.isUndefined(value))
      return this[element.type == 'select-one' ?
        'selectOne' : 'selectMany'](element);
    else {
      var opt, currentValue, single = !Object.isArray(value);
      for (var i = 0, length = element.length; i < length; i++) {
        opt = element.options[i];
        currentValue = this.optionValue(opt);
        if (single) {
          if (currentValue == value) {
            opt.selected = true;
            return;
          }
        }
        else opt.selected = value.include(currentValue);
      }
    }
  },

  selectOne: function(element) {
    var index = element.selectedIndex;
    return index >= 0 ? this.optionValue(element.options[index]) : null;
  },

  selectMany: function(element) {
    var values, length = element.length;
    if (!length) return null;

    for (var i = 0, values = []; i < length; i++) {
      var opt = element.options[i];
      if (opt.selected) values.push(this.optionValue(opt));
    }
    return values;
  },

  optionValue: function(opt) {
    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
  }
};

/*--------------------------------------------------------------------------*/


Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
  initialize: function($super, element, frequency, callback) {
    $super(callback, frequency);
    this.element   = $(element);
    this.lastValue = this.getValue();
  },

  execute: function() {
    var value = this.getValue();
    if (Object.isString(this.lastValue) && Object.isString(value) ?
        this.lastValue != value : String(this.lastValue) != String(value)) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  }
});

Form.Element.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});

/*--------------------------------------------------------------------------*/

Abstract.EventObserver = Class.create({
  initialize: function(element, callback) {
    this.element  = $(element);
    this.callback = callback;

    this.lastValue = this.getValue();
    if (this.element.tagName.toLowerCase() == 'form')
      this.registerFormCallbacks();
    else
      this.registerCallback(this.element);
  },

  onElementEvent: function() {
    var value = this.getValue();
    if (this.lastValue != value) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  },

  registerFormCallbacks: function() {
    Form.getElements(this.element).each(this.registerCallback, this);
  },

  registerCallback: function(element) {
    if (element.type) {
      switch (element.type.toLowerCase()) {
        case 'checkbox':
        case 'radio':
          Event.observe(element, 'click', this.onElementEvent.bind(this));
          break;
        default:
          Event.observe(element, 'change', this.onElementEvent.bind(this));
          break;
      }
    }
  }
});

Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});
(function() {

  var Event = {
    KEY_BACKSPACE: 8,
    KEY_TAB:       9,
    KEY_RETURN:   13,
    KEY_ESC:      27,
    KEY_LEFT:     37,
    KEY_UP:       38,
    KEY_RIGHT:    39,
    KEY_DOWN:     40,
    KEY_DELETE:   46,
    KEY_HOME:     36,
    KEY_END:      35,
    KEY_PAGEUP:   33,
    KEY_PAGEDOWN: 34,
    KEY_INSERT:   45,

    cache: {}
  };

  var docEl = document.documentElement;
  var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl
    && 'onmouseleave' in docEl;

  var _isButton;
  if (Prototype.Browser.IE) {
    var buttonMap = { 0: 1, 1: 4, 2: 2 };
    _isButton = function(event, code) {
      return event.button === buttonMap[code];
    };
  } else if (Prototype.Browser.WebKit) {
    _isButton = function(event, code) {
      switch (code) {
        case 0: return event.which == 1 && !event.metaKey;
        case 1: return event.which == 1 && event.metaKey;
        default: return false;
      }
    };
  } else {
    _isButton = function(event, code) {
      return event.which ? (event.which === code + 1) : (event.button === code);
    };
  }

  function isLeftClick(event)   { return _isButton(event, 0) }

  function isMiddleClick(event) { return _isButton(event, 1) }

  function isRightClick(event)  { return _isButton(event, 2) }

  function element(event) {
    event = Event.extend(event);

    var node = event.target, type = event.type,
     currentTarget = event.currentTarget;

    if (currentTarget && currentTarget.tagName) {
      if (type === 'load' || type === 'error' ||
        (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
          && currentTarget.type === 'radio'))
            node = currentTarget;
    }

    if (node.nodeType == Node.TEXT_NODE)
      node = node.parentNode;

    return Element.extend(node);
  }

  function findElement(event, expression) {
    var element = Event.element(event);
    if (!expression) return element;
    var elements = [element].concat(element.ancestors());
    return Selector.findElement(elements, expression, 0);
  }

  function pointer(event) {
    return { x: pointerX(event), y: pointerY(event) };
  }

  function pointerX(event) {
    var docElement = document.documentElement,
     body = document.body || { scrollLeft: 0 };

    return event.pageX || (event.clientX +
      (docElement.scrollLeft || body.scrollLeft) -
      (docElement.clientLeft || 0));
  }

  function pointerY(event) {
    var docElement = document.documentElement,
     body = document.body || { scrollTop: 0 };

    return  event.pageY || (event.clientY +
       (docElement.scrollTop || body.scrollTop) -
       (docElement.clientTop || 0));
  }


  function stop(event) {
    Event.extend(event);
    event.preventDefault();
    event.stopPropagation();

    event.stopped = true;
  }

  Event.Methods = {
    isLeftClick: isLeftClick,
    isMiddleClick: isMiddleClick,
    isRightClick: isRightClick,

    element: element,
    findElement: findElement,

    pointer: pointer,
    pointerX: pointerX,
    pointerY: pointerY,

    stop: stop
  };


  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
    m[name] = Event.Methods[name].methodize();
    return m;
  });

  if (Prototype.Browser.IE) {
    function _relatedTarget(event) {
      var element;
      switch (event.type) {
        case 'mouseover': element = event.fromElement; break;
        case 'mouseout':  element = event.toElement;   break;
        default: return null;
      }
      return Element.extend(element);
    }

    Object.extend(methods, {
      stopPropagation: function() { this.cancelBubble = true },
      preventDefault:  function() { this.returnValue = false },
      inspect: function() { return '[object Event]' }
    });

    Event.extend = function(event, element) {
      if (!event) return false;
      if (event._extendedByPrototype) return event;

      event._extendedByPrototype = Prototype.emptyFunction;
      var pointer = Event.pointer(event);

      Object.extend(event, {
        target: event.srcElement || element,
        relatedTarget: _relatedTarget(event),
        pageX:  pointer.x,
        pageY:  pointer.y
      });

      return Object.extend(event, methods);
    };
  } else {
    Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__;
    Object.extend(Event.prototype, methods);
    Event.extend = Prototype.K;
  }

  function _createResponder(element, eventName, handler) {
    var registry = Element.retrieve(element, 'prototype_event_registry');

    if (Object.isUndefined(registry)) {
      CACHE.push(element);
      registry = Element.retrieve(element, 'prototype_event_registry', $H());
    }

    var respondersForEvent = registry.get(eventName);
    if (Object.isUndefined(respondersForEvent)) {
      respondersForEvent = [];
      registry.set(eventName, respondersForEvent);
    }

    if (respondersForEvent.pluck('handler').include(handler)) return false;

    var responder;
    if (eventName.include(":")) {
      responder = function(event) {
        if (Object.isUndefined(event.eventName))
          return false;

        if (event.eventName !== eventName)
          return false;

        Event.extend(event, element);
        handler.call(element, event);
      };
    } else {
      if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED &&
       (eventName === "mouseenter" || eventName === "mouseleave")) {
        if (eventName === "mouseenter" || eventName === "mouseleave") {
          responder = function(event) {
            Event.extend(event, element);

            var parent = event.relatedTarget;
            while (parent && parent !== element) {
              try { parent = parent.parentNode; }
              catch(e) { parent = element; }
            }

            if (parent === element) return;

            handler.call(element, event);
          };
        }
      } else {
        responder = function(event) {
          Event.extend(event, element);
          handler.call(element, event);
        };
      }
    }

    responder.handler = handler;
    respondersForEvent.push(responder);
    return responder;
  }

  function _destroyCache() {
    for (var i = 0, length = CACHE.length; i < length; i++) {
      Event.stopObserving(CACHE[i]);
      CACHE[i] = null;
    }
  }

  var CACHE = [];

  if (Prototype.Browser.IE)
    window.attachEvent('onunload', _destroyCache);

  if (Prototype.Browser.WebKit)
    window.addEventListener('unload', Prototype.emptyFunction, false);


  var _getDOMEventName = Prototype.K;

  if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) {
    _getDOMEventName = function(eventName) {
      var translations = { mouseenter: "mouseover", mouseleave: "mouseout" };
      return eventName in translations ? translations[eventName] : eventName;
    };
  }

  function observe(element, eventName, handler) {
    element = $(element);

    var responder = _createResponder(element, eventName, handler);

    if (!responder) return element;

    if (eventName.include(':')) {
      if (element.addEventListener)
        element.addEventListener("dataavailable", responder, false);
      else {
        element.attachEvent("ondataavailable", responder);
        element.attachEvent("onfilterchange", responder);
      }
    } else {
      var actualEventName = _getDOMEventName(eventName);

      if (element.addEventListener)
        element.addEventListener(actualEventName, responder, false);
      else
        element.attachEvent("on" + actualEventName, responder);
    }

    return element;
  }

  function stopObserving(element, eventName, handler) {
    element = $(element);

    var registry = Element.retrieve(element, 'prototype_event_registry');

    if (Object.isUndefined(registry)) return element;

    if (eventName && !handler) {
      var responders = registry.get(eventName);

      if (Object.isUndefined(responders)) return element;

      responders.each( function(r) {
        Element.stopObserving(element, eventName, r.handler);
      });
      return element;
    } else if (!eventName) {
      registry.each( function(pair) {
        var eventName = pair.key, responders = pair.value;

        responders.each( function(r) {
          Element.stopObserving(element, eventName, r.handler);
        });
      });
      return element;
    }

    var responders = registry.get(eventName);

    if (!responders) return;

    var responder = responders.find( function(r) { return r.handler === handler; });
    if (!responder) return element;

    var actualEventName = _getDOMEventName(eventName);

    if (eventName.include(':')) {
      if (element.removeEventListener)
        element.removeEventListener("dataavailable", responder, false);
      else {
        element.detachEvent("ondataavailable", responder);
        element.detachEvent("onfilterchange",  responder);
      }
    } else {
      if (element.removeEventListener)
        element.removeEventListener(actualEventName, responder, false);
      else
        element.detachEvent('on' + actualEventName, responder);
    }

    registry.set(eventName, responders.without(responder));

    return element;
  }

  function fire(element, eventName, memo, bubble) {
    element = $(element);

    if (Object.isUndefined(bubble))
      bubble = true;

    if (element == document && document.createEvent && !element.dispatchEvent)
      element = document.documentElement;

    var event;
    if (document.createEvent) {
      event = document.createEvent('HTMLEvents');
      event.initEvent('dataavailable', true, true);
    } else {
      event = document.createEventObject();
      event.eventType = bubble ? 'ondataavailable' : 'onfilterchange';
    }

    event.eventName = eventName;
    event.memo = memo || { };

    if (document.createEvent)
      element.dispatchEvent(event);
    else
      element.fireEvent(event.eventType, event);

    return Event.extend(event);
  }


  Object.extend(Event, Event.Methods);

  Object.extend(Event, {
    fire:          fire,
    observe:       observe,
    stopObserving: stopObserving
  });

  Element.addMethods({
    fire:          fire,

    observe:       observe,

    stopObserving: stopObserving
  });

  Object.extend(document, {
    fire:          fire.methodize(),

    observe:       observe.methodize(),

    stopObserving: stopObserving.methodize(),

    loaded:        false
  });

  if (window.Event) Object.extend(window.Event, Event);
  else window.Event = Event;
})();

(function() {
  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
     Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */

  var timer;

  function fireContentLoadedEvent() {
    if (document.loaded) return;
    if (timer) window.clearTimeout(timer);
    document.loaded = true;
    document.fire('dom:loaded');
  }

  function checkReadyState() {
    if (document.readyState === 'complete') {
      document.stopObserving('readystatechange', checkReadyState);
      fireContentLoadedEvent();
    }
  }

  function pollDoScroll() {
    try { document.documentElement.doScroll('left'); }
    catch(e) {
      timer = pollDoScroll.defer();
      return;
    }
    fireContentLoadedEvent();
  }

  if (document.addEventListener) {
    document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false);
  } else {
    document.observe('readystatechange', checkReadyState);
    if (window == top)
      timer = pollDoScroll.defer();
  }

  Event.observe(window, 'load', fireContentLoadedEvent);
})();

Element.addMethods();

/*------------------------------- DEPRECATED -------------------------------*/

Hash.toQueryString = Object.toQueryString;

var Toggle = { display: Element.toggle };

Element.Methods.childOf = Element.Methods.descendantOf;

var Insertion = {
  Before: function(element, content) {
    return Element.insert(element, {before:content});
  },

  Top: function(element, content) {
    return Element.insert(element, {top:content});
  },

  Bottom: function(element, content) {
    return Element.insert(element, {bottom:content});
  },

  After: function(element, content) {
    return Element.insert(element, {after:content});
  }
};

var $continue = new Error('"throw $continue" is deprecated, use "return" instead');

var Position = {
  includeScrollOffsets: false,

  prepare: function() {
    this.deltaX =  window.pageXOffset
                || document.documentElement.scrollLeft
                || document.body.scrollLeft
                || 0;
    this.deltaY =  window.pageYOffset
                || document.documentElement.scrollTop
                || document.body.scrollTop
                || 0;
  },

  within: function(element, x, y) {
    if (this.includeScrollOffsets)
      return this.withinIncludingScrolloffsets(element, x, y);
    this.xcomp = x;
    this.ycomp = y;
    this.offset = Element.cumulativeOffset(element);

    return (y >= this.offset[1] &&
            y <  this.offset[1] + element.offsetHeight &&
            x >= this.offset[0] &&
            x <  this.offset[0] + element.offsetWidth);
  },

  withinIncludingScrolloffsets: function(element, x, y) {
    var offsetcache = Element.cumulativeScrollOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = Element.cumulativeOffset(element);

    return (this.ycomp >= this.offset[1] &&
            this.ycomp <  this.offset[1] + element.offsetHeight &&
            this.xcomp >= this.offset[0] &&
            this.xcomp <  this.offset[0] + element.offsetWidth);
  },

  overlap: function(mode, element) {
    if (!mode) return 0;
    if (mode == 'vertical')
      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
        element.offsetHeight;
    if (mode == 'horizontal')
      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
        element.offsetWidth;
  },


  cumulativeOffset: Element.Methods.cumulativeOffset,

  positionedOffset: Element.Methods.positionedOffset,

  absolutize: function(element) {
    Position.prepare();
    return Element.absolutize(element);
  },

  relativize: function(element) {
    Position.prepare();
    return Element.relativize(element);
  },

  realOffset: Element.Methods.cumulativeScrollOffset,

  offsetParent: Element.Methods.getOffsetParent,

  page: Element.Methods.viewportOffset,

  clone: function(source, target, options) {
    options = options || { };
    return Element.clonePosition(target, source, options);
  }
};

/*--------------------------------------------------------------------------*/

if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
  function iter(name) {
    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
  }

  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
  function(element, className) {
    className = className.toString().strip();
    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
  } : function(element, className) {
    className = className.toString().strip();
    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
    if (!classNames && !className) return elements;

    var nodes = $(element).getElementsByTagName('*');
    className = ' ' + className + ' ';

    for (var i = 0, child, cn; child = nodes[i]; i++) {
      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
          (classNames && classNames.all(function(name) {
            return !name.toString().blank() && cn.include(' ' + name + ' ');
          }))))
        elements.push(Element.extend(child));
    }
    return elements;
  };

  return function(className, parentElement) {
    return $(parentElement || document.body).getElementsByClassName(className);
  };
}(Element.Methods);

/*--------------------------------------------------------------------------*/

Element.ClassNames = Class.create();
Element.ClassNames.prototype = {
  initialize: function(element) {
    this.element = $(element);
  },

  _each: function(iterator) {
    this.element.className.split(/\s+/).select(function(name) {
      return name.length > 0;
    })._each(iterator);
  },

  set: function(className) {
    this.element.className = className;
  },

  add: function(classNameToAdd) {
    if (this.include(classNameToAdd)) return;
    this.set($A(this).concat(classNameToAdd).join(' '));
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set($A(this).without(classNameToRemove).join(' '));
  },

  toString: function() {
    return $A(this).join(' ');
  }
};

Object.extend(Element.ClassNames.prototype, Enumerable);

/*--------------------------------------------------------------------------*/


var Metaphone = {
  get: function(string) {
    var words = string.strip().toUpperCase().without(/[^A-Z ]/).split(' ');
    return words.collect(Metaphone._metaphone).join(' ').strip().gsub(/( )\1+/, ' ');
  },
  
  _metaphone: function(metaphone) {
    Metaphone._transformations.each(
      function(transformation) {
        metaphone = metaphone.gsub(transformation[0], transformation[1]);
      }
    );
    return metaphone;
  },
  
  _transformations: [
    [/([BCDFHJKLMNPQRSTVWXYZ])\1+/, '#{1}'], // Remove doubled consonants except G.
    [/^AE/, 'E'],
    [/^[GKP]N/, 'N'],
    [/^WR/, 'R'],
    [/^X/, 'S'],
    [/^WH/, 'W'],
    [/MB$/, 'M'],
    [/(?!^)SCH/, 'SK'],
    [/TH/, '0'],
    [/T?CH|SH/, 'X'],
    [/C(?=IA)/, 'X'],
    [/[ST](?=I[AO])/, 'X'],
    [/S?C(?=[IEY])/, 'S'],
    [/[CQ]/, 'K'],
    [/DG(?=[IEY])/, 'J'],
    [/D/, 'T'],
    [/G(?=H[^AEIOU])/, ''],
    [/GN(ED)?/, 'N'],
    [/([^G]|^)G(?=[IEY])/, '#{1}J'],
    [/G+/, 'K'],
    [/PH/, 'F'],
    [/([AEIOU])H(?=\b|[^AEIOU])/, '#{1}'],
    [/[WY](?![AEIOU])/, ''],
    [/Z/, 'S'],
    [/V/, 'F'],
    [/(?!^)[AEIOU]+/, ''],
  ]
};

var DigestAuthentication = {
  ha1: function(options) {
    return (options.username + ':' + options.realm + ':' + options.password).md5();
  },
  
  ha2: function(options) {
    return (options.method.upperCase() + ':' + options.uri.lowerCase()).md5();
  },
  
  // Receives username, realm, password or ha1, method, uri
  // and generates a digest authentication header.
  header: function(options) {
    var nonce = DigestAuthentication.nonce();

    var response = DigestAuthentication.response({
      username: options.username,
      realm: options.realm,
      password: options.password,
      ha1: options.ha1,
      method: options.method,
      uri: options.uri,
      nonce: nonce
    });
    
    var header = 'Digest username="#{username}", realm="#{realm}", uri="#{uri}", nonce="#{nonce}", response="#{response}"';
    return header.interpolate({
      username: options.username,
      realm: options.realm,
      uri: options.uri.lowerCase(),
      nonce: nonce,
      response: response
    });
  },
  
  // Returns Unix Timestamp in seconds:
  nonce: function() {
    return Timestamp.get(true);
  },
  
  // Called by both generate and parse.
  // Receives username, realm, password or ha1, method, uri, nonce
  // and generates a digest authentication header response value.
  response: function(options) {
    var ha1 = options.ha1 || DigestAuthentication.ha1(options);
    var ha2 = DigestAuthentication.ha2(options);
    return (ha1 + ':' + options.nonce + ':' + ha2).md5();
  }
};

var Schema = Class.create({
  initialize: function(schema, options) {
    this.schema = Object.copy(schema);
    this.ancestor = options.ancestor || {};
    this.representation = options.representation || {};
    this.successor = options.successor || {};
    this.action = options.action;
    this.ensureChanges = options.ensureChanges;
    this.properties = Object.keys(this.schema);
  },

  ensure: function() {
    this.properties.each(
      function(property) {
        var schema = this.schema[property];
        var object = this.successor[property];
        if (schema.type == 'proxy') return;
        
        // Apply createOnly and referentialIntegrity for '...Id' properties:
        if (/Ids?$/.test(property)) {
          if (schema.createOnly !== false) schema.createOnly = true;
          if (schema.referentialIntegrity !== false) schema.referentialIntegrity = true;
        }
        
        // Apply createOnly for polymorphic '...Type' properties:
        var polymorph = property.extract(/(.+)Type$/);
        if (polymorph && this.schema[polymorph + 'Id']) {
          // Uses schema for '...Type' property and not '...Id' property:
          if (schema.createOnly !== false) schema.createOnly = true;
        }

        // Allow createOnly to be set at either property or items level:
        if (schema.items && Object.isDefined(schema.items.createOnly)) {
          schema.createOnly = schema.items.createOnly;
          delete schema.items.createOnly;
        }

        // Allow readOnly to be set at either property or items level:
        if (schema.items && Object.isDefined(schema.items.readOnly)) {
          schema.readOnly = schema.items.readOnly;
          delete schema.items.readOnly;
        }
        
        // Allow referentialIntegrity to be set at either property or items level:
        if (schema.items && Object.isDefined(schema.items.referentialIntegrity)) {
          schema.referentialIntegrity = schema.items.referentialIntegrity;
          delete schema.items.referentialIntegrity;
        }
        
        this.ensurePropertySchema(property, schema, object);
      }.bind(this)
    );
  },
  
  ensurePropertySchema: function(property, schema, object) {
    // Give booleans some extra affection:
    if (schema.type.include('boolean')) {
      schema.title = (schema.title || property) + ' flag';
    }

    this.ensureType(property, schema, object);
    if (!schema.optional) this.ensurePresent(property, schema, object);
    if (schema.createOnly) this.ensureCreateOnly(property, schema, object);
    if (schema.readOnly) this.ensureReadOnly(property, schema, object);
    
    if (Object.isNumber(object)) {
      
      // Better to ensure schema.min is a number instead of coercing to a boolean
      // since schema.min may be 0 and would then evaluate to false:
      if (Object.isNumber(schema.min)) this.ensureNumberMin(property, schema, object);
      if (Object.isNumber(schema.max)) this.ensureNumberMax(property, schema, object);
      if (Object.isInteger(object) && schema.referentialIntegrity) {
        this.ensureReferentialIntegrity(property, schema, object);
      }
    
    } else if (Object.isString(object)) {
      
      if (schema.optional && object.empty()) return;
      if (Object.isNumber(schema.minLength)) this.ensureStringMinLength(property, schema, object);
      if (Object.isNumber(schema.maxLength)) this.ensureStringMaxLength(property, schema, object);
      if (schema.pattern) this.ensureStringPattern(property, schema, object);
      if (schema.format) this.ensureStringFormat(property, schema, object);
      if (schema.category) this.ensureStringCategory(property, schema, object);
    
    } else if (Object.isArray(object)) {
      
      if (schema.optional && object.empty()) return;
      if (Object.isNumber(schema.minItems)) this.ensureArrayMinItems(property, schema, object);
      if (Object.isNumber(schema.maxItems)) this.ensureArrayMaxItems(property, schema, object);
      if (schema.items) this.ensureArrayItemSchemas(property, schema, object);
      if (schema.referentialIntegrity) {
        this.ensureReferentialIntegrity(property, schema, object);
      }
      
    }
  },
  
  // Called for arrays if schema.items.
  // Items is an object representing a schema for each item.
  // Ensures each array item meets the items schema definition.
  ensureArrayItemSchemas: function(property, schema, array) {
    array.each(
      function(item) {
        // Let schema.items schema inherit title from schema if necessary:
        if (schema.title && !schema.items.title) schema.items.title = schema.title;
        this.ensurePropertySchema(property, schema.items, item);
      }.bind(this)
    );
  },
  
  // Called for arrays if schema.minItems is a number.
  // Ensures array.length is no greater than limit.
  ensureArrayMaxItems: function(property, schema, array) {
    if (array.length > schema.maxItems) {
      var message = '422 Please provide at most #{inflection}.';
      throw message.interpolate({
        inflection: (schema.maxItems + ' ' + (schema.title || property)).inflect()
      });
    }
  },
  
  // Called for arrays if schema.minItems is a number.
  // Ensures array.length is no less than limit.
  ensureArrayMinItems: function(property, schema, array) {
    if (array.length < schema.minItems) {
      var message = '422 Please provide at least #{inflection}.';
      throw message.interpolate({
        inflection: (schema.minItems + ' ' + (schema.title || property)).inflect()
      });
    }
  },
  
  // Called when 'createOnly' = true.
  // Ensures property value is not overrided except if previously null or undefined.
  // In other words, property can be written to, but only once, not thereafter.
  ensureCreateOnly: function(property, schema, object) {    
    // For more information on how this works see ensureReadOnly method below.
    if (Object.isUndefined(this.representation[property])) return;
    
    if (this.ensureChanges && this.action == 'update') {
      // Serialize to JSON to compare non-array or array items:
      var ancestor = JSON.stringify(this.ancestor[property]);
      var successor = JSON.stringify(this.representation[property]);

      if (ancestor !== successor) {
        var message = '422 #{property} is create-only and cannot be updated.';
        throw message.interpolate({
          property: (schema.title || property).upperCase(0) // Show plural if plural.
        });
      }
    }
  },
  
  // Called for numbers or integers if schema.max is a number.
  // Ensures number or integer is no greater than limit.
  ensureNumberMax: function(property, schema, number) {
    if (number > schema.max) {
      var message = '422 Please ensure your #{property} is not more than #{number}.';
      throw message.interpolate({
        property: (schema.title || property).singular(),
        number: schema.max
      });
    }
  },
  
  // Called for numbers or integers if schema.min is a number.
  // Ensures number or integer is no less than limit.
  ensureNumberMin: function(property, schema, number) {
    if (number < schema.min) {
      var message = '422 Please ensure your #{property} is not less than #{number}.';
      throw message.interpolate({
        property: (schema.title || property).singular(),
        number: schema.min
      });
    }
  },
  
  // Called when 'optional' = false or undefined ('optional' = false by default).
  // Ensures object is present.
  // Applies only to strings and arrays.
  // Other types should rely on type attribute for presence.
  // Strings are not present for '' or ' ' but are for ' 0 '.
  // Arrays are present for length !== 1.
  ensurePresent: function(property, schema, object) {
    var present = true;
    if (Object.isString(object) && object.empty()) present = false;
    if (Object.isArray(object) && object.length === 0) present = false;
    if (!present) {
      var message = '422 Please provide your #{property}.';
      throw message.interpolate({ property: (schema.title || property).singular() });
    }
  },
  
  // Called when 'readOnly' = true.
  // Ensures property value is not overrided except if previously null or undefined.
  // In other words, property can be written to, but only once, not thereafter.
  ensureReadOnly: function(property, schema, object) {
    // this.representation represents the difference between ancestor and successor,
    // i.e. client-requested changes and so some properties of representation may be
    // undefined, i.e. the client has not asked for them to be set. If the the client
    // has not requested a change for the given property, we return right away.
    // If the client has asked for the property to be set to something other than
    // what it was, we raise a read-only exception.
    if (Object.isUndefined(this.representation[property])) return;
    
    if (this.ensureChanges) {
      // Serialize to JSON to compare non-array or array items:
      var ancestor = JSON.stringify(this.ancestor[property]);
      var successor = JSON.stringify(this.representation[property]);

      if (ancestor !== successor) {
        var message = '422 #{property} is read-only and cannot be updated.';
        throw message.interpolate({
          property: (schema.title || property).upperCase(0) // Show plural if plural.
        });
      }
    }
  },
  
  // Called for integers or arrays if property ends with 'Id' or 'Ids'.
  // Ensures that related resources exist.
  // Does not ensure referential integrity on client.
  ensureReferentialIntegrity: function(property, schema, object) {
    if (Client) return;
    var ids = Object.isInteger(object) ? [ object ] : object.unique();

    // Return if ids empty:
    if (ids.empty()) return;
    
    // Determine associated model (polymorphic or conventional):
    var association = property.extract(/(.+)Ids?$/);
    if (this.schema[association + 'Type']) {
      // Handle polymorphic assocation (this prevents '...Type' proxy properties):
      var model = this.successor[association + 'Type'];
      if (!model) {
        throw '422 Please provide a valid ' + property.singular() + '.';
      }
    } else {
      // Handle conventional association:
      var model = association.upperCase(0);
    }
    
    // Ensure associated model exists:
    if (!Model[model]) {
      throw Exception.get(property, model + ' model undefined');
    }

    // Remove valid ids from ids:
    Model[model].get({ id: ids }).each(
      function(object) {
        ids = ids.without(object.id);
      }
    );
    
    if (ids.present()) {
      var exception = '422 Please provide a valid ' + property.singular();
      exception += ': ' + model + ': ' + ids.join(', ');
      exception += (ids.length == 1 ? ' does' : ' do') + ' not exist.';
      throw exception;
    }
  },
  
  // Called for strings if schema.category.
  // Ensures string is included in list of possible categories.
  ensureStringCategory: function(property, schema, string) {
    if (!schema.category.include(string)) {
      var categories = schema.category.clone();
      if (categories.length > 1) {
        categories.pop();
        categories = categories.join(', ') + ' or ' + schema.category.last();
      }
      var message = '422 Please provide a valid #{property} (#{categories}).';
      throw message.interpolate({
        property: (schema.title || property).singular(),
        categories: categories
      });
    }
  },
  
  // Called for strings if schema.format.
  // Ensures string matches predefined format: datetime, date, time, email, URI.
  ensureStringFormat: function(property, schema, string) {
    var formats = {
      datetime: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, // ISO 8601. UTC only as per spec.
      date: /^\d{4}-\d{2}-\d{2}$/,
      time: /^\d{2}:\d{2}:\d{2}$/,
      email: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,6}$/i,
      // Absolute URI requires protocol etc. Relative URI requires leading slash followed by non-spaces.
      uri: /^((http|https):\/\/([A-Z0-9-]+\.)?[A-Z0-9.-]+\.[A-Z]{2,6}\S*|\/\S+)$/i
    };
    if (!formats[schema.format].test(string)) {
      var message = '422 Please ensure your #{property} is a valid #{format}.';
      throw message.interpolate({
        property: (schema.title || property).singular(),
        format: schema.format
      });
    }
  },
  
  // Called for strings if schema.maxLength is a number.
  // Ensures string length is no greater than limit.
  ensureStringMaxLength: function(property, schema, string) {
    if (string.length > schema.maxLength) {
      var message = '422 Please ensure your #{property} is at most #{length} #{characters}.';
      throw message.interpolate({
        property: (schema.title || property).singular(),
        length: schema.maxLength,
        characters: schema.maxLength === 1 ? 'character' : 'characters'
      });
    }
  },
  
  // Called for strings if schema.minLength is a number.
  // Ensures string length is no less than limit.
  ensureStringMinLength: function(property, schema, string) {
    if (string.length < schema.minLength) {
      var message = '422 Please ensure your #{property} is at least #{length} #{characters}.';
      throw message.interpolate({
        property: (schema.title || property).singular(),
        length: schema.minLength,
        characters: schema.minLength === 1 ? 'character' : 'characters'
      });
    }
  },
  
  // Called for strings if schema.pattern.
  // Ensures string matches pattern.
  ensureStringPattern: function(property, schema, string) {
    if (!schema.pattern.test(string)) {
      var message = '422 Please provide a valid #{property}.';
      throw message.interpolate({ property: (schema.title || property).singular() });
    }
  },
  
  // Ensures object type.
  // 7.0 is regarded as an integer or number but 7.1 is regarded as a number.
  ensureType: function(property, schema, object) {
    // Reserved words must be quoted:
    var types = {
      'string': Object.isString(object),
      'number': Object.isNumber(object),
      'integer': Object.isInteger(object),
      'boolean': Object.isBoolean(object),
      'array': Object.isArray(object),
      'null': object === null
    };
    // Type can be a string or an array. Convert to an array in both cases:
    var match = [schema.type].flatten().any(
      function(type) {
        return types[type];
      }
    );
    if (!match) {
      var message = '422 Please provide a valid #{property}.';
      throw message.interpolate({ property: (schema.title || property).singular() });
    }
  }
});

var Model = {
  initialize: function() {
    var setStaticMethod = function(model, method) {
      Model[model][method] = function(model, method, object, options) {
        return new Model[model]()[method](object, options);
      }.curry(model, method);
    };

    Object.each(Model,
      function(model) {
        if (['Base', 'initialize', 'methods', 'models'].include(model)) return;
        // Set class names:
        Model[model].addMethods({ _className: model });
        // Set models array to help with iteration over models:
        Model.models.push(model);
        // Set static methods:
        Model.methods.each(setStaticMethod.curry(model));
      }
    );
  },

  methods: ['get', 'mutable', 'set', 'unset'],

  models: []
};

Model.Base = Class.create({
  _ensureId: function() {
    if (!Object.isInteger(this.id) || this.id < 1) {
      throw '422 Please provide a valid id.';
    }
  },
  
  _ensureSchema: function() {
    var schema = new Schema(this.schema, {
      ancestor: this._ancestor,
      representation: this._representation,
      successor: this,
      action: this._action,
      ensureChanges: !this._mutable
    });
    schema.ensure();
  },
  
  _executeCallbacks: function(value) {
    Object.keys(this).sort().each(
      function(key) {
        if (key.startsWith(value)) {
          if (value == 'beforeValidate') {
            if (key.startsWith('beforeValidateOnCreate')) return;
            if (key.startsWith('beforeValidateOnUpdate')) return;
          }
          this[key]();
        }
      }.bind(this)
    );
  },
  
  _initializeUndefinedProperties: function() {
    this._properties.each(
      function(property) {
        // Ignore proxy properties:
        if (this.schema[property].type == 'proxy') return;

        if (Object.isUndefined(this[property]) || this[property] === null) {
          // Schema.type can be string or array. Include method works for both.
          if (this.schema[property].type.include('null')) {
            this[property] = null;
          } else if (Object.isDefined(this.schema[property].value)) {
            this[property] = this.schema[property].value;
          } else if (this.schema[property].type.include('string')) {
            this[property] = '';
          } else if (this.schema[property].type.include('array')) {
            this[property] = [];
          }
        }
      }.bind(this)
    );
  },
  
  _save: function(options) {
    options = Object.extend({
      ancestor: this._ancestor, // Enable ancestor to be removed from indexes.
      indexes: this.indexes
    }, options);
    
    var object = {};
    this._properties.concat(['id', 'created', 'updated']).each(
      function(property) {
        // Ignore proxy properties on Server.
        // On Client we need to store proxy properties along with object.
        if (Server && this.schema[property] && this.schema[property].type == 'proxy') return;
        object[property] = this[property];
      }.bind(this)
    );
    Database.set(this._model + '/' + object.id, object, options);
  },
  
  _setDatetimes: function() {
    var dateTime = 'UTC'.toDateTime('ISO8601');
    if (this.isCreate()) {
      this.created = this._representation.created || dateTime;
    } else {
      this.created = this._ancestor.created;
    }
    this.updated = dateTime;
  },
  
  _setProxyProperties: function(representation) {
    // Avoid setting proxy properties if representation is an empty array:
    if (Object.isArray(representation) && representation.empty()) return;
    
    var models = {};
    var proxies = [];
    
    var loadForeignModel = function(model) {
      if (!Model[model]) {
        var message = 'Foreign model ' + model + ' undefined for proxy property';
        throw Exception.get(this._className, message);
      } else if (!models[model]) {
        models[model] = Model[model].get().index('id', true);
      }
    }.bind(this);
    
    this._properties.each(
      function(property) {
        if (this.schema[property].type != 'proxy') return;
        
        var polymorphicProperty;
        var foreignModel;
        var foreignProperty;
        var foreignKey = this._properties.find(
          function(foreignKey) {            
            var association = foreignKey.extract(/(.+)Ids?$/);

            if (association && property.startsWith(association)) {
              // Set polymorphicProperty or foreignModel:
              if (this.schema[association + 'Type']) {
                // Object[polymorphicProperty] determines foreignModel:
                polymorphicProperty = association + 'Type';
                // Load possible foreign models for polymorphicProperty:
                this.schema[polymorphicProperty].category.each(loadForeignModel);
              } else {
                foreignModel = association.upperCase(0);
                loadForeignModel(foreignModel);
              }
              
              // Set foreignProperty, eg. userName -> name:
              foreignProperty = property.sub(association, '').lowerCase(0);
              if (foreignKey.endsWith('s')) {
                // Property is plural for many-to-many associations,
                // singular foreignProperty for these situations:
                foreignProperty = foreignProperty.singular();
              }
              
              // Set foreignKey, eg. userId:
              return true;
            }
          }.bind(this)
        );
        
        if (!foreignKey) {
          var message = 'No foreign key for proxy property ' + property;
          throw Exception.get(this._className, message);
        }
        
        proxies.push({
          foreignModel: foreignModel, // eg. User (for non-polymorphic associations).
          foreignKey: foreignKey, // eg. userId (on Review model).
          foreignProperty: foreignProperty, // eg. name (on User model).
          property: property, // eg. userName (on Review model).
          polymorphicProperty: polymorphicProperty // eg. '...Type' or undefined.
        });
      }.bind(this)
    );

    var setProxyProperties = function(object) {
      proxies.each(
        function(proxy) {
          var foreignModel = object[proxy.polymorphicProperty] || proxy.foreignModel;
          if (proxy.foreignKey.endsWith('s')) {
            // Many-to-many, e.g. 'cuisineNames' or polymorphic proxy property.
            // Uses 'Cuisine' model or '...Type' model.
            var foreignValue = object[proxy.foreignKey].collect(
              function(foreignKey) {
                var foreignObject = models[foreignModel][foreignKey] || {};
                return foreignObject[proxy.foreignProperty]; 
              }
            );
          } else {
            // Many-to-one, e.g. 'cuisineName' or polymorphic proxy property:
            // Uses 'Cuisine' model or '...Type' model.
            var foreignObject = models[foreignModel][ object[proxy.foreignKey] ] || {};
            var foreignValue = foreignObject[proxy.foreignProperty];
          }
          object[proxy.property] = foreignValue;
        }
      );
    };

    if (proxies.present()) {
      if (Object.isArray(representation)) {
        representation.each(setProxyProperties);
      } else {
        setProxyProperties(representation);
      }
    }
  },
  
  _typecastRepresentation: function(representation) {
    Object.each(representation,
      function(key, value) {
        var schema = (key == 'id' ? { type: 'integer' } : this.schema[key]) || {};

        if (schema.type == 'proxy') return; // Ignore proxy properties.
        
        if (Object.isString(value)) {
          representation[key] = this._typecastValue(value, schema);
          
        } else if (Object.isArray(value) && schema.items) {
          representation[key] = value.collect(
            function(element) {
              if (Object.isString(element) && element.blank() && schema.items.type.exclude('string')) {
                // Ignore blank strings where strings are not allowed by schema.items.
                return null;
              } else {
                return this._typecastValue(element, schema.items);
              }
            }.bind(this)
          );
          if (schema.items.type.exclude('null')) {
            // Compact null elements where not allowed by schema.items.
            representation[key] = representation[key].compact();
          }
        }
      }.bind(this)
    );

    return representation;
  },
  
  _typecastValue: function(value, schema) {
    if (!Object.isString(value) || !schema || !schema.type) return value;

    var type = schema.type;
    
    if (type.include('null') && value.blank()) {
      return null;
      
    } else if (type.include('integer') && Object.isInteger(value.integer())) {
      // Typecasts to integer: '0' -> 0, 'a' -> NaN.
      return value.integer();
      
    } else if (type.include('number') && Object.isNumber(value.number())) {
      // Typecasts to number: '0' -> 0, 'a' -> NaN.
      return value.number();
      
    } else if (type.include('boolean') && (value == 'true' || value == 'false')) {
      // Typecasts to boolean: 'true' -> true, 'false' -> false.
      return value == 'true' ? true : false;
      
    } else if (type.include('array')) {
      // Typecasts to array: '2' -> [2] according to schema.items, '' -> [].
      return value.blank() ? [] : [ this._typecastValue(value.strip(), schema.items) ];

    } else {
      return value; 
    }
  },
  
  _validate: function() {
    // Run general validations:
    this._ensureId();
    this._ensureSchema();
    // Run specific validations:
    this._executeCallbacks('ensure');
  },

  initialize: function() {
    this._model = this._className.underscore().dasherize().plural();
    this._properties = Object.keys(this.schema);
  },
  
  isCreate: function() {
    return this._action == 'create';
  },
  
  isUpdate: function() {
    return this._action == 'update';
  },
  
  mutable: function(flag) {
    this._mutable = Object.isUndefined(flag) ? true : flag;
    // Return Model class instance to enable method chaining:
    return this;
  }
});

var Html = {  
  get: function(name, attributes, innerHTML) {
    if (Object.isString(attributes)) {
      innerHTML = attributes;
      attributes = {};
    }
    var tag = '<' + name;
    Object.each(attributes,
      function(key, value) {
        key = key.underscore().dasherize();
        if (key == 'class-name') key = 'class';
        tag += ' ' + key + '="' + value + '"';
      }
    );
    tag += '>' + (innerHTML || '') + '</' + name + '>';
    
    return tag;
  },
  
  setTagMethods: function(tags) {
    tags.each(function(tag) { Html[tag] = Html.get.curry(tag); });
  }
};

Html.setTagMethods(['a', 'br', 'div', 'hr', 'h2', 'h5', 'img', 'input', 'li', 'ol', 'p', 'span', 'ul']);

var SlopeOne = Class.create({
  initialize: function(options) {
    this.predictors = options.predictors;
    this.ratings = options.ratings;
    this.userIdProperty = options.userIdProperty || 'userId';
    this.itemIdProperty = options.itemIdProperty || 'itemId';
    this.ratingProperty = options.ratingProperty || 'rating';
    this.debug = options.debug;
    
    this.itemId1Property = this.itemIdProperty + '1';
    this.itemId2Property = this.itemIdProperty + '2';
    
    if (this.ratings) {
      this._setItemAndUserRatingsIndexes();
      if (!this.predictors) this.getPredictors(); // Sets this.predictors.
    }
    this._setPredictorsIndex();
  },
  
  getHighlyRatedItems: function(options) {
    if (this.debug) var timer = new Timer();
    var id = this.highlyRatedItemId;
    var items = this.getRelatedItems(id, options);
    if (options && options.itemIds) {
      if (options.itemIds.include(id)) items.unshift(id);
    } else {
      items.unshift(id);
    }
    if (this.debug) timer.log('getHighlyRatedItems');
    return items;
  },
  
  getRecommendedItems: function(userId, options) {
    if (this.debug) var timer = new Timer();

    options = options || {};
    if (!this.userItemRatingIndex[userId]) {
      if (this.debug) timer.log('getRecommendedItems');
      return [];
    }
    
    var threshold = options.threshold || 0;
    if (options.aboveUserAverageRating) {
      var average = this.userAverageRatingIndex[userId];
      threshold = [threshold, average.sum / average.count].max();
    }

    var ratings = [];
    (options.itemIds || this.itemIds).each(
      function(itemId) {
        var rating = {};
        rating[this.itemIdProperty] = itemId;
        var rated = this.userItemRatingIndex[userId][itemId];
        if (!rated) {
          rating[this.ratingProperty] = this.getPredictedUserRating(userId, itemId);
          if (rating[this.ratingProperty] >= threshold) {
            ratings.push(rating); // Use predicted rating.
          }
        } else if (!options.unrated) {
          rating[this.ratingProperty] = rated.sum / rated.count;
          if (rating[this.ratingProperty] >= threshold) {
            ratings.push(rating); // Use actual rating.
          }
        }
      }.bind(this)
    );

    // Order by rating descending AND itemIdProperty, since rating may be the same
    // across many items and one may want to compare SlopeOne from different contexts
    // which may or may not provide options.itemIds which would influence the order
    // and provide confusing though accurate results.
    var items = ratings.order('-rating', this.itemIdProperty).pluck(this.itemIdProperty);
    
    if (this.debug) timer.log('getRecommendedItems');
    
    return items;
  },
  
  getRelatedItems: function(itemId1, options) {
    if (this.debug) var timer = new Timer();
    
    options = options || {};

    if (options.itemIds) {
      var itemIds = {};
      options.itemIds.each(function(id) { itemIds[id] = true; });
    } else {
      var itemIds = false;
    }
    
    var predictors = [];
    Object.each(this.predictorsIndex[itemId1],
      function(itemId2, p) {
        if (itemIds && !itemIds[itemId2]) return;
        if (p.count < 1) return; // Ignore empty predictors.
        
        var predictor = { average: p.sum / p.count, distance: Math.abs(p.sum / p.count) };
        if (predictor.distance > options.threshold) return;
        predictor[this.itemId2Property] = itemId2;
        predictors.push(predictor);
      }.bind(this)
    );
    // Predictor.average represents average difference between item 1 and item 2.
    // I.e. average rating for item 1 - average rating for item 2 = average difference.
    // If average difference is positive, item 1 is rated higher than item 2.
    // If average difference is negative, item 2 is rated higher than item 1.
    var items = predictors.order('distance', 'average').pluck(this.itemId2Property);
    
    if (this.debug) timer.log('getRelatedItems');
    
    return items;
  },
  
  getPredictedUserRating: function(userId, itemId1) {
    if (this.debug) var timer = new Timer();
    
    var numerator = 0;
    var denominator = 0;
    (this.userRatingsIndex[userId] || []).each(
      function(rating) {
        var itemId2 = rating[this.itemIdProperty];
        var predictor = (this.predictorsIndex[itemId1] || {})[itemId2];
         // Ignore empty predictors:
        if (predictor && predictor.count > 0) {
          var average = predictor.sum / predictor.count;
          numerator += predictor.count * (rating[this.ratingProperty] + average);
          denominator += predictor.count;
        }
      }.bind(this)
    );
    
    if (this.debug) timer.log('getPredictedUserRating');
    
    return denominator === 0 ? 0 : numerator / denominator;
  },
  
  getPredictors: function() {
    if (this.debug) var timer = new Timer();
    
    // Avoid using 'bind(this)' in the nested loop below:
    var userRatingsIndex = this.userRatingsIndex;
    var userIdProperty = this.userIdProperty;
    var itemIdProperty = this.itemIdProperty;
    var itemId1Property = this.itemId1Property;
    var itemId2Property = this.itemId2Property;
    var ratingProperty = this.ratingProperty;
    var debug = this.debug;
        
    var added = {};
    var index = {};
    
    this.ratings.each(
      function(r1) {
        userRatingsIndex[ r1[userIdProperty] ].each(
          function(r2) {
            // r1 would have been recently added and must not yet exist in r2:
            if (added[r2.id]) { return; } else { added[r1.id] = true; }
            
            // Ignore pairs consisting of the same itemId:
            if (r1[itemIdProperty] === r2[itemIdProperty]) return;
            
            var d = r1[ratingProperty] - r2[ratingProperty];
            var p = (index[ r1[itemIdProperty] ] || {})[ r2[itemIdProperty] ];
            
            if (p) {
              p.count++;
              p.sum += d;
            } else {
              if (!index[ r1[itemIdProperty] ]) index[ r1[itemIdProperty] ] = {};
              p = index[ r1[itemIdProperty] ][ r2[itemIdProperty] ] = { count: 1, sum: d };
              p[itemId1Property] = r1[itemIdProperty];
              p[itemId2Property] = r2[itemIdProperty];
            }
          }
        );
      }
    );
    
    var predictors = [];
    Object.each(index,
      function(itemId1, subindex) {
        Object.each(subindex,
          function(itemId2, predictor) {
            predictor.id = Id.get();
            predictors.push(predictor);
          }
        );
      }
    );
    
    if (this.debug) timer.log('getPredictors');

    return this.predictors = predictors;
  },
  
  // Returns predictors that need to be set, after a given rating is either added,
  // changed or unset. Pass 'r1' for additions or changes, or undefined for unsets.
  // Pass 'ancestor' for changes or unsets. For unsets pass 'r1' as 'ancestor'.
  getUpdatedPredictors: function(r1, ancestor) {
    if (this.debug) var timer = new Timer();
    
    var addD = true;
    
    if (Object.isUndefined(r1) && ancestor) {
      // Ancestor was unset and predictors need to be adjusted.
      r1 = ancestor;
      addD = false;
      
      var dMultiplier = -1; // Used to take old difference out of sum.
      var dAdjustment = 0;
      var countAdjustment = -1;
    } else if (ancestor) {
      // Ancestor was changed and predictors need to be adjusted.
      var dMultiplier = -1; // Used to take old difference out of sum.
      var dAdjustment = r1[this.ratingProperty] - ancestor[this.ratingProperty];
      var countAdjustment = 0;
    } else {
      var dMultiplier = 0;
      var dAdjustment = 0;
      var countAdjustment = 1;
    }
    
    var updated = [];
    var updatedIndex = {};
    var index = this.predictorsIndex;
    
    this.userRatingsIndex[ r1[this.userIdProperty] ].each(
      function(r2) {
        var itemId1 = r1[this.itemIdProperty];
        var itemId2 = r2[this.itemIdProperty];
        
        // Ignore pairs consisting of the same itemId:
        if (itemId1 === itemId2) return;
        
        var d = r1[this.ratingProperty] - r2[this.ratingProperty];
        var dAdj = dAdjustment;
        var p = (index[itemId1] || {})[itemId2];
        
        // this.predictorsIndex is a symmetrical index for which half of the index
        // is virtual, i.e. not represented by underlying predictor resources and
        // lacking id, created, updated properties.
        // If r1.itemId1:r2.itemId2 is not found or has no id, check the inverse
        // to try and find the underlying resource which must be updated:
        if (!p || !p.id) {
          itemId1 = r2[this.itemIdProperty];
          itemId2 = r1[this.itemIdProperty];
          d = -d;
          dAdj = -dAdjustment;
          p = (index[itemId1] || {})[itemId2];
        }
        
        if (p) {
          p.count += countAdjustment;
          p.sum += (d * dMultiplier) + (addD ? d : 0) + dAdj;
          // Add to update array once, then adjust count, sum by reference
          // which will affect p if already in the update array.
          // We might land up here if a user gives 2 ratings to itemId 152
          // then updates a rating given to itemId 73.
          // The updated rating would then be compared against each of those
          // given to itemId 152, and the underlying predictor updated twice.
          if (!updatedIndex[p.id]) {
            updatedIndex[p.id] = true;
            updated.push(p);
          }
        } else {
          if (!index[itemId1]) index[itemId1] = {};
          p = index[itemId1][itemId2] = { id: Id.get(), count: 1, sum: d };
          p[this.itemId1Property] = itemId1;
          p[this.itemId2Property] = itemId2;
          updated.push(p);
        }
      }.bind(this)
    );
    
    if (this.debug) timer.log('getUpdatedPredictors');
    
    return updated;
  },
  
  _setPredictorsIndex: function() {
    if (this.debug) var timer = new Timer();
    
    var itemId1 = this.itemId1Property;
    var itemId2 = this.itemId2Property;
    var index = {};
    this.predictors.each(
      function(p1) {        
        // Calculate p2 inverse of p1:
        var p2 = { count: p1.count, sum: -p1.sum };
        p2[this.itemId1Property] = p1[this.itemId2Property];
        p2[this.itemId2Property] = p1[this.itemId1Property];
        
        // Set first-half of index:
        if (!index[ p1[this.itemId1Property] ]) index[ p1[this.itemId1Property] ] = {};
        index[ p1[this.itemId1Property] ][ p1[this.itemId2Property] ] = p1;
        
        // Set second-half of index:
        if (!index[ p2[this.itemId1Property] ]) index[ p2[this.itemId1Property] ] = {};
        index[ p2[this.itemId1Property] ][ p2[this.itemId2Property] ] = p2;
      }.bind(this)
    );
    this.predictorsIndex = index;
    
    if (this.debug) timer.log('_setPredictorsIndex');
  },
  
  _setItemAndUserRatingsIndexes: function() {
    if (this.debug) var timer = new Timer();
    
    var rI = {}; // Rating item index.
    var hR = 0; // Highest rating encountered.
    
    var itemIds = [];
    var itemIdsIndex = {};
    var uAR = {}; // User average rating index.
    var uIR = {}; // User item rating index.
    var uR = {}; // User ratings index.
    
    this.ratings.each(
      function(r) {
        // Set rating item index:
        if (!rI[ r[this.ratingProperty] ]) rI[ r[this.ratingProperty] ] = [];
        rI[ r[this.ratingProperty] ].push(r[this.itemIdProperty]);
        hR = [hR, r[this.ratingProperty] ].max();
        
        // Add itemId to itemIds if not yet added:
        if (!itemIdsIndex[ r[this.itemIdProperty] ]) {
          itemIds.push(r[this.itemIdProperty]);
          itemIdsIndex[ r[this.itemIdProperty] ] = true;
        }
        
        // Set user average rating:
        if (!uAR[ r[this.userIdProperty] ]) uAR[ r[this.userIdProperty] ] = { count: 0, sum: 0 };
        uAR[ r[this.userIdProperty] ].count++;
        uAR[ r[this.userIdProperty] ].sum += r[this.ratingProperty];
        
        // Set user item rating (allow multiple ratings by the same user for the same item):
        if (!uIR[ r[this.userIdProperty] ]) uIR[ r[this.userIdProperty] ] = {};
        uIR[ r[this.userIdProperty] ][ r[this.itemIdProperty] ] = { count: 0, sum: 0 };
        uIR[ r[this.userIdProperty] ][ r[this.itemIdProperty] ].count++;
        uIR[ r[this.userIdProperty] ][ r[this.itemIdProperty] ].sum += r[this.ratingProperty];
        
        // Set user ratings:
        if (!uR[ r[this.userIdProperty] ]) uR[ r[this.userIdProperty] ] = [];
        uR[ r[this.userIdProperty] ].push(r);
      }.bind(this)
    );

    this.highlyRatedItemId = (rI[hR] || [undefined]).random();
    this.itemIds = itemIds;
    this.userAverageRatingIndex = uAR;
    this.userItemRatingIndex = uIR;
    this.userRatingsIndex = uR;
    
    if (this.debug) timer.log('_setItemAndUserRatingsIndexes');
  }
});

var Inflector = {
  inflections: {
    plural: [
      [/(quiz)$/i, "$1zes"],
      [/^(ox)$/i, "$1en"],
      [/([m|l])ouse$/i, "$1ice"],
      [/(matr|vert|ind)ix|ex$/i, "$1ices"],
      [/(x|ch|ss|sh)$/i, "$1es"],
      [/([^aeiouy]|qu)y$/i, "$1ies"],
      [/(hive)$/i, "$1s"],
      [/(?:([^f])fe|([lr])f)$/i, "$1$2ves"],
      [/sis$/i, "ses"],
      [/([ti])um$/i, "$1a"],
      [/(buffal|tomat)o$/i, "$1oes"],
      [/(bu)s$/i, "$1ses"],
      [/(alias|status)$/i, "$1es"],
      [/(octop|vir)us$/i, "$1i"],
      [/(ax|test)is$/i, "$1es"],
      [/^(n)ews$/i, "$1ews"],
      [/^(c)hild$/i, "$1hildren"],
      [/^(m)an$/i, "$1en"],
      [/^(w)oman$/i, "$1omen"],
      [/^(s)ex$/i, "$1sexes"],
      [/^(g)oose$/i, "$1eese"],
      [/^(p)erson$/i, "$1eople"],
      [/^(m)ove$/i, "$1oves"],
      [/s$/i, "s"],
      [/$/i, "s"]
    ],
    singular: [
      [/(quiz)zes$/i, "$1"],
      [/(matr)ices$/i, "$1ix"],
      [/(vert|ind)ices$/i, "$1ex"],
      [/^(ox)en/, "$1"],
      [/(alias|status)es$/i, "$1"],
      [/(octop|vir)i$/i, "$1us"],
      [/(cris|ax|test)es$/i, "$1is"],
      [/(shoe)s$/i, "$1"],
      [/(o)es$/i, "$1"],
      [/(bus)es$/i, "$1"],
      [/([m|l])ice$/i, "$1ouse"],
      [/(x|ch|ss|sh)es$/i, "$1"],
      [/(m)ovies$/i, "$1ovie"],
      [/(s)eries$/i, "$1eries"],
      [/([^aeiouy]|qu)ies$/i, "$1y"],
      [/([lr])ves$/i, "$1f"],
      [/(tive)s$/i, "$1"],
      [/(hive)s$/i, "$1"],
      [/([^f])ves$/i, "$1fe"],
      [/(^analy)ses$/i, "$1sis"],
      [/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, "$1$2sis"],
      [/([ti])a$/i, "$1um"],
      [/^(n)ews$/i, "$1ews"],
      [/^(c)hildren$/i, "$1hild"],
      [/^(m)en$/i, "$1an"],
      [/^(w)omen$/i, "$1oman"],
      [/^(s)exes$/i, "$1sex"],
      [/^(g)eese$/i, "$1oose"],
      [/^(p)eople$/i, "$1erson"],
      [/^(m)oves$/i, "$1ove"],
      [/s$/i, ""]
    ],       
    neutral: ['sheep', 'fish', 'series', 'species', 'money', 'rice', 'information', 'equipment']
  },
  
  ordinal: function(number) {
    if (11 <= parseInt(number) % 100 && parseInt(number) % 100 <= 13) {
      return number + 'th';
    } else {
      switch (parseInt(number) % 10) {
        case 1: return number + 'st';
        case 2: return number + 'nd';
        case 3: return number + 'rd';
        default: return number + 'th';
      }
    }
  },
  
  inflect: function(phrase) {
    var words = phrase.split(' '), number = parseInt(words.first()), subject = words.last();
    if (!words.length || words.length == 1 || (number !== 0 && !number)) return phrase;
    return phrase.gsub(subject, number == 1 ? subject.singular() : subject.plural());
  },
  
  plural: function(word) {
    var wordLowerCased = word.toLowerCase();
    for (var i = 0; i < Inflector.inflections.neutral.length; i++)
      if (wordLowerCased == Inflector.inflections.neutral[i]) return word;
    for (var i = 0; i < Inflector.inflections.plural.length; i++)
      if (Inflector.inflections.plural[i][0].test(word)) return word.replace(Inflector.inflections.plural[i][0], Inflector.inflections.plural[i][1]);
    return word;
  },
  
  singular: function(word) {
    var wordLowerCased = word.toLowerCase();
    for (var i = 0; i < Inflector.inflections.neutral.length; i++)
      if (wordLowerCased == Inflector.inflections.neutral[i]) return word;
    for (var i = 0; i < Inflector.inflections.singular.length; i++)
      if (Inflector.inflections.singular[i][0].test(word)) return word.replace(Inflector.inflections.singular[i][0], Inflector.inflections.singular[i][1]);
    return word;
  },
  
  possessive: function(phrase) {
    var letters = phrase.toArray();
    if (!letters.length || letters.length == 1 || letters.last() == "'" || letters[letters.length - 2] == "'") return phrase;
    return phrase + (letters.last().toLowerCase() == 's' ? "'" : "'s");
  }
};

var Search = Class.create({
  common: 'a and are be by can do does from get here how i if in is it me my not ' +
          'of our than the there this to way what will with you your',
    
  initialize: function(options) {
    this.options = options;
    if (Object.isDefined(options.common)) this.common = options.common;
    this.indexes = [];
    
    options.objects.each(
      function(object) {
        var index = { object: object, text: [] };
        options.properties.each(
          function(property) {
            index.text.push(object[property] || '');
          }
        );
        index.text = index.text.join(' ');
        this.indexes.push(index);
      }.bind(this)
    );
  },
  
  query: function(query) {
    var words = query.split(/ +/);
    // Excluding short words in the presence of longer words helps to prevent the
    // results from bouncing as the start of a new word is appended to a query.
    var excludeShortWords = words.any(function(word) { return word.length > 2; });
    var string = words.findAll(
      function(word) {
        if (excludeShortWords && word.length <= 2) return;
        if (word.length < this.options.minKeywordLength) return;
        if (this.common) return this.common.exclude(word);
        return true;
      }.bind(this)
    ).join(' ');
    
    if (string.blank()) return [];
    
    var pattern = RegExp.escape(string);
    var replacements = {
      'a': '(a|à|ä)',
      'c': '(c|ç)',
      'e': '(e|è|é|ê)',
      'i': '(i|î|ï)',
      'o': '(o|ô|ö)',
      'u': '(u|ü)'
    };
    Object.each(replacements,
      function(letter, alternatives) {
        pattern = pattern.gsub(letter, alternatives);
      }
    );
    pattern = pattern.split(/ /).join('|');
    var regex = new RegExp(pattern, 'i');
    
    var results = [];
    this.indexes.each(
      function(index) {
        index.frequency = index.text.split(regex).length - 1;
        if (index.frequency >= (this.options.threshold || 1)) results.push(index);
      }.bind(this)
    );
    
    return results.order('-frequency').pluck('object');
  }
});

var Id = {
  get: function() {
    // Attempt to generate a unique id and raise an exception after five attempts:
    var id;
    var attempts = 0;
    do {
      if ((attempts++) >= 5) {
        var message = 'Unable to generate a unique id after ' + attempts + ' attempts';
        throw Exception.get('Id', message);
      }
      id = this._getId();
    } while (this._used[id]);
    
    // Prevent id from being used by subsequent calls to Id.get: 
    this._used[id] = true;
    this._usedLength++;
    
    // Garbage collect Id._used:
    if (this._usedLength > 10000) {
      this._used = {};
      this._usedLength = 0;
    }

    return id;
  },
  
  _getId: function() {
    var seed = $R(100, 999).random();
    return (Timestamp.get() + '' + seed) * 1;
  },
  
  _used: {},
  
  _usedLength: 0
};

var Md5 = {
  hexDigest : function(s) {
    return Md5.binToHex(Md5.core(Md5.strToBin(s), s.length * 8)).toLowerCase();
  },
  
  //Calculate the MD5 of an array of little-endian words, and a bit length
  core : function(x, len) {
    x[len >> 5] |= 0x80 << ((len) % 32);
    x[(((len + 64) >>> 9) << 4) + 14] = len;

    var a = 1732584193, b = -271733879, c = -1732584194, d = 271733878;

    for(var i = 0; i < x.length; i += 16) {
      
      var olda = a, oldb = b, oldc = c, oldd = d;

      a = Md5.ff(a, b, c, d, x[i+ 0], 7, -680876936);
      d = Md5.ff(d, a, b, c, x[i+ 1], 12, -389564586);
      c = Md5.ff(c, d, a, b, x[i+ 2], 17, 606105819);
      b = Md5.ff(b, c, d, a, x[i+ 3], 22, -1044525330);
      a = Md5.ff(a, b, c, d, x[i+ 4], 7, -176418897);
      d = Md5.ff(d, a, b, c, x[i+ 5], 12, 1200080426);
      c = Md5.ff(c, d, a, b, x[i+ 6], 17, -1473231341);
      b = Md5.ff(b, c, d, a, x[i+ 7], 22, -45705983);
      a = Md5.ff(a, b, c, d, x[i+ 8], 7, 1770035416);
      d = Md5.ff(d, a, b, c, x[i+ 9], 12, -1958414417);
      c = Md5.ff(c, d, a, b, x[i+10], 17, -42063);
      b = Md5.ff(b, c, d, a, x[i+11], 22, -1990404162);
      a = Md5.ff(a, b, c, d, x[i+12], 7, 1804603682);
      d = Md5.ff(d, a, b, c, x[i+13], 12, -40341101);
      c = Md5.ff(c, d, a, b, x[i+14], 17, -1502002290);
      b = Md5.ff(b, c, d, a, x[i+15], 22, 1236535329);

      a = Md5.gg(a, b, c, d, x[i+ 1], 5, -165796510);
      d = Md5.gg(d, a, b, c, x[i+ 6], 9, -1069501632);
      c = Md5.gg(c, d, a, b, x[i+11], 14, 643717713);
      b = Md5.gg(b, c, d, a, x[i+ 0], 20, -373897302);
      a = Md5.gg(a, b, c, d, x[i+ 5], 5, -701558691);
      d = Md5.gg(d, a, b, c, x[i+10], 9, 38016083);
      c = Md5.gg(c, d, a, b, x[i+15], 14, -660478335);
      b = Md5.gg(b, c, d, a, x[i+ 4], 20, -405537848);
      a = Md5.gg(a, b, c, d, x[i+ 9], 5, 568446438);
      d = Md5.gg(d, a, b, c, x[i+14], 9, -1019803690);
      c = Md5.gg(c, d, a, b, x[i+ 3], 14, -187363961);
      b = Md5.gg(b, c, d, a, x[i+ 8], 20, 1163531501);
      a = Md5.gg(a, b, c, d, x[i+13], 5, -1444681467);
      d = Md5.gg(d, a, b, c, x[i+ 2], 9, -51403784);
      c = Md5.gg(c, d, a, b, x[i+ 7], 14, 1735328473);
      b = Md5.gg(b, c, d, a, x[i+12], 20, -1926607734);

      a = Md5.hh(a, b, c, d, x[i+ 5], 4, -378558);
      d = Md5.hh(d, a, b, c, x[i+ 8], 11, -2022574463);
      c = Md5.hh(c, d, a, b, x[i+11], 16, 1839030562);
      b = Md5.hh(b, c, d, a, x[i+14], 23, -35309556);
      a = Md5.hh(a, b, c, d, x[i+ 1], 4, -1530992060);
      d = Md5.hh(d, a, b, c, x[i+ 4], 11, 1272893353);
      c = Md5.hh(c, d, a, b, x[i+ 7], 16, -155497632);
      b = Md5.hh(b, c, d, a, x[i+10], 23, -1094730640);
      a = Md5.hh(a, b, c, d, x[i+13], 4, 681279174);
      d = Md5.hh(d, a, b, c, x[i+ 0], 11, -358537222);
      c = Md5.hh(c, d, a, b, x[i+ 3], 16, -722521979);
      b = Md5.hh(b, c, d, a, x[i+ 6], 23, 76029189);
      a = Md5.hh(a, b, c, d, x[i+ 9], 4, -640364487);
      d = Md5.hh(d, a, b, c, x[i+12], 11, -421815835);
      c = Md5.hh(c, d, a, b, x[i+15], 16, 530742520);
      b = Md5.hh(b, c, d, a, x[i+ 2], 23, -995338651);

      a = Md5.ii(a, b, c, d, x[i+ 0], 6, -198630844);
      d = Md5.ii(d, a, b, c, x[i+ 7], 10, 1126891415);
      c = Md5.ii(c, d, a, b, x[i+14], 15, -1416354905);
      b = Md5.ii(b, c, d, a, x[i+ 5], 21, -57434055);
      a = Md5.ii(a, b, c, d, x[i+12], 6, 1700485571);
      d = Md5.ii(d, a, b, c, x[i+ 3], 10, -1894986606);
      c = Md5.ii(c, d, a, b, x[i+10], 15, -1051523);
      b = Md5.ii(b, c, d, a, x[i+ 1], 21, -2054922799);
      a = Md5.ii(a, b, c, d, x[i+ 8], 6, 1873313359);
      d = Md5.ii(d, a, b, c, x[i+15], 10, -30611744);
      c = Md5.ii(c, d, a, b, x[i+ 6], 15, -1560198380);
      b = Md5.ii(b, c, d, a, x[i+13], 21, 1309151649);
      a = Md5.ii(a, b, c, d, x[i+ 4], 6, -145523070);
      d = Md5.ii(d, a, b, c, x[i+11], 10, -1120210379);
      c = Md5.ii(c, d, a, b, x[i+ 2], 15, 718787259);
      b = Md5.ii(b, c, d, a, x[i+ 9], 21, -343485551);

      a = Md5.add(a, olda);
      b = Md5.add(b, oldb);
      c = Md5.add(c, oldc);
      d = Md5.add(d, oldd);
    }
    return Array(a, b, c, d);
  },

  //These functions implement the four basic operations the algorithm uses.
  cmn : function(q, a, b, x, s, t) {
    return Md5.add(Md5.rotate(Md5.add(Md5.add(a, q), Md5.add(x, t)), s), b);
  },
  
  ff : function(a, b, c, d, x, s, t) {
    return Md5.cmn((b & c) | ((~b) & d), a, b, x, s, t);
  },
  
  gg : function(a, b, c, d, x, s, t) {
    return Md5.cmn((b & d) | (c & (~d)), a, b, x, s, t);
  },
  
  hh : function(a, b, c, d, x, s, t) {
    return Md5.cmn(b ^ c ^ d, a, b, x, s, t);
  },

  ii : function(a, b, c, d, x, s, t) {
    return Md5.cmn(c ^ (b | (~d)), a, b, x, s, t);
  },

  //Add integers, wrapping at 2^32. This uses 16-bit operations internally to work around bugs in some JS interpreters.
  add : function(x, y) {
    var lsw = (x & 0xFFFF) + (y & 0xFFFF);
    var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
    return (msw << 16) | (lsw & 0xFFFF);
  },

  //Bitwise rotate a 32-bit number to the left.
  rotate : function(num, cnt) {
    return (num << cnt) | (num >>> (32 - cnt));
  },

  //Convert a string to an array of little-endian words
  strToBin : function(str) {
    var bin = Array();
    var mask = (1 << 8) - 1;
    for(var i = 0; i < str.length * 8; i += 8)
      bin[i>>5] |= (str.charCodeAt(i / 8) & mask) << (i%32);
    return bin;
  },

  //Convert an array of little-endian words to a hex string.
  binToHex : function(binarray) {
    var hex_tab = true ? "0123456789ABCDEF" : "0123456789abcdef";
    var str = "";
    for(var i = 0; i < binarray.length * 4; i++) {
      str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
             hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
    }
    return str;
  }
};

var Timestamp = {
  // Returns millisecond UTC timestamp:
  get: function(seconds) {
    var timestamp = new Date().getTime();
    if (seconds) timestamp = (timestamp / 1000).round();
    return timestamp;
  }
};

var Benchmark = function(procedure, iterations, description) {
  var elapsed = 0;
  (iterations = iterations || 1).times(
    function() {
      elapsed -= Timestamp.get(); procedure(); elapsed += Timestamp.get();
    }
  );
  Log((description ? description + ': ' : '') + (elapsed / iterations) + 'ms');
};

var File = {
  contentType: function(file) {
    return ContentType.getContentType(file);
  },
  
  extension: function(file) {
    return file.extract(/\.([A-Z]+)$/i);
  }
};

var Timer = Class.create({
  elapsed: function() {
    return Timestamp.get() - this.timestamp;
  },
  
  initialize: function() {
    this.timestamp = Timestamp.get();
  },
  
  log: function(namespace) {
    Log((namespace ? namespace + ': ' : '') + this.elapsed() + 'ms');
  }
});

var BloomFilter = Class.create({  
  get: function(key) {
    var indices = this._getIndices(key);
    for (var index = 0, length = indices.length; index < length; ++index) {
		  if (!this.filter[ indices[index] ]) return false;
		}
		return true;
  },
  
  hash: function(key, seed) {
    return Murmur.get(key, seed);
  },
  
  initialize: function(options) {
    Object.extend(this, options);
    if (!this.k || !this.m) this._setKM();
    if (!this.filter) this._setFilter();
  },
  
  set: function(key) {
    var indices = this._getIndices(key);
    for (var index = 0, length = indices.length; index < length; ++index) {
		  this.filter[ indices[index] ] = true;
		}
  },
  
  _getIndices: function(key) {
    // Use Enhanced Double Hashing for optimal performance:
    // http://www.cc.gatech.edu/fac/Pete.Manolios/pub/bloom-filters-verification.pdf
    var x = this.hash(key, 0).abs() % this.m;
    var y = this.hash(key, 1).abs() % this.m;
    var indices = [x];
    for (var index = 1; index < this.k; ++index) {
      x = (x + y) % this.m;
      y = (y + index) % this.m;
      indices[index] = x;
    }
    return indices;
  },
  
  _setFilter: function() {
    this.filter = new Array(this.m);
    for (var index = 0; index < this.m; ++index) {
      this.filter[index] = false;
    }
  },
  
  // Set optimal k and m given options.e and options.n.
  _setKM: function() {
    // Ensure options.e and options.n are always provided:
    if (!this.e) {
      throw '422 Please provide a valid Bloom filter error rate (e).';
    } else if (!this.n) {
      throw '422 Please specify the number of possible Bloom filter elements (n).';
    }
		this.k = 1;
		this.m = undefined;
		for (var k = 1, m; k <= 100; k++) {
			m = (-1 * k * this.n) / (Math.log(1 - Math.pow(this.e, (1 / k))));
			if (!this.m || (m < this.m) ) {
				this.m = m;
				this.k = k;
			}
		}
		this.m = Math.floor(this.m) + 1;
	}
});

var Base64 = {
	_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",

	encode : function (input) {
		var output = "";
		var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
		var i = 0;

		input = Base64._utf8_encode(input);

		while (i < input.length) {

			chr1 = input.charCodeAt(i++);
			chr2 = input.charCodeAt(i++);
			chr3 = input.charCodeAt(i++);

			enc1 = chr1 >> 2;
			enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
			enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
			enc4 = chr3 & 63;

			if (isNaN(chr2)) {
				enc3 = enc4 = 64;
			} else if (isNaN(chr3)) {
				enc4 = 64;
			}

			output = output +
			this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
			this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);

		}

		return output;
	},

	decode : function (input) {	  
		var output = "";
		var chr1, chr2, chr3;
		var enc1, enc2, enc3, enc4;
		var i = 0;

		input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

		while (i < input.length) {

			enc1 = this._keyStr.indexOf(input.charAt(i++));
			enc2 = this._keyStr.indexOf(input.charAt(i++));
			enc3 = this._keyStr.indexOf(input.charAt(i++));
			enc4 = this._keyStr.indexOf(input.charAt(i++));

			chr1 = (enc1 << 2) | (enc2 >> 4);
			chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
			chr3 = ((enc3 & 3) << 6) | enc4;

			output = output + String.fromCharCode(chr1);

			if (enc3 != 64) {
				output = output + String.fromCharCode(chr2);
			}
			if (enc4 != 64) {
				output = output + String.fromCharCode(chr3);
			}

		}

		output = Base64._utf8_decode(output);

		return output;
	},

	_utf8_encode : function (string) {  
		string = string.replace(/\r\n/g,"\n");
		var utftext = "";

		for (var n = 0; n < string.length; n++) {

			var c = string.charCodeAt(n);

			if (c < 128) {
				utftext += String.fromCharCode(c);
			}
			else if((c > 127) && (c < 2048)) {
				utftext += String.fromCharCode((c >> 6) | 192);
				utftext += String.fromCharCode((c & 63) | 128);
			}
			else {
				utftext += String.fromCharCode((c >> 12) | 224);
				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
				utftext += String.fromCharCode((c & 63) | 128);
			}

		}

		return utftext;
	},

	_utf8_decode : function (utftext) {	  
		var string = "";
		var i = 0;
		var c = c1 = c2 = 0;

		while ( i < utftext.length ) {

			c = utftext.charCodeAt(i);

			if (c < 128) {
				string += String.fromCharCode(c);
				i++;
			}
			else if((c > 191) && (c < 224)) {
				c2 = utftext.charCodeAt(i+1);
				string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
				i += 2;
			}
			else {
				c2 = utftext.charCodeAt(i+1);
				c3 = utftext.charCodeAt(i+2);
				string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
				i += 3;
			}

		}

		return string;
	}
};

var Inspect = function(object) {
  Log(JSON.stringify(object));
};

var Exception = {
  // Strips whitespace.
  // Adds a full-stop after removing '.', '?' and '!'.
  // Prepends namespace if provided.
  get: function(namespace, e) {
    var format = function(e) {
      return String(e).without(/(\.|\?|!)$/) + '.';
    };
    
    if (!e) {
      return format(namespace);
    } else {
      return namespace + ': ' + format(e);
    }
  }
};

var ContentType = {
  contentTypes: {},
  
  getContentType: function(file) {
    var extension = File.extension(file).toLowerCase();
    return Object.keys(ContentType.contentTypes).find(
      function(key) {
        var contentTypes = ContentType.contentTypes[key];
        if (Object.isArray(contentTypes)) {
          return contentTypes.include(extension);
        } else {
          // Use '==' instead of 'include' to prevent finding 'json' for 'js':
          return contentTypes == extension;
        }
      }
    );
  },
  
  getExtension: function(contentType) {
    return [ContentType.contentTypes[contentType]].flatten().first();
  }
};

var Murmur = {
  get: function(key, seed) {
    var bytes = key.toString().getBytes();
    var length = bytes.length;
    var length_4 = length >> 2;
    var length_m = length_4 << 2;
  	var m = 0x5bd1e995;
  	var r = 24;
  	var hash = seed ^ length;

    for (var i = 0; i < length_4; i++) {
      var i_4 = i << 2;
      var k = bytes[i_4 + 3];
      k = k << 8;
      k = k | (bytes[i_4 + 2] & 0xff);
      k = k << 8;
      k = k | (bytes[i_4 + 1] & 0xff);
      k = k << 8;
      k = k | (bytes[i_4 + 0] & 0xff);
      k *= m;
      k ^= k >>> r;
      k *= m;
      hash *= m;
      hash ^= k;
    }

    var left = length - length_m;
    if (left !== 0) {
      if (left >= 3) {
        hash ^= bytes[length - 3] << 16;
      }
      if (left >= 2) {
        hash ^= bytes[length - 2] << 8;
      }
      if (left >= 1) {
        hash ^= bytes[length - 1];
      }
      hash *= m;
    }

    hash ^= hash >>> 13;
    hash *= m;
    hash ^= hash >>> 15;

    return hash;
  }
};

var Datetime = {
  /*
  Javascript's date object:
    Given timestamp,
    Assumes timestamp is expressed in UTC, and
    Adjusts for local time zone offset when using local time get... functions
  Thus we:
    Express Datetime.now() as DateObject.utc() adjusted for local time zone offset and
    Use UTC date functions on Date objects to avoid further adjustments
  I.E.:
    milliseconds given to Date object is the intended absolute time
  */
  
  convert : function(datetime, format, fixClockOffset) {
    if (Datetime[datetime.toString().toLowerCase()]) {
      // Adjust for clock offset (unless specified not) as relative datetime provided.
      if (Object.isUndefined(fixClockOffset)) var fixClockOffset = true;
      datetime = Datetime.datetime(Datetime[datetime.toLowerCase()](), fixClockOffset);
    } else if ((/[0-9]{10}/).test(datetime)) {
      datetime = Datetime.datetime(datetime * 1000);
    } else {
      datetime = Datetime.datetime(Datetime.custom(datetime));
    }
    if (format == 'T') var convertToInteger = true;
    format = format || 'ISO8601';
    ['MONTH', 'DAY', 'month', 'day', 'Y', 'M', 'D', 'd', 'h', 'm', 's', 'T'].each(
      function(element, index) {
        format = format.gsub(element, '<' + index + '>');
      }
    ).each(
      function(element, index) {
        format = format.gsub('<' + index + '>', datetime[element]);
      }
    );
    if (format == 'ISO8601') {
      format = datetime.Y + '-' + datetime.M + '-' + datetime.D + 'T';
      format += datetime.h + ':' + datetime.m + ':' + datetime.s + 'Z';
    } else if (format == 'RFC2822') {
      format = datetime.day + ', ' + datetime.d + ' ' + datetime.month + ' ';
      format += datetime.Y + ' ' + datetime.h + ':' + datetime.m + ':';
      format += datetime.s + ' GMT';
    };
    if (convertToInteger) format = format * 1;
    return format;
  },
  
  now : function() {
    return Datetime.utc() + Datetime.timeZoneOffset();
  },
  
  yesterday : function() {
    return Datetime.now() - 86400000;    
  },
  
  today : function() {
    return Datetime.now();
  },
  
  tomorrow : function() {
    return Datetime.now() + 86400000;
  },
  
  utc : function() {
    var dateObject = new Date();
    return dateObject.getTime(); //expressed in milliseconds since Epoch, UTC
  },
  
  timeZoneOffset : function() {
    var dateObject = new Date();
    return -(dateObject.getTimezoneOffset() * 60000);
  },
  
  custom : function(datetime) {
    datetime = datetime.strip();
    var year = datetime.substring(0,4) * 1;
		var month = datetime.substring(5,7) * 1;
		var date = datetime.substring(8,10) * 1;
		var hours = datetime.substring(11,13) * 1;
		var minutes = datetime.substring(14,16) * 1;
		var seconds = datetime.substring(17,19) * 1;
    var dateObject = new Date(year, month - 1, date, hours, minutes, seconds);
    return dateObject.getTime() + Datetime.timeZoneOffset(); // Prevents double UTC adjustment below
  },
  
  monthLength : function(year, month) {
    var date = new Date(year, month, 0);
    return date.getDate();
  },
  
  months : function(options) {
    options = options || {};
    
    var length = options.from && options.until ? 9999 : options.length || 12;
    var from = options.from || 'now'.toDateTime('Y-M');
    var until = options.until || '9999-12';

    var month = from;
    var yearIndex = month.split('-').first();
    var monthIndex = month.split('-').last();
    var monthName = Datetime.monthNames[monthIndex - 1];

    var months = [], index = 0;
    
    while(index < length && month >= from && month <= until) {
      if (options.options) month = {
        key : month,
        value : yearIndex + ' ' + monthName
      };
      months.push(month);
      monthIndex++;
      if (monthIndex > 12) {
        monthIndex = 1;
        yearIndex++;
      }
      month = yearIndex + '-' + monthIndex.toPaddedString(2);
      monthName = Datetime.monthNames[monthIndex - 1];
      index++;
    }
    return months;
  },
  
  calculate : function(date, interval) {
    var unit = interval.split(' ').last().substr(0, 1).toLowerCase();
    interval = interval.split(' ').first() * 1;
    var year = date.substr(0, 4) * 1;
    var month = date.substr(5, 2);
    var day = date.substr(8, 2);
    if (unit == 'y') year = year + interval;
    return [year, month, day ? day : null].compact().join('-');
  },
  
  dates : function(options) {
    options = options || {};
    
    var length = options.from && options.until ? 9999 : options.length || 365;
    var from = options.from || (options.month ? options.month + '-01' : 'now'.toDateTime('Y-M-D'));
    var until = options.until || (options.month ? options.month + '-31' : '9999-12-31');
    var include = options.month || '';
    
    var date = from;
    var year = date.toDateTime('Y').integer();
    var month = date.toDateTime('M').integer();
    var monthLength = Datetime.monthLength(year, month);
    var day = date.toDateTime('D').integer();
    var dayName = date.toDateTime('DAY');
    var dayNameIndex = Datetime.dayNames.indexOf(dayName);
    
    var dates = [], index = 0;

    while(index < length && date >= from && date <= until) {
      if (date.include(include) && (!options.days || options.days.include(dayName))) {
        if (options.options) {
          if (options.format == 'D MONTH Y') {
            var value = day.toPaddedString(2) + ' ' + Datetime.monthNames[month - 1] + ' ' + year;
          } else {
            var value = day.toPaddedString(2) + ' ' + dayName;
          }
          date = {
            key : date,
            value : value
          };
        }
        dates.push(date);
      }
      day++;
      if (day > monthLength) {
        day = 1;
        month++;
        if (month > 12) {
          month = 1;
          year++;
        }
        monthLength = Datetime.monthLength(year, month);
      }
      date = year + '-' + month.toPaddedString(2) + '-' + day.toPaddedString(2);
      dayNameIndex++;
      if (dayNameIndex > 6) dayNameIndex = 0;
      dayName = Datetime.dayNames[dayNameIndex];
      index++;
    }

    return dates;
  },

  datetime : function(milliseconds, fixClockOffset) {
    //if (fixClockOffset && ClockOffset.isMaterial()) milliseconds += ClockOffset.offset() * 1000;
    var dateObject = new Date(milliseconds);
    return {
  		Y : dateObject.getUTCFullYear(),
		  M : (dateObject.getUTCMonth() + 1).toPaddedString(2),
  		D : dateObject.getUTCDate().toPaddedString(2),
  		d : dateObject.getUTCDate(),
  		h : dateObject.getUTCHours().toPaddedString(2),
  		m : dateObject.getUTCMinutes().toPaddedString(2),
  		s : dateObject.getUTCSeconds().toPaddedString(2),
  		MONTH : Datetime.monthNames[dateObject.getUTCMonth()],
  		DAY : Datetime.dayNames[dateObject.getUTCDay()],
  		month : Datetime.monthNames[dateObject.getUTCMonth()].truncate(3, ''),
  		day : Datetime.dayNames[dateObject.getUTCDay()].truncate(3, ''),
  		T : (milliseconds / 1000).round()
  	};
  },
  
  dayNames : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
  
  monthNames : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
};

var ClockOffset = {
  client : 0, // UTC timestamp
  
  // Returns true if absolute value of server/client clock offset > 2 hours
  // Returns undefined if unknown
  isMaterial : function() {
    if (!ClockOffset.client || !ClockOffset.server) {
      return undefined;
    } else {
      return ClockOffset.offset().abs() > 7200;
    }
  },
  
  offset : function() {
    return ClockOffset.server - ClockOffset.client;
  },
  
  // Parses Date header such as 'Mon, 26 Jan 2020 11:19:07 GMT'
  parseDateHeader : function(dateHeader) {
    var matches = dateHeader.match(/^\w+, (\d+) (\w+) (\d+) (\d+:\d+:\d+) GMT$/i);    
    var monthName = Datetime.monthNames.find(
      function(monthName) {
        return monthName.startsWith(matches[2]);
      }
    );
    var month = (Datetime.monthNames.indexOf(monthName) + 1).toPaddedString(2);
    return matches[3] + '-' + month + '-' + matches[1] + ' ' + matches[4];
  },
  
  server : 0, // UTC timestamp
  
  setServer : function(datetime) {
    var pattern = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
    if (!pattern.test(datetime)) datetime = ClockOffset.parseDateHeader(datetime);
    ClockOffset.client = 'UTC'.toDateTime('T', false);
    ClockOffset.server = datetime.toDateTime('T', false);
  }
};

Model.Base.addMethods({
  get: function(parameters) {
    parameters = parameters || {};
    
    if (Object.isInteger(parameters) || Object.isArray(parameters)) {
      // Set parameters (if integer or array) to parameters.id:
      parameters = { id: parameters };
    } else if (Object.isString(parameters)) {
      // Typecast parameters (if string) to integer parameters.id:
      parameters = { id: parameters * 1 || undefined };
    } else if (Object.isString(parameters.id)) {
      // Typecast parameters.id from string to integer:
      parameters.id = parameters.id * 1 || undefined;
    }

    this._action = Object.isInteger(parameters.id) ? 'show' : 'index';
    
    if (this._action == 'show') {
      // Get single object from database:
      var representation = Database.get(this._model + '/' + parameters.id);
    } else {
      // Get collection from database using indexes if possible:
      var representation = Database.get(this._model, parameters, {
        indexes: this.indexes
      });
    }
    
    // Do not set proxy properties, as they are already set.

    return representation;
  },
  
  set: function(representation, options) {
    // Typecast representation.id to integer:
    representation.id = representation.id * 1 || undefined;
    // Ensure that representation.id is an integer:
    if (!Object.isInteger(representation.id)) throw '422 Please provide an id.';
        
    this._ancestor = Database.get(this._model + '/' + representation.id) || {};
    
    this._representation = this._typecastRepresentation(representation);
        
    this._action = this._ancestor.id ? 'update' : 'create';
        
    // Merge ancestor and representation onto class instance:
    Object.extend(this, Object.copy(this._ancestor));
    Object.extend(this, Object.copy(this._representation));

    // Initialize null or default values where possible on create or update.
    // Doing so on update helps where schema has been extended after create.
    this._initializeUndefinedProperties();
    
    // Set 'created' and 'updated' properties:
    this._setDatetimes();
    
    // Set proxy properties after initializing properties, to take default values
    // into account if provided. Proxy properties for _ancestor and _representation
    // are not set as these represent the original object if any and changes made
    // if any respectively and not the virtual object which is represented by 'this'.
    this._setProxyProperties(this);
    
    if (this.isCreate()) this._executeCallbacks('beforeValidateOnCreate');
    if (this.isUpdate()) this._executeCallbacks('beforeValidateOnUpdate');
    this._executeCallbacks('beforeValidate');
    
    this._validate();

    if (this.isCreate()) this._executeCallbacks('beforeCreate');
    if (this.isUpdate()) this._executeCallbacks('beforeUpdate');
    this._executeCallbacks('beforeSet');

    // Collect object from 'this', ignore proxy properties and save to database:
    this._save(options);
    
    if (this.isCreate()) this._executeCallbacks('afterCreate');
    if (this.isUpdate()) this._executeCallbacks('afterUpdate');
    this._executeCallbacks('afterSet');
    
    // Collect object from 'this', after changes made by callbacks if any
    // and include proxy properties:
    representation = {};
    this._properties.concat(['id', 'created', 'updated']).each(
      function(property) {
        representation[property] = this[property];
      }.bind(this)
    );
    return representation;
  },
  
  unset: function(parameters, options) {
    if (Object.isInteger(parameters) || Object.isString(parameters)) {
      parameters = { id: parameters };
    }
    // Typecast parameters.id to integer:
    parameters.id = parameters.id * 1 || undefined;
    if (!Object.isInteger(parameters.id)) throw '422 Please provide an id.';

    this._action = 'destroy';
    
    var object = Database.get(this._model + '/' + parameters.id);
    if (!object) return undefined;
    
    Object.extend(this, object);

    this._executeCallbacks('beforeUnset');
    Database.unset(this._model + '/' + parameters.id, options);
    this._executeCallbacks('afterUnset');

    return undefined;
  }
});

var Flash = {
  flash : {},
  
  set : function(namespace, key, value) {
    if (!Flash.flash[namespace]) Flash.flash[namespace] = {};
    Flash.flash[namespace][key] = value;
  },
    
  get : function(namespace, key) {
    var flash = Flash.flash[namespace] || {};
    return key ? flash[key] : flash;
  },
  
  unset : function(namespace, key) {
    if (Flash.flash[namespace]) {
      delete Flash.flash[namespace][key];
    }
  }
};

var Video = {
  quicktime : function(options) {
    return('<object classid="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B" codebase="http://www.apple.com/qtactivex/qtplugin.cab" width="#{width}" height="#{height}"><param name="src" value="#{uri}"/><param name="autoplay" value="false"/><param name="controller" value="true"/><param name="scale" value="tofit"/><param name="showlogo" value="true"/><!--[if !IE]>--><object type="video/quicktime" data="#{uri}" width="#{width}" height="#{height}"><param name="autoplay" value="false"/><param name="controller" value="true"/><param name="scale" value="tofit"/><param name="showlogo" value="true"/></object><!--<![endif]--></object>').interpolate({
      uri : options.uri,
      width : options.width,
      height : options.height
    });
  }
};

var Controller = {};

var Controllers = {};

Controller.Base = Class.create({  
  afterInitializeShowDeferredImages: function() {
    $('Base.view').select('img[deferred]').each(
      function(image) {
        image.writeAttribute({
          src: image.readAttribute('deferred')
        });
      }
    );
  },
  
  authenticated: function() {
    return true;
  },
  
  initialize: function() {},
  
  $: function(id) {
    return $(this.className + '.' + id);
  },

  $F: function(id) {
    return $F(this.className + '.' + id);
  },
  
  getFlash: function(key) {
    return Flash.get(this.className, key);
  },
  
  header: function() {
    var href = '';
    var components = Parameters.uri.sub('/#', '').removeQueryString().sub(/\/[0-9]+$/, '').split('/');
    return components.inject('',
      function(header, component, index) {
        var name = component.gsub('-', ' ').toTitleCase();
        if (components.length > 1 && index < components.length - 1) {
          href += (href ? '/' : '/#') + component;
          name = Html.a({ href: href }, name);
        }
        return header + (index > 0 ? ' / ' : '') + name;
      }
    );
  },
  
  setFlash: function(key, value) {
    Flash.set(this.className, key, value);
  },
  
  title: function() {
    var components = Parameters.uri.sub('/#', '').removeQueryString().sub(/\/[0-9]+$/, '').split('/');
    return components.last().gsub('-', ' ').toTitleCase();
  },
  
  hide: function() {
    $('Base.view').hide();
    Loading.show();
  },
  
  scrollToBookmark: function(bookmark) {
    bookmark = bookmark || Parameters.bookmark;
    var element = $(bookmark == 'top' ? 'Base.top' : Parameters.controller + '.' + bookmark);
    if (!element) element = $('Base.top');
    // Defer scroll to help with multiple calls to scrollToBookmark:
    element.scrollTo.bind(element).defer();
  },
  
  show: function() {
    Loading.hide();
    $('Base.view').show();
  }
});

var UnsetUndo = {
  observe: function(options) {    
    $(options.control).observe('click',
      this.unset.bindAsEventListener(this, options)
    );
  },
  
  undo: function() {
    var options = this._queue.pop();
    clearTimeout(options.timeout);
    Notice.hide();
    if (options.context == Dispatcher.classInstance.controller) {
      var element = $(options.element);
      if (element) element.show();
      if (options.afterUndo) options.afterUndo();
    }
  },
  
  unset: function(event, options) {
    // Cache undo anchor tag:
    if (!this._a) {
      this._a = Html.a({
        href: '',
        onclick: 'UnsetUndo.undo(); return false;'
      }, 'Undo');
    }
    
    // Hide element:
    var element = $(options.element);
    if (element) element.hide();
    
    // Set timeout for collection:
    if (options.unset) {
      var unset = options.unset.curry(options.model, options.id);
    } else {
      var model = Model[options.model];
      var unset = model.unset.bind(model, options.id);
    }
    options.timeout = setTimeout(unset, (7).seconds());
    
    // Show success notice and undo link:
    var item = options.model.underscore().gsub('_', ' ');
    Notice.success('Your ' + item + ' was successfully deleted.', this._a);
    
    // Push options into queue and trigger callback:
    UnsetUndo._queue.push(options);
    if (options.afterUnset) options.afterUnset();
  },
  
  _queue: []
};

var Config = {
  Controller: {},
  Database: {},
  DigestAuthentication: {},
  Http: {},
  Router: {},
  WindowNameRequest: {}
};

var Dispatcher = Class.create({  
  initialize: function() {
    if (Prototype.Browser.IE) {
      // Return in a little while if iFrame is not yet loaded:
      if (!$('Base.history').contentWindow || !$('Base.history').contentWindow.document) {
        this.initialize.bind(this).delay(0.1); // Seconds.
      } else {
        this.monitorIE.bind(this).repeat(10); // Milliseconds.
      }
    } else {
      this.monitor.bind(this).repeat(10); // Milliseconds.
    }
  },
  
  disable: function() {
    this.disabled = true;
  },
  
  dispatch: function(proxy) {
    var dispatched = Timestamp.get();
    Loading.hide();
    
    // Stop observing all events registered through Element.observe.
    // For persistent events use Event.observe.
    Object.each(Element.events,
      function(id) {
        Event.stopObserving(id);
        delete Element.events[id];
      }
    );

    if (this.controller) {
      this.executeCallbacks('beforeUnload');
      // Restore view:
      $('Base.view').update('');
      $(this.controller.className).update(this.view);
      // Controller callbacks may still be able to run after deleting
      // this.controller. Set controller keys to undefined to help with this.
      // Any callbacks that could change global state must check for
      // this.controller.unloaded and abort manually:
      Object.keys(this.controller).each(
        function(key) {
          this.controller[key] = undefined;
        }.bind(this)
      );
      this.controller.unloaded = true;
      delete this.controller;
    }

    if (!proxy) {
      Router.route();
      History.set();
    }

    var proto = Controller[Parameters.controller].prototype;
    if (!proto.authenticated()) {
      if (proto.authenticateController && proto.authenticateUri) {
        Parameters.controller =  proto.authenticateController;
        Parameters.uri = proto.authenticateUri;
      }
    }

    // Extend controller:
    Controller[Parameters.controller].addMethods({
      className: Parameters.controller,
      dispatch: this.dispatch.bind(this),
      dispatched: dispatched
    });

    // Copy and remove view:
    this.view = $(Parameters.controller).replaceContent('');
    $('Base.view').update(this.view).show();

    // Instantiate controller:
    this.controller = new Controller[Parameters.controller]();
    
    // Run callbacks only if controller still in authority:
    if (this.controller.className == Parameters.controller) {
      this.controller.scrollToBookmark();
      this.executeCallbacks('afterInitialize');
    }
  },
  
  enable: function() {
    this.disabled = false;
  },
  
  executeCallbacks: function(callback) {
    Object.keys(this.controller).each(
      function(method) {
        if (method.startsWith(callback)) this.controller[method]();
      }.bind(this)
    );
  },
  
  getBookmark: function() {
    return window.location.hash.toString().extract(/^[^?.]+\.([^?]+)/) || undefined;
  },
  
  // Relative URI excluding bookmark since bookmark does not affect history.
  getIFrameUri: function() {
    var element = $('Base.history').contentWindow.document.getElementById('uri');
    return element ? element.innerHTML : '/#home';
  },

  // Relative URI excluding bookmark since bookmark does not affect history.
  getHashUri: function() {
    var components = ('/' + (window.location.hash || '#home')).split('?');
    var uri = components[0].split('.')[0];
    var query = components[1];
    if (query) uri += '?' + query;
    return uri;
  },
  
  monitor: function(repeat) {
    if (this.disabled) return;
    
	  var hashUri = this.getHashUri();
	  var bookmark = this.getBookmark();
	  
	  if (this.hashUri != hashUri) {
      this.hashUri = hashUri;
		  this.dispatch();
    } else if (Parameters.bookmark != bookmark) {
      Parameters.bookmark = bookmark;
      this.controller.scrollToBookmark();
    }
  },
    
  monitorIE: function(repeat) {
    if (this.disabled) return;
    
	  var hashUri = this.getHashUri();
	  var bookmark = this.getBookmark();
	  var iFrameUri = this.getIFrameUri();
	  
	  // Detect hash changes before iframe changes to enable direct links
	  // to Sexbyfood pages from external pages:
	  if (this.hashUri != hashUri) {
      this.hashUri = hashUri;
      this.setIFrameUri(hashUri);
		  this.dispatch();
    } else if (this.iFrameUri != iFrameUri) {
      this.iFrameUri = iFrameUri;
      this.setHashUri(iFrameUri);
		  this.dispatch();
    } else if (Parameters.bookmark != bookmark) {
      Parameters.bookmark = bookmark;
      this.controller.scrollToBookmark();
    }
  },
  
  setHashUri: function(uri) {
    // Strip '/#' from uri as this is added back by getHashUri()
    window.location.hash = uri.sub('/#', '');
    this.hashUri = uri;
  },
  
  setIFrameUri: function(uri) {
    try {
      iFrame = $('Base.history').contentWindow.document;
      iFrame.open();
      iFrame.write('<html><body><div id="uri">' + uri + '</div></body></html>');
      iFrame.close();
    } catch (e) {}
    this.iFrameUri = uri;
  }
});

var ProgressIndicator = Class.create({
  initialize : function(element, time) {
    this.element = $(element);
    this.time = time;
    this.percentage = 0;
    // Provide 20 points per second granularity:
    new PeriodicalExecuter(this.indicate.bindAsEventListener(this), 0.05);
  },
  
  indicate : function(executer) {
    if (!this.element.visible()) executer.stop();
    
    this.element.update(this.percentage.floor().toPaddedString(2) + '%');
    // Achieve 80% of points in 20% of time and 20% of points in 80% of time:
    if (this.percentage < 80) {
      this.percentage += 80 / (this.time * 0.2) / 20;
    } else if (this.percentage < 97) {
      this.percentage += 20 / (this.time * 0.8) / 20;
    } else if (this.percentage < 99) {
      this.percentage += 10 / (this.time * 0.9) / 20;
    } else {
      executer.stop();
    }
  }
});

var EditDelete = Class.create({
  observeUnsetUndo: function() {
    UnsetUndo.observe({
      context: this.options.context,
      element: this.show,
      control: this.handles.destroy,
      model: this.options.model,
      id: this.options.id,
      afterUnset: this.options.afterUnset,
      afterUndo: this.options.afterUndo
    });
  },
  
  initialize: function(show, options) {
    this.show = $(show);
    this.options = options;
    this.update = this.show.next();
    this.form = this.update.down('form');
    this.button = this.update.down('input[type="button"]');
    this.setHandles();
    this.registerObservers();
    this.observeUnsetUndo();
  },
  
  onMouseOut: function() {
    this.handles.update.hide();
    this.handles.destroy.hide();
  },
  
  onMouseOver: function() {
    this.handles.update.show();
    this.handles.destroy.show();
  },
  
  registerObservers: function() {
    this.show.observe('mouseover', this.onMouseOver.bind(this));
    this.show.observe('mouseout', this.onMouseOut.bind(this));
    this.button.observe('click', this.save.bind(this));
    this.handles.update.observe('click', this.toggleInterface.bind(this));
    this.handles.cancel.observe('click', this.toggleInterface.bind(this));
  },
  
  save: function() {
    try {
      var representation = this.form.serialize();
      representation.id = this.options.id;
      representation = Model[this.options.model].set(representation);
      this.toggleInterface();
      if (this.options.afterSave) {
        this.options.afterSave(this.show, representation);
      }
      var descriptor = this.options.model.underscore().gsub('_', ' ');
      Notice.success('Your ' + descriptor + ' was successfully saved.');
    } catch (error) {
      Notice.error(error);
    }
  },
  
  setHandles: function() {
    this.handles = {};
    this.handles.cancel = this.update.down('a[title="Cancel"]');
    this.handles.update = this.show.down('a[title="Edit"]');
    this.handles.destroy = this.show.down('a[title="Delete"]');
  },
    
  showEdit: function() {
    this.show.show();
    this.toggleInterface();
  },
  
  showView: function() {
    this.show.hide();
    this.toggleInterface();
  },
  
  toggleInterface: function() {
    if (this.show.visible()) {
      this.show.hide();
      this.update.show();
      if (this.options.beforeEdit) this.options.beforeEdit(this.show);
    } else {
      this.update.hide();
      this.show.show();
      if (this.options.afterCancel) this.options.afterCancel(this.show);
    }
  }
});

var Loading = {
  hide: function() {
    $('Base.loading-percentage').update();
    $('Base.loading').hide();
  },
  
  setPercentage: function(percentage) {
    $('Base.loading-percentage').update(percentage.floor() + '%');
  },
  
  show: function() {
    $('Base.loading-percentage').update();
    $('Base.loading').show();
  }
};

var GoogleApi = {
  initialize : function(callback) {
    if (!GoogleApi.callbacks) GoogleApi.callbacks = [];
    GoogleApi.callbacks.push(callback);
    if (!GoogleApi.requested) {
      var script = document.createElement('script');
      script.src = 'http://www.google.com/jsapi?key=ABQIAAAATEnH0xiP2KQdy7SbotUQjRSkdhWJpufcReO8oFSry28ETHLMoxT4-iMGyU1X06W_4qoYzAQUXsfzCg&callback=GoogleApi.setLoaded';
      script.type = 'text/javascript';
      document.getElementsByTagName('head')[0].appendChild(script);
      GoogleApi.requested = true;
    } else if (GoogleApi.loaded) {
      GoogleApi.executeCallbacks();
    }
  },
  
  executeCallbacks : function() {
    GoogleApi.callbacks.each(
      function(callback) {
        callback();
      }
    );
    GoogleApi.callbacks = [];
  },
  
  setLoaded : function() {
    GoogleApi.loaded = true;
    GoogleApi.executeCallbacks();
  }
};

var WindowNameRequest = Class.create({
  cleanup: function() {
    if (this.timeoutId) window.clearTimeout(this.timeoutId);
    // Leave frame behind in DOM (otherwise Safari appears to miscount requests):
    this.frame.contentWindow.name = this.frame.identify();
    // Remove form from DOM:
    this.form.remove();
  },
  
  initialize: function(uri, options) {
    this.setOptions(uri, options);
    this.insertFrame();
    this.insertForm();
    this.insertHeaders();
    this.insertParameters();
    this.insertRepresentation();
    this.startTimer();
    this.form.submit();
  },
  
  insertForm: function() {
    this.form = new Element('form', {
      target: this.frame.identify(),
      method: this.options.method,
      action: this.uri,
      id: this.id + 'Form'
    });
    Element.insert(document.body, this.form);
  },
  
  insertFrame: function() {
    var style = 'position:absolute; top:0; left:0; width:1px; height:1px; visibility:hidden';
    var html = '<iframe class="iframe" style="' + style + '" name="#{id}Frame" id="#{id}Frame"></iframe>';
    Element.insert(document.body, html.interpolate({ id: this.id }));
    this.frame = $(this.id + 'Frame');
    this.frame.observe('load', this.monitor.bind(this));
  },
  
  insertHeaders: function() {
    this.insertHiddenInput('_X-Window-Name', 'true');
    var headers = this.options.requestHeaders;
    Object.each(headers,
      function(key) {
        this.insertHiddenInput('_' + key, headers[key]);
      }.bind(this)
    );
  },
  
  insertHiddenInput: function(name, value) {
    // No need to escape double quotes, Prototype converts " to &quot;
    // null => 'null', undefined => 'undefined', true => 'true', false => 'false'
    this.form.insert(new Element('input', {
      'type': 'hidden',
      'name': name,
      'value': value + ''
    }));
  },
  
  insertParameters: function() {
    Object.each(this.options.parameters, this.insertHiddenInput.bind(this));
  },
  
  insertRepresentation: function() {
    // this.options.postBody is expected to be a JSON string.
    try {
      var representation = JSON.parse(this.options.postBody);
    } catch(e) {
      var representation = {};
    }
    Object.each(representation, this.insertHiddenInput.bind(this));
  },
  
  monitor: function() {
    try {
      // Ignore onload for about:blank
      if (this.frame.contentWindow.location.href == 'about:blank') return;
		} catch (e) {}
		if (!this.redirected) {
			try {
  			this.redirected = true;
			  this.frame.contentWindow.location.href = Config.WindowNameRequest.localResource;
  		} catch (e) {}
		} else if (!this.responded) {
		  try {
  		  this.respond(this.frame.contentWindow.name || '');
  		  this.responded = true;
  		  this.cleanup();
  		} catch (e) {}
		}
  },
  
  respond: function(response) {
    try {
      response = JSON.parse(response);
    } catch(e) {
       // Server returned invalid JSON or intermediate gateway error.
      response = { status: '502 Bad Gateway', headers: {} };
      response.body = response.status;
    }

    var status = response.status.substr(0,3).number();

    if (status === 200 || status === 201) {
      if (this.options.onSuccess) {
        this.options.onSuccess({
          status: status,
          headers: response.headers,
          body: response.body
        });
      }
    } else {
      if (this.options.onFailure) {
        this.options.onFailure({
          status: status,
          headers: response.headers,
          body: response.body
        });
      }
    }
  },
  
  setOptions: function(uri, options) {
    this.uri = uri;
    this.options = options || {};
    this.id = 'WindowNameRequest' + 'UTC'.toDateTime('T') + $R(1000,9999).random();
  },
  
  startTimer: function(request) {
    if (this.options.timeout) {
      this.timeoutId = window.setTimeout(
        function() {
          if (!this.responded) {
            this.responded = true;
            this.cleanup();
            if (this.options.onTimeout) this.options.onTimeout();
          }
        }.bind(this), this.options.timeout
      );
    }
  }
});

var Router = {
  camelizeParameterKeys : function() {
    Parameters = Object.keys(Parameters).inject({},
      function(parameters, key) {
        parameters[key.camelize()] = Parameters[key];
        return parameters;
      }
    );
  },
  
  // Converts '#jimi-hendrix/are-you-experienced/121?associate-id=121&seared-tuna=true.hear-my-train-coming'
  // to Parameters.controller, Parameters.id, Parameters extended by query, Parameters.uri and Parameters.bookmark
  route : function() {
    var patterns = [
      '^#',
      '([a-z0-9-/]+?)', // Controller
      '(/([0-9]+))?', // Id
      '(\\.([a-z0-9-]+))?', // Bookmark
      '(\\?(.+))?', // Query
      '$'
    ];
    var regex = new RegExp(patterns.join(''), 'i');
    var matches = window.location.hash.toString().match(regex) || [];
    matches[1] = (matches[1] || 'home').sub(/\/$/, '');

    Parameters = {};
    Object.extend(Parameters, (matches[7] || '').toQueryParams());
    // Override query parameters if necessary:
    Parameters.controller = matches[1].constantize();
    Parameters.id = matches[3];
    Parameters.bookmark = matches[5];

    // Relative, without bookmark since bookmark does not affect history
    Parameters.uri = '/#' + matches[1];
    if (matches[3]) Parameters.uri += '/' + matches[3];
    if (matches[7]) Parameters.uri += '?' + matches[7];
    
    if (!Controller[Parameters.controller]) {
      var route = Router.routeController(matches[1]);
      if (!route) {
        Parameters = {
          controller : 'PageNotFound',
          uri : 'page-not-found' // Used by Controller.Base.title()
        };
      }
    }
    
    Router.camelizeParameterKeys();
  },
  
  routeController : function(path) {
    return Config.Router.routes.find(
      function(route) {
        matches = path.match(new RegExp('^' + route[1] + '$', 'i'));
        if (matches) {
          Parameters.controller = route.first();
          if (route[2]) Router.routeParameters(route, matches);
          return true;
        }
      }
    );
  },
  
  routeParameters : function(route, matches) {
    matches.shift();
    matches.each(
      function(value, index) {
        var key = route[2][index];
        if (key) Parameters[key] = value;
      }
    );
  }
};

var Options = {
  // Inserts option into options if necessary, maintaining sort order
  extend : function(options, option) {
    var position = 0;
    options.each(
      function(element, index) {
        if (element.key < option.key) {
          position = index + 1;
        } else if (element.value == option.value) {
          position = -1;
        }
      }
    );
    if (position >= 0) {
      var clone = options.clone();
      clone.splice(position, 0, option);
      return clone;
    } else {
      return options;
    }
  }
};

var Queue = Class.create({
  initialize: function(options) {
    this._queue = [];
    this._processed = 0;
    this.options = options || {};
  },
  
  push: function(task, options) {
    this._queue.push({ task: task, options: options || {} });
    this.start();
  },
  
  start: function() {
    if (!this._process) {
      this._process = this._processQueue.bind(this).repeat(25);
    }
  },
  
  stop: function() {
    if (this._process) {
      this._process.stop();
      delete this._process;
    }
  },
  
  _processQueue: function() {
    var start = new Date().getTime();
    var jobs;
    var job;
    var index;
    
    while (this._queue.length > 0 && (new Date() - start < 75)) {
      jobs = this._queue.splice(0, this.options.batch || 1);
      index = jobs.length;
      while(--index >= 0) {
        job = jobs[index];
        job.task();
        this._processed++;
        if (job.options.onComplete) job.options.onComplete();
      }
    }

    if (this._queue.length === 0) {
      this.stop();
      if (this.options.onComplete) {
        if (this.options.length && this.options.length > this._processed) return;
        // Reset counter to enable onComplete to be called multiple times:
        this._processed = 0;
        this.options.onComplete();
      }
    }
  }
});

// Logs window.applicationCache events to Console.
// Call ApplicationCache.listen() to activate.
var ApplicationCache = {
  listen: function() {
    try {
      cache = window.applicationCache;
      Event.observe(cache, 'checking', ApplicationCache.onEvent.curry('Checking'));
      Event.observe(cache, 'error', ApplicationCache.onEvent.curry('Error'));
      Event.observe(cache, 'noupdate', ApplicationCache.onEvent.curry('No Update'));
      Event.observe(cache, 'downloading', ApplicationCache.onEvent.curry('Downloading'));
      Event.observe(cache, 'progress', ApplicationCache.onEvent.curry('Progress'));
      Event.observe(cache, 'updateready', ApplicationCache.onEvent.curry('Update Ready'));
      Event.observe(cache, 'cached', ApplicationCache.onEvent.curry('Cached'));
    } catch (e) {
      Log('ApplicationCache: window.applicationCache not supported by this browser.');
    }
  },
  
  onEvent: function(status, event) {
    Log('ApplicationCache: ' + status + '.');
  }
};

var Notice = {
  error: function(notice, extra) {
    // Debug where notice is not a string:
    if (!notice.without) alert(notice);
    
    // Remove leading HTTP status code if any:
    notice = notice.without(/^[0-9]{3} /);
    
    // Hide notice container until this.show:
    $('Base.notice').hide();
    
    var element = $('Base.notice-text');
    element.update(notice);
    element.removeClassName('success');
    element.addClassName('error');
    
    // Update and show 'extra' span or hide:
    this._setExtra(extra);
    
    // Ensure that subsequent notices disable fade for this notice:
    this._noticeId++;
    this._show(this._noticeId);
  },
  
  hide: function() {
    $('Base.notice').hide();
    this._noticeId++;
  },
  
  success: function(notice, extra) {    
    // Hide notice container until this.show:
    $('Base.notice').hide();
    
    var element = $('Base.notice-text');
    element.update(notice);
    element.removeClassName('error');
    element.addClassName('success');

    // Update and show 'extra' span or hide:
    this._setExtra(extra);
    
    // Ensure that subsequent notices disable fade for this notice:
    this._noticeId++;
    this._show(this._noticeId);
  },
  
  _hide: function(noticeId) {
    if (Notice._noticeId !== noticeId) return;
    var element = $('Base.notice').show();
    var opacity = 100;
    
    var setOpacity = function() {
      if (Notice._noticeId === noticeId) {
        element.setOpacity(opacity / 100);
        opacity = opacity - 1;
        if (opacity >= 0) {
          setOpacity.delay(0.01);
        } else {
          $('Base.notice').hide();
        }
      }
    };
    
    setOpacity();
  },
  
  _noticeId: 0,
  
  _setExtra: function(extra) {
    $('Base.notice-extra').show()[extra ? 'update' : 'hide'](extra);
    if (extra) {
      var link = $('Base.notice-extra').down('a');
      if (link) link.addClassName('plain');
    }
  },
  
  _show: function(noticeId) {
    if (Notice._noticeId !== noticeId) return;
    var element = $('Base.notice').show();
    var opacity = 0;
    
    var setOpacity = function() {
      if (Notice._noticeId === noticeId) {
        element.setOpacity(opacity / 100);
        opacity = opacity + 4;
        if (opacity <= 100) setOpacity.delay(0.001);
      }
    };
    
    setOpacity();
    this._hide.curry(noticeId).delay(5);
  }
};

var LocalStorage = {
  clear: function(options) {
    options = options || {};
    if (!options.onFailure && !options.onSuccess) {
      return this._clear(options);
    }
    this._enqueue(this._clear.bind(this, options));
  },
  
  exists: !!window.localStorage,
  
  get: function(key, options) {
    options = options || {};
    if (!options.onFailure && !options.onSuccess) {
      return this._get(key, options);
    }
    this._enqueue(this._get.bind(this, key, options));
  },
  
  key: function(index, options) {
    options = options || {};
    if (!options.onFailure && !options.onSuccess) {
      return this._key(index, options);
    }
    this._enqueue(this._key.bind(this, index, options));
  },
  
  length: function(options) {
    options = options || {};
    if (!options.onFailure && !options.onSuccess) {
      return this._length(options);
    }
    this._enqueue(this._length.bind(this, options));
  },
  
  set: function(key, object, options) {
    options = options || {};
    if (!options.onFailure && !options.onSuccess) {
      return this._set(key, object, options);
    }
    this._enqueue(this._set.bind(this, key, object, options));
  },
  
  unset: function(key, options) {
    options = options || {};
    if (!options.onFailure && !options.onSuccess) {
      return this._unset(key, options);
    }
    this._enqueue(this._unset.bind(this, key, options));
  },
  
  _clear: function(options) {
    localStorage.clear();
    if (options.onSuccess) options.onSuccess(options);
  },
  
  _enqueue: function(task) {
    if (!this._queue) this._queue = new Queue({ batch: 10 });
    this._queue.push(task);
  },
  
  _get: function(key, options) {
    var realKey = (options.namespace ? options.namespace + '.' : '') + key;
    var string = localStorage.getItem(realKey);
    var object;
    if (string === null) {
      object = undefined;
    } else {
      if (options.privateKey) string = string.decrypt(options.privateKey);
      try {
        object = string.toObject();
      } catch(e) {
        object = undefined;
      }
    }
    if (options.onSuccess) {
      options.onSuccess(key, object, options);
    } else {
      return object;
    }
  },
  
  _key: function(index, options) {
    var key = localStorage.key(index); // Returns key at given numeric index.
    if (options.onSuccess) {
      options.onSuccess(index, key, options);
    } else {
      return key;
    }
  },
  
  _length: function(options) {
    var length = localStorage.length;
    if (options.onSuccess) {
      options.onSuccess(length, options);
    } else {
      return length;
    }
  },
  
  _set: function(key, object, options) {
    var string = JSON.stringify(object);
    if (options.privateKey) string = string.encrypt(options.privateKey);
    try {
      var realKey = (options.namespace ? options.namespace + '.' : '') + key;
      localStorage.setItem(realKey, string);
    } catch (exception) {
      if (options.onFailure) {
        Log('LocalStorage._set: ' + exception.message);
        return options.onFailure(exception.message, key, object, options);
      }
    }
    if (options.onSuccess) options.onSuccess(key, object, options);
  },
    
  _unset: function(key, options) {
    var realKey = (options.namespace ? options.namespace + '.' : '') + key;
    localStorage.removeItem(realKey);
    if (options.onSuccess) options.onSuccess(key, options);
  }
};

var Http = {
  get: function(uri, options) {
    return this._request('GET', uri, options);
  },
  
  put: function(uri, options) {
    return this._request('PUT', uri, options);
  },
  
  'delete': function(uri, options) {
    return this._request('DELETE', uri, options);
  },
  
  _onComplete: function(request, callback, response) {
    // Prototype may call onSuccess with an undefined or zero status code
    // in the event that the server is terminated before serving the request.
    // Watch out for this and delegate to onTimeout if this happens.
    if (response && response.status) {
      var status = response.status;
      var headers = response.headers || {};
      if (response.getAllHeaders) {
        (response.getAllHeaders() || '').split('\r\n').each(
          function(header) {
            var header = header.split(':');
            var key = header.shift(); // Remove first element and return.
            var value = header.join(':').without(/^\s+/); // Remove leading whitespace.
            if (key) headers[key] = value; // Ignore header end newline.
          }
        );
      }
      var body = response.body || response.responseText;
    } else {
      callback = 'onTimeout';
      var status = 504;
      var headers = {};
      var body = '504 Gateway Timeout';
    }
    
    if (request[callback]) {
      request[callback](body, {
        status: status,
        headers: headers,
        body: body,
        latency: request.timer.elapsed()
      });
    }
  },
    
  _request: function(method, uri, request) {
    request = request || {};
    request.method = method;
    request.headers = request.headers || {};
    request.timer = new Timer();
    
    if (request.method != 'GET' && request.method != 'POST') {
      request.headers['X-HTTP-Method-Override'] = request.method;
      request.method = 'POST';
    }
    
    var options = {
      asynchronous: !request.synchronous,
      method: request.method,
      parameters: request.parameters,
      postBody: request.body,
      requestHeaders: request.headers,
      onSuccess: this._onComplete.curry(request, 'onSuccess'),
      onFailure: this._onComplete.curry(request, 'onFailure'),
      onTimeout: this._onComplete.curry(request, 'onTimeout'),
      timeout: Config.Http.timeout
    };
    
    if (!navigator.onLine) return options.onTimeout();
    
    // Determine sessionDomain and requestDomain:
    var sessionDomain = window.location.protocol + '//' + window.location.host;
    var requestDomain = uri.extract(/^(https?:\/\/[^\/]+)\//);
    
    // Convert relative URI into absolute URI:
    if (!requestDomain) {
      uri = (requestDomain = sessionDomain) + '/' + uri.without(/^\//);
    }

    if (requestDomain == sessionDomain) {
      new Ajax.Request(uri, options); // Same domain.
    } else {
      new WindowNameRequest(uri, options); // Cross domain.
    }
  }
};

// Requires: options.link, options.text, options.beforeShow.
// Enables link to show and hide help text.
var Help = Class.create({  
  initialize : function(options) {
    this.options = options;
    this.options.link = $(this.options.link);
    this.options.link.observe('click', this.toggle.bind(this));
  },
  
  reset : function() {
    this.options.link.update('Help');
    this.options.text.hide();
  },
  
  toggle : function(event) {
    if (this.options.link.innerHTML == 'Help') {
      if (this.options.beforeShow) this.options.beforeShow();
      this.options.link.update('Hide');
      this.options.text.show();
    } else {
      this.options.link.update('Help');
      this.options.text.hide();
    }
  }
});

var Database = {
  adapter: function() {
    return this[Config.Database.adapter];
  },

  get: function(key, parameters, options) {
    return this.adapter().get(key, parameters, options);
  },
  
  set: function(key, representation, options) {
    this.adapter().set(key, representation, options);
  },
  
  unset: function(key, options) {
    this.adapter().unset(key, options);
  },
  
  initialize: function(options) {
    this.adapter().initialize(options);
  },
  
  start: function() {
    return this.adapter().start();
  },
  
  status: function() {
    return this.adapter().status();
  },
  
  stop: function() {
    return this.adapter().stop();
  }
};

var Log = function(object) {
 if (window.console) console.info(object + '');
 return object;
};

var IFrameFormRequest = Class.create({
  initialize: function(form, options) {
    this.form = $(form);
    this.options = options;
    this.insertIFrame();
    this.setFormAttributes();
    this.request();
  },
  
  insertIFrame: function() {
    this.iframe = this.form.identify() + Id.get();
    var iframe = new Element('iframe', { 'class': 'iframe', 'name': this.iframe });
    this.form.insert({ after: iframe });
  },
    
  request: function() {
  	frames[this.iframe].document.body.innerHTML = '';
  	this.form.submit();
  	
  	new PeriodicalExecuter(
  		function(poll) {
  		  if (!frames[this.iframe]) poll.stop();
  		  
  		  var body = frames[this.iframe].document.body;
  		  var response = body.innerText || body.textContent;
  		  
  			if (response) {
					poll.stop();
          if (/^[0-9]{3} /.test(response)) {
            var error = response.without(/^[0-9]{3} /);
            if (this.options.onFailure) this.options.onFailure(error);
          } else {
            var representation = JSON.parse(response);
  					if (this.options.onSuccess) this.options.onSuccess(representation);
  				}
  			}
  		}.bind(this),
  		0.1
  	);
  },
  
  setFormAttributes: function() {
  	this.form.writeAttribute('target', this.iframe);
  	this.form.writeAttribute('action', this.options.uri);
  	
  	// Set X-HTTP-Method-Override header:
  	if (['PUT', 'DELETE'].include(this.options.method)) {
  	  this.form.writeAttribute('method', 'POST');
      this.form.insert(new Element('input', {
        'type': 'hidden',
        'name': '_X-HTTP-Method-Override',
        'value': this.options.method
      }));
  	} else {
  	  this.form.writeAttribute('method', this.options.method);
  	}
  	
  	// Set encoding:
  	if (this.form.down('input[type="file"]')) {
  	  this.form.writeAttribute('enctype', 'multipart/form-data');
  	}
  	
  	Object.each(this.options.headers || {},
  	  function(key, value) {
  	    if (key == 'Accept' && value == 'application/json') {
  	      // Ask for text/json to prevent browser from downloading response.
  	      value = 'text/json';
  	    }
      	this.form.insert(new Element('input', {
          'type': 'hidden',
          'name': '_' + key,
          'value': value
        }));
      }.bind(this)
    );
  }
});

if (!this.JSON) this.JSON = {};

(function () {
  if (typeof Date.prototype.toJSON !== 'function') {
    // Format integers to have at least two digits.
    function f(n) {
      return n < 10 ? '0' + n : n;
    }

    Date.prototype.toJSON = function (key) {
      if (isFinite(this.valueOf())) {
        var Y = this.getUTCFullYear();
        var M = f(this.getUTCMonth() + 1);
        var D = f(this.getUTCDate());
        var h = f(this.getUTCHours());
        var m = f(this.getUTCMinutes());
        var s = f(this.getUTCSeconds());
        return Y + '-' + M + '-' + D + 'T' + h + ':' + m + ':' + s + 'Z';
      } else {
        return null;
      }
    };

    String.prototype.toJSON = function (key) { return this.valueOf(); };
    Number.prototype.toJSON = String.prototype.toJSON;
    Boolean.prototype.toJSON = String.prototype.toJSON;
  }

  var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
  var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
  var gap;
  var indent;
  var meta = {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '"' : '\\"',
    '\\': '\\\\'
  };
  var rep;

  // If the string contains no control characters, no quote characters, and no
  // backslash characters, then we can safely slap some quotes around it.
  // Otherwise we must also replace the offending characters with safe escape
  // sequences.
  function quote(string) {
    escapable.lastIndex = 0;
    if (escapable.test(string)) {
      string = string.replace(escapable,
        function(a) {
          var c = meta[a];
          if (typeof c === 'string') {
            return c;
          } else {
            return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
          }
        }
      );
    }
    return '"' + string + '"';
  }

  // Produce a string from holder[key].
  function str(key, holder) {
    var i; // Loop counter.
    var k; // Member key.
    var v; // Member value.
    var length;
    var mind = gap;
    var partial;
    var value = holder[key];

    // If the value has a toJSON method, call it to obtain a replacement value.
    if (value && typeof value === 'object' && typeof value.toJSON === 'function') {
      value = value.toJSON(key);
    }

    // If we were called with a replacer function, then call the replacer to
    // obtain a replacement value.
    if (typeof rep === 'function') value = rep.call(holder, key, value);

    // What happens next depends on the value's type.
    switch (typeof value) {
      case 'string':
        return quote(value);

      // JSON numbers must be finite. Encode non-finite numbers as null.
      case 'number':
        return isFinite(value) ? String(value) : 'null';

      // If the value is a boolean or null, convert it to a string. Note:
      // typeof null does not produce 'null'. The case is included here in
      // the remote chance that this gets fixed someday.
      case 'boolean':
      case 'null':
        return String(value);

      // If the type is 'object', we might be dealing with an object or an array or
      // null.
      case 'object':
        // Due to a specification blunder in ECMAScript, typeof null is 'object',
        // so watch out for that case.
        if (!value) return 'null';

        // Make an array to hold the partial results of stringifying this object value.
        gap += indent;
        partial = [];

        // Is the value an array?
        if (Object.isArray(value)) {
          // The value is an array. Stringify every element. Use null as a
          // placeholder for non-JSON values.
          length = value.length;
          for (i = 0; i < length; i += 1) partial[i] = str(i, value) || 'null';

          // Join all of the elements together, separated with commas,
          // and wrap them in brackets.
          if (partial.length === 0) {
            v = '[]';
          } else if (gap) {
            v = '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']';
          } else {
            v = '[' + partial.join(',') + ']';
          }
          gap = mind;
          return v;
        }

        if (rep && typeof rep === 'object') {
          // If the replacer is an array, use it to select the members to be stringified:
          length = rep.length;
          for (i = 0; i < length; i += 1) {
            k = rep[i];
            if (typeof k === 'string') {
              v = str(k, value);
              if (v) partial.push(quote(k) + (gap ? ': ' : ':') + v);
            }
          }
        } else {
          // Otherwise, iterate through all of the keys in the object:
          for (k in value) {
            if (Object.hasOwnProperty.call(value, k)) {
              v = str(k, value);
              if (v) partial.push(quote(k) + (gap ? ': ' : ':') + v);
            }
          }
        }

        // Join all of the member texts together, separated with commas,
        // and wrap them in braces.
        if (partial.length === 0) {
          v = '{}';
        } else if (gap) {
          v = '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}';
        } else {
          v = '{' + partial.join(',') + '}';
        }
        gap = mind;
        return v;
      // End of case 'object'.
    }
  }

  // If the JSON object does not yet have a stringify method, give it one.
  if (typeof JSON.stringify !== 'function') {
    // The stringify method takes a value and an optional replacer, and an optional
    // space parameter, and returns a JSON text. The replacer can be a function
    // that can replace values, or an array of strings that will select the keys.
    // A default replacer method can be provided. Use of the space parameter can
    // produce text that is more easily readable.
    JSON.stringify = function(value, replacer, space) {
      var i;
      gap = ''; // See definition above.
      indent = ''; // See definition above.
      
      if (typeof space === 'number') {
        // If the space parameter is a number, make an indent string containing
        // that many spaces:
        for (i = 0; i < space; i += 1) indent += ' ';
      } else if (typeof space === 'string') {
        // If the space parameter is a string, it will be used as the indent string.
        indent = space;
      }

      // If there is a replacer, it must be a function or an array.
      // Otherwise, throw an error.
      rep = replacer;
      if (replacer && typeof replacer !== 'function') {
        if (typeof replacer !== 'object' || typeof replacer.length !== 'number') {
          throw new Error('JSON.stringify');
        }
      }

      // Make a fake root object containing our value under the key of ''.
      // Return the result of stringifying the value.
      return str('', { '': value });
    };
  }

  // If the JSON object does not yet have a parse method, give it one.
  if (typeof JSON.parse !== 'function') {
    // The parse method takes a text and an optional reviver function, and returns
    // a JavaScript value if the text is a valid JSON text.
    JSON.parse = function(text, reviver) {
      var j;

      // The walk method is used to recursively walk the resulting structure so
      // that modifications can be made.
      function walk(holder, key) {
        var k, v, value = holder[key];
        if (value && typeof value === 'object') {
          for (k in value) {
            if (Object.hasOwnProperty.call(value, k)) {
              v = walk(value, k);
              if (v !== undefined) {
                value[k] = v;
              } else {
                delete value[k];
              }
            }
          }
        }
        return reviver.call(holder, key, value);
      }

      // Parsing happens in four stages. In the first stage, we replace certain
      // Unicode characters with escape sequences. JavaScript handles many characters
      // incorrectly, either silently deleting them, or treating them as line endings.
      cx.lastIndex = 0;
      if (cx.test(text)) {
        text = text.replace(cx,
          function (a) {
            return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
          }
        );
      }

      // In the second stage, we run the text against regular expressions that look
      // for non-JSON patterns. We are especially concerned with '()' and 'new'
      // because they can cause invocation, and '=' because it can cause mutation.
      // But just to be safe, we want to reject all unexpected forms.

      // We split the second stage into 4 regexp operations in order to work around
      // crippling inefficiencies in IE's and Safari's regexp engines. First we
      // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
      // replace all simple value tokens with ']' characters. Third, we delete all
      // open brackets that follow a colon or comma or that begin the text. Finally,
      // we look to see that the remaining characters are only whitespace or ']' or
      // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
      if (/^[\],:{}\s]*$/.test(
        text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
        replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
        replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

        // In the third stage we use the eval function to compile the text into a
        // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
        // in JavaScript: it can begin a block or an object literal. We wrap the text
        // in parens to eliminate the ambiguity.
        j = eval('(' + text + ')');

        // In the optional fourth stage, we recursively walk the new structure, passing
        // each name/value pair to a reviver function for possible transformation.
        return typeof reviver === 'function' ? walk({ '': j }, '') : j;
      }

      // If the text is not JSON parseable, then a SyntaxError is thrown.
      throw new SyntaxError('JSON.parse');
    };
  }
})();

var Cookie = {
  set: function(name, value, days) {
    attributes = '';
  	if (days) {
  		var date = new Date();
  		date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
  		attributes += ';expires=' + date.toGMTString();
  	}
    attributes += ';path=/';
  	document.cookie = name + '=' + encodeURIComponent(JSON.stringify(value)) + attributes;
  },

  get: function(name) {
  	var cookies = document.cookie.split(';'), value = undefined;
  	cookies.each(
  	  function(cookie) {
  	    var pair = cookie.split('=');
    	  if (pair.first().strip() == name) value = decodeURIComponent(pair.last().strip());
    	}
    );
    if (Object.isString(value)) value = JSON.parse(value || undefined);
    return value;
  },

  unset: function(name) {
  	Cookie.set(name, '', -1);
  }
};

var SelectAdd = Class.create({
  initialize: function(select, options) {
    this.select = $(select).observe('change', this.onChange.bind(this));
    this.model = options.model;
    this.setHandlers(options);
    this.insertTextBox();
    this.insertButton();
    this.setOptions();
  },
  
  setHandlers: function(options) {
    this.handlers = {};
    this.handlers.representation = options.representation;
    this.handlers.option = options.option;
    this.handlers.afterAdd = options.afterAdd;
  },
  
  insertTextBox: function() {
    var row = this.select.up();
    row.insert({after: '<div class="element" style="display:none"><input value="" style="width:164px"></div>'});
    this.textbox = row.next().down();
    this.textbox.observe('keypress',
      function(event) {
        if (event.keyCode == Event.KEY_RETURN) this.addHide();
      }.bind(this)
    );
  },
  
  insertButton: function() {
    var html = '<div class="element" style="display:none"><input type="button" value="Add" style="width:80px"></div>';
    html += '<div class="loader" style="display:none"></div>';
    var row = this.textbox.up();
    row.insert({after: html });
    this.button = row.next().down();
    this.button.observe('click', this.addHide.bind(this));
  },
  
  onChange: function(event) {
    if ($F(this.select) == 'add') {
      this.show();
    } else {
      this.hide();
    }
  },
  
  show: function() {
    this.textbox.enable().up().show();
    this.button.enable().up().show();
    this.textbox.focus();
  },
  
  hide: function() {
    this.textbox.clear().up().hide();
    this.button.loaded().up().hide();
  },
  
  addHide: function() {
    if (this.textbox.present()) {
      this.add();
    } else {
      this.select.selectedIndex = 0;
      this.hide();          
    }
  },
  
  add: function() {
    this.button.loading('Adding');
    var representation = this.handlers.representation(this.textbox);
    representation.id = Id.get();
    try {
      representation = Model[this.model].set(representation);
      var option = this.handlers.option(representation);
      this.setOptions(option);
      this.hide();
      this.handlers.afterAdd(representation);
    } catch (error) {
      this.button.loaded();
      this.textbox.focus();
      Notice.error(error);
    }
  },
  
  setOptions: function(option) {
    this.getOptions();
    if (option) this.options.push(option);
    this.options.order('value');
    var descriptor = this.model.underscore().gsub('_', ' ');
    this.options.unshift({ key: 'all', value: 'Choose ' + descriptor });
    this.options.push({ key: 'add', value: 'Add ' + descriptor });
    this.select.setValue({ options: this.options, value: (option || {}).key || null });
  },
  
  getOptions: function() {
    this.options = [];
    (this.select.options.length).times(
      function(index) {
        var option = this.select.options[index];
        var key = option.value, value = option.text;
        if (key != 'all' && key != 'add') {
          this.options.push({ key: key, value: value });
        }
      }.bind(this)
    );
  }
});

var Caption = Class.create({
  afterDeselect : function() {},

  afterSelect : function() {},
  
  callHandler : function() {
    if (this.input.checked) {
      this.afterSelect();
    } else {
      this.afterDeselect();
    }
  },

  initialize : function(element, options) {
    this.caption = $(element);
    this.caption.setStyle({cursor : 'pointer'});
    this.caption.observe('click', this.toggle.bind(this));
    this.input = this.caption.previous().down();
    this.input.observe('click', this.callHandler.bind(this));
    options = options || {};
    if (options.afterDeselect) this.afterDeselect = options.afterDeselect;
    if (options.afterSelect) this.afterSelect = options.afterSelect;
  },
  
  toggle : function() {
    this.input.checked = !this.input.checked;
    this.callHandler();
  }
});

var Session = {
  // Flush in-memory Session.user object to LocalStorage.
  // Use whenever updating Session.user object directly.
  flush: function() {
    if (LocalStorage.exists) {
      LocalStorage.set('Session.user', Session.user);
    }
  },
  
  // Initializes Session.user object from LocalStorage or empty object.
  // Session access or querying must be via Session.user object.
  initialize: function() {
    if (LocalStorage.exists) {
      Session.user = LocalStorage.get('Session.user') || {};
    } else {
      Session.user = {};
    }
  },
  
  // Receives email, and password or ha1.
  // Calls onFailure with appropriate error message where session fails.
  // Calls onSuccess if provided or Session.redirect where session succeeds.
  set: function(options) {
    options = options || {};
    
    var username = (options.email || '').strip().toLowerCase();
    var ha1 = options.ha1;

    if (!username) return options.onFailure('Please provide your email address.');
    if (!ha1) {
      var password = (options.password || '').strip();
      if (!password) return options.onFailure('Please provide your password.');
      var realm = Config.DigestAuthentication.realm;
      ha1 = (username + ':' + realm + ':' + password).md5();
    };
    
    if (LocalStorage.exists) {
      var user = LocalStorage.get(username, {
        namespace: 'Session',
        privateKey: ha1
      });
      if (user) return this._set(user, options);
    }
    
    var uri = '/api/users';
    Http.get(uri, {
      headers: {
        Accept: 'application/json',
        Authorization: DigestAuthentication.header({
          username: username,
          realm: Config.DigestAuthentication.realm,
          ha1: ha1,
          method: 'GET',
          uri: uri
        })
      },
      parameters: {
        email: username
      },
      onSuccess: function(body) {
        var user = body.toObject().first();
        if (user) {
          this._set(user, options);
        } else {
          options.onFailure('Please provide a valid email address and password.');
        }
      }.bind(this),
      onFailure: function(error, response) {
        error = error.without(/^[0-9]{3} /);
        if (response.status == 401) {
          options.onFailure('Please provide a valid email address and password.');
        } else {
          options.onFailure(error);
        }
      },
      onTimeout: function(error) {
        error = 'Please reconnect to the Internet to enable';
        error += ' your user account for offline access.';
        options.onFailure(error);
      }
    });
  },
  
  unset: function(options) {
    options = options || {};
    
    Session.user = {};
    Session.flush();
    Dispatcher.classInstance.disable();
    Database.initialize({
      onFailure: options.onFailure,
      onProgress: options.onProgress,
      onSuccess: function() {
        Dispatcher.classInstance.enable();
        if (Session.afterUnset) {
          Session.afterUnset(options);
        } else if (options.onSuccess) {
          options.onSuccess();
        }
      }
    });
  },
  
  _set: function(user, options) {
    if (LocalStorage.exists) {
      LocalStorage.set(user.email, user, {
        namespace: 'Session',
        privateKey: user.ha1
      });
    }
    
    Session.user = user;
    Session.flush();
    Dispatcher.classInstance.disable();
    Database.initialize({
      onFailure: options.onFailure,
      onProgress: options.onProgress,
      onSuccess: function() {
        Dispatcher.classInstance.enable();
        if (Session.afterSet) {
          Session.afterSet(options);
        } else if (options.onSuccess) {
          options.onSuccess();
        }
      }
    });
  }
};

var Parameters = {};

var Replication = {
  /*
  push update log to the server
  
  pull changes from the server since a specific date and time
  
  setChanges
  getChanges
  getSharedModels
  getUserModels
  
  pull all user data from the server
  
  pull all shared data from the server
  
  initialize
  
  accept additions to the update log
  */
  getSharedModels: function(options) {
    var queue = new Queue(Model.shared.length, options);
    Model.shared.each(
      function(model) {
        queue.push(this._getModel.bind(this, model));
      }.bind(this)
    );
  },
  
  getUserModels: function(options) {
    var queue = new Queue(Model.user.length, options);
    Model.user.each(
      function(model) {
        queue.push(this._getModel.bind(this, model));
      }.bind(this)
    );
  },
  
  initialize: function() {
    
  },
  
  _getModel: function(className, options) {
    var model = new Model[className]();
    var uri = '/api/' + model._model;
    var headers = { 'Accept': 'application/json' };
    if (model.permissions) {
      headers['Authorization'] = DigestAuthentication.header({
        username: Session.user.email,
        realm: Config.DigestAuthentication.realm,
        ha1: Session.user.ha1,
        method: 'GET',
        uri: uri
      });
    }
    Http.get(uri, {
      headers: headers,
      onFailure: options.onFailure,
      onSuccess: function(body) {
        try {
          var object = {};
          object[className] = JSON.parse(body);
          options.onSuccess(object);
        } catch (error) {
          options.onFailure('502 Bad Gateway');
          Log(error);
        }
      },
      onTimeout: options.onFailure
    });
  }
};

var History = {
  _history: [],
  
  get: function(complete) {
    return this._history.slice(0, complete ? undefined : this._history.length - 1);
  },
  
  pop: function() {
    this._history.pop();
  },
  
  set: function() {
    this._history.push(Object.copy(Parameters));
  }
};

var AddRemove = Class.create({
  add: function(event) {
    var first = this.elements.first();
    if (first) this.getRemoveLink(first).show();
    
    var last = this.elements.last();
    if (last) this.getAddLink(last).hide();
    
    this.div.insert(this.content);
    var element = this.div.childElements().last();
    this.elements.push(element);
    
    if (this.elements.length > 1) {
      if (event) this.focusText(element);
    } else {
      if (!this.options.repeatLabels) element.down('.label').update(this.label);
      this.getRemoveLink(element).hide();
    }

    if (this.options.afterAdd) this.options.afterAdd(element, this, event);
    return element;
  },
  
  anyPresent: function() {
    return this.elements.any(
      function(element) {
        return this.present(element);
      }.bind(this)
    );
  },
    
  focusText: function(element) {
    var text = element.down('input[type="text"]') || element.down('textarea');
    if (text) text.focus();
  },
  
  getAddLink: function(element) {
    var row = this.table ? element.down('.row', this.options.rowIndex) : element;
    return row.childElements().last();
  },
  
  getRemoveLink: function(element) {
    return this.getAddLink(element).previous();
  },
  
  initialize: function(div, options) {
    this.div = $(div);
    this.options = options || {};
    this.elements = [];
    
    this.table = !!this.div.down('.table');
    var row = this.div.down('.row', this.options.rowIndex);
    row.insert('<div class="element"><a href="" onclick="return false">Remove</a></div>');
    row.insert('<div class="element"><a href="" onclick="return false">Add Another</a></div>');

    if (!this.options.repeatLabels) {
      var label = this.div.down('.label');
      if (label) this.label = label.replaceContent('&nbsp;');
    }

    this.content = this.div.replaceContent('');
    this.add();

    this.div.observe('keypress', this.onChange.bind(this));
    this.div.observe('change', this.onChange.bind(this));
    this.div.observe('click', this.onClick.bind(this));
    
    return this;
  },
  
  onChange: function(event) {
    var first = this.elements.first();
    if (first) this.getRemoveLink(first).show();
  },
  
  onClick: function(event) {
    var element = event.element();
    var value = element.innerHTML;
    
    if (value == 'Add Another') {
      this.add(event);
    } else if (value == 'Remove') {
      this.remove(event);
    }
  },
  
  present: function(element) {
    return (this.options.present || []).any(
      function(property) {
        var field = element.down('[name="' + property + '"]');
        return field ? field.getValue().present() : false;
      }
    );
  },
  
  remove: function(event, element) {
    element = element || event.element().up(this.table ? '.table' : '.row');

    element.remove();
    this.elements = this.elements.without(element);
    
    if (this.elements.present()) {
      if (!this.options.repeatLabels) {
        var label = this.elements.first().down('.label');
        if (label) label.update(this.label);
      }
      this.getAddLink(this.elements.last()).show();
    } else {
      this.add();
    }
    
    if (this.options.afterRemove) this.options.afterRemove(element, this, event);
    return element;
  },
  
  reset: function() {
    this.elements.each(this.remove.bind(this, undefined));
  },
  
  save: function() {
    var options = this.options;
    var set = {};
    this.elements.each(
      function(element) {
        if (this.present(element)) {
          if (options.representation) {
            var object = options.representation(element);
          } else {
            var object = element.down('form').serialize();
          }
          object = Object.extend(object, options.parameters);
          if (!object.id) {
            object.id = Id.get();
            element.down('[name="id"]').setValue(object.id);
          }
          Model[options.model].set(object);
          set[object.id] = true;
        }
      }.bind(this)
    );
    if (options.required && Object.keys(set).length < 1) {
      var descriptor = options.model.underscore().gsub('_', ' ').toLowerCase() + '.';
      throw '422 Please provide at least one ' + descriptor;
    }
    Model[options.model].get(options.parameters).each(
      function(object) {
        if (!set[object.id]) {
          if (options.unset) {
            options.unset(object.id);
          } else {
            Model[options.model].unset(object.id);
          }
        }
      }
    );
  }
});

var Map = Class.create({
  setPopup : function() {
    function popup() {
      this.coordinates = new google.maps.LatLng(0, 0);
    };
    popup.prototype = new google.maps.Overlay();
    popup.prototype.initialize = function(map) {
      this.map = map;
      this.div = new Element('div').absolutize().hide();
      this.div.setStyle({
        'backgroundImage' : 'url(/images/overlay.png)',
        'backgroundRepeat' : 'no-repeat',
        'backgroundPosition' : '0px 0px',
        'width' : '160px',
        'height' : '191px',
        'overflow' : 'hidden',
        'padding' : '1px 0px 1px 0px'
      });
      this.map.getPane(google.maps.MAP_FLOAT_PANE).insert(this.div);
      this.shadow = new Element('div').absolutize().hide();
      this.shadow.setStyle({
        'backgroundImage' : 'url(/images/overlay-shadow.png)',
        'backgroundRepeat' : 'no-repeat',
        'backgroundPosition' : '0px 0px',
        'width' : '242px',
        'height' : '105px'
      });
      this.shadow.setOpacity(0.1);
      this.map.getPane(google.maps.MAP_FLOAT_SHADOW_PANE).insert(this.shadow);
    };
    popup.prototype.remove = function() {
      this.div.parentNode.removeChild(this.div);
      this.shadow.parentNode.removeChild(this.shadow);
    };
    popup.prototype.copy = function() {
      return new popup();
    };
    popup.prototype.redraw = function(force) {
      if (!force) return;
      var position = this.map.fromLatLngToDivPixel(this.coordinates);
      this.div.setStyle({
        'top' : (position.y - this.div.getHeight() - 32) + 'px',
        'left' : (position.x - 18) + 'px'
      });
      this.shadow.setStyle({
        'top' : (position.y - this.shadow.getHeight() - 32) + 'px',
        'left' : (position.x - 5) + 'px'
      });
    };
    popup.prototype.hide = function() {
      this.div.hide();
      this.shadow.hide();
    };
    popup.prototype.move = function(coordinates) {
      this.coordinates = coordinates;
      this.redraw(true);
      var position = this.map.fromLatLngToDivPixel(this.coordinates);
      var point = new google.maps.Point(position.x, position.y - 100);
      this.map.panTo(this.map.fromDivPixelToLatLng(point));
    };
    popup.prototype.update = function(html) {
      this.div.update(html).show();
      this.shadow.show();
    };
    this.popup = new popup();
    this.map.addOverlay(this.popup);
  },
  
  hidePopup : function() {
    this.popup.hide();
  },
  
  afterLoad : function() {},
  
  attachObservers : function() {
    google.maps.Event.addListener(this.map, 'click', function(overlay, latlng) {
      if (latlng) this.popup.hide();
    }.bind(this));
    google.maps.Event.addListener(this.map, 'mousemove', function() {
      if (!this.fixedLinks) this.fixLinks();
    }.bind(this));
  },
  
  fixLinks : function() {
    this.element.select('a').invoke('addClassName', 'plain');
    this.fixedLinks = true;
  },
  
  initialize : function(element, options) {
    this.element = $(element);
    this.options = options || {};
    if (this.options.afterLoad) this.afterLoad = this.options.afterLoad;
    GoogleApi.initialize(this.getMaps.bind(this));
  },
  
  getMaps : function() {
    google.load('maps', '2', {callback : this.setMap.bind(this)});
  },
  
  loaded : function() {
    return !!this.map;
  },
  
  setMap : function() {
    this.map = new google.maps.Map2(this.element, {backgroundColor : '#FFFFFF'});
    this.setCenter(this.options.latitude, this.options.longitude, this.options.zoom);
    this.setMode(this.options.mode);
    this.setControls();
    this.setCorners();
    this.setPopup();
    this.attachObservers();
    this.markers = [];
    this.afterLoad();
    this.refresh();
  },
  
  showPopup : function(html, marker) {
    this.popup.update(html);
    this.popup.move(marker.getLatLng());
  },
  
  refresh : function() {
    if (!this.map) return;
    this.map.checkResize();
    this.setCenter(this.options.latitude, this.options.longitude, this.options.zoom);
  },
  
  reset : function() {
    if (!this.map) return;
    this.setCenter(this.options.latitude, this.options.longitude, this.options.zoom);
    this.setMode(this.options.mode);
    this.showCorners();
    this.hidePopup();
    this.removeMarkers();
    this.refresh();
  },
  
  removeMarkers : function() {
    this.markers.each(
      function(marker) {
        this.map.removeOverlay(marker);
      }.bind(this)
    );
  },
  
  setMarker : function(options) {
    var coordinates = new google.maps.LatLng(options.latitude, options.longitude);
    var marker = new google.maps.Marker(coordinates, {title : options.title});
    var callback = options.onClick || function() {};
    google.maps.Event.addListener(marker, 'click', callback.curry(marker));
    this.markers.push(marker);
    this.map.addOverlay(marker);
  },
  
  setCenter : function(latitude, longitude, zoom) {
    var center = new google.maps.LatLng(latitude || 0, longitude || 0);
    this.map.setCenter(center, zoom || 2);
  },
  
  setControls : function() {
    this.map.addControl(new google.maps.LargeMapControl());
    this.map.addControl(new google.maps.MapTypeControl());
    this.map.enableScrollWheelZoom();
    this.map.enableContinuousZoom();
  },
  
  setMode : function(mode) {
    var modes = {
      street : google.maps.NORMAL_MAP,
      satellite : google.maps.SATELLITE_MAP,
      hybrid : google.maps.HYBRID_MAP,
      terrain : google.maps.PHYSICAL_MAP
    };
    this.map.addMapType(google.maps.PHYSICAL_MAP);
    this.map.setMapType(modes[mode || 'street']);
  },
  
  setCorners : function() {
    this.corners = [];
    var position = {
      'top-left' : [0, 1],
      'top-right' : [1, 1],
      'bottom-right' : [1, 0],
      'bottom-left' : [0, 0]
    };
    Object.keys(position).each(
      function(key) {
        var x = position[key].first(), y = position[key].last();
        this.corners.push(new google.maps.ScreenOverlay('images/corner-' + key + '.png', 
          new google.maps.ScreenPoint(x, y, 'fraction', 'fraction'),
          new google.maps.ScreenPoint(x * 8, y * 8),
          new google.maps.ScreenSize(8, 8, 'pixels', 'pixels')
        ));
        this.map.addOverlay(this.corners.last());
      }.bind(this)
    );
  },
  
  showCorners : function() {
    this.corners.each(
      function(corner) {
        corner.show();
      }
    );
  }
});

Database.LocalStorage = {
  // Get an object or collection or undefined.
  // Returns objects by value not by reference.
  get: function(key, parameters, options) {
    parameters = Object.copy(parameters);
    options = options || {};
    
    if (key.include('/')) {
      var object = this._cache[key];
      return object ? Object.copy(object) : undefined;
    } else {
      var collection = this._getCollection(key) || [];
      if (Object.keys(parameters)) collection = collection.findAll(parameters);
      return collection;
    }
  },

  initialize: function(options) {
    options = options || {};
    options.initialize = true;
    
    var online = navigator.onLine;
    var slow = !Prototype.Browser.WebKit;
    var database = LocalStorage.exists && this._getCompleteFlag(Session.user);
    
    // Initialize change log from local storage or []:
    this._initializeChanges();
    // Initialize push and pull processes:
    this._initializeReplication();
    
    if (online && (slow || !database)) {
      // Set cache from API (fallible).
      // If this fails set cache from LocalStorage.
      this._setCacheFromApi(options);
    } else if (database) {
      this._setCacheFromLocalStorage(options);
    } else {
      this._triggerOnFailureCallback(options);
    }
  },

  // Set an object.
  set: function(key, object, options) {
    if (key.include('/')) {
      // Create local copy of object:
      object = Object.copy(object);
      
      // Get ancestor:
      var ancestor = this._cache[key] || {};
      
      // Set cache object:
      this._setCacheObject(key, object);
      
      // Set local storage object:
      this._setLocalStorageObject(key, object);
      
      // Append key and delta to change log:
      var delta = Object.copy(Object.delta(object, ancestor));
      this._setChange(key, delta, options);
    }
  },

  // Unset an object.
  unset: function(key, options) {
    if (key.include('/')) {
      // Unset cache object:
      
      // Unset local storage object:
      this._unsetLocalStorageObject(key);
      
      // Append key to change log:
      this._setChange(key, undefined, options);
    }
  },
  
  // Flushes _changes to local storage.
  // Called by _setLocalStorageFromCache after calling LocalStorage.clear.
  _flushChangesToLocalStorage: function() {
    // Persist to LocalStorage if possible:
    if (LocalStorage.exists) {
      if (Session.user.id) {
        var options = { namespace: Session.user.id, privateKey: Session.user.ha1 };
      } else {
        var options = {};
      }
      LocalStorage.set('changes', this._changes, options);
    }
  },
  
  // Initialize change log from local storage or [].
  // Can be used on initialize only.
  _initializeChanges: function() {
    if (LocalStorage.exists) {
      this._changes = LocalStorage.get('changes') || [];
      if (Session.user.id) {
        var options = { namespace: Session.user.id, privateKey: Session.user.ha1 };
        var changes = LocalStorage.get('changes', options) || [];
        // Merge shared and user changes and maintain sequential order:
        this._changes = this._changes.concat(changes);
        this._changes.order('timestamp');
      }
    } else {
      this._changes = [];
    }
  },

  _initializeReplication: function() {
    if (!this._pullProcess) {
      this._pullProcess = this._pull.bind(this).repeat((7).minutes());
    }
    if (!this._pushProcess) {
      this._pushProcess = this._push.bind(this).repeat(100); // Milliseconds.
    }
  },
  
  // Called by get method.
  // Returns objects by value not by reference.
  _getCollection: function(index) {
    var keys = this._cache[index];
    if (keys) {
      // Cache reference to this._cache for optimum lookup performance:
      var cache = this._cache;
      var collection = [];
      var properties;
      var value;
      var object;
      var copy;
      
      // Use native loops for optimum performance:
      for (var i = 0, l = keys.length; i < l; ++i) {
        object = cache[ keys[i] ];

        // Create properties once using first object found:
        if (!properties) properties = Object.keys(object);

        // Create shallow copy of object:
        copy = {};
        for (var pi = 0, pl = properties.length; pi < pl; ++pi) {
          value = object[ properties[pi] ];
          copy[ properties[pi] ] = (value && value.array) ? value.slice(0) : value;
        }

        // Pass object by value not by reference:
        collection.push(copy);
      }
      return collection;
    }
  },
  
  _getCompleteFlag: function(user) {
    if (LocalStorage.exists) {
      // Get public database complete flag:
      var sharedNamespace = LocalStorage.get('complete');
      // Get user database complete flag:
      var userNamespace = true;
      var privateKey = true;
      if (user && user.id) {
        userNamespace = LocalStorage.get('complete', {
          privateKey: user.ha1,
          namespace: user.id
        });
        // Make sure we can decrypt user database.
        // Short values may be decrypted if privateKey is incorrect towards the end.
        // If user object can be returned then we are probably fine.
        privateKey = LocalStorage.get('users/' + user.id, {
          privateKey: user.ha1,
          namespace: user.id
        });
      }
    
      return sharedNamespace && userNamespace && privateKey;
    }
  },
  
  _getUpdated: function(user) {
    if (!this._updated && LocalStorage.exists) {
      if (user && user.id) {
        var options = { privateKey: user.ha1, namespace: user.id };
      } else {
        var options = {};
      }
      this._updated = LocalStorage.get('updated', options);
    }
    return this._updated;
  },
  
  _pull: function() {
    // Return if offline:
    if (!navigator.onLine) return;
    
    var since = this._getUpdated(Session.user);
    if (!since) return this._setCacheFromApi();
    
    var uri = '/api/changes';
    var headers = { 'Accept': 'application/json', 'If-Modified-Since': since };
    if (Session.user.id) {
      headers['Authorization'] = DigestAuthentication.header({
        username: Session.user.email,
        realm: Config.DigestAuthentication.realm,
        ha1: Session.user.ha1,
        method: 'GET',
        uri: uri
      });
    }
        
    Http.get(uri, {
      headers: headers,
      onSuccess: function(body, response) {
        var updated = response.headers['Last-Modified'] || '';
        this._setUpdated(updated, Session.user);
        if (!body) return;
        Object.each(body.toObject(),
          function(index, changes) {
            changes.each(
              function(object) {
                var key = index + '/' + object.id;
                // Updated is an ISO8601 date and comparison is lexicographic:
                var updated = (this._cache[key] || {}).updated || '';

                if (object.updated > updated) {
                  this._setCacheObject(key, object);
                  this._setLocalStorageObject(key, object);
                } else if (Object.isUndefined(object.updated)) {
                  this._unsetCacheObject(key);
                  this._unsetLocalStorageObject(key);
                }
              }.bind(this)
            );
          }.bind(this)
        );
      }.bind(this)
    });
  },
  
  _push: function() {
    if (!navigator.onLine || this._pushing) return;
    
    var change = this._changes.first();
    if (change) {
      change.options = change.options || {};
      this._pushing = true;
    } else {
      return;
    }
    
    var key = change.unset || change.set;
    var method = change.unset ? 'delete' : 'put';
    var uri = '/api/' + key;
    var headers = {
      'Accept': 'application/json',
      'Content-Type': 'application/json; charset=UTF-8'
    };
    if (Session.user.id) {
      headers['Authorization'] = DigestAuthentication.header({
        username: Session.user.email,
        realm: Config.DigestAuthentication.realm,
        ha1: Session.user.ha1,
        method: method.upperCase(),
        uri: uri
      });
    }
    var body = change.delta ? JSON.stringify(change.delta) : '';

    Http[method](uri, {
      headers: headers,
      body: body,
      onFailure: function(error, response) {
        this._pushing = false;
        if (response.status >= 400 && response.status < 500) {
          var model = key.split('/')[0].gsub('-', ' ').titleCase().singular();
          var id = key.split('/')[1];
          error = 'The server rejected your change to ' + model + ' ' + id;
          error += ': ' + (response.body || '').without(/^[0-9]{3} /);
          Notice.error(error);
          this._unsetChange(change);
        }
        if (change.options.onFailure) change.options.onFailure(response);
      }.bind(this),
      onSuccess: function(body, response) {
        this._pushing = false;
        if (method == 'set') {
          var object = body.toObject();
          this._setCacheObject(key, object);
          this._setLocalStorageObject(key, object);
        };
        this._unsetChange(change);
        if (change.options.onSuccess) change.options.onSuccess(response);
      }.bind(this),
      onTimeout: function(response) {
        this._pushing = false;
        if (change.options.onTimeout) change.options.onTimeout(response);
      }.bind(this)
    });
  },
  
  // Sets cache atomically.
  // Can be used on initialize or periodically.
  // Triggers options.onSuccess if provided.
  // Triggers _setCacheFromLocalStorage on failure if on initialize.
  _setCacheFromApi: function(options) {
    options = options || {};
    
    if (!navigator.onLine) {
      // Try to set cache from LocalStorage if on initialize.
      if (options.initialize) this._setCacheFromLocalStorage(options);
      return;
    }
    
    var total = Model.models.length;
    var requests = total;
    var collections = {};
    var error;
    
    var onComplete = function() {
      Log('Database.LocalStorage._setCacheFromApi.onComplete: Arrived.');
      if (error) {
        Log('Database.LocalStorage._setCacheFromApi.onComplete: Error: ' + error);
        // Fail silently.
        // Try to set cache from LocalStorage if on initialize.
        if (options.initialize) this._setCacheFromLocalStorage(options);
      } else {
        // Clear cache:
        this._cache = {};
        // Set collections to cache synchronously:
        Object.each(collections, this._setCollection.bind(this));
        this._setUpdated('UTC'.toDateTime('Y-M-D'), Session.user);
        // Set cache to local storage asynchronously:
        this._setLocalStorageFromCache(options);
        if (options.onSuccess) options.onSuccess();
      }
    }.bind(this);
    
    var onFailure = function(e) {
      // Set error:
      error = e;
      // Call onProgress callback if provided:
      if (options.onProgress) options.onProgress((total - requests) / total * 100);
      if ((--requests) === 0) onComplete();
    };

    var onSuccess = function(index, collection) {
      // Set temporary collections cache:
      collections[index] = collection;
      // Call onProgress callback if provided:
      if (options.onProgress) options.onProgress((total - requests) / total * 100);
      if ((--requests) === 0) onComplete();
    };
    
    Model.models.each(
      function(model) {
        var classInstance = new Model[model]();
        var index = classInstance._model;
        var uri = '/api/' + index;
        var headers = { 'Accept': 'application/json' };

        if (classInstance.authorization) {
          if (!Session.user.ha1) return requests--;
          headers['Authorization'] = DigestAuthentication.header({
            username: Session.user.email,
            realm: Config.DigestAuthentication.realm,
            ha1: Session.user.ha1,
            method: 'GET',
            uri: uri
          });
        }

        Http.get(uri, {
          headers: headers,
          onFailure: onFailure,
          onSuccess: function(body) {
            try {
              onSuccess(index, JSON.parse(body));
            } catch (error) {
              onFailure('502 Bad Gateway');
              Log(error);
            }
          },
          onTimeout: onFailure
        });
      }
    );
  },

  // Sets cache asynchronously.
  // Can be used on initialize only.
  // Triggers options.onFailure or options.onSuccess if provided.
  _setCacheFromLocalStorage: function(options) {
    options = options || {};
    var timer = new Timer();
    
    // Trigger onFailure callback if necessary:
    if (!LocalStorage.exists || !this._getCompleteFlag(Session.user)) {
      return this._triggerOnFailureCallback(options);
    }

    // Cache Session.user as it may change before completion.
    var user = Object.extend({}, Session.user);

    // Clear cache:
    this._cache = {};
    
    // Cache reference to this._cache for optimum lookup performance:
    var cache = this._cache;
    var length = LocalStorage.length();
    var total = length;
    var remaining = length;
    var progressions = 0; // Used to limit calls to onProgress callback.

    var onSuccess = function(key, object) {
      var index = key.extract(/^(.+)\//);
      if (index) {
        // Set index to cache (if key contains '/'):
        if (!cache[index]) cache[index] = [];
        cache[index].push(key); // Keys are unique.
        // Set object to cache:
        cache[key] = object;
      }
      
      // Call onProgress callback if provided:
      if (options.onProgress) {
        if ((progressions++) == (Prototype.Browser.WebKit ? 2000 : 200)) {
          options.onProgress((length - remaining) / length * 100);
          progressions = 0;
        }
      }
      
      // On completion call onSuccess callback if provided:
      if ((--remaining) === 0) {
        this._pull(); // Don't wait for _pull timer, start right away.
        timer.log('Database.LocalStorage: _setCacheFromLocalStorage completed');
        if (options.onSuccess) options.onSuccess();
      }
    }.bind(this);
    
    var sharedRegex = /^[A-Z]+/i;
    var sharedOptions = { onSuccess: onSuccess };

    var userRegex = new RegExp('^' + user.id + '\\.(.+)');
    var userOptions = { namespace: user.id, privateKey: user.ha1 };
    userOptions.onSuccess = onSuccess;
            
    var key;
    for (var index = 0; index < length; ++index) {
      key = LocalStorage.key(index);
      
      if (sharedRegex.test(key)) {
        LocalStorage.get(key, sharedOptions);
      } else if (user.id && (key = key.extract(userRegex))) {
        LocalStorage.get(key, userOptions);
      } else {
        total--;
        remaining--;
      }
    }
  },

  _setCacheObject: function(key, object) {
    // Set object:
    // Let LocalStorage._queue reference old object.
    delete this._cache[key];
    this._cache[key] = object;
    
    // Set primary index:
    // Let LocalStorage._queue continue to reference previous object.
    var index = key.split('/')[0];
    var keys = this._cache[index] || [];
    delete this._cache[index];
    this._cache[index] = keys.put(key);
  },
    
  // Appends change to changes log on object set or unset.
  _setChange: function(key, delta, options) {
    var change = delta ? { set: key, delta: delta } : { unset: key };
    change.timestamp = Timestamp.get();
    change.options = options;
    this._changes.push(change);
    
    // Persist to local storage synchronously:
    if (LocalStorage.exists) {
      if (Session.user.id) {
        var options = { namespace: Session.user.id, privateKey: Session.user.ha1 };
        var changes = LocalStorage.get('changes', options) || [];
        LocalStorage.set('changes', changes.post(change), options);
      } else {
        var changes = LocalStorage.get('changes') || [];
        LocalStorage.set('changes', changes.post(change));
      }
    }
  },
  
  // Called by _setCacheFromApi to set collections to cache.
  _setCollection: function(index, collection) {
    // Cache reference to this._cache for optimum lookup performance:
    var cache = this._cache;
    var keys = [];
    var key;
    var object;
    var namespace = index + '/';
    
    // Set collection objects:
    for (var i = 0, l = collection.length; i < l; ++i) {
      object = collection[i];
      key = namespace + object.id;
      cache[key] = object;
      keys.push(key);
    }
    
    // Set primary index:
    cache[index] = keys;
  },

  _setCompleteFlag: function(flag, user) {
    // Set public database complete flag:
    LocalStorage.set('complete', flag);
    // Set user database complete flag:
    if (user && user.id) {
      LocalStorage.set('complete', flag, {
        privateKey: user.ha1,
        namespace: user.id
      });
    }
  },
  
  // Called by _setCacheFromApi to persist cache to local storage.
  // Clears local storage on failure.
  _setLocalStorageFromCache: function(options) {
    if (!LocalStorage.exists) return;
    var timer = new Timer();
    
    // Cache Session.user as it may change before completion.
    var user = Object.extend({}, Session.user);

    // Set shared and user database complete flags to false:
    this._setCompleteFlag(false, user);
    
    // Cache reference to this._cache for optimum lookup performance:
    var cache = this._cache;
    var remaining = 0;
    var error;
    
    var onComplete = function() {
      if (error) {
        LocalStorage.clear();
        this._flushChangesToLocalStorage();
        Session.flush();
        Notice.error(error);
      } else {
        this._setCompleteFlag(true, user);
        if (options.initialize && Session.user.id && Session.user.id == user.id) {
          Notice.success('Your user account is now enabled for offline access.');
        }
        timer.log('Database.LocalStorage: _setLocalStorageFromCache completed');
      }
    }.bind(this);
    
    var onFailure = function(e) {
      error = e;
      if ((--remaining) === 0) onComplete();
    };
    
    var onSuccess = function() {
      if ((--remaining) === 0) onComplete();
    };

    Model.models.each(
      function(model) {
        var classInstance = new Model[model]();
        var index = classInstance._model;
        var options = { onFailure: onFailure, onSuccess: onSuccess };
        
        if (classInstance.authorization) {
          if (!user.ha1) return;
          options.namespace = user.id;
          options.privateKey = user.ha1;
        }
        
        // Set model objects asynchronously:
        var keys = cache[index];
        remaining += keys.length;
        for (var i = 0, l = keys.length; i < l; ++i) {
          LocalStorage.set(keys[i], cache[ keys[i] ], options);
        }
      }.bind(this)
    );
  },
  
  _setLocalStorageObject: function(key, object) {
    if (!LocalStorage.exists) return;

    var index = key.split('/')[0];
    var classInstance = new Model[index.constantize().singular()]();
    
    var options = { onFailure: Notice.error.bind(Notice) };
    
    if (classInstance.authorization) {
      options.namespace = Session.user.id;
      options.privateKey = Session.user.ha1;
    }
        
    // Set object asynchronously:
    LocalStorage.set(key, object, options);
  },
  
  _setUpdated: function(updated, user) {
    this._updated = updated;
    if (LocalStorage.exists) {
      // Set public database updated flag:
      LocalStorage.set('updated', this._updated);
      // Set user database updated flag:
      if (user && user.id) {
        LocalStorage.set('updated', this._updated, {
          privateKey: user.ha1,
          namespace: user.id
        });
      }
    }
  },
  
  _triggerOnFailureCallback: function(options) {
    options = options || {};
    
    if (options.onFailure) {
      if (!LocalStorage.exists) {
        var error = 'Your browser does not support offline access.';
        var href = 'http://www.apple.com/safari';
        options.onFailure(error, Html.a({ href: href }, 'Use Safari'));
      } else if (!this._getCompleteFlag(Session.user)) {
        var error = 'Please reconnect to the Internet to enable';
        error += ' your user account for offline access.';
        options.onFailure(error);
      }
    }
  },
  
  _unsetCacheObject: function(key) {
    // Unset object:
    // LocalStorage._queue will continue to reference previous object as it should.
    delete this._cache[key];
    
    // Set primary index:
    // Let LocalStorage._queue continue to reference previous object.
    var index = key.split('/')[0];
    var keys = this._cache[index] || [];
    delete this._cache[index];
    this._cache[index] = keys.without(key);
  },
  
  // Removes change from changes log after change has been pushed to the server.
  _unsetChange: function(change) {
    // Remove first change from changes log:
    this._changes.shift();

    // Updated local storage synchronously:
    if (LocalStorage.exists) {
      var unset = function(change, options) {
        var changes = LocalStorage.get('changes', options) || [];
        // Compare using keys since 'change' carries change.options:
        var key = change.unset ? 'unset' : 'set';
        if (changes[0] && changes[0][key] == change[key]) {
          // We've found the right local storage namespace:
          // Remove first change from local storage changes log:
          LocalStorage.set('changes', changes.slice(1), options);
          return true;
        }
      };
      if (!unset(change, {}) && Session.user.id) {
        var options = { namespace: Session.user.id, privateKey: Session.user.ha1 };
        unset(change, options);
      }
    }
  },
  
  _unsetLocalStorageObject: function(key) {
    if (!LocalStorage.exists) return;
        
    var index = key.split('/')[0];
    var classInstance = new Model[index.constantize().singular()]();
    
    var options = { onFailure: Notice.error.bind(Notice) };
    
    if (classInstance.authorization) {
      options.namespace = Session.user.id;
      options.privateKey = Session.user.ha1;
    }
    
    // Unset object asynchronously:
    LocalStorage.unset(key, options);
  }
};

Database.Sql = {
  get: function(model, parameters, options) {
    this._getDatabase(model).transaction(
      function(transaction) {
        var query = 'SELECT * FROM `' + model + '`;';
        var onSuccess = function(transaction, result) {
          var objects = [];
          var rows = result.rows;
          for (var index = 0, length = rows.length; index < length; ++index) {
            objects.push(rows.item(index).representation.toObject());
          }
          timer.log('success' + rows.length);
          options.onSuccess(objects);
        };
        var onFailure = this._onFailureHandler.curry(options);
        transaction.executeSql(query, [], onSuccess, onFailure);
      }.bind(this)
    );
  },
  
  initialize: function(options) {
    Log('Database.Sql: Initializing...');
    this._openDatabases({
      onFailure: options.onFailure,
      onSuccess: this._cacheDatabases.bind(this, options)
    });
  },
  
  set: function(model, object, options) {
    this._getDatabase(model).transaction(
      function(transaction) {
        var query = 'REPLACE INTO `' + model + '` (id, representation) VALUES (?, ?);';
        transaction.executeSql(query, [ object.id, JSON.stringify(object) ]);
      },
      this._onFailureHandler.curry(options),
      options.onSuccess
    );
  },
  
  unset: function(model, object, options) {
    this._getDatabase(model).transaction(
      function(transaction) {
        var query = 'DELETE * FROM `' + model + '` WHERE id = ?;';
        transaction.executeSql(query, [ object.id ]);
      },
      this._onFailureHandler.curry(options),
      options.onSuccess
    );
  },
  
  _cache: {},
  
  _cacheDatabases: function(options) {
    Log('Database.Sql: Caching databases...');
    var queue = new Queue(2, options);
    queue.push(this._cacheDatabase.bind(this, 'Shared'));
    queue.push(this._cacheDatabase.bind(this, Session.user.email));
  },
  
  _cacheDatabase: function(database, options) {
    if (!database) return options.onSuccess();
        
    var query = '';
    Model.models.each(
      function(model) {
        if (this._ignoreModel(database, model)) return;
        if (query) query += ' UNION ALL ';
        query += 'SELECT "' + model + '" AS model, * FROM `' + model + '`';
      }.bind(this)
    );

    this._databases[database].transaction(
      function(transaction) {            
        transaction.executeSql(query, [],
          function(transaction, result) {
            var rows = result.rows;
            var row;
            for (var index = 0, length = rows.length; index < length; ++index) {
              row = rows.item(index);
              if (!this._cache[row.model]) this._cache[row.model] = {};
              this._cache[row.model][row.id] = row.representation.toObject();
            }
          }.bind(this)
        );
      }.bind(this),
      this._onFailureHandler.curry(options),
      function() {
        Log('Database.Sql: Cached database: ' + database + '.');
        options.onSuccess();
      }
    );
  },
    
  _getDatabase: function(model) {
    if (Model[model].prototype.authorization) return this._databases[Session.user.email];
    return this._databases.Shared;
  },
  
  _ignoreModel: function(database, model) {
    if (database == 'Shared') {
      if (Model[model].prototype.authorization == true) return true;
    } else {
      if (Model[model].prototype.authorization != true) return true;
    }
  },
  
  _onFailureHandler: function(options, error) {
    options.onFailure(error.message);
  },
  
  _openDatabases: function(options) {
    Log('Database.Sql: Opening databases...');
    this._databases = {};
    var queue = new Queue(2, options);
    queue.push(this._openDatabase.bind(this, 'Shared'));
    queue.push(this._openDatabase.bind(this, Session.user.email));
  },
  
  _openDatabase: function(database, options) {
    if (!database) return options.onSuccess();
    var connection = openDatabase(database, '1', database, (25).megabytes());
    connection.transaction(
      function(transaction) {
        Model.models.each(
          function(model) {
            if (this._ignoreModel(database, model)) return;
            var query = 'CREATE TABLE IF NOT EXISTS `' + model + '` ' +
              '(id INTEGER NOT NULL PRIMARY KEY, representation TEXT NOT NULL);';
            transaction.executeSql(query);
          }.bind(this)
        );
      }.bind(this),
      this._onFailureHandler.curry(options),
      function() {
        Log('Database.Sql: Opened database: ' + database + '.');
        this._databases[database] = connection;
        options.onSuccess();
      }.bind(this)
    );
  }
};

Object.extend(Number.prototype, {
  day: function() {
    return this.days();
  },
  
  days: function() {
    return this * 24 * 60 * 60 * 1000;
  },
  
  hour: function() {
    return this.hours();
  },
  
  hours: function() {
    return this * 60 * 60 * 1000;
  },
  
  isOdd: function() {
    return !!(this % 2);
  },
  
  kilobyte: function() {
    return this.kilobytes();
  },
  
  // Returns equivalent number of kilobytes expressed as bytes:
  kilobytes: function() {
    return this * 1024;
  },
  
  megabyte: function() {
    return this.megabytes();
  },
  
  // Returns equivalent number of megabytes expressed as bytes.
  megabytes: function() {
    return this * 1024 * 1024;
  },
  
  minute: function() {
    return this.minutes();
  },
  
  // Returns equivalent number of minutes expressed as milliseconds.
  minutes: function() {
    return this * 60 * 1000;
  },
  
  number: true,
  
  ordinal: function() {
    return Inflector.ordinal(this);
  },
  
  round: function(decimals) {
    decimals = decimals || 0;
    return Math.round(this * Math.pow(10, decimals)) / Math.pow(10, decimals);
  },
  
  second: function() {
    return this.seconds();
  },
  
  seconds: function() {
    return this * 1000;
  },
  
  toDateTime: function(format) {
    return Datetime.convert(this, format);
  }
});

Object.extend(ObjectRange.prototype, {
  random : function() {
    var ceiling = this.exclusive ? this.end - 1 : this.end;
    var floor = this.start - 1;
    return Math.floor((ceiling - floor) * Math.random()) + this.start;
  }
});

Object.extend(String.prototype, {
  // Converts 'mamas/papas/california-dreaming' to 'MamasPapasCaliforniaDreaming'
  // Converts 'seatingArea' to 'SeatingArea'
  constantize: function() {
    return this.underscore().gsub('/', '-').dasherize().capitalize().camelize();
  },
  
  // See comments for String.encrypt below.
  decrypt: function(key) {
    // Recreate expanded pseudo-random key using plaintext salt:
  	var keys = [];
  	var salt = this.substr(-1).charCodeAt(0);
  	for (var index = 0, length = key.length; index < length; ++index) {
  	  keys.push(salt); // Push salt before key to avoid exposing first key character.
  	  keys.push(key.charCodeAt(index));
  	}
  	
  	// Decrypt ciphertext to plaintext:
  	var plaintext = [];
  	var charCode;
  	var keysLength = keys.length;
  	for (var index = 0, length = this.length; index < length; ++index) {
  	  // Calculate plain character code and reverse mod if out of range:
  		if ((charCode = this.charCodeAt(index) - keys[index % keysLength]) < 0) {
  		  charCode = charCode + 65536;
  		}
  		// Generate plain character from character code:
  		plaintext[index] = String.fromCharCode(charCode);
  	}
  	// Remove salt:
    plaintext.splice(-1);
  
  	return plaintext.join('');
  },
  
  // Simple fast encryption based on the 'One-Time-Pad'.
  // Secure if key length >= plaintext length and key is random and used only once.
  encrypt: function(key) {
  	// Create expanded pseudo-random key using a salt:
  	// Math.random returns 0...1 (inclusive of 0, exclusive of 1).
  	// Salt must be > 0 and < 65536 to function as a valid character code.
  	// For a more secure salt use: salt = 1 + Math.round(Math.random() * 65535);
  	var keys = [];
  	var salt = 100 + Math.round(Math.random() * 100);
  	for (var index = 0, length = key.length; index < length; ++index) {
  	  keys.push(salt); // Push salt before key to avoid exposing first key character.
  	  keys.push(key.charCodeAt(index));
  	}
	
  	// Encrypt plaintext to ciphertext:
  	var ciphertext = [];
  	var charCode;
  	var keysLength = keys.length;
  	for (var index = 0, length = this.length; index < length; ++index) {
  	  // Calculate cipher character code and mod if out of range:
  		if ((charCode = this.charCodeAt(index) + keys[index % keysLength]) >= 65536) {
  		  charCode = charCode - 65536;
  		}
  		// Generate cipher character from character code:
  		ciphertext[index] = String.fromCharCode(charCode);
  	}
  	// Append salt:
  	ciphertext[index] = String.fromCharCode(salt);
	
  	return ciphertext.join('');
  },
  
  exclude: function(pattern) {
    return !this.include(pattern);
  },
  
  // Returns first value matched against a regular expression or else an empty string.
  extract: function(pattern) {
    var matches = this.match(pattern);
    // Ignore first match if substrings found:
    if (matches && matches.length > 1) matches.shift();
    return matches ? matches.first() : '';
  },
  
  getBytes: function() {
    var bytes = [];
    for (var index = 0, length = this.length; index < length; ++index) {
      bytes[index] = this.charCodeAt(index);
    }
    return bytes;
  },
  
  inflect: function() {
    return Inflector.inflect(this);
  },
    
  lowerCase: function(offset, limit) {
    if (offset >= 0) {
      if (!limit) limit = 1;
      return this.substr(0, offset) + this.substr(offset, limit).toLowerCase() + this.substr(offset + limit);
    } else {
      return this.toLowerCase();
    }
  },
  
  md5: function() {
    return Md5.hexDigest(this);
  },
  
  metaphone: function() {
    return Metaphone.get(this);
  },
  
  plural: function() {
    return Inflector.plural(this);
  },
  
  possessive: function() {
    return Inflector.possessive(this);
  },
  
  present: function() {
    return !this.blank();
  },
  
  s: function(count) {
    if (count === 1) {
      return this;
    } else {
      return this + 's';
    }
  },
  
  singular: function() {
    return Inflector.singular(this);
  },
  
  string: true,
  
  toDateTime: function(format, fixClockOffset) {
    return Datetime.convert(this, format, fixClockOffset);
  },
  
  toObject: function() {
    return JSON.parse(this);
  },
  
  integer: function() {
    return (this * 1).round();
  },

  number: function() {
    return this * 1;
  },
  
  toTitleCase: function() {
    return this.titleCase();
  },
  
  titleCase: function() {
    return this.split(' ').inject('',
      function(sentence, word, index) {
        if (index > 0) sentence += ' ';
        if (/[A-Z]\.[A-Z]\./i.test(word)) {
          return sentence + word.toUpperCase();
        } else {
          return sentence + word.upperCase(0);
        }
      }
    );
  },
  
  transliterate: function() {
    var result = this;
    var replacements = [
      [/à|ä/, 'a'], [/À|Ä/, 'A'],
      [/ç/, 'c'], [/Ç/, 'C'],
      [/è|é|ê/, 'e'], [/È|É|Ê/, 'E'],
      [/î|ï/, 'i'], [/Î|Ï/, 'I'],
      [/ô|ö/, 'o'], [/Ô|Ö/, 'O'],
      [/ü/, 'u'], [/Ü/, 'U']
    ];
    replacements.each(
      function(replacement) {
        result = result.gsub(replacement.first(), replacement.last());
      }
    );
    return result;
  },
  
  upperCase: function(offset, limit) {
    if (offset >= 0) {
      if (!limit) limit = 1;
      return this.substr(0, offset) + this.substr(offset, limit).toUpperCase() + this.substr(offset + limit);
    } else {
      return this.toUpperCase();
    }
  },
  
  // Returns uri component for string.
  // Converts to lowercase.
  // Converts accented characters to literal equivalent.
  // Strips trailing whitespace.
  uriComponent: function() {
    var result = this.strip().toLowerCase().transliterate();
    return result.gsub('-', ' ').without(/[^a-z0-9 ]/).gsub(/[ ]+/, '-');
  },
  
  without: function(pattern) {
    return this.gsub(pattern, '');
  }
});

// Do not extend Object.prototype as this prevents use of objects as
// key/value data structures. Attach static methods to Object instead.
Object.extend(Object, {
  delta: function(successor, ancestor) {
    var delta = {};
    Object.each(successor,
      function(property, value) {
        if (JSON.stringify(value) !== JSON.stringify(ancestor[property])) {
          delta[property] = value;
        }
      }
    );
    return delta;
  },
  
  each: function(object, iterator) {
    var index = 0;
    for (var key in object) iterator(key, object[key], index++);
  },
  
  isBoolean: function(object) {
    return object === true || object === false;
  },
  
  isDefined: function(object) {
    return typeof object !== 'undefined';
  },
  
  // Regards 7.0 as integer.
  isInteger: function(object) {
    return Object.isNumber(object) && object % 1 === 0;
  },
  
  isObject: function(object) {
    return object && object.constructor && object.constructor === Object;
  },
  
  copy: function(object) {
    return JSON.stringify(object || {}).toObject();
  },

  queryString: function(object) {
    var pairs = [];
    var value;
    for (var key in object) {
      value = object[key];
      if (typeof value !== 'undefined') {
        if (value === null) value = '';
        pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
      }
    }
    return pairs.sort().join('&');
  }
});

Object.extend(Array.prototype, {
  array: true,
  
  empty: function() {
    return this.length === 0;
  },
  
  exclude: function(element) {
    return !this.include(element);
  },

  // Finds array objects that match parameters.
  // Matches number values against equivalent string conditions.
  // Other types are enforced.
  // Enforces order, offset or limit parameters.
  findAllByParameters: function(parameters, union) {
    var results = [];
    var conditions = [];
    
    // Parse parameters into conditions:
    Object.keys(parameters).each(
      function(property) {
        if (property != 'offset' && property != 'limit' && property != 'order') {
          var condition = { property: property };
          var value = parameters[property];
          
          if (/^~/.test(value)) {
            condition.mode = '~';
            condition.value = value.substr(1);
            
          } else if (/^</.test(value)) {
            condition.mode = '<';
            condition.value = value.substr(1);
            
          } else if (/^>/.test(value)) {
            condition.mode = '>';
            condition.value = value.substr(1);
          
          } else if (Object.isArray(value)) {
            condition.mode = 'include';
            condition.value = value;

          } else {
            condition.mode = '==';
            condition.value = value;
          }
          
          conditions.push(condition);
        }
      }
    );

    // Cache as much as possible outside of the loop:
    var length = this.length;
    var method = union ? 'any' : 'all';
    var object;
    var push;
    
    // Loop!:
    for (var index = 0; index < length; ++index) {
      object = this[index];
      push = conditions[method](
        function(condition) {
          var value = object[condition.property];
          
          if (condition.mode == '==') {
            if (Object.isArray(value)) {
              return value.any(
                function(value) {
                  return value == condition.value;
                }
              );
            } else {
              // 0 == '0' => true
              // 1 == '1' => true
              // true == 'true' => false
              // undefined == '0' => false
              // null == '0' => false
              return value == condition.value;
            }
            
          } else if (condition.mode == '~') {
            return Object.isString(value) && value.include(condition.value);
            
          } else if (condition.mode == '<') {
            return value < condition.value;
            
          } else if (condition.mode == '>') {
            return value > condition.value;
            
          } else if (condition.mode == 'include') {
            if (Object.isArray(value)) {
              return value.any(
                function(value) {
                  return condition.value.include(value);
                }
              );
            } else {
              // On the subject of type coercion:
              // ['1', '2'].include(1) returns true.
              // ['false'].include(false) returns false.
              // [false].include(false) returns true.
              return condition.value.include(value);
            }
          }
        }
      );
      if (push) results.push(object);
    }
    
    if (parameters.order) results.order(parameters.order);
    if (parameters.offset) results = results.offset(parameters.offset);
    if (parameters.limit) results = results.limit(parameters.limit);
    
    return results;
  },
  
  // Indexes array objects by property key.
  // Maps a collection against each index key unless unique is passed.
  index: function(key, unique) {
    var result = {}, object, indexKey;
    for (var index = 0, length = this.length; index < length; ++index) {
      object = this[index];
      indexKey = object[key];
      
      if (unique) {
        result[indexKey] = object;
      } else {
        if (!result[indexKey]) result[indexKey] = [];
        result[indexKey].push(object);
      }
    }
    return result;
  },

  // Order array objects by property values.
  // Order keys can be passed in as multiple arguments or as an array.
  // Sorts in place (i.e. by reference) and returns array.
  // null, undefined, 0 are == '' not < '' and have no priority or lack thereof against strings.
  order: function() {
    var predicates = $A(arguments).flatten().collect(
      function(property) {
        return {
          property: property.without(/^-/, ''),
          sign: property.startsWith('-') ? -1 : 1
        };
      }
    );
    
    return this.sort(
      function(a, b) {
        var swap = 0;
        var predicate;
        var property;
        var sign;
        var x;
        var y;
        for (var index = 0; index < predicates.length; ++index) {
          predicate = predicates[index];
          property = predicate.property;
          sign = predicate.sign;
          x = a[property];
          y = b[property];
          if (swap === 0 && x < y) {
            swap = -1 * sign;
          } else if (swap === 0 && x > y) {
            swap = 1 * sign;
          }
        }
        return swap;
      }
    );
  },
  
  post: function(element) {
    this.push(element);
    return this;
  },
  
  present: function() {
    return this.length > 0;
  },
  
  put: function(element) {
    if (this.exclude(element)) this.push(element);
    return this;
  },
  
  random: function() {
    if (this.length > 0) {
      return this[$R(1, this.length).random() - 1];
    } else {
      return undefined;
    }
  },

  limit: function(limit) {
    return this.slice(0, limit);
  },
  
  offset: function(offset) {
    return this.slice(offset);
  },
  
  sentence: function() {
    if (this.length === 0) {
      return '';
    } else if (this.length === 1) {
      return this[0].toString();
    } else {
      return this.slice(0, -1).join(', ') + ' and ' + this[this.length - 1];
    }
  },
  
  sortNumeric: function() {
    return this.sortBy(
      function(element) {
        return element * 1;
      }
    );
  },
  
  complement: function(b) {
    var array = [];
    var push;
    for (var index = 0, length = this.length; index < length; index++ ) {
      push = true;
      for (var bindex = 0, blength = b.length; bindex < blength; bindex++) {
        if (b[bindex] === this[index]) push = false;
      }
      if (push) array.push(this[index]);
    }
    return array;
  },

  toString: function() {
    return JSON.stringify(this);
  },

  unique: function() {
    var array = [];
    
    // Use separate indexes to avoid toString() collisions:
    var pushed = { string: {}, other: {} };
    var value, type, token;
    
    for (var index = 0, length = this.length; index < length; ++index) {
      value = this[index];
      token = value;
      
      if (Object.isString(value)) {
        type = 'string';
      } else if (Object.isNumber(value) || Object.isUndefined(value)) {
        type = 'other';
      } else if (value === true || value === false || value === null) {
        type = 'other';
      } else if (Object.isObject(value) || Object.isArray(value)) {
        type = 'other';
        token = JSON.stringify(value);
      } else {
        // Ignore function, instance, element, etc. duplicates:
        type = 'special';
      }
      
      if (!pushed[type]) {
        array.push(value);        
      } else if (!pushed[type][token]) {
        array.push(value);
        pushed[type][token] = true;
      }
    }

    return array;
  }
});

Array.prototype.find = Array.prototype.find.wrap(
  function(find, iterator, context) {
    if (Object.isFunction(iterator)) {
      return find(iterator, context); // Proceed using the original function.
    } else {
      var parameters = iterator || {};
      if (Object.isInteger(parameters) || Object.isString(parameters)) {
        parameters = { id: parameters * 1 };
      } else if (Object.isArray(parameters)) {
        parameters = { id: parameters };
      }
      parameters.limit = 1;
      var union = context;
      return this.findAllByParameters(parameters, union).first();
    }
  }
);

Array.prototype.findAll = Array.prototype.findAll.wrap(
  function(findAll, iterator, context) {
    if (Object.isFunction(iterator)) {
      return findAll(iterator, context); // Proceed using the original function.
    } else {
      var parameters = iterator || {};
      if (Object.isInteger(parameters) || Object.isString(parameters)) {
        parameters = { id: parameters * 1 };
      } else if (Object.isArray(parameters)) {
        parameters = { id: parameters };
      }
      var union = context;
      return this.findAllByParameters(parameters, union);
    }
  }
);

// Replace Prototype's slow uniq method with faster unique method:
Array.prototype.uniq = Array.prototype.unique;

Element.events = {};
Element.addMethods({
  fadeIn: function(element) {
    element.show();
    var opacity = 0;
    var original = element.getOpacity() || 1;
    
    var setOpacity = function() {
      element.setOpacity(opacity);
      opacity = opacity + 0.01;
      if (opacity <= original) setOpacity.delay(0.02);
    };
    
    setOpacity();
  },
  
  fadeOut: function(element) {
    element.show();
    var opacity = element.getOpacity() || 1;
    
    var setOpacity = function() {
      element.setOpacity(opacity);
      opacity = opacity - 0.01;
      if (opacity >= 0) {
        setOpacity.delay(0.02);
      } else {
        element.hide();
      }
    };
    
    setOpacity();
  },
  
  increment: function(element, label) {
    element = $(element);
    var quantity = element.innerHTML.without(/[^0-9]/) * 1 + 1;
    if (element.innerHTML.startsWith(label)) {
      element.update(label.s(quantity) + ' ' + quantity);
    } else {
  		element.update(quantity + ' ' + label.s(quantity));
  	}
		return element;
  },
  
  observe: function(element, eventName, handler) {
    Element.events[element.identify()] = true;
    return Event.observe(element, eventName, handler);
  }
});

var processJSON = function(json) {
  // Remove leading '[' and trailing ']':
  json = json.substr(1, json.length - 2);
  // Split string into collection:
  var collection = json.split(',{"');
  // Correct individual objects:
  var index = collection.length;
  while(--index >= 0) {
    // Add leading '{"':
    collection[index] = '{"' + collection[index];
  };
};

/*
Initialize:
  new Open(listingId, {
    hideNotices : function() {}, // Required
    onFailure : function(error) {}, // Required
    showNotices : function(notices) {}, // Required
    id : reservationId, // Optional
    mode : 'External' // Optional. Or 'Internal'. External by default.
  });
Methods:
  open.constrain({
    month : month,
    date : date,
    time : time,
    people : people,
    seatingArea : seatingAreaId
  });

  open.months();
  open.dates();
  open.times();
  open.people();
  open.seatingAreas();
  open.tables();
*/
var Open = Class.create({
  // Returns hash of maximum group size that can arrive for a given time.
  // Where time not present in the hash, any group size up to default arrival rate
  // can be accommodated.
  // { time : people }
  arrivalRates : function(date) {
    if (!this.cache.arrivalRates[date]) {
      var ids = [];
      // Exclude this.reservation from calculations:
      if (this.reservation) ids.push(this.reservation.id);
      this.cache.arrivalRates[date] = this.reservationsTimes(date).inject({},
        function(arrivalRates, reservation) {
          if (!ids.include(reservation.id)) {
            ids.push(reservation.id);
            // Use reservation.arrivalTime as reservation.time is mapped out
            // by turn-time at this stage:
            var time = reservation.arrivalTime;
            if (!arrivalRates[time]) arrivalRates[time] = this.resources.listing.arrivalRate;
            arrivalRates[time] -= reservation.people;
          }
          return arrivalRates;
        }.bind(this)
      );
      if (this.reservation) {
        // Ensure this.reservation can still arrive:
        if (this.cache.arrivalRates[date][this.reservation.time] <= 0) {
          this.cache.arrivalRates[date][this.reservation.time] = this.reservation.people;
        }
      }
    }
    return this.cache.arrivalRates[date];
  },
  
  // Constrains times, people, seatingAreas, tables methods by time, people, seatingArea criteria.
  constrain : function(constraints) {
    this.constraints = constraints;
  },
  
  // Provides date options for a given month, e.g. '2020-12'.
  dates : function() {
    this.validateConstraints(0);
    return this.dateOptions();
  },
  
  // Used by dates() and validateConstraints()
  dateOptions : function() {
    var options = Datetime.dates({
      month : this.constraints.month,
      days : Object.keys(this.setup.days),
      options : true
    });
    options = options.findAll(
      function(option) {
        var date = option.key;
        var day = option.value.split(' ').last();
        return this.dateTimes(date, day).present() && !this.falsePositives.dates.include(date);
      }.bind(this)
    );
    if (this.reservation && this.reservation.date.toMonth() == this.constraints.month) {
      options = Options.extend(options, {
        key : this.reservation.date,
        value : this.reservation.date.toDateTime('D DAY')
      });
    }
    var selectedKey = options.inject(null,
      function(selectedKey, option, index) {
        if (option.key == this.constraints.date) selectedKey = option.key;
        if (!selectedKey && option.key > this.constraints.date) selectedKey = option.key;
        return selectedKey;
      }.bind(this)
    );
    if (!selectedKey) selectedKey = (options.first() || {}).key;
    // Ensures this.constraints.date is valid (exists in options):
    this.constraints.date = selectedKey;
    return {
      options : options,
      value : selectedKey
    };
  },
  
  dateTimes : function(date, day) {
    if (!this.cache.dateTimes[date]) {
      this.cache.dateTimes[date] = [];
      day = day || date.toDateTime('DAY');
      var open = (this.setup.days[day] || []).concat(this.setup.specificDates.open[date] || []);
      var closed = this.setup.specificDates.closed[date] || [];
      var earliestDateTime = this.earliestDateTime();
      for (var index = 0; index < 24; index = index + 0.5) {
        var time = index.toTime();
        var timeOpen = open.any(function(range) { return range.include(time); });
        var timeClosed = closed.any(function(range) { return range.include(time); });
        var enoughNotice = (date + 'T' + time + 'Z') > earliestDateTime;
        if (timeOpen && !timeClosed && enoughNotice) this.cache.dateTimes[date].push(time);
      }
    }
    return this.cache.dateTimes[date];
  },
  
  // Returns earliest dateTime for which a restaurant can receive reservations.
  earliestDateTime : function() {
    if (!this.cache.earliestDateTime) {
      if (this.options.mode == 'Internal') {
        this.cache.earliestDateTime = '0000-00-00 00:00:00';
      } else {
        var timestamp = 'UTC'.toDateTime('T');
        // Adjust for restaurant location:
        timestamp += this.resources.listing.utcOffset * 3600;
        // Fast forward:
        timestamp += this.resources.listing.noticePeriod * 3600;
        this.cache.earliestDateTime = timestamp.toDateTime();
      }
    }
    return this.cache.earliestDateTime;
  },
  
  hasGeneralAllocation : function(reservation) {
    return reservation.seatingAreaIds.length == 1 && reservation.tableIds.empty();
  },
  
  initialize : function(listingId, options) {
    this.listingId = listingId * 1;
    this.options = options;
    if (this.options.id) this.options.id = this.options.id * 1;
    this.cache = {
      arrivalRates : {},
      dateTimes : {},
      earliestDateTime : null,
      reservationsTimes : {},
      reservedSeatingAreasTimes : {},
      reservedTablesTimes : {},
      seatingAreasTimes : {},
      tablesTimes : {},
      validConstraints : []
    };
    if (!this.options.mode || this.options.mode != 'Internal') {
      this.options.mode = 'External';
    }
    this.setup = {};

    this.resources = {};
    this.resources.hours = Model.Hour.get({ listingId: this.listingId });
    this.resources.notices = Model.Notice.get({ listingId: this.listingId });
    this.resources.reservations = Model.Reservation.get({ listingId: this.listingId });
    this.resources.seatingAreas = Model.SeatingArea.get({ listingId: this.listingId });
    this.resources.specificDates = Model.SpecificDate.get({ listingId: this.listingId });
    this.resources.tables = Model.Table.get({ listingId: this.listingId });
    this.resources.listing = Model.Listing.get(this.listingId);
    
    // Triggers this.options.onInvalidId if id not found.
    // Needs to run before this.setupSeatingAreas().
    if (this.options.id) this.setupReservation(); // May set this.failed
    this.setupDays(); // May set this.failed
    this.setupSeatingAreas(); // May set this.failed
    this.setupTables();
    this.setupSpecificDates();
    this.setupConstraints();
    this.setupFalsePositives();
  },
  
  // From one year before user's current system time.
  // Or from one year before listing created if reservation present.
  // Until user's current system time plus two years.
  months : function() {
    this.validateConstraints(0);
    return this.monthOptions();
  },
  
  // Used by months() and validateConstraints()
  monthOptions : function() {
    var today = 'now'.toDateTime('Y-M');
    
    var from = Datetime.calculate(this.reservation ? this.resources.listing.created.toMonth() : today, '-1 Year');
    var earliestMonth = this.earliestDateTime().toMonth();
    if (from < earliestMonth) from = earliestMonth;
    
    var until = Datetime.calculate(today, '+2 Years');
    if (until < earliestMonth) until = Datetime.calculate(earliestMonth, '+2 Years');
    
    var options = Datetime.months({from : from, until : until, options : true});
    options = options.findAll(
      function(option) {
        return !this.falsePositives.months.include(option.key);
      }.bind(this)
    );
    if (this.reservation) {
      options = Options.extend(options, {
        key : this.reservation.date.toMonth(),
        value : this.reservation.date.toDateTime('Y MONTH')
      });
    }
    var selectedKey = options.inject(null,
      function(selectedKey, option, index) {
        if (option.key == this.constraints.month) selectedKey = option.key;
        if (!selectedKey && option.key > this.constraints.month) selectedKey = option.key;
        return selectedKey;
      }.bind(this)
    );
    if (!selectedKey) selectedKey = (options.first() || {}).key;
    // Ensure that this.constraints.month is valid (exists in options).
    this.constraints.month = selectedKey;
    return {
      options : options,
      value : selectedKey
    };
  },
  
  people : function() {
    this.validateConstraints(0);
    var capacities = this.seatingAreasTimes(this.constraints.date).inject([],
      function(capacities, seatingAreaTime) {
        if (this.pushSeatingAreaTime(seatingAreaTime, 'people')) {
          capacities.push(seatingAreaTime.capacity);
        }
        return capacities;
      }.bind(this)
    );
    var options = [{key : '', value : 'People'}];
    var max = capacities.max() || 0;
    if (this.constraints.time) {
      var arrivalRate = this.arrivalRates(this.constraints.date)[this.constraints.time];
      if (Object.isUndefined(arrivalRate)) arrivalRate = this.resources.listing.arrivalRate;
    } else {
      var arrivalRate = this.resources.listing.arrivalRate;
    }
    if (this.options.mode == 'External') {
      if (max > arrivalRate) max = arrivalRate;
    }
    max.times(
      function(index) {
        var people = (index + 1).toPaddedString(2);
        var qualifier = people == 1 ? ' Person' : ' People';
        options.push({
          key : people * 1,
          value : people + qualifier
        });
      }
    );
    return {
      options : options,
      value : this.constraints.people
    };
  },
  
  pushSeatingAreaTime : function(seatingAreaTime, exception) {
    var conditions = {
      time : this.constraints.time ? seatingAreaTime.time == this.constraints.time : true,
      seatingArea : this.constraints.seatingArea ? seatingAreaTime.id == this.constraints.seatingArea : true
    };
    if (this.constraints.people) {
      var arrivalRate = this.arrivalRates(this.constraints.date)[seatingAreaTime.time];
      if (Object.isUndefined(arrivalRate)) arrivalRate = this.resources.listing.arrivalRate;
      conditions.people = seatingAreaTime.capacity >= this.constraints.people;
      if (this.options.mode == 'External') {
        conditions.people = conditions.people && arrivalRate >= this.constraints.people;
      }
    } else {
      conditions.people = true;
    }
    return Object.keys(conditions).all(
      function(key) {
        return key != exception ? conditions[key] : true;
      }
    );
  },
  
  // Returns relevant reservations expanded by turn time
  // [{ id, time, people, seatingAreaId, tableIds, arrivalTime }]
  reservationsTimes : function(date) {
    if (!this.cache.reservationsTimes[date]) {
      this.cache.reservationsTimes[date] = [];
      var boundary = (this.resources.listing.turnTime.toHours() * 2 - 1) / 2;
      this.resources.reservations.each(
        function(reservation) {
          if (reservation.date != date) return;
          if (this.reservation && this.reservation.id == reservation.id) return;
          if (!reservation.visible) return;
          if (!['Accepted', 'Confirmed', 'Seated'].include(reservation.category)) return;
          var index = reservation.time.toHours() - boundary;
          var limit = index + (boundary * 2);
          for (index; index <= limit; index = index + 0.5) {
            if (index < 24) { // Ignore times extending into next day
              this.cache.reservationsTimes[date].push({
                id : reservation.id,
                time : index.toTime(),
                people : reservation.people,
                seatingAreaIds : reservation.seatingAreaIds,
                tableIds : reservation.tableIds,
                arrivalTime : reservation.time
              });
            }
          }
        }.bind(this)
      );
    }
    return this.cache.reservationsTimes[date];
  },
  
  // [{ time, capacity, tableId }] Used by seatingAreasTimes
  reservedCapacitiesTimes : function(date, seatingAreaId) {
    return this.reservedTablesTimes(date).inject([],
      function(map, reservedTablesTime) {
        reservedTablesTime.tableIds.each(
          function(tableId) {
            var table = this.setup.table[tableId];
            if (table && table.seatingAreaId == seatingAreaId) {
              map.push({
                time : reservedTablesTime.time,
                capacity : table.capacity,
                id : table.id
              });
            }
          }.bind(this)
        );
        return map;
      }.bind(this)
    );
  },
  
  // [{ time, people }] Used by seatingAreasTimes
  reservedPeopleTimes : function(date, seatingAreaId) {
    return this.reservedSeatingAreasTimes(date).inject([],
      function(map, reservedSeatingAreaTime) {
        // reservedSeatingAreasTimes ensures that seatingAreaIds length == 1
        if (reservedSeatingAreaTime.seatingAreaIds.include(seatingAreaId)) {
          map.push({
            time : reservedSeatingAreaTime.time,
            people : reservedSeatingAreaTime.people
          });
        }
        return map;
      }
    );
  },
  
  reservedSeatingAreasTimes : function(date) {
    if (!this.cache.reservedSeatingAreasTimes[date]) {
      var reservationsTimes = this.reservationsTimes(date).partition(this.hasGeneralAllocation);
      this.cache.reservedSeatingAreasTimes[date] = reservationsTimes.first();
      this.cache.reservedTablesTimes[date] = reservationsTimes.last();
    }
    return this.cache.reservedSeatingAreasTimes[date];
  },
  
  reservedTablesTimes : function(date) {
    if (!this.cache.reservedTablesTimes[date]) {
      var reservationsTimes = this.reservationsTimes(date).partition(this.hasGeneralAllocation);
      this.cache.reservedSeatingAreasTimes[date] = reservationsTimes.first();
      this.cache.reservedTablesTimes[date] = reservationsTimes.last();
    }
    return this.cache.reservedTablesTimes[date];
  },
  
  seatingAreas : function() {
    this.validateConstraints(0);
    var seatingAreas = this.seatingAreasTimes(this.constraints.date).inject([],
      function(seatingAreas, seatingAreaTime) {
        if (this.pushSeatingAreaTime(seatingAreaTime, 'seatingArea')) {
          seatingAreas.push(seatingAreaTime.id);
        }
        return seatingAreas;
      }.bind(this)
    );
    var options = [], multiple = false;
    seatingAreas.unique().each(
      function(id) {
        // Add 'Multiple Areas' last to ensure better order
        if (id == 'Multiple') return multiple = true;
        options.push({
          key : id,
          value : this.setup.seatingArea[id].name
        });
      }.bind(this)
    );
    options = options.sortBy(
      function(option) {
        return option.value;
      }
    );
    options.unshift({key : '', value : 'Seating Area'});
    if (this.options.mode == 'Internal' && multiple) {
      options.push({ key : 'Multiple', value : 'Multiple Areas' });
    }
    return {
      options : options,
      value : this.constraints.seatingArea
    };
  },
  
  // [{ time, id, capacity, tableIds }]
  seatingAreasTimes : function(date) {
    if (!this.cache.seatingAreasTimes[date]) {
      this.cache.seatingAreasTimes[date] = [];
      var times = this.dateTimes(date);
      var arrivalRates = this.arrivalRates(date);
      this.setup.seatingAreas.each(
        function(seatingArea) {
          var reservedPeopleTimes = this.reservedPeopleTimes(date, seatingArea.id);
          var reservedCapacitiesTimes = this.reservedCapacitiesTimes(date, seatingArea.id);
          var tableIds = this.setup.tables.inject([],
            function(tableIds, table) {
              if (table.seatingAreaId == seatingArea.id) tableIds.push(table.id);
              return tableIds;
            }
          );
          times.each(
            function(time) {
              if (this.options.mode == 'External') {
                // Ignore time if no groups can arrive for it:
                if (arrivalRates[time] <= 0) return;
              }
              var capacity = seatingArea.capacity;
              reservedPeopleTimes.each(
                function(reservedPeopleTime) {
                  if (reservedPeopleTime.time != time) return;
                  capacity = capacity - reservedPeopleTime.people;
                }
              );
              reservedCapacitiesTimes.each(
                function(reservedCapacitiesTime) {
                  if (reservedCapacitiesTime.time != time) return;
                  tableIds = tableIds.without(reservedCapacitiesTime.id);
                  capacity = capacity - reservedCapacitiesTime.capacity;
                }
              );
              if (capacity > 0) {
                this.cache.seatingAreasTimes[date].push({
                  time : time,
                  id : seatingArea.id,
                  capacity : capacity,
                  tableIds : tableIds
                });
              }
            }.bind(this)
          );
        }.bind(this)
      );
      if (this.reservation && this.reservation.date == date) {
        if (this.hasGeneralAllocation(this.reservation)) {
          this.cache.seatingAreasTimes[date].push({
            time : this.reservation.time,
            id : this.reservation.seatingAreaIds.first(), // Reservation has only one seating area.
            capacity : this.reservation.people,
            tableIds : [] // Reservation has no tables.
          });
        } else {
          // what if only one seating area with tables?
          // what if more than one seating area with tables?
          this.reservation.seatingAreaIds.each(
            function(seatingAreaId) {
              var capacity = 0;
              var tableIds = this.reservation.tableIds.inject([],
                function(tableIds, tableId) {
                  var table = this.setup.table[tableId];
                  if (table && table.seatingAreaId == seatingAreaId) {
                    capacity += table.capacity;
                    tableIds.push(table.id);
                  }
                  return tableIds;
                }.bind(this)
              );
              this.cache.seatingAreasTimes[date].push({
                time : this.reservation.time,
                id : seatingAreaId,
                capacity : [this.reservation.people, capacity].max(),
                tableIds : tableIds
              });
            }.bind(this)
          );
        }
      }
      if (this.options.mode == 'Internal') {
        var multipleTimes = this.cache.seatingAreasTimes[date].inject({},
          function(times, seatingAreaTime) {
            var time = seatingAreaTime.time, id = seatingAreaTime.id;
            if (!times[time]) times[time] = {};
            if (!times[time][id]) {
              times[time][id] = {
                capacity : seatingAreaTime.capacity,
                tableIds : seatingAreaTime.tableIds
              };
            } else {
              times[time][id] = {
                capacity : [times[time][id].capacity, seatingAreaTime.capacity].max(),
                tableIds : times[time][id].tableIds.concat(seatingAreaTime.tableIds)
              };
            }
            return times;
          }
        );
        Object.keys(multipleTimes).each(
          function(time) {
            var capacity = 0, tableIds = [];
            Object.values(multipleTimes[time]).each(
              function(seatingArea) {
                capacity += seatingArea.capacity;
                tableIds = tableIds.concat(seatingArea.tableIds);
              }
            );
            this.cache.seatingAreasTimes[date].push({
              time : time,
              id : 'Multiple',
              capacity : capacity,
              tableIds : tableIds // Not necessarily unique.
            });
          }.bind(this)
        );
      }
    }
    return this.cache.seatingAreasTimes[date];
  },
  
  setupFalsePositives : function() {
    // Populated by validateConstraints method. Used by months and dates method.
    this.falsePositives = { months : [], dates : [] };
  },
  
  setupConstraints : function() {
    var date = this.reservation ? this.reservation.date : 'now'.toDateTime('Y-M-D');
    if (this.reservation) {
      if (this.reservation.seatingAreaIds.length == 1) {
        var seatingArea = this.reservation.seatingAreaIds.first();
      } else {
        var seatingArea = 'Multiple';
      }
    } else {
      var seatingArea = null;
      var controller = Controller.DashboardReservations;
      if (controller && controller.date) date = controller.date;
    }
    this.constraints = {
      month : date.toMonth(),
      date : date,
      time : this.reservation ? this.reservation.time : null,
      people : this.reservation ? this.reservation.people : null,
      seatingArea : seatingArea
    };
  },
  
  setupDays : function() {
    this.setup.days = this.resources.hours.inject({},
      function(days, hour) {
        if (!days[hour.day]) days[hour.day] = [];
        days[hour.day].push($R(hour.from, hour.until)); // $R() is inclusive by default
        return days;
      }
    );
    if (this.resources.hours.empty() && !this.failed) {
      this.options.onFailure('No further reservations can be accepted at present.');
      this.failed = true;
    }
  },
  
  setupReservation : function() {
    var reservation = this.resources.reservations.find(this.options.id);
    if (reservation) {
      this.reservation = reservation;
    } else if (!this.failed) {
      this.options.onFailure('Reservation ID: ' + this.options.id + ' is not valid.');
      this.options.id = undefined;
      this.failed = true;
    }
  },
  
  setupSeatingAreas : function() {
    this.setup.seatingArea = {};
    this.setup.seatingAreas = [];
    this.resources.seatingAreas.each(
      function(seatingArea) {
        this.setup.seatingArea[seatingArea.id] = seatingArea;
        if (seatingArea.visible) this.setup.seatingAreas.push(seatingArea);
      }.bind(this)
    );
    if (this.setup.seatingAreas.empty() && !this.failed) {
      this.options.onFailure('No further reservations can be accepted at present.');
      this.failed = true;
    }
  },
  
  setupSpecificDates : function() {
    this.setup.specificDates = this.resources.specificDates.inject({ open : {}, closed : {} },
      function(dates, object) {
        var category = object.category == 'Open' ? 'open' : 'closed';
        var date = object.date;
        if (!dates[category][date]) dates[category][date] = [];
        dates[category][date].push($R(object.from, object.until));
        return dates;
      }
    );
  },
  
  setupTables : function() {
    this.setup.table = {};
    this.setup.tables = [];
    this.resources.tables.each(
      function(table) {
        this.setup.table[table.id] = table;
        if (table.visible) this.setup.tables.push(table);
      }.bind(this)
    );
  },
  
  // Provides this.options.showNotices with an array of relevant notices.
  // Otherwise calls this.options.hideNotices.
  showRelevantNotices : function() {
    var context = {
      date : this.constraints.date,
      day : this.constraints.date.toDateTime('DAY'),
      time : this.constraints.time,
      people : this.constraints.people * 1,
      seatingArea : this.constraints.seatingArea
    };
    var notices = this.resources.notices.findAll(
      function(notice) {
        if (this.options.mode == 'Internal' && notice.audience == 'Customers') return false;
        if (this.options.mode == 'External' && notice.audience == 'Managers') return false;
        return notice.conditions.split(',').all(
          function(condition) {
            var matches = condition.match(/([a-z]+)(\<|=|\>)(.+)/i);
            var key = matches[1], sign = matches[2], value = matches[3];
            if (key == 'people') value = value * 1;
            if (sign == '<') {
              return context[key] < value;
            } else if (sign == '=') {
              return context[key] == value;
            } else if (sign == '>') {
              return context[key] >= value;
            } else {
              return false;
            }
          }
        );
      }.bind(this)
    );
    notices = notices.collect(
      function(notice) {
        return notice.body.strip();
      }
    );
    if (notices.present()) {
      this.options.showNotices(notices);
    } else {
      this.options.hideNotices();
    }
  },
  
  tables : function() {
    this.validateConstraints(0);
    var tableIds = this.seatingAreasTimes(this.constraints.date).inject([],
      function(tableIds, seatingAreaTime) {
        if (this.pushSeatingAreaTime(seatingAreaTime, 'table')) {
          seatingAreaTime.tableIds.each(
            function(tableId) {
              if (!tableIds.include(tableId)) tableIds.push(tableId);
            }
          );
        }
        return tableIds;
      }.bind(this)
    );
    var options = [];
    tableIds.each(
      function(tableId) {
        options.push({
          key : tableId,
          value : this.setup.table[tableId].name
        });
      }.bind(this)
    );
    options = options.sortBy(
      function(option) {
        return option.value;
      }
    );
    options.unshift({key : '', value : 'Table'});
    return { options : options };
  },
  
  times : function() {
    this.validateConstraints(0);
    var times = this.seatingAreasTimes(this.constraints.date).inject([],
      function(times, seatingAreaTime) {
        if (this.pushSeatingAreaTime(seatingAreaTime, 'time')) {
          times.push(seatingAreaTime.time);
        }
        return times;
      }.bind(this)
    );
    var options = [{key : '', value : 'Time'}];
    times.unique().sort().each(
      function(time) {
        options.push({
          key : time,
          value : time.substr(0, 5)
        });
      }
    );
    return {
      options : options,
      value : this.constraints.time
    };
  },
  
  validateConstraints : function(stack) {
    if (!this.cache.validConstraints.include(JSON.stringify(this.constraints))) {
      var times = false, people = false, seatingAreas = false;
      var valid = this.seatingAreasTimes(this.constraints.date).any(
        function(seatingAreaTime) {
          if (!times && this.pushSeatingAreaTime(seatingAreaTime, 'time')) times = true;
          if (!people && this.pushSeatingAreaTime(seatingAreaTime, 'people')) people = true;
          if (!seatingAreas && this.pushSeatingAreaTime(seatingAreaTime, 'seatingArea')) seatingAreas = true;
          return times && people && seatingAreas;
        }.bind(this)
      );
      if (!valid) {
        if (!this.constraints.time && !this.constraints.people && !this.constraints.seatingArea) {
          this.falsePositives.dates.push(this.constraints.date);
          if (this.dateOptions().options.empty()) { // Corrects this.constraints.date
            this.falsePositives.months.push(this.constraints.month);
            // Correct this.constraints.month and set date since
            // dateOptions() may not be reliable in this case:
            this.constraints.date = this.monthOptions().options.first().key + '-01';
          }
        } else {
          this.constraints.time = null;
          this.constraints.people = null;
          this.constraints.seatingArea = null;
        }
        // Validate constraints again to see if everything is fine after these changes.
        // Avoid too much recursion.
        if (stack <= 365) {
          return this.validateConstraints(stack + 1);
        } else {
          var error = 'Your restaurant cannot receive further reservations at present.';
          this.options.onFailure(error);
        }
      } else {
        // Garbage collect cache.validConstraints
        if (this.cache.validConstraints.length > 50) this.cache.validConstraints = [];
        this.cache.validConstraints.push(JSON.stringify(this.constraints));
      }
    }
    // Now that this.constraints is correctly set, show relevant notices:
    this.showRelevantNotices();
  }
});

Model.EmailAccount = Class.create(Model.Base, {
  schema: {
    userId: { type: 'integer' },    
    listingId: { type: 'integer' },
    email: { type: 'string' },
    host: { type: 'string' },
    port: { type: 'integer', min: 1 },
    ssl: { type: 'boolean', value: false },
    username: { type: 'string' },
    password: { type: 'string' },
    category: { type: 'string', category: ['IMAP'] },
    folder: { type: 'string' },
    marker: { type: ['null', 'string'] },
    signature: { type: ['null', 'string'] }
  },
  
  permissions: {
    user: 'unset'
  },
  
  authorization: true,
  
  indexes: ['userId']
});

Model.Table = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    seatingAreaId: { type: 'integer', createOnly: false },
    name: { type: 'string' },
    capacity: { type: 'integer', min: 1 },
    visible: { type: 'boolean', value: true }
  },
  
  permissions: {
    listing: 'set'
  }
});

Model.SeatingArea = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    name: { type: 'string' },
    capacity: { type: 'integer', min: 1 },
    description: { type: ['null', 'string'] },
    visible: { type: 'boolean', value: true }
  },
  
  permissions: {
    listing: 'set'
  }
});

Model.Area = Class.create(Model.Base, {
  schema: {
    cityId: { type: 'integer' },
    name: { type: 'string', createOnly: true },
    uri: { type: 'string', createOnly: true, format: 'uri' }
  },
  
  beforeValidate: function() {
    this.uri = '/#';
  },
  
  beforeSet: function() {
    var city = Model.City.get(this.cityId);
    var state = Model.State.get(city.stateId);
    var country = Model.Country.get(state.countryId);
    var components = [ country.name, state.name, city.name, this.name ];
    this.uri = '/#' + components.invoke('uriComponent').join('/');
  }
});

Model.State = Class.create(Model.Base, {
  schema: {
    countryId: { type: 'integer' },
    name: { type: 'string', createOnly: true },
    uri: { type: 'string', createOnly: true, format: 'uri' }
  },
  
  beforeValidate: function() {
    this.uri = '/#';
  },
  
  beforeSet: function() {
    var country = Model.Country.get(this.countryId);
    var components = [ country.name, this.name ];
    this.uri = '/#' + components.invoke('uriComponent').join('/');
  }
});

Model.Post = Class.create(Model.Base, {
  schema: {
    userId: { type: 'integer' },
    userName: { type: 'proxy' },
    title: { type: 'string' },
    body: { type: 'string' }
  },
  
  permissions: {
    user: 'unset'
  }
});

Model.Currency = Class.create(Model.Base, {
  schema: {
    name: { type: 'string' },
    symbol: { type: 'string' },
    exchange: { type: 'number' }
  }
});

Model.Hour = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    day: { type: 'string', category: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] },
    from: { type: 'string', format: 'time' },
    until: { type: 'string', format: 'time' }
  },
  
  permissions: {
    listing: 'unset'
  },
  
  ensurePositiveRange: function() {
    if (this.until < this.from) {
      throw '422 Please provide a positive time range.';
    }
  }
});

Model.Contact = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    userId: { type: ['null', 'integer'] },
    developerId: { type: ['null', 'integer'] },
    name: { type: 'string', title: 'contact name' },
    title: { type: ['null', 'string'] },
    company: { type: ['null', 'string'] },
    description: { type: ['null', 'string'] },
    visible: { type: 'boolean', value: true }
  },
  
  permissions: {
    developer: 'get',
    listing: 'set',
    user: 'set'
  },
  
  authorization: true,
  
  indexes: ['developerId', 'listingId', 'userId'],
  
  beforeSet: function() {
    this.name = this.name.titleCase();
    if (this.title) this.title = this.title.titleCase();
    if (this.company) this.company = this.company.titleCase();
  }
});

Model.ContactNumber = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    contactId: { type: 'integer' },
    userId: { type: ['null', 'integer'] },
    developerId: { type: ['null', 'integer'] },
    number: { type: 'string' },
    category: { type: 'string', category: ['Work', 'Home', 'Mobile', 'Pager', 'Fax'] }
  },
  
  permissions: {
    developer: 'get',
    listing: 'unset',
    user: 'set'
  },
  
  authorization: true,
  
  indexes: ['developerId', 'listingId', 'userId']
});

Model.Note = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    userId: { type: 'integer' },
    userName: { type: 'proxy' },
    developerId: { type: ['null', 'integer'] },
    parentId: { type: 'integer' },
    parentType: { type: 'string', category: ['Contact', 'Listing', 'Reservation'] },
    body: { type: 'string', title: 'note' },
    subject: { type: ['null', 'string'] }
  },
  
  permissions: {
    developer: 'get',
    listing: 'unset',
    user: 'set'
  },
  
  authorization: true,
  
  indexes: ['developerId', 'listingId', 'userId']
});

Model.ContactAddress = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    contactId: { type: 'integer' },
    userId: { type: ['null', 'integer'] },
    developerId: { type: ['null', 'integer'] },
    street: { type: ['null', 'string'] },
    city: { type: ['null', 'string'] },
    state: { type: ['null', 'string'] },
    zip: { type: ['null', 'string'] },
    country: { type: ['null', 'string'] },
    category: { type: 'string', category: ['Work', 'Home'] }
  },

  permissions: {
    developer: 'get',
    listing: 'unset',
    user: 'set'
  },
  
  authorization: true,
  
  indexes: ['developerId', 'listingId', 'userId'],
    
  ensureAddressPresent: function() {
    if (!this.street && !this.city && !this.state && !this.zip && !this.country) {
      throw '422 Please provide an address for your contact.';
    }
  }
});

Model.SpecificDate = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    date: { type: 'string', format: 'date' },
    from: { type: 'string', format: 'time' },
    until: { type: 'string', format: 'time' },
    category: { type: 'string', category: ['Open', 'Closed'] }
  },
  
  permissions: {
    listing: 'unset'
  },
  
  ensurePositiveRange: function() {
    if (this.until < this.from) {
      throw '422 Please provide a positive time range.';
    }
  }
});

Model.Notice = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    audience: { type: 'string', category: ['Customers', 'Managers', 'Everyone'] },
    body: { type: 'string' },
    conditions: { type: 'string', title: 'notice condition' }
  },
  
  permissions: {
    listing: 'unset'
  },
  
  ensureConditionsFormat: function() {
    var formats = [
      /^date(\<|=|\>)\d\d\d\d-\d\d-\d\d$/,
      /^day=(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)$/,
      /^time(\<|=|\>)\d\d:\d\d:\d\d$/,
      /^people(\<|=|\>)[0-9]+$/,
      /^seatingArea=[0-9]+$/
    ];
    var valid = this.conditions.split(',').all(
      function(condition) {
        return formats.any(
          function(format) {
            return !!condition.match(format);
          }
        );
      }
    );
    if (!valid) {
      throw '422 Please ensure your notice conditions are valid.';
    }
  }
});

Model.ContactWebsite = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    contactId: { type: 'integer' },
    userId: { type: ['null', 'integer'] },
    developerId: { type: ['null', 'integer'] },
    website: { type: 'string', format: 'uri' },
    category: { type: 'string', category: ['Work', 'Personal'] }
  },
  
  permissions: {
    developer: 'get',
    listing: 'unset',
    user: 'set'
  },
  
  authorization: true,
  
  indexes: ['developerId', 'listingId', 'userId']
});

Model.ContactDate = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    contactId: { type: 'integer' },
    userId: { type: ['null', 'integer'] },
    developerId: { type: ['null', 'integer'] },
    year: { type: ['null', 'integer'], min: 1900, max: 2100 },
    month: { type: ['null', 'integer'], min: 1, max: 12 },
    day: { type: ['null', 'integer'], min: 1, max: 31 },
    category: { type: 'string', category: ['Birthday', 'Anniversary'] }
  },
  
  permissions: {
    developer: 'get',
    listing: 'unset',
    user: 'set'
  },
  
  authorization: true,
  
  indexes: ['developerId', 'listingId', 'userId'],
  
  ensureDatePresent: function() {
    if (!this.year && !this.month && !this.day) {
      throw '422 Please provide a date for your contact.';
    }
  }
});

Model.User = Class.create(Model.Base, {
  schema: {
    developerId: { type: ['null', 'integer'] },
    name: { type: 'string' },
    email: { type: 'string', format: 'email' },
    ha1: { type: 'string' }
  },
  
  permissions: {
    developer: 'get',
    user: 'set'
  },
  
  authorization: true,
  
  indexes: ['developerId', 'email']
});

Model.Country = Class.create(Model.Base, {
  schema: {
    currencyId: { type: 'integer' },
    name: { type: 'string', createOnly: true },
    uri: { type: 'string', createOnly: true, format: 'uri' }
  },
  
  beforeValidate: function() {
    this.uri = '/#';
  },
  
  beforeSet: function() {
    this.uri = '/#' + this.name.uriComponent();
  }
});

Model.Associate = Class.create(Model.Base, {
  schema: {
    userId: { type: 'integer' },
    name: { type: ['null', 'string'] },
    message: { type: ['null', 'string'] }
  },
  
  permissions: {
    user: 'set'
  },
  
  authorization: true,
  
  indexes: ['userId']
});

Model.City = Class.create(Model.Base, {
  schema: {
    stateId: { type: 'integer' },
    name: { type: 'string', createOnly: true },
    uri: { type: 'string', createOnly: true, format: 'uri' },
    latitude: { type: ['null', 'number'], createOnly: true },
    longitude: { type: ['null', 'number'], createOnly: true },
    userId: { type: ['null', 'integer'] }
  },
  
  beforeValidate: function() {
    this.uri = '/#';
  },
  
  beforeSet: function() {
    var state = Model.State.get(this.stateId);
    var country = Model.Country.get(state.countryId);
    var components = [ country.name, state.name, this.name ];
    this.uri = '/#' + components.invoke('uriComponent').join('/');
  }
});

Model.ContactEmail = Class.create(Model.Base, {
  schema: {
    listingId: { type: 'integer' },
    contactId: { type: 'integer' },
    userId: { type: ['null', 'integer'] },
    developerId: { type: ['null', 'integer'] },
    email: { type: 'string', format: 'email' },
    category: { type: 'string', category: ['Work', 'Personal'] }
  },
  
  permissions: {
    developer: 'get',
    listing: 'unset',
    user: 'set'
  },
  
  authorization: true,
  
  indexes: ['developerId', 'listingId', 'userId']
});

Model.Suburb = Class.create(Model.Base, {
  schema: {
    cityId: { type: 'integer' },
    areaId: { type: ['null', 'integer'] },
    name: { type: 'string', createOnly: true },
    uri: { type: 'string', createOnly: true, format: 'uri' }
  },
  
  beforeValidate: function() {
    this.uri = '/#';
  },
  
  beforeSet: function() {
    var area = this.areaId ? Model.Area.get(this.areaId) : { name: '-' };
    var city = Model.City.get(this.cityId);
    var state = Model.State.get(city.stateId);
    var country = Model.Country.get(state.countryId);
    var components = [ country.name, state.name, city.name, area.name, this.name ];
    this.uri = '/#' + components.invoke('uriComponent').join('/');
  }
});

Model.Developer = Class.create(Model.Base, {
  schema: {
    userId: { type: 'integer', referentialIntegrity: true }
  },
  
  permissions: {
    user: 'set'
  },
  
  authorization: true,
  
  indexes: ['userId']
});

Model.Reservation = Class.create(Model.Base, {
  schema: {
    associateId: { type: ['null', 'integer'] },
    contactId: { type: 'integer', title: 'contact', createOnly: false },
    developerId: { type: ['null', 'integer'] },
    listingId: { type: 'integer', title: 'listing' },
    userId: { type: ['null', 'integer'], title: 'user' },
    seatingAreaIds: { type: 'array', createOnly: false, items: { type: 'integer' }, title: 'seating area' },
    tableIds: { type: 'array', createOnly: false, optional: true, items: { type: 'integer' }, title: 'table' },
    date: { type: 'string', format: 'date' },
    time: { type: 'string', format: 'time' },
    people: { type: 'integer', min: 1, title: 'number of people' },
    category: { type: 'string', category: ['Accepted', 'Confirmed', 'Seated', 'Cancelled', 'Dishonored'] },
    mode: { type: 'string', category: ['External', 'Internal'], createOnly: true },
    referred: { type: 'boolean', createOnly: true, value: false },
    cleared: { type: 'boolean', createOnly: true, value: false },
    visible: { type: 'boolean', value: true }
  },
  
  permissions: {
    developer: 'get',
    listing: 'set',
    user: 'set'
  },
  
  authorization: true,
  
  indexes: ['developerId', 'listingId', 'userId']
});

Model.Listing = Class.create(Model.Base, {
  schema: {
    name: { type: 'string', optional: true },
    phone: { type: 'string', optional: true },
    email: { type: 'string', format: 'email', optional: true },
    website: { type: 'string', format: 'uri', optional: true, format: 'uri' },
    street: { type: 'string', optional: true },
    suburbId: { type: ['null', 'integer'], createOnly: false },
    areaId: { type: ['null', 'integer'], createOnly: false },
    cityId: { type: ['null', 'integer'], createOnly: false },
    stateId: { type: ['null', 'integer'], createOnly: false },
    countryId: { type: ['null', 'integer'], createOnly: false },
    zip: { type: 'string', optional: true },
    currencyId: { type: ['null', 'integer'] },
    arrivalRate: { type: 'integer', min: 1, value: 20 },
    noticePeriod: { type: 'integer', min: 0, value: 1 },
    turnTime: { type: 'string', format: 'time', value: '06:00:00' },
    userIds: { type: 'array', createOnly: false, items: { type: 'integer' } },
    utcOffset: { type: 'integer', value: 0 },
    visible: { type: 'boolean', readOnly: true, value: false }
  },
  
  permissions: {
    user: 'set'
  }
});

var Chart = {};

Chart.Base = Class.create({
  setData: function(options) {
    this.maxPoints = this.getMaxPoints(options.data);
    this.maxPoint = this.getMaxPoint(options.data);
    this.setDataX(options.data, options.labels);
    this.setDataY();
    this.setParameters(options);
  },
  
  setDataX: function(data, labels) {
    Object.keys(data).sort().each(
      function(key, index) {
        this.addLegend(key);
        this.addPoints(data[key]);
        this.addLabels(labels, index);
        this.addStyles();
        this.addColors(index);
      }.bind(this)
    );
  },
  
  setDataY: function() {
    this.addLabels([
      '',
      (this.maxPoint * 1/4).round(1),
      (this.maxPoint * 2/4).round(1),
      (this.maxPoint * 3/4).round(1),
      this.maxPoint
    ]);
  },
  
  setParameters: function(options) {
    this.parameters = {
      cht: 'ls',
      chs: options.width + 'x' + options.height,
      chdl: this.legends.join('|'),
      chxt: this.axes.join(','),
      chd: 't:' + this.points.join('|'),
      chxl: this.labels.join('|'),
      chls: this.styles.join('|'),
      chco: this.colors.join(',')
    };
  },
  
  addLegend: function(legend) {
    if (!this.legends) this.legends = [];
    this.legends.push(legend);
  },
  
  addAxis: function(axis) {
    if (!this.axes) this.axes = [];
    this.axes.push(axis);
  },
  
  addPoints: function(data) {
    if (!this.points) this.points = [];
    data = data.collect(
      function(point) {
        return (point / this.maxPoint * 100).round(1);
      }.bind(this)
    );
    this.points.push(data.join());
  },
  
  addLabels: function(labels, index) {
    if (index === 0 && this.isDateLabel(labels.first())) {
      this.pushLabels('x', this.getDayLabels(labels));
      this.pushLabels('x', this.getMonthLabels(labels));
    } else if (index === 0) {
      this.pushLabels('x', labels);
    } else if (index > this.labels.length - 1) {
      this.pushLabels('x', labels.collect(function() {return '';}));
    } else if (!index) {
      this.pushLabels('y', labels);
    }
  },
  
  pushLabels: function(axis, labels) {
    this.addAxis(axis);
    if (!this.labels) this.labels = [];
    this.labels.push(this.labels.length + ':|' + labels.join('|'));
  },
  
  addStyles: function() {
    if (!this.styles) this.styles = [];
    this.styles.push('2,1,0'); // Thickness, Length Of Solid, Length Of Blank
  },
  
  addColors: function(index) {
    if (!this.colors) this.colors = [];
    this.colors.push(['0066CC', 'C6D9FD', 'CCCCCC', '333333', '800000'][index]);
  },
  
  isDateLabel: function(label) {
    return (/\d\d\d\d-\d\d-\d\d/).test(label);
  },
  
  getDayLabels: function(labels) {
    return labels.collect(
      function(label) {
        return label.substr(8,2);
      }
    );
  },
  
  getMonthLabels: function(labels) {
    var month = null;
    return labels.collect(
      function(label, index) {
        label = label.toDateTime('MONTH Y');
        if (label != month && (index > 0 || labels.length == 1)) {
          month = label;
          return label;
        } else {
          return '';
        }
      }
    );
  },
    
  getMaxPoints: function(data) {
    return Object.keys(data).collect(
      function(key) {
        return data[key].length;
      }
    ).max();
  },
  
  getMaxPoint: function(data) {
    return Object.keys(data).collect(
      function(key) {
        return data[key].max();
      }
    ).max();
  },
  
  src: function() {
    var src = 'http://chart.apis.google.com/chart?';
    return src + Object.keys(this.parameters).inject('',
      function(result, key) {
        return result + key + '=' + encodeURIComponent(this.parameters[key]) + '&';
      }.bind(this)
    ).sub(/&$/, '');
  }
});

Chart.Line = Class.create(Chart.Base, {
  initialize: function(element, options) {
    this.element = $(element);
    this.setData(options);
    this.element.insert(Html.img({
      height: options.height,
      width: options.width,
      src: this.src()
    }));
  }
});

var Keyboard = Class.create({
  attachEvents : function() {
    Event.observe(document, 'click', this.onClick.bind(this));
    Event.observe(document, 'mousemove', this.onMouseMove.bind(this));
    Event.observe(document, 'mouseup', this.onMouseUp.bind(this));
    Event.observe(window, 'scroll', this.onScroll.bind(this));
    
    this.keyboard.observe('mousedown', this.onMouseDown.bind(this));
    this.keyboard.observe('mouseover', this.onMouseOver.bind(this));
    this.keyboard.observe('mouseout', this.onMouseOut.bind(this));
  },
  
  hide : function() {
    this.keyboard.hide();
  },
  
  initialize : function(keyboard, options) {
    this.keyboard = $(keyboard);

    this.setOptions(options);
    this.setKeyboard();
    this.setMode();
    this.attachEvents();
  },
  
  modes : {
    0 : [
      { key : 'q' }, { key : 'w' }, { key : 'e' }, { key : 'r' }, { key : 't' }, { key : 'y' },
      { key : 'u' }, { key : 'i' }, { key : 'o' }, { key : 'p' }, { key : 'a' }, { key : 's' },
      { key : 'd' }, { key : 'f' }, { key : 'g' }, { key : 'h' }, { key : 'j' }, { key : 'k' },
      { key : 'l' },
      { key : 'ABC', value : 'Mode1', width : 49, shaded : true },
      { key : 'z' }, { key : 'x' }, { key : 'c' }, { key : 'v' }, { key : 'b' }, { key : 'n' },
      { key : 'm' },
      { key : '←', value : 'Backspace', width : 49, shaded : true },
      { key : '.?123', value : 'Mode2', width : 68, shaded : true },
      { key : 'Space', value : ' ', width : 220 },
      { key : 'Return', value : "\r\n", width : 68, shaded : true }
    ],
    1 : [
      { key : 'Q' }, { key : 'W' }, { key : 'E' }, { key : 'R' }, { key : 'T' }, { key : 'Y' },
      { key : 'U' }, { key : 'I' }, { key : 'O' }, { key : 'P' }, { key : 'A' }, { key : 'S' },
      { key : 'D' }, { key : 'F' }, { key : 'G' }, { key : 'H' }, { key : 'J' }, { key : 'K' },
      { key : 'L' },
      { key : 'abc', value : 'Mode0', width : 49, shaded : true },
      { key : 'Z' }, { key : 'X' }, { key : 'C' }, { key : 'V' }, { key : 'B' }, { key : 'N' },
      { key : 'M' },
      { key : '←', value : 'Backspace', width : 49, shaded : true },
      { key : '.?123', value : 'Mode3', width : 68, shaded : true },
      { key : 'Space', value : ' ', width : 220 },
      { key : 'Return', value : "\r\n", width : 68, shaded : true }
    ],
    2 : [
      { key : '1' }, { key : '2' }, { key : '3' }, { key : '4' }, { key : '5' }, { key : '6' },
      { key : '7' }, { key : '8' }, { key : '9' }, { key : '0' }, { key : '-' }, { key : '/' },
      { key : ':' }, { key : ';' }, { key : '(' }, { key : ')' }, { key : '$' }, { key : '&amp;', value : '&' },
      { key : '@' }, { key : '"' }, { key : '_' }, { key : '#' }, { key : '+' }, { key : '=' },
      { key : '.' }, { key : ',' }, { key : '?' }, { key : '!' }, { key : "'" },
      { key : '←', value : 'Backspace', shaded : true },
      { key : 'abc', value : 'Mode0', width : 68, shaded : true },
      { key : 'Space', value : ' ', width : 220 },
      { key : 'Return', value : "\r\n", width : 68, shaded : true }
    ],
    3 : [
      { key : '1' }, { key : '2' }, { key : '3' }, { key : '4' }, { key : '5' }, { key : '6' },
      { key : '7' }, { key : '8' }, { key : '9' }, { key : '0' }, { key : '-' }, { key : '/' },
      { key : ':' }, { key : ';' }, { key : '(' }, { key : ')' }, { key : '$' }, { key : '&amp;', value : '&' },
      { key : '@' }, { key : '"' }, { key : '_' }, { key : '#' }, { key : '+' }, { key : '=' },
      { key : '.' }, { key : ',' }, { key : '?' }, { key : '!' }, { key : "'" },
      { key : '←', value : 'Backspace', shaded : true },
      { key : 'ABC', value : 'Mode1', width : 68, shaded : true },
      { key : 'Space', value : ' ', width : 220 },
      { key : 'Return', value : "\r\n", width : 68, shaded : true }
    ]
  },
  
  move : function() {
    this.keyboard.setStyle({
      left : this.options.left + 'px',
      top : this.options.top + 'px'
    });
  },
  
  onClick : function(event) {
    var element = event.element();
    if (element.descendantOf(this.keyboard)) {
       // Setting this.text selection in onMouseDown is overruled in IE by
       // the subsequent onClick event despite calls to event.stop() wherever
       // possible. This works cross-browser:
      this.text.setSelection(this.selection);
    } else if (element.match('textarea') || element.match('input[class="text"]')) {
      this.text = element;
      this.returnHidesKeyboard = !element.match('textarea');
      this.show();
    } else if (element != this.keyboard) {
      this.hide();
    }
  },
  
  onMouseDown : function(event) {
    var element = event.element();
    
    if (element.descendantOf(this.keyboard)) {
      element.setStyle({
        background : this.options.keyPressedBackground,
        color : this.options.keyPressedColor
      });
      
      var key = element.readAttribute('value');
      
      if (key.startsWith('Mode')) {
        this.options.mode = key.without('Mode');
        this.setMode();
        this.selection = this.text.getSelection();
      } else if (key == "\r\n" && this.returnHidesKeyboard) {
        this.hide();
      } else {
        var value = this.text.getValue();
        var selection = this.text.getSelection();

        var beforeCaret = value.slice(0, selection.start);
        var afterCaret = value.slice(selection.end);

        if (key == 'Backspace') {
          key = ''; // Delete selection.
          if (selection.start === selection.end) {
            // Delete single character:
            beforeCaret = beforeCaret.substr(0, selection.start - 1);
            selection.start--;
          }
        } else {
          selection.start++;
        }

        this.text.setValue(beforeCaret + key + afterCaret);
        // Use selection.start for both start and end:
        this.selection = { start: selection.start, end: selection.start };
      }
      
      event.stop(); // Prevents elements from being selected.
      
    } else if (element == this.keyboard) {
      this.dragging = true;
      this.dragOffsetTop = this.options.top - event.pointerY();
      this.dragOffsetLeft = this.options.left - event.pointerX();
      
      event.stop(); // Prevents elements from being selected.
    }
  },
  
  onMouseMove : function(event) {
    if (!this.dragging) return;
    // Update this.viewport as it may have changed during mouse movement:
    this.viewport = document.viewport.getScrollOffsets();
    // Event pointers are relative to document top and include viewport already:
    this.options.left = event.pointerX() + this.dragOffsetLeft;
    this.options.top = event.pointerY() + this.dragOffsetTop;
    this.move();
  },
  
  onMouseOut : function(event) {
    var element = event.element();
    if (element.descendantOf(this.keyboard)) {
      var shaded = element.readAttribute('shaded');
      var options = this.options;
      element.setStyle({
        background : shaded ? options.keyShadedBackground : options.keyBackground,
        color : shaded ? options.keyShadedColor : options.keyColor,
        cursor : 'default'
      });
    }
  },
  
  onMouseOver : function(event) {
    var element = event.element();
    if (element.descendantOf(this.keyboard)) {
      element.setStyle({
        background : this.options.keyHighlightedBackground,
        color : this.options.keyHighlightedColor,
        cursor : 'pointer'
      });
    }
  },
  
  onMouseUp : function(event) {
    this.dragging = false;
    var element = event.element();
    if (element.descendantOf(this.keyboard)) {
      element.setStyle({
        background : this.options.keyHighlightedBackground,
        color : this.options.keyHighlightedColor
      });
      // In case onClick is not triggered in IE:
      this.text.setSelection(this.selection);
    }
  },
  
  onScroll : function(event) {
    var viewport = document.viewport.getScrollOffsets();
    this.options.left += viewport.left - this.viewport.left;
    this.options.top += viewport.top - this.viewport.top;
    this.move();
    this.viewport = viewport;
  },
  
  setKeyboard : function() {
    this.keyboard.absolutize();
    this.keyboard.setBorderRadius(this.options.borderRadius + 'px');
    this.keyboard.setOpacity(this.options.opacity);
    this.keyboard.setStyle({
      background : this.options.background,
      height : this.options.height + 'px',
      lineHeight : this.options.keyHeight + 'px',
      textAlign : 'center',
      width : this.options.width + 'px'
    });
    this.move();
    this.setKeys();
  },
  
  setKeys : function() {
    var options = this.options;
    
    var row = new Element('div');
    var rowIndex = 0;
    var left = 0;
    
    var insertRow = function() {
      // Center row vertically and horizontally:
      var rowLeft = (options.width - left + options.keyPadding) / 2;
      var rowTop = rowIndex * (options.keyHeight + options.keyPadding) + options.keyPadding;
      row.setStyle({ position : 'absolute', left : rowLeft + 'px', top : rowTop + 'px' });
      this.keyboard.insert(row);
      row = new Element('div');
      rowIndex++;
      left = 0;
    }.bind(this);
    
    Object.keys(this.modes).each(
      function(mode) {
        rowIndex = 0;
        
        this.modes[mode].each(
          function(key, index) {
            var width = key.width || options.keyWidth;
            
            if (left + width > options.width) insertRow();

            var div = new Element('div', {
              mode : mode,
              value : key.value || key.key,
              shaded : !!key.shaded
            });
            div.setBorderRadius(this.options.borderRadius + 'px');
            div.setStyle({
              height : options.keyHeight + 'px',
              width : width + 'px',
              background : key.shaded ? options.keyShadedBackground : options.keyBackground,
              color : key.shaded ? options.keyShadedColor : options.keyColor,
              position : 'absolute',
              left : left + 'px',
              top : '0px'
            });
            div.update(key.key);
            row.insert(div);
            
            left = left + width + options.keyPadding;
          }.bind(this)
        );
        
        insertRow();
      }.bind(this)
    );
  },
  
  setMode : function() {
    this.keyboard.select('div[mode]').invoke('hide');
    this.keyboard.select('div[mode="' + this.options.mode + '"]').invoke('show');
  },
  
  setOptions : function(options) {
    this.options = {};
    this.options.keyHeight = 40;
    this.options.keyPadding = 8;
    this.options.keyWidth = 30;

    this.options.keyBackground = '#DDDDDD';
    this.options.keyColor = '#333333';
    this.options.keyHighlightedBackground = '#FFFFFF';
    this.options.keyHighlightedColor = '#333333';
    this.options.keyPressedBackground = '#999999';
    this.options.keyPressedColor = '#FFFFFF';
    this.options.keyShadedBackground = '#999999';
    this.options.keyShadedColor = '#FFFFFF';
    
    this.options.background = '#666666';
    this.options.borderRadius = 4;
    this.options.height = 216;
    this.options.left = 600;
    this.options.mode = 1;
    this.options.opacity = 0.8;
    this.options.top = 100;
    this.options.width = 388;
    Object.extend(this.options, options);
    
    this.viewport = document.viewport.getScrollOffsets();
    this.options.left += this.viewport.left;
    this.options.top += this.viewport.top;
  },
  
  show : function() {
    this.keyboard.show();
  }
});

var Rating = Class.create({
  afterSet : function(element) {},
  
  afterUnset : function(element) {},
  
	initialize : function(options) {
	  this.index = null;
	  this.input = $(options.input);
	  this.rating = $(options.rating);
	  this.caption = $(options.caption);
	  
	  this.allowUnset = Object.isDefined(options.allowUnset) ? options.allowUnset : true;
	  this.unsetValue = options.unsetValue || 0;

	  if (options && options.afterSet) this.afterSet = options.afterSet;
	  if (options && options.afterUnset) this.afterUnset = options.afterUnset;
	  this.wrapper = this.rating.wrap('div');
	  this.wrapper.setStyle({cursor : 'pointer', height : '23px'});
    this.wrapper.observe('mousemove', this.onMouseMove.bind(this));
    this.wrapper.observe('mouseout', this.onMouseOut.bind(this));
    this.wrapper.observe('click', this.onClick.bind(this));
    this.onMouseOut();
	},
	
	captions : ['Unrated', 'Hate It', 'Don\'t Like It', 'Like It', 'Really Like It', 'Love It'],
	
	classNames : ['zero', 'one', 'two', 'three', 'four', 'five'],

	onMouseMove : function(event) {
	  var index = this.getIndexFromPointer(event);
	  if (index != this.index) {
	    this.index = index;
      this.setRatingClass(index);
      this.setCaption(index);
    }
	},
	
	onMouseOut : function(event) {
	  this.index = this.getIndexFromInput();
	  this.setRatingClass(this.index);
    this.setCaption(this.index);
	},
	
	onClick : function(event) {
	  var index = this.getIndexFromPointer(event);
	  var unset = (index - this.getIndexFromInput() === 0) && this.allowUnset;
	  if (unset) index = this.unsetValue;
  	this.setRatingClass(index);
    this.setCaption(index);
  	this.setIndexToInput(index);
  	if (!unset) {
  	  this.afterSet(this.input);
  	} else {
  	  this.afterUnset(this.input);
  	}
	},
	
	getIndexFromPointer : function(event) {
	  var x = Event.pointerX(event) - this.wrapper.cumulativeOffset().left;
	  return Math.floor(x / 16) + 1;
	},
	
	getIndexFromInput : function() {
	  var rating = this.input.getValue();
	  if (!rating || rating < 0) rating = 0;
	  if (rating > 5) rating = 5;
	  return rating;
	},
	
	reset : function() {
	  this.input.setValue(0);
	  this.onMouseOut();
	},
	
	setIndexToInput : function(index) {
	  this.input.setValue(index);
	},
	
	setRatingClass : function(index) {
	  this.rating.classNames().each(
	    function(className){
        if (className != 'rating') this.rating.removeClassName(className);
      }.bind(this)
    );
	  this.rating.addClassName(this.classNames[index]);
	},
	
	setCaption : function(index) {
	  var caption = this.captions[index];
	  if (this.caption) {
	    this.caption.update(caption);
	  } else {
	    this.wrapper.writeAttribute({ title: caption });
	    this.rating.writeAttribute({ title: caption });
	  }
	},
	
	setValue : function(rating) {
	  this.input.setValue(rating);
	  this.onMouseOut();
	}
});

var QuestionsInterface = Class.create({
  $: function(id) {
    return $(this.namespace + id);
  },
  
  // Requires div, options.audience, options.uri, options.onSuccess parameters.
  // Generates a question interface.
  initialize: function(div, options) {
    this.div = div;
    this.namespace = this.div.identify().split('.').first() + '.';
    this.options = options;
    this.setInterface();
  },
  
  insertInterface: function() {
    var html = '<div style="display:none" id="#{namespace}categories"></div>' +
    '<h2 style="display:none" id="#{namespace}question"></h2>' +
    '<div style="display:none" id="#{namespace}answer"></div>' +
    '<h2 style="display:none" id="#{namespace}related-questions-header">Related Questions</h2>' +
    '<ul style="display:none" id="#{namespace}related-questions"></ul>';
    this.div.update(html.interpolate({ namespace: this.namespace }));
  },
  
  insertCategories: function() {
    this.$('categories').update('').show();
    var template = '<h2 id="#{namespace}#{anchor}">#{category}</h2><ul>#{questions}</ul><hr>';
    Object.keys(this.categories).sort().each(
      function(category) {
        this.$('categories').insert(template.interpolate({
          namespace: this.namespace,
          anchor: category.uriComponent(),
          category: category,
          questions: this.categories[category].join('')
        }));
      }.bind(this)
    );
  },
  
  insertRelatedQuestions: function() {
    var questions = Model.Question.get({
      audience: this.options.audience,
      category: this.question.category,
      order: 'question'
    }).findAll(
      function(question) {
        return question.id != this.question.id;
      }.bind(this)
    );
    if (questions.present()) {
      this.$('related-questions-header').show();
      this.$('related-questions').update('').show();
      questions.each(
        function(question) {
          var href = this.options.uri + '/' + question.id;
          var html = Html.li(Html.a({ href: href }, question.question));
          this.$('related-questions').insert(html);
        }.bind(this)
      );
      this.$('related-questions-header').insert({ before: '<hr>' });
      this.$('related-questions').insert({ after: '<hr>' });
    }
  },
  
  setCategories: function() {
    var questions = Model.Question.get({ audience: this.options.audience });
    this.categories = {};
    questions.order('question').each(
      function(question) {
        var category = question.category;
        var href = this.options.uri + '/' + question.id;
        var html = Html.li(Html.a({ href: href }, question.question));
        if (!this.categories[category]) this.categories[category] = [];
        this.categories[category].push(html);
      }.bind(this)
    );
  },
  
  setInterface: function() {
    this.insertInterface();
    if (Parameters.id) {
      this.question = Model.Question.get(Parameters.id);
      var question = this.question.question;
      if ([1, 77].include(Session.user.id)) {
        var a = Html.a({ href: '/#oracle/question/' + this.question.id }, 'Edit');
        question += Html.span({ className: 'h5 padding-left' }, a);
      }
      this.$('question').update(question).show();
      this.$('answer').update(this.question.answer.toP()).show();
      this.insertRelatedQuestions();
    } else {
      this.setCategories();
      this.insertCategories();
    }
    this.options.onSuccess();
    // Scroll to category bookmark.
    // Spares controller from having to calculate category element id.
    if (!Parameters.id && this.options.bookmark) {
      var element = $(this.namespace + this.options.bookmark);
      // Let Controller.Base.scrollToBookmark run first and then we go:
      if (element) element.scrollTo.bind(element).defer();
    }
  }
});

var TourLabel = Class.create({
  attachEvents: function() {
    Event.observe(document, 'mousemove', this.onMouseMove.bind(this));
    Event.observe(document, 'mouseup', this.onMouseUp.bind(this));
    this.element.observe('mousedown', this.onMouseDown.bind(this));
  },
  
  move: function() {
    if (Object.isInteger(this.bottom)) {
      this.element.setStyle({ bottom: this.bottom + 'px' });
    }
    if (Object.isInteger(this.left)) {
      this.element.setStyle({ left: this.left + 'px' });
    }
    if (Object.isInteger(this.right)) {
      this.element.setStyle({ right: this.right + 'px' });
    }
    if (Object.isInteger(this.top)) {
      this.element.setStyle({ top: this.top + 'px' });
    }

    if (this.dragging) {
      var status = 'Top: ' + this.top + ' ';
      status += 'Right: ' + this.right + ' ';
      status += 'Bottom: ' + this.bottom + ' ';
      status += 'Left: ' + this.left;
      window.status = status;
    }
  },
  
  onMouseDown: function(event) {
    this.dragging = true;
    this.dragOffsetTop = this.top - event.pointerY();
    this.dragOffsetRight = this.right + event.pointerX();
    this.dragOffsetBottom = this.bottom + event.pointerY();
    this.dragOffsetLeft = this.left - event.pointerX();
    event.stop(); // Prevents elements from being selected.
  },
  
  onMouseMove: function(event) {
    if (!this.dragging) return;
    // Update this.viewport as it may have changed during mouse movement:
    this.viewport = document.viewport.getScrollOffsets();
    // Event pointers are relative to document top and include viewport already:
    if (Object.isInteger(this.top)) {
      this.top = event.pointerY() + this.dragOffsetTop;
    }
    if (Object.isInteger(this.right)) {
      this.right = this.dragOffsetRight - event.pointerX();
    }
    if (Object.isInteger(this.bottom)) {
      this.bottom = this.dragOffsetBottom - event.pointerY();
    }
    if (Object.isInteger(this.left)) {
      this.left = event.pointerX() + this.dragOffsetLeft;
    }
    this.move();
  },
  
  onMouseUp: function(event) {
    this.dragging = false;
    window.status = '';
  },
  
  initialize: function(text, options) {
    this.options = options;
    
    this.top = options.top;
    this.right = options.right;
    this.bottom = options.bottom;
    this.left = options.left;
    
    this.element = new Element('div', { className: 'round-corners' });
    this.element.setStyle({
      background: '#333333',
      color: '#FFFFFF',
      fontSize: '12px',
      lineHeight: '23px',
      padding: '8px 15px 8px 15px',
      position: 'absolute',
      textAlign: 'center',
      width: options.width + 'px',
      zIndex: 20,
      WebkitBoxShadow: '0px 4px 4px #CCCCCC'
    });
    this.element.setOpacity(0.8);
    this.element.update(text);
    if (this.options.enter) {
      this.element.hide();
      this.element.fadeIn.bind(this.element).delay(this.options.enter / 1000);
    }
    if (this.options.exit) {
      this.element.fadeOut.bind(this.element).delay(this.options.exit / 1000);
    }
    $('Base.view').insert(this.element);
    this.move();
    if (this.options.moveable) this.attachEvents();
    if (this.options.point) this.insertPointer();
  },
  
  insertPointer: function() {
    var pointer = new Element('div');
    pointer.setStyle({
      fontSize: '0px',
      lineHeight: '0%',
      position: 'absolute',
      width: '0px',
      zIndex: 20
    });
    this.element.insert(pointer);
    
    var dimensions = this.element.getDimensions();
    var horizontal = ((dimensions.width - 10) / 2).round() + 'px';
    var vertical = ((dimensions.height - 10) / 2).round() + 'px';
    var color = '5px solid #333333';
    var transparent = '5px solid transparent';
    
    if (this.options.point == 'north') {
      pointer.setStyle({ left: horizontal, top: '-5px' });
      pointer.setStyle({
        borderBottom: color,
        borderLeft: transparent,
        borderRight: transparent
      });
    } else if (this.options.point == 'east') {
      pointer.setStyle({ right: '-5px', top: vertical });
      pointer.setStyle({
        borderBottom: transparent,
        borderLeft: color,
        borderTop: transparent
      });
    } else if (this.options.point == 'west') {
      pointer.setStyle({ left: '-5px', top: vertical });
      pointer.setStyle({
        borderBottom: transparent,
        borderRight: color,
        borderTop: transparent
      });
    } else {
      // Point South by default:
      pointer.setStyle({ bottom: '-5px', left: horizontal });
      pointer.setStyle({
        borderLeft: transparent,
        borderRight: transparent,
        borderTop: color
      });
    }
  }
});

var HeaderSearch = Class.create({  
  initialize: function() {
    var timer = new Timer();
    if (!HeaderSearch._notes) {
      HeaderSearch._setNotes();
      HeaderSearch._setNotes.repeat((20).seconds());
    }
    this._query = $F('Base.search-query').strip();
    if (this._query.present()) {
      this._results = {};
      this._searchContacts();
      this._searchReservations();
      $('Base.search-results-table').update(this._getResultsHtml());
      $('Base.search-results').show();
    } else {
      $('Base.search-results').hide();
    }
    timer.log('HeaderSearch');
  },
  
  _getResultsHtml: function() {
    var html = '';
    var categories = Object.keys(this._results).length;
    Object.each(this._results,
      function(category, objects) {
        if (category == 'Contacts') {
          var limit = 7;
        } else if (category == 'Reservations') {
          var limit = 7;
        }
        if (categories == 1) limit = 7;
        objects.limit(limit).each(
          function(object, index) {
            var caption = index === 0 ? category + ':' : '&nbsp;';
            var label = Html.div({ className: 'label xs' }, caption);
            var element = Html.div({ className: 'element' }, object.a);
            var row = Html.div({ className: 'row' }, label + element);
            html += row;
          }
        );
      }
    );
    if (!html) {
      var left = Html.div({ className: 'left' }, 'No Results Found');
      var row = Html.div({ className: 'row' }, left);
      html += row;
    }
    return html;
  },
  
  _searchContacts: function() {
    var results = {};

    var contacts = Model.Contact.get({
      listingId: Session.user.listingId,
      visible: true
    });

    var searchContacts = new Search({
      common: false,
      objects: contacts,
      properties: ['name', 'company', 'title', 'description']
    });
    searchContacts.query(this._query).each(
      function(contact) {
        results[contact.id] = contact;
      }.bind(this)
    );
        
    var searchNotes = new Search({
      common: false,
      objects: HeaderSearch._notes.contacts,
      properties: ['body']
    });
    searchNotes.query(this._query).each(
      function(note) {
        var contact = Model.Contact.get(note.parentId);
        if (contact) results[contact.id] = contact;
      }.bind(this)
    );
    
    Object.each(results,
      function(id, contact) {
        if (!this._results.Contacts) this._results.Contacts = [];
        var name = contact.name.truncate(28);
        var href = '/#dashboard/contacts/contact/' + contact.id;
        var title = '';
        if (contact.company) title = contact.company;
        if (contact.title) title += (title ? ' ' : '') + contact.title;
        this._results.Contacts.push({
          name: name,
          a: Html.a({ href: href, title: title }, name)
        });
      }.bind(this)
    );
  },
  
  _searchReservations: function() {
    var search = new Search({
      common: false,
      objects: HeaderSearch._notes.reservations,
      properties: ['body']
    });
    var reservations = {};
    search.query(this._query).each(
      function(note, index) {
        var reservation = Model.Reservation.get(note.parentId);
        if (reservation) reservations[reservation.id] = reservation;
      }.bind(this)
    );
    Object.each(reservations,
      function(id, reservation) {
        if (!this._results.Reservations) this._results.Reservations = [];
        var contact = Model.Contact.get(reservation.contactId);
        var name = contact.name.truncate(28);
        var href = '/#dashboard/reservations/reservation/' + reservation.id;
        var title = reservation.date.toDateTime('Y MONTH D DAY');
        this._results.Reservations.push({
          name: name,
          a: Html.a({ href: href, title: title }, name)
        });
      }.bind(this)
    );
  }
});

HeaderSearch._setNotes = function() {
  HeaderSearch._notes = {};
  HeaderSearch._notes.contacts = Model.Note.get({
    listingId: Session.user.listingId,
    parentType: 'Contact'
  });
  HeaderSearch._notes.reservations = Model.Note.get({
    listingId: Session.user.listingId,
    parentType: 'Reservation'
  });
};

var NoteHistory = Class.create({
  initialize: function(element, options) {
    element.update('');
    
    var today = 'now'.toDateTime('DAY d MONTH Y');
    var utcOffset = Model.Listing.get(Session.user.listingId).utcOffset.hours();
    
    options.notes.order('-id').each(
      function(note, index) {
        // Adjust created date time to local time:
        var created = (note.created.toDateTime('T').seconds() + utcOffset) / 1000;
        // Set date to time (if today) or else date:
        var date = created.toDateTime('DAY d MONTH Y');
        if (date == today) date = created.toDateTime('h:m');
        date = Html.span({ className: 'grey-9 h5' }, date);
        
        // Set parent:
        var reservation;
        var parent = '';
        var href;
        var a;
        if (note.parentType != options.parentType) {
          if (note.parentType == 'Contact') {
            parent = (Model.Contact.get(note.parentId) || {}).name;
            href = '/#dashboard/contacts/contact/' + note.parentId;
            a = Html.a({ href: href }, parent);
            parent = Html.span({ className: 'h5 padding-left' }, a);
          } else if (note.parentType == 'Listing') {
            parent = '';
          } else if (note.parentType == 'Reservation') {
            reservation = Model.Reservation.get(note.parentId);
            parent = Model.Contact.get(reservation.contactId).name + ' Reservation';
            href = '/#dashboard/reservations/reservation/' + note.parentId;
            a = Html.a({ href: href }, parent);
            parent = Html.span({ className: 'h5 padding-left' }, a);
          }
        }
        
        // Set user name:
        name = Html.span({ className: 'grey-9 h5 padding-left' }, note.userName);
        
        // Set subject:
        if (note.subject) {
          var subject = Html.span({ className: 'grey-9 h5 padding-left' }, note.subject);
        } else {
          var subject = '';
        }

        // Set body:
        var body = note.body.strip().escapeHTML().autoLink();

        // Set paragraph and insert:
        body = body + "\n" + date + parent + name + subject;
        element.insert(body.toP());
        if (index < options.notes.length - 1) {
          element.insert(Html.hr({ className: 'margin-16' }));
        }
      }
    );
  }
});

var Reservation = Class.create({
  $: function(id) {
    return $(this.name + '.' + id);
  },
  
  $F: function(id) {
    return $F(this.name + '.' + id);
  },
  
  appendAssociateId: function(uri) {
    if (!this.options.sexbyfood && this.options.associateId) {
      uri += '?associate-id=' + this.options.associateId;
    }
    return uri;
  },
  
  attachObservers: function() {
    this.$('month').observe('change', this.onChangeReservation.bind(this));
    this.$('date').observe('change', this.onChangeReservation.bind(this));
    this.$('time').observe('change', this.onChangeReservation.bind(this));
    this.$('people').observe('change', this.onChangeReservation.bind(this));
    this.$('seating-area').observe('change', this.onChangeReservation.bind(this));
    this.$('contact-name').observe('keypress',
      function(event) {
        if (event.keyCode == Event.KEY_RETURN) this.$('contact-email').focus();
      }.bind(this)
    );
    this.$('contact-email').observe('keypress',
      function(event) {
        if (event.keyCode == Event.KEY_RETURN) this.$('contact-number').focus();
      }.bind(this)
    );
    this.$('contact-number').observe('keypress',
      function(event) {
        if (event.keyCode == Event.KEY_RETURN) this.saveReservation();
      }.bind(this)
    );
    this.$('button').observe('click', this.saveReservation.bind(this));
  },
  
  attachResizeObserver: function() {
    this.resizeObserver = new PeriodicalExecuter(this.observeResize.bind(this), 0.02);
  },
  
  disable: function() {
    this.$('month').disable();
    this.$('date').disable();
    this.$('time').disable();
    this.$('people').disable();
    this.$('seating-area').disable();
    this.$('loader').show();
    this.$('button').disable();
  },
  
  enable: function() {
    this.$('month').enable();
    this.$('date').enable();
    this.$('time').enable();
    this.$('people').enable();
    this.$('seating-area').enable();
    this.$('loader').hide();
    this.$('button').enable();
  },
    
  request: function(parameters) {
    parameters.listingId = this.options.listingId;
    
    this.disable();
    
    Http.get(this.domain + '/api/open', {
      headers: {
        Accept: 'application/json'
      },
      parameters: parameters,
      onFailure: function(error) {
        error = error.without(/^[0-9]{3} /);
        if (this.options.sexbyfood) {
          Notice.error(error);
        } else {
          this.$('message').error(error);
        }
        this.$('table').hide();
        this.enable();
        if (this.options.onFailure) this.options.onFailure(error);
      }.bind(this),
      onSuccess: function(body) {
        var open = JSON.parse(body);
        
        if (!this.loaded) {
          if (!this.options.header) this.updateHeader(open.restaurant);
          // If reservation flash is present, overrides contact, sets note and constrains Open.
          // If seatingAreaIds is present, shows contact interface.
          if (this.options.sexbyfood) this.restoreReservation();
          this.loaded = true;
          if (this.options.onLoaded) this.options.onLoaded();
        }
        
        this.$('month').setValue(open.months);
        this.$('date').setValue(open.dates);
        this.$('time').setValue(open.times);
        this.$('people').setValue(open.people);
        this.$('seating-area').setValue(open.seatingAreas);
        
        if (open.notices) {
          this.$('notice').update(open.notices.join('<br>')).up('.row').show();
        } else {
          this.$('notice').up('.row').hide();
        }
        
        this.enable();
      }.bind(this),
      onTimeout: function(error) {
        error = 'Please reconnect to the Internet.';
        if (this.options.sexbyfood) {
          Notice.error(error);
        } else {
          this.$('message').error(error);
        }
        this.$('table').hide();
        this.enable();
        if (this.options.onFailure) this.options.onFailure(error);
      }.bind(this)
    });
  },
  
  explainHowItWorks: function() {
    this.$('how-it-works').show();
    var uri = this.appendAssociateId(this.domain + '/#home');
    this.$('how-it-works').down('a').writeAttribute('href', uri);
  },
  
  initialize: function(div, options) {
    this.div = $(div).update('');
    this.name = this.div.identify();
    this.options = {
      autoResize: true,
      explainHowItWorks: true,
      onFailure: function(error) {},
      sexbyfood: false,
      skinny: true
    };
    Object.extend(this.options, options);
    this.domain = this.options.domain || 'http://www.sexbyfood.com';
    this.insertInterface();
    this.attachObservers();
    if (!this.options.sexbyfood) {
      // Monitor div size to keep iFrame dynamically sized and delay
      // this.attachResizeObserver() to prevent initial flicker in Safari:
      if (this.options.autoResize !== false) {
        this.attachResizeObserver.bind(this).delay(0.3);
      }
    }
    if (this.options.learnMoreLink) {
      this.showLinkToLearnMore();
    } else {
      this.explainHowItWorks();
    }
    if (this.options.header) this.$('header').update(this.options.header);
    if (this.options.sexbyfood) {
      this.request({});
    } else {
      // Wait awhile before jetting off WindowNameRequests, as these would prevent
      // the parent page from loading until they have returned:
      this.request.bind(this, {}).defer();
    }
  },
  
  insertInterface: function() {
    this.div.insert(this.interfaceTemplate().evaluate({ namespace: this.name }));
  },
  
  // 'interface' is a future reserved word in ECMAScript:
  interfaceTemplate: function() {
    var style = 'margin:2px 0px 6px 0px; width:100%; display:block; -webkit-box-sizing:border-box; -moz-box-sizing:border-box; -ms-box-sizing:border-box;';
    var template =
      '<h2>' +
      '<span id="#{namespace}.header">Reservations</span>' + 
      '<span class="padding-left h5">' +
      '<a href="" style="display:none" id="#{namespace}.learn-more-link">Learn More</a>' +
      '</span></h2>' +
      '<p id="#{namespace}.how-it-works" style="display:none">' +
      'Reservations are placed instantly and reliably through the ' +
      '<a href="">Sexbyfood</a> network.<br/>' +
      'A confirmation of your reservation will be sent immediately to your email address.' +
      '</p>' +
      '<form onsubmit="return false" id="#{namespace}.form">' +
        '<p id="#{namespace}.message" style="display:none"></p>' +
        '<div class="table" id="#{namespace}.table"' + (this.options.skinny ? ' style="min-width:530px"' : '') + '>' +
          '<div class="row" style="width:' + (this.options.skinny ? '530' : '890') + 'px">' +
            '<input type="hidden" name="listingId" id="#{namespace}.listing-id">' +
            '<input type="hidden" name="associateId" id="#{namespace}.associate-id">' +
            '<input type="hidden" name="developerId" id="#{namespace}.developer-id">' +
            '<input type="hidden" name="category" id="#{namespace}.category">' +
            '<div class="label xs">Reservation:</div>' +
            '<div class="element s">' +
              '<select class="select" disabled id="#{namespace}.month">' +
                '<option value="">Month</option>' +
              '</select>' +
            '</div>' +
            '<div class="element s">' +
              '<select class="select" name="date" disabled id="#{namespace}.date">' +
                '<option value="">Day</option>' +
              '</select>' +
            '</div>' +
            '<div class="element s">' +
              '<select class="select" name="time" disabled id="#{namespace}.time">' +
                '<option value="">Time</option>' +
              '</select>' +
            '</div>';
    if (this.options.skinny) {
      template +=
          '</div>' +
          '<div class="row" style="width:530px">' +
            '<div class="label xs">&nbsp;</div>';
    }
    template +=
            '<div class="element s">' +
              '<select class="select" name="people" disabled id="#{namespace}.people">' +
                '<option value="">People</option>' +
              '</select>' +
            '</div>' +
            '<div class="element s">' +
              '<select class="select" name="seatingAreaIds" disabled id="#{namespace}.seating-area">' +
                '<option value="">Seating Area</option>' +
              '</select>' +
            '</div>' +
            '<div class="loader" id="#{namespace}.loader"></div>' +
            '<div class="element" id="#{namespace}.progress"></div>' +
          '</div>' +
          '<div class="row" style="display:none">' +
            '<div class="label xs">&nbsp;</div>' +
            '<div class="element xxxl" id="#{namespace}.notice"></div>' +
          '</div>' +
          '<div class="row">' +
            '<div class="label xs">Note:</div>' +
            '<div class="element ' + (this.options.skinny ? 'xl' : 'xxxl') + '">' +
              '<textarea class="textarea" style="height:64px" id="#{namespace}.note-body"></textarea>' +
            '</div>' +
            '<div class="element">(Optional)</div>' +
          '</div>' +
          '<div id="#{namespace}.contact" style="display:none">' +
            '<div class="row">' +
              '<div class="label xs">Name:</div>' +
              '<div class="element xl">' +
                '<input class="text" style="' + style + '" name="contactName" id="#{namespace}.contact-name">' +
              '</div>' +
            '</div>' +
            '<div class="row">' +
              '<div class="label xs">Email:</div>' +
              '<div class="element xl">' +
                '<input class="text" style="' + style + '" name="contactEmail" id="#{namespace}.contact-email">' +
              '</div>' +
            '</div>' +
            '<div class="row">' +
              '<div class="label xs">Tel:</div>' +
              '<div class="element xl">' +
                '<input class="text" style="' + style + '" name="contactNumber" id="#{namespace}.contact-number">' +
              '</div>' +
            '</div>' +
          '</div>' +
          '<div class="row">' +
            '<div class="label xs">&nbsp;</div>' +
            '<div class="element s">' +
              '<input type="button" class="button" value="Reserve" disabled="disabled" id="#{namespace}.button">' +
            '</div>' +
            '<div class="loader" style="display:none"></div>' +
          '</div>' +
        '</div>' +
      '</form>';
  	return new Template(template);
  },
  
  observeResize: function() {
    // frameElement may require a couple milliseconds to initialize or may not exist at all:
    if (!frameElement) return;
    var divHeight = this.div.getHeight();
	  if (this.divHeight != divHeight) {
	    this.divHeight = divHeight;
	    // Ignore divHeight when zero (this.insertInterface() not yet complete)
	    // to avoid hiding frame completely:
	    if (divHeight > 0) frameElement.height = divHeight + 5; // frameElement must be slightly larger than div.
  	}
  },

  onChangeReservation: function(event) {
    this.request({
      month: this.$F('month'),
      date: this.$F('date'),
      time: this.$F('time'),
      people: this.$F('people'),
      seatingArea: this.$F('seating-area')
    });
    if (event.element().identify().endsWith('area')) this.$('contact').show();
  },
  
  restoreReservation: function() {
    var reservation = Flash.get('ReservationClass', this.options.listingId);
    if (reservation) {
      this.request({
        month: reservation.date.toMonth(),
        date: reservation.date,
        time: reservation.time,
        people: reservation.people,
        seatingArea: reservation.seatingAreaIds
      });
      this.$('note-body').setValue(reservation.noteBody);
      this.$('contact-name').setValue(reservation.contactName);
      this.$('contact-email').setValue(reservation.contactEmail);
      this.$('contact-number').setValue(reservation.contactNumber);
      if (reservation.seatingAreaIds) this.$('contact').show();
    }
  },
  
  saveReservation: function() {
    this.$('message').hide();
    this.$('button').loading('Reserving');
    this.$('listing-id').setValue(this.options.listingId);
    this.$('associate-id').setValue(this.options.associateId);
    this.$('developer-id').setValue(this.options.developerId);
    this.$('category').setValue('Accepted');

    var reservation = this.$('form').serialize();

    // Only add note if present, it's optional:
    var noteBody = this.$F('note-body');
    if (noteBody) reservation.noteBody = noteBody;
    
    if (this.options.sexbyfood) {
      // Enables future instances to restore interface values if save redirects or fails:
      Flash.set('ReservationClass', this.options.listingId, reservation);
      // Sexbyfood reservation interfaces use Session.user.id.
      // Redirect to Sign In if no Session present.
      if (!Session.user.id) return window.location = '/#sign-in';
      this.options.userId = Session.user.id;
    }
    
    if (this.options.userId) {
      // Sexbyfood website will pass Session.user.id or redirect to Sign In.
      // Associate website can pass own userId to earn associated Reward Points.
      reservation.userId = this.options.userId;
    } else {
      reservation.userEmail = this.$F('contact-email');
      // userName and userPassword will be only used if Reservation model
      // userEmail lookup finds no existing user and has to create one:
      reservation.userName = this.$F('contact-name');
      reservation.userPassword = $R(10000, 99999).random();
    }
    reservation.tableIds = '';
    reservation.mode = 'External';
    // Restaurants can set this.options.referred => false to avoid referral charges
    // for reservations from their own website.
    reservation.referred = this.options.referred === false ? false : true;

    Http.put(this.domain + '/api/reservations/' + Id.get(), {
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json; charset=UTF-8'
      },
      body: JSON.stringify(reservation),
      onSuccess: function(body) {
        var success = 'Your reservation is confirmed and your table is waiting.';
        if (this.options.sexbyfood) {
          Flash.unset('ReservationClass', this.options.listingId);
          Notice.success(success);
          this.$('message').success(success).down().removeClassName('success');
        } else {
          this.$('message').success(success);
        }
        this.$('button').loaded();
        this.$('table').hide();
      }.bind(this),
      onFailure: function(error) {
        error = error.without(/^[0-9]{3} /);
        if (this.options.sexbyfood) {
          Notice.error(error);
        } else {
          this.$('message').error(error);
        }
        this.$('button').loaded();
        this.$('contact').show();
      }.bind(this)
    });
  },
  
  showLinkToLearnMore: function() {
    var uri = this.appendAssociateId(this.domain + '/#learn-more/reservations');
    this.$('learn-more-link').writeAttribute('href', uri).show();
  },
  
  // Adds restaurant name to header once Open has finished loading if no header option provided.
  updateHeader: function(restaurant) {
    var header = restaurant + ' Reservations';
    this.$('header').update(header);
  }
});

var Application = Class.create({
  initialize: function() {
    if (Session.user.listingId == 102) {
      $('Base.logo').down().hide().next().show();
    } else if (Session.user.listingId == 103) {
      $('Base.logo').down().hide().next(1).show();
    }
    Log('Welcome to Sexbyfood. For information on our Developer API please ' +
      'visit http://www.sexbyfood.com/#developers. Thank you for your support.');
    
    Event.observe(window, 'offline', function() {
      Notice.error('You have been disconnected from the Internet.');
    });
    Event.observe(window, 'online', function() {
      Notice.success('You have been reconnected to the Internet.');
    });
    
    var handler = function(event) {
      new Keyboard('Base.keyboard');
      Event.stopObserving($('Base.keyboard-hotspot'), 'click', handler);
    };
    Event.observe($('Base.keyboard-hotspot'), 'click', handler);
    
    Event.observe('Base.sign-out', 'click',
      function() {
        $('Base.sign-out').update('Signing Out');
        var timer = new Timer();
        var ignoringProgress = true;
        Session.unset.bind(Session, {
          onFailure: Notice.error.bind(Notice),
          onProgress: function(percentage) {
            if (ignoringProgress && timer.elapsed() <= (1).seconds()) {
              return;
            } else {
              ignoringProgress = false;
            }
            percentage = percentage.round().toPaddedString(2);
            $('Base.sign-out').update('Signing Out: ' + percentage + '%');
          },
          onSuccess: function() {
            window.location.hash = Parameters.controller == 'SignIn' ? 'home' : 'sign-in';
          }
        }).delay(0.1);
      }
    );
    
    ApplicationCache.listen();
    
    Event.observe('Base.search-query', 'keyup',
      function(event) {
        new HeaderSearch();
      }
    );

    ApplicationCache.listen();
    
    Event.observe(document, 'click',
      function(event) {
        var element = event.element();
        var query = $('Base.search-query');
        var panel = $('Base.search-results');
        if (element != query && element != panel && !element.descendantOf(panel)) {
          $('Base.search-query').clear();
          $('Base.search-results').hide();
        }
      }
    );
    
    HeaderSearch._setNotes.bind(HeaderSearch).defer();
  }
});

var Ticker = Class.create({
  interpolateTemplate: function(index, options) {
    return this._templates[index].interpolate(options);
  },
  
  initialize: function() {
    this._tickers = [];
    this._calls();
  },
  
  _referredSources: function() {
    Model.Reservation.get({ listingId: Session.user.listingId, referred: true });
  },
  
  _templates: [
    '#{action} #{quantity} #{qualifier} in the last 7 days.',
    'Received #{quantity} #{qualifier} from #{website} in the last 7 days.',
    'You now have #{quantity} #{qualifier}.',
    'Your restaurant is at #{quantity} capacity for the next 7 days.'
  ]
});

Controller.HelpQuestions = Class.create(Controller.Base, {
  header: function() {
    var components = [ 'Help'.toLink('/#help') ];
    var audience = Parameters.audience.capitalize();
    components.push(Parameters.id ? audience.toLink('/#help/' + Parameters.audience) : audience);
    return components.join(' / ');
  },
  
  initialize: function() {
    this.hide();
    new QuestionsInterface(this.$('questions-interface'), {
      audience: Parameters.audience.titleCase(),
      bookmark: Parameters.bookmark,
      uri: '/#help/' + Parameters.audience,
      onSuccess: function() {
        this.show();
      }.bind(this)
    });
  }
});

Controller.OracleLists = Class.create(Controller.Base, {
  activate: function() {
    this.hide();
    this.getResources({ onSuccess: this.setInterface.bind(this) });
  },
  
  authenticated: function() {
    return [1, 77].include(Session.user.id);
  },
  
  collections: {
    'listings, lists': {}
  },
    
  initialize: function() {
    this.hide();
    this.$('add').observe('click',
      function() {
        window.location.hash = 'oracle/lists/list';
      }
    );
    this.template = new Template(this.$('lists').innerHTML);
    this.getResources({ onSuccess: this.setInterface.bind(this) });
  },
  
  setInterface: function() {
    this.$('lists').update('');
    this.resources.lists.order('-created').each(
      function(list) {
        this.$('lists').insert(
          this.template.evaluate({
            title: list.title,
            date: list.created.toDateTime('month D Y')
          })
        );
        var anchor = this.$('lists').select('a').last();
        anchor.writeAttribute('href', '/#oracle/lists/list/' + list.id);
      }.bind(this)
    );
    if (this.resources.lists.present()) this.$('lists').show();
    this.show();
  }
});

Controller.Blog = Class.create(Controller.Base, {
  initialize: function() {
    var template = this.$('posts').replaceContent('');
    var html = [];
    Model.Post.get().order('-created').each(
      function(post) {
        var href = '/#' + post.id.toString().toBookmarkId();
        html.push(template.interpolate({
          id: post.id,
          title: Html.a({ href: href }, post.title),
          name: post.userName,
          date: post.created.toDateTime('MONTH d Y'),
          body: post.body.toP()
        }));
      }.bind(this)
    );
    this.$('posts').insert(html.join(''));
    this.elapsed();
  }
});

Controller.Contact = Class.create(Controller.Base, {  
  initialize: function() {
    if (Session.user.listingId) this.$('emergency').show();
  }
});

Controller.PageNotFound = Class.create(Controller.Base, {});

Controller.OracleInterviews = Class.create(Controller.Base, {
  activate : function() {
    this.hide();
    this.getResources({ onSuccess : this.setInterface.bind(this) });
  },
  
  authenticate : function() {
    return [1, 77].include(Session.user.id);
  },
  
  collections : {
    'interviews, listings' : {}
  },
  
  deactivate : function() {
    this.$('interviews').hide();
  },
  
  initialize : function() {
    this.$('add').observe('click',
      function() {
        window.location.hash = 'oracle/interviews/interview';
      }
    );
    this.template = new Template(this.$('interviews').innerHTML);
  },
  
  setInterface : function() {
    this.$('interviews').update('');
    this.resources.interviews.order('-created').each(
      function(interview) {
        var listing = this.resources.listings.find(interview.listingId);
        this.$('interviews').insert(
          this.template.evaluate({
            name : listing.name,
            date : interview.created.toDateTime('month D Y')
          })
        );
        var anchor = this.$('interviews').select('a').last();
        anchor.writeAttribute('href', '/#oracle/interviews/interview/' + interview.id);
      }.bind(this)
    );
    if (this.resources.interviews.present()) this.$('interviews').show();
    this.show();
  }
});

Controller.DashboardContactsUpdate = Class.create(Controller.Base, {
  attachAddRemove: function(name) {
    this[name] = new AddRemove(this.$(name), {
      model: ('contact-' + name).constantize().singular(),
      parameters: this.parameters,
      present: [
        'number',
        'email',
        'website',
        'street',
        'city',
        'state',
        'country',
        'zip',
        'month',
        'day',
        'year'
      ],
      repeatLabels: name == 'address' ? true : false
    });
  },
  
  authenticated: function() {
    return Session.user.listingId;
  },
  
  beforeUnload: function() {
    if (this.nameObserver) this.nameObserver.stop();
  },
    
  header: function() {
    var components = [Html.a({ href: '/#dashboard' }, 'Dashboard')];
    var history = History.get().findAll(
      function(entry) {
        var self = ['DashboardContactsContact', 'DashboardContactsUpdate'];
        return self.exclude(entry.controller);
      }
    );
    var previous = history.last();
    
    if (previous.controller == 'DashboardReservations') {
      var href = '/#dashboard/reservations';
      components.push(Html.a({ href: href }, 'Reservations'));
      
    } else if (previous.controller == 'DashboardReservationsReservation') {
      var href = '/#dashboard/reservations';
      components.push(Html.a({ href: href }, 'Reservations'));
      
      var content = previous.id ? 'Reservation' : 'Add Reservation';
      components.push(Html.a({ href: previous.uri }, content));
      
    }
    
    components.push(Parameters.id ? 'Contact' : 'Add Contact');
    
    return components.join(' / ');
  },
  
  initialize: function() {
    this.contact = Parameters.id ? Model.Contact.get(Parameters.id) : {};
    if (!this.contact.id) {
      if (Parameters.id) return this.pageNotFound();
      this.contact.id = Id.get();
      this.contact.listingId = Session.user.listingId;
    }
    this.parameters = {
      contactId: this.contact.id,
      listingId: this.contact.listingId
    };
    
    this.insertYears();
    this.attachAddRemove('numbers');
		this.attachAddRemove('emails');
		this.attachAddRemove('websites');
		this.attachAddRemove('addresses');
		this.attachAddRemove('dates');
		this.$('interface').observe('keypress',
		  function(event) {
		    if (event.element() == this.$('description')) return;
		    if (event.keyCode == Event.KEY_RETURN) this.save();
		  }.bind(this)
		);
    this.$('button').observe('click', this.save.bind(this));
    
    if (Parameters.id) {
      var href = '/#dashboard/contacts/contact/' + Parameters.id;
      this.$('cancel').writeAttribute('href', href);
      this.setContact();
      this.setContactNumbers();
      this.setContactEmails();
      this.setContactWebsites();
      this.setContactAddresses();
      this.setContactDates();
      this.observeName();
    } else {
      this.$('cancel').hide();
    }
    this.elapsed();
  },
  
  insertYears: function() {
    var options = [{ key: '', value: 'Year' }];
    var year = 'UTC'.toDateTime('Y');
    (100).times(
      function(index) {
        options.push({key: year, value: year});
        year--;
      }
    );
    this.$('dates').down('select', 2).setValue({ options: options });
  },
  
  observeName: function() {
    this.nameObserver = new Form.Element.Observer(this.$('name'), 0.06,
		  function(element, value) {
		    if (value.blank()) {
		      this.$('header').hide();
		    } else {
  		    this.$('header-name').update(value);
  		    this.$('header').show();
  		  }
		  }.bind(this)
		);
  },
  
  save: function() {
    try {
      var contact = this.$('form').serialize();
      contact.listingId = this.contact.listingId;
      contact.id = this.contact.id;
      Model.Contact.set(contact);
      this.numbers.save();
      this.emails.save();
      this.websites.save();
      this.addresses.save();
      this.dates.save();
      if (Parameters.id) {
        var message = 'Your contact was successfully updated.';
        var extra = '';
        window.location = '/#dashboard/contacts/contact/' + Parameters.id;
      } else {
        var message = 'Your contact was successfully added.';
        var href = '/#dashboard/contacts/contact/' + this.contact.id;
        var extra = Parameters.id ? '' : Html.a({ href: href }, 'View');
        this.parameters.contactId = Id.get();
        this.$('form').clear();
        this.numbers.reset();
        this.emails.reset();
        this.websites.reset();
        this.addresses.reset();
        this.dates.reset();
        this.$('name').focus();
        this.$('name').blur();
      }
      Notice.success(message, extra);
    } catch (error) {
      Notice.error(error);
    }
  },
  
  setContact: function() {
    this.$('header-name').update(this.contact.name);
    this.$('header').show();
    this.$('name').setValue(this.contact.name);
    this.$('company').setValue(this.contact.company);
    this.$('title').setValue(this.contact.title);
    this.$('description').setValue(this.contact.description);
  },
  
  setContactAddresses: function() {
    Model.ContactAddress.get({ contactId: Parameters.id }).each(
      function(address, index) {
        if (index > 0) this.addresses.add();
        var element = this.addresses.elements.last();
        element.down('input[name="id"]').setValue(address.id);
        element.down('input[name="street"]').setValue(address.street);
        element.down('input[name="city"]').setValue(address.city);
        element.down('input[name="state"]').setValue(address.state);
        element.down('input[name="country"]').setValue(address.country);
        element.down('input[name="zip"]').setValue(address.zip);
        element.down('select').setValue(address.category);
      }.bind(this)
    );
  },
  
  setContactDates: function() {
    Model.ContactDate.get({ contactId: Parameters.id }).each(
      function(date, index) {
        if (index > 0) this.dates.add();
        var element = this.dates.elements.last();
        element.down('input[name="id"]').setValue(date.id);
        element.down('select[name="month"]').setValue(date.month || '');
        element.down('select[name="day"]').setValue(date.day || '');
        element.down('select[name="year"]').setValue(date.year || '');
        element.down('select[name="category"]').setValue(date.category);
      }.bind(this)
    );
  },
  
  setContactEmails: function() {
    Model.ContactEmail.get({ contactId: Parameters.id }).each(
      function(email, index) {
        if (index > 0) this.emails.add();
        var element = this.emails.elements.last();
        element.down('input[name="id"]').setValue(email.id);
        element.down('input[name="email"]').setValue(email.email);
        element.down('select').setValue(email.category);
      }.bind(this)
    );
  },
  
  setContactNumbers: function() {
    Model.ContactNumber.get({ contactId: Parameters.id }).each(
      function(number, index) {
        if (index > 0) this.numbers.add();
        var element = this.numbers.elements.last();
        element.down('input[name="id"]').setValue(number.id);
        element.down('input[name="number"]').setValue(number.number);
        element.down('select').setValue(number.category);
      }.bind(this)
    );
  },
  
  setContactWebsites: function() {
    Model.ContactWebsite.get({ contactId: Parameters.id }).each(
      function(website, index) {
        if (index > 0) this.websites.add();
        var element = this.websites.elements.last();
        element.down('input[name="id"]').setValue(website.id);
        element.down('input[name="website"]').setValue(website.website);
        element.down('select').setValue(website.category);
      }.bind(this)
    );
  },
  
  title: function() {
    return Parameters.id ? 'Contact' : 'Add Contact';
  }
});

Controller.DashboardSetupHours = Class.create(Controller.Base, {  
  afterAddHour: function(element, addRemove, event) {
    if (!event) return;

    if (element.previous()) {
      var selects = element.select('select');
      selects.each(
        function(select, index) {
          var previous = element.previous().select('select')[index];
          select.selectedIndex = previous.selectedIndex;
          select.refreshSelect();
        }
      );

      // Increment day index:
      var day = selects.length == 3 ? selects[0] : selects[2];
      var index = day.selectedIndex;
      if (index === 0) return;
      day.selectedIndex = index == day.options.length - 1 ? 1 : index + 1;
      day.refreshSelect();
    }
  },
  
  authenticated: function() {
    return Session.user.listingId;
  },
  
  header: '<a href="/#dashboard">Dashboard</a> / Setup',
  
  initialize: function() {
    this.existing = {};
    this.given = {};
    
    this.insertTimes(this.$('hours-from'));
    this.insertTimes(this.$('hours-until'), true);
    
    var from = this.$('hours-from').innerHTML;
    var until = this.$('hours-until').innerHTML;
    
    this.$('specific-dates-open-from').update(from);
    this.$('specific-dates-open-until').update(until);
    this.$('specific-dates-closed-from').update(from);
    this.$('specific-dates-closed-until').update(until);

    this.arrivalRateHelp = new Help({
      link: this.$('arrival-rate-help-link'),
      text: this.$('arrival-rate-help-text')
    });
    this.noticeHelp = new Help({
      link: this.$('notice-period-help-link'),
      text: this.$('notice-period-help-text')
    });
    this.turnTimeHelp = new Help({
      link: this.$('turn-time-help-link'),
      text: this.$('turn-time-help-text')
    });
    this.hours = new AddRemove(this.$('hours'), {
      afterAdd: this.afterAddHour.bind(this),
      model: 'Hour',
      parameters: {
        listingId: Session.user.listingId
      },
      present: ['day'],
      required: true
    });
    this.specificDatesOpen = new AddRemove(this.$('specific-dates-open'), {
      afterAdd: this.afterAddHour.bind(this),
      model: 'SpecificDate',
      parameters: {
        category: 'Open',
        listingId: Session.user.listingId
      },
      present: ['from'],
      representation: function(element) {
        var object = element.down('form').serialize();
        var values = element.select('select').invoke('getValue');
        object.date = values[0] + '-' + values[1] + '-' + values[2];
        return object;
      }
    });
    this.specificDatesClosed = new AddRemove(this.$('specific-dates-closed'), {
      afterAdd: this.afterAddHour.bind(this),
      model: 'SpecificDate',
      parameters: {
        category: 'Closed',
        listingId: Session.user.listingId
      },
      present: ['from'],
      representation: function(element) {
        var object = element.down('form').serialize();
        var values = element.select('select').invoke('getValue');
        object.date = values[0] + '-' + values[1] + '-' + values[2];
        return object;
      }
    });
    this.$('button').observe('click', this.save.bind(this));
    
    this.$('specific-dates-open').observe('change', this.onChangeSpecificDate.bind(this));
    this.$('specific-dates-closed').observe('change', this.onChangeSpecificDate.bind(this));
    
    this.setInterface();
  },
  
  insertDates: function(element) {
    var options = Datetime.dates({
      from: 'now'.toDateTime('Y-M-D'),
      length: 365,
      options: true,
      format: 'D MONTH Y'
    });
    options.unshift({ key: '', value: $F(element) });
    element.setValue({ options: options });
  },
  
  insertTimes: function(element, until) {
    var options = [{key: '', value: $F(element)}];
    for(var index = 0; index < 24; index = index + 0.5) {
      var key = index.toTime();
      var value = (until ? index + 0.5 : index).toTime().substr(0, 5);
      options.push({key: key, value: value});
    }
    element.setValue({ options: options });
  },
  
  minWidth: function() {
    return '720px';
  },
  
  onChangeSpecificDate: function(event, month, select) {
    if (event && event.element().readAttribute('title') != 'Month') return;
    
    month = month || event.element().getValue() * 1;
    select = select || event.element().up().next().down('select');
    
    if ([2, 4, 6, 9, 11].include(month)) {
      select.remove(31);
      if (month == 2) {
        select.remove(30);
      } else if (select.length == 30) {
        select.add(new Option(30, 30));
      }
    } else {
      if (select.length == 30) {
        select.add(new Option(30, 30));
        select.add(new Option(31, 31));
      } else if (select.length == 31) {
        select.add(new Option(31, 31));
      }
    }
  },
    
  save: function() {
    try {
      var listing = this.$('form').serialize();
      listing.id = Session.user.listingId;
      Model.Listing.set(listing);
      this.hours.save();
      this.specificDatesClosed.save();
      this.specificDatesOpen.save();
      Notice.success('Your hours were successfully saved.');
    } catch (error) {
      Notice.error(error);
    }
  },
  
  setArrivalRate: function(listing) {
    var options = [];
    (20).times(
      function(index) {
        var people = (index + 1) * 10;
        options.push({ key: people, value: people + ' People' });
      }
    );
    this.$('arrival-rate').setValue({
      options: options,
      value: listing.arrivalRate
    });
  },
  
  setHours: function(representation) {
    representation.each(
      function(hour, index) {
        if (index > 0) this.hours.add();
        this.hours.elements.last().down('[name="id"]').setValue(hour.id);
        this.hours.elements.last().down('[name="listingId"]').setValue(hour.listingId);
        this.hours.elements.last().down('[name="day"]').setValue(hour.day);
        this.hours.elements.last().down('[name="from"]').setValue(hour.from);
        this.hours.elements.last().down('[name="until"]').setValue(hour.until);
      }.bind(this)
    );
  },
  
  setInterface: function() {
    var listing = Model.Listing.get(Session.user.listingId);
    
    var days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];

    var hours = Model.Hour.get({ listingId: Session.user.listingId });
    hours = hours.order('from').sortBy(
      function(hour) {
        return days.indexOf(hour.day);
      }
    );
    this.setHours(hours);

    var dates = Model.SpecificDate.get({ listingId: Session.user.listingId });
    dates = dates.order('date', 'from').partition(
      function(element) {
        return element.category == 'Open';
      }
    );
    this.setSpecificDates('open', dates.first());
    this.setSpecificDates('closed', dates.last());

    this.setArrivalRate(listing);
    this.setNoticePeriod(listing);
    this.setTurnTime(listing);

    this.elapsed();
  },
  
  setNoticePeriod: function(listing) {
    var options = [];
    (72).times(
      function(index) {
        var key = index + 1;
        var value = key.toPaddedString(2) + ' Hour';
        if (value != '01 Hour') value += 's';
        options.push({ key: key, value: value });
      }
    );
    this.$('notice-period').setValue({
      options: options,
      value: listing.noticePeriod
    });
  },
  
  setSpecificDates: function(category, representation) {
    var addRemove = this['specificDates' + category.toTitleCase()];
    representation.each(
      function(date, index) {
        if (index > 0) addRemove.add();
        var element = addRemove.elements.last();
        element.down('[name="id"]').setValue(date.id);
        element.down('[name="listingId"]').setValue(date.listingId);
        
        var selects = element.select('select');
        var dateComponents = date.date.split('-');

        selects[0].selectedIndex = dateComponents[0] - 2007;
        selects[0].refreshSelect();
        selects[1].selectedIndex = dateComponents[1];
        selects[1].refreshSelect();
        // Update days for selected month:
        this.onChangeSpecificDate(undefined, dateComponents[1], selects[2]);
        
        selects[2].selectedIndex = dateComponents[2];
        selects[2].refreshSelect();
        selects[3].setValue(date.from);
        selects[4].setValue(date.until);
      }.bind(this)
    );
  },
  
  setTurnTime: function(listing) {
    var options = [];
    (24).times(
      function(index) {
        var hour = (index + 1).toPaddedString(2);
        var key = hour + ':00:00';
        var value = (hour + ' Hour').inflect();
        options.push({ key: key, value: value });
      }
    );
    this.$('turn-time').setValue({
      options: options,
      value: listing.turnTime
    });
  },

  title: function() {
    return 'Setup';
  }
});

Controller.Home = Class.create(Controller.Base, {
  beforeUnload: function() {
    $('Base.logo').setStyle({ cursor: 'pointer' });
    $('Base.logo').writeAttribute('onclick', 'return true');
  },
  
  header: 'A Web-Based Reservations Book For Restaurateurs',
  
  initialize: function() {
    $('Base.logo').setStyle({ cursor: 'default' });
    $('Base.logo').writeAttribute('onclick', 'return false');
    this.$('button').observe('click', this.start.bind(this));
  },
  
  start: function() {
    var user = this.setUser();
    alert(JSON.stringify(user));
    var listing = this.setListing(user);
    alert(JSON.stringify(listing));
  },
  
  setListing: function(user) {
    var listing = {};
    listing.id = Id.get();
    listing.userIds = [user.id];
    return Model.Listing.set(listing);
  },
  
  setUser: function() {
    var user = {};
    user.id = Id.get();
    user.name = String(user.id);
    user.email = user.id + '@sexbyfood.com';
    var a1 = user.email + ':' + Config.DigestAuthentication.realm + ':' + user.id;
    user.ha1 = a1.md5();
    return Model.User.set(user);
  },
  
  title: 'A Web-Based Reservations Book For Restaurateurs'
});

Controller.Hq = Class.create(Controller.Base, {
  initialize: function() {
    this.oracles = Model.Oracle.get({
      created: '>' + ('UTC'.toDateTime('T') - (86400 * 30)).toDateTime('Y-M-D')
    });
    this.setData();
  },
  
  authenticated: function() {
    return [1, 77].include(Session.user.id);
  },
  
  findAllByDate: function(date) {
    return this.oracles.findAll(
      function(oracle) {
        return oracle.created.startsWith(date);
      }
    );
  },
  
  countByCategory: function(oracles, category) {
    var oracles = oracles.findAll(
      function(oracle) {
        var regex = new RegExp ('^' + category, 'i');
        return regex.test(oracle.body);
      }
    );
    return oracles ? oracles.length : 0;
  },
  
  setData: function() {
    this.dates = this.getDates();
    this.data = {
      'Calls': [],
      'Commits': [],
      'Drop-Ins': [],
      'Emails': [],
      'Meetings': []
    };
    this.dates.each(
      function(date) {
        var oracles = this.findAllByDate(date);
        Object.keys(this.data).each(
          function(key) {
            var count = this.countByCategory(oracles, key.singular());
            this.data[key].push(count);
          }.bind(this)
        );
      }.bind(this)
    );
    new Chart.Line(this.$('chart'), {
      data: this.data,
      labels: this.dates,
      width: 880,
      height: 330
    });
    this.show();
  },
  
  getDates: function() {
    var today = 'UTC'.toDateTime('T');
    var day = 24 * 60 * 60;
    var dates = [];
    (30).times(
      function(index) {
        var timestamp = today - ((29 - index) * day);
        var date = (timestamp + '').toDateTime('Y-M-D');
        dates.push(date);
      }
    );
    return dates;
  }
});

Controller.SignIn = Class.create(Controller.Base, {	
	createAccountOrSignIn: function() {
	  if (this.$('new-user').checked) {
		  Flash.set('SignInCreateAccount', 'email', this.$F('email'));
			window.location = '/#contact';
			var contact = Html.a({ href: 'mailto:cara@garisch.com' }, 'Cara Garisch');
			Notice.success('Please contact ' + contact + ' to get started.');
		} else {
			this.signIn();
		}
	},
	
  beforeUnload: function() {
    $('Base.sign-in').update('Sign In'.toLink('/#sign-in'));
  },
  
  initialize: function() {    
		this.$('new-user').observe('click', this.newUser.bind(this));
		this.$('new-user-caption').observe('click', this.newUser.bind(this));
		this.$('old-user').observe('click', this.oldUser.bind(this));
		this.$('old-user-caption').observe('click', this.oldUser.bind(this));
		this.$('email').observe('keypress',
		  function(event) {
		    if (event.keyCode == Event.KEY_RETURN) this.$('password').activate();
		  }.bind(this)
		);
		this.$('password').observe('keypress',
		  function(event) {
		    if (event.keyCode == Event.KEY_RETURN) this.createAccountOrSignIn();
		  }.bind(this)
		);
		this.$('password').observe('focus',
		  function() {
		    this.$('old-user').checked = true;
		    this.$('button').value = 'Sign In';
		  }.bind(this)
		);
		this.$('button').observe('click', this.createAccountOrSignIn.bind(this));
		$('Base.sign-in').update('Sign In');
		this.$('email').activate();
	},
	
	newUser: function() {
		this.$('new-user').checked = true;
		this.$('button').value = 'Continue';
	},
	
	oldUser: function() {
		this.$('old-user').checked = true;
		this.$('button').value = 'Sign In';
		this.$('password').activate();
	},
	
	signIn: function() {
		this.$('button').loading('Signing In');
		Session.set({
		  email: this.$F('email'),
		  password: this.$F('password'),
		  onFailure: function(error) {
		    Notice.error(error);
		    this.$('button').loaded();
		  }.bind(this),
		  onProgress: function(percentage) {
		    this.$('progress').update(percentage.round().toPaddedString(2) + '%');
		  }.bind(this)
		});
	}
});

Controller.DashboardContactsContact = Class.create(Controller.Base, {
  addNote: function() {
    try {
      Model.Note.set({
        id: Id.get(),
        listingId: Session.user.listingId,
        parentId: Parameters.id,
        parentType: 'Contact',
        userId: Session.user.id,
        body: this.$F('note-body')
      });
      this.$('note-body').clear();
      this.setNotes();
      Notice.success('Your note was successfully added.');
    } catch (error) {
      Notice.error(error);
    }
  },
    
  attachEditDeleteToNotes: function(notes) {
    notes.each(
      function(note) {
        new EditDelete(this.$('notes-' + note.id), {
          context: this,
          model: 'Note',
          id: note.id,
          afterUndo: function() {
            this.hideEmptyNoteDates();
          }.bind(this),
          afterUnset: function(element) {
            this.hideEmptyNoteDates();
          }.bind(this),
          beforeEdit: function(element) {
            var note = element.down('.element').innerHTML;
            element.next().down('textarea').setValue(note).focus();
          }.bind(this),
          afterSave: function(element, note) {
            element.down('.element').update(note.body);
          }.bind(this)
        });
      }.bind(this)
    );
  },

  formatUserName: function(name) {
    var names = (name || '').strip().split(' ').collect(
      function(name, index) {
        if (!name.blank() && index < 5) {
          return index === 0 ? name.truncate(7, '..') : name.substr(0, 1);
        } else {
          return null;
        }
      }
    );
    return names.compact().join(' ');
  },
    
  header: function() {
    var components = [Html.a({ href: '/#dashboard' }, 'Dashboard')];
    var history = History.get().findAll(
      function(entry) {
        var self = ['DashboardContactsContact', 'DashboardContactsUpdate'];
        return self.exclude(entry.controller);
      }
    );
    var previous = history.last() || {};
    
    if (previous.controller == 'DashboardReservations') {
      var href = '/#dashboard/reservations';
      components.push(Html.a({ href: href }, 'Reservations'));
      
    } else if (previous.controller == 'DashboardReservationsReservation') {
      var href = '/#dashboard/reservations';
      components.push(Html.a({ href: href }, 'Reservations'));
      
      var content = previous.id ? 'Reservation' : 'Add Reservation';
      components.push(Html.a({ href: previous.uri }, content));
      
    }
    
    components.push(Parameters.id ? 'Contact' : 'Add Contact');
    
    return components.join(' / ');
  },
  
  hideEmptyNoteDates: function() {
    this.$('notes').childElements().each(
      function(element) {
        if (!element.match('div')) return;
        var empty = element.childElements().all(
          function(child) {
            return !child.visible();
          }
        );
        if (empty) {
          element.previous('p').hide();
        } else {
          element.previous('p').show();
        }
      }
    );
  },
  
  initialize: function() {
    this.contact = Parameters.id ? Model.Contact.get(Parameters.id) : undefined;
    if (!this.contact) return this.pageNotFound();

    this.setContact();
    this.setReservations();
    this.setNotes();
    
    this.elapsed();
  },
  
  insertInformation: function(index, information) {
    this.$('information').show().insert(this.information.interpolate({
      label: index === 0 ? information.label : '&nbsp;',
      data: information.data,
      category: information.category ? information.category : '&nbsp;'
    }));
  },
  
  setContact: function() {
    // Set contact name:
    this.$('contact-header').update(this.contact.name);
    
    // Set edit anchor tag:
    var href = '/#dashboard/contacts/update/' + this.contact.id;
    this.$('edit').down().writeAttribute('href', href);
    
    // Observe UnsetUndo on delete anchor tag:
    UnsetUndo.observe({
      context: this,
      element: this.$('contact'),
      control: this.$('delete'),
      model: 'Contact',
      id: this.contact.id,
      afterUnset: function() {
        this.$('edit').hide();
        this.$('delete').hide();
        this.$('deleted').show();
      }.bind(this),
      afterUndo: function() {
        this.$('deleted').hide();
        this.$('delete').show();
        this.$('edit').show();
      }.bind(this),
      unset: function(model, id) {
        var representation = { id: id, visible: false };
        Model.Contact.set(representation);
      }
    });
    
    // Set contact title:
    if (this.contact.title || this.contact.company) {
      var title = [this.contact.company, this.contact.title].compact().join(' ');
      this.$('title').update(title).show();
    }
    
    // Set contact description:
    if (this.contact.description) {
      this.$('description').update(this.contact.description).show();
    }
    
    // Set contact information template:
    this.information = this.$('information').replaceContent();
    
    // Set contact information:
    this.setContactNumbers();
    this.setContactEmails();
    this.setContactWebsites();
    this.setContactAddresses();
    this.setContactDates();
  },
  
  setContactAddresses: function() {
    Model.ContactAddress.get({ contactId: Parameters.id }).each(
      function(address, index) {
        var city = [address.city, address.zip, address.state].compress().join(', ') || null;
        var data = [address.street, city, address.country].compress();
        if (data[0]) data[0] += '<span class="padding-left">' + address.category + '</span>';
        if (address.city || address.state) {
          var location = [address.street, address.city, address.state, address.country];
          location = location.compress().join(', ');
          var uri = 'http://maps.google.com/maps?q=' + encodeURIComponent(location);
          data.push('Map'.toLink(uri));
        }
        this.insertInformation(index, {
          label: 'Address:',
          data: data.join('<br/>'),
          category: null
        });
      }.bind(this)
    );
  },
  
  setContactDates: function() {
    Model.ContactDate.get({ contactId: Parameters.id }).each(
      function(date, index) {
        var data = [date.day, Datetime.monthNames[date.month - 1], date.year].compact().join(' ');
        this.insertInformation(index, {
          label: 'Date:',
          data: data,
          category: date.category
        });
      }.bind(this)
    );
  },
  
  setContactEmails: function() {
    Model.ContactEmail.get({ contactId: Parameters.id }).each(
      function(email, index) {
        this.insertInformation(index, {
          label: 'Email:',
          data: email.email.toEmail(email.email),
          category: email.category
        });
      }.bind(this)
    );
  },
  
  setContactNumbers: function() {
    Model.ContactNumber.get({ contactId: Parameters.id }).each(
      function(number, index) {
        this.insertInformation(index, {
          label: 'Phone:',
          data: number.number,
          category: number.category
        });
      }.bind(this)
    );
  },
  
  setContactWebsites: function() {
    Model.ContactWebsite.get({ contactId: Parameters.id }).each(
      function(website, index) {
        this.insertInformation(index, {
          label: 'Website:',
          data: website.website.toLink(website.website),
          category: website.category
        });
      }.bind(this)
    );
  },
  
  // Idempotent.
  setNotes: function() {
    if (!this.note) {
      // Set notes header:
      var name = this.contact.name.possessive();
      this.$('notes-header').update(name + ' History');
            
      // Set event handlers for add note form:
      this.$('note-button').observe('click', this.addNote.bind(this));
      
      // Set note template:
      this.note = this.$('notes').replaceContent();
    }

    // Set notes:    
    var notes = Model.Note.get({
      parentId: Parameters.id,
      parentType: 'Contact'
    });
    
    new NoteHistory(this.$('notes'), {
      notes: notes,
      parentType: 'Contact'
    });
  },
  
  setReservations: function() {
    var name = this.contact.name.possessive();
    this.$('reservations-header').update(name + ' Reservations');
    
    var template = this.$('reservations').replaceContent();
    var reservations = Model.Reservation.get({ contactId: Parameters.id });
    
    reservations.each(
      function(reservation, index) {
        var uri = '/#dashboard/reservations/reservation/' + reservation.id;
        var qualifier = reservation.people == 1 ? ' Person' : ' People';
        this.$('reservations').insert(template.interpolate({
          label: index === 0 ? 'Reservations:' : '&nbsp;',
          date: reservation.date.toDateTime('Y MONTH D DAY').toLink(uri),
          time: reservation.time.substr(0, 5),
          people: reservation.people + qualifier,
          category: reservation.category
        }));
      }.bind(this)
    );
    
    if (reservations.empty()) {
      this.$('reservations-hr').hide();
      this.$('reservations-header').hide();
      this.$('reservations').hide();
    }
  },
  
  title: 'Contact'
});

Controller.Dashboard = Class.create(Controller.Base, {
  add: function() {
    try {
      Model.Note.set({
        id: Id.get(),
        listingId: Session.user.listingId,
        userId: Session.user.id,
        parentId: Session.user.listingId,
        parentType: 'Listing',
        body: this.$F('body')
      });
      this.$('body').clear().focus();
      this.setNotes();
    } catch (error) {
      Notice.error(error);
    }
  },
  
  authenticated: function() {
    return Session.user.listingId;
  },
  
  beforeUnload: function() {
    $('Base.dashboard').update(Html.a({ href: '/#dashboard' }, 'Dashboard'));
    this.refresh.stop();
  },
  
  initialize: function() {
    $('Base.dashboard').update('Dashboard');
    this.$('body').focus();
    this.$('body').observe('keypress',
      function(event) {
        if (event.keyCode == Event.KEY_RETURN) this.add();
      }.bind(this)
    );
    this.$('button').observe('click', this.add.bind(this));
    this.setNotes();
    this.refresh = this.setNotes.bind(this).repeat((10).seconds());
    this.elapsed();
  },
  
  setNotes: function() {    
    var notes = Model.Note.get({
      listingId: Session.user.listingId,
      created: '>' + 'UTC'.toDateTime('Y-M-D') + ' 00:00:00'
    });
    new NoteHistory(this.$('notes'), { notes: notes });
  }
});

Controller.SignInChangePassword = Class.create(Controller.Base, {	
	changePassword: function(email) {
	  var password = this.$F('step-3-password') || null;
	  if (password != this.$F('step-3-password-confirmation')) {
	    this.$('step-3-button').loaded();
	    this.$('step-3-progress').hide();
	    Notice.error('Please confirm your new password.');
	    return;
	  }
	  
	  email = email.strip().toLowerCase();
	  var a1 = email + ':' + Config.DigestAuthentication.realm + ':' + password;
	  var user = { id: this.userId(), email: email, ha1: a1.md5() };
	  
		Model.User.set(user, {
		  onSuccess: this.updateSession.bind(this, email, password),
		  onFailure: function(error) {
		    this.$('step-3-button').loaded();
		    this.$('step-3-progress').hide();
	      Notice.error(error);
		  }.bind(this),
		  onTimeout: function(error) {
		    this.$('step-3-button').loaded();
		    this.$('step-3-progress').hide();
		    Notice.error('Please reconnect to the Internet.');
		  }
		});
	},

	deliverPasswordAssistance: function() {
	  var email = this.$F('step-1-email');
		if (email.blank()) {
		  Notice.error('Please provide your email address.');
		} else {
  		this.$('step-1-button').loading();
  		Http.put('/api/password-assistances/' + Id.get(), {
  		  headers: {
  		    'Content-Type': 'application/json; charset=UTF-8'
  		  },
  		  body: JSON.stringify({ email: this.$F('step-1-email') }),
  		  onSuccess: function(body) {
  		    this.$('step-1-button').loaded();
  		    this.$('step-1').hide();
  		    this.$('step-2').show();
  		  }.bind(this),
  		  onFailure: function(error, response) {
  		    this.$('step-1-button').loaded();
  		    if (response.status == 404) {
  		      error = 'The email you entered does not match any accounts on record.';
  		    }
		      Notice.error(error);
  		  }.bind(this),
  		  onTimeout: function(error) {
  		    Notice.error('Please reconnect to the Internet.');
  		  }
  		});
		}
	},
	
	header: Html.a({ href: '/#sign-in' }, 'Sign In') + ' / Change Password',
	
	ha1: function() {
	  return Parameters.hash.substr(0,32);
	},
	
	initialize: function() {
		this.$('step-1-email').observe('keypress',
		  function(event) {
		    if (event.keyCode == Event.KEY_RETURN) this.deliverPasswordAssistance();
		  }.bind(this)
		);
		this.$('step-1-button').observe('click', this.deliverPasswordAssistance.bind(this));
		this.$('step-3-password-confirmation').observe('keypress',
		  function(event) {
		    if (event.keyCode == Event.KEY_RETURN) this.setSession();
		  }.bind(this)
		);
		this.$('step-3-button').observe('click', this.setSession.bind(this));
		if (Parameters.hash) {
		  this.$('step-3').show();
		  this.$('step-3-email').focus();
		} else {
		  this.$('step-1').show();
  		this.$('step-1-email').focus();
		}
	},
	
	setSession: function() {
		var email = this.$F('step-3-email');
	  if (email.blank()) return Notice.error('Please provide your email address.');
		this.$('step-3-button').loading('Saving');
		
    Session.set({
		  email: email,
		  ha1: this.ha1(),
		  onFailure: function(error) {
		    if (error == 'Please provide a valid email address and password.') {
  		    error = 'Your password assistance link is no longer valid.';
  		  }
		    this.$('step-3-button').loaded();
		    Notice.error(error);
		  }.bind(this),
		  onProgress: function(percentage) {
		    percentage = (percentage / 2).round().toPaddedString(2);
		    this.$('step-3-progress').update(percentage + '%');
		  }.bind(this),
		  onSuccess: this.changePassword.bind(this, email)
		});
	},
	
	title: 'Change Password',
	
	updateSession: function(email, password) {
	  Session.set({
		  email: email,
		  password: password,
		  onFailure: function(error) {
		    this.$('step-3-button').loaded();
		    this.$('step-3-progress').hide();
		    Notice.error(error);
		  }.bind(this),
		  onProgress: function(percentage) {
		    percentage = (50 + (percentage / 2)).round().toPaddedString(2);
		    this.$('step-3-progress').update(percentage + '%');
		  }.bind(this),
		  onSuccess: function() {
		    this.afterInitializeUpdateNavigation();
		    this.$('step-3-button').loaded();
		    this.$('step-3-progress').hide();
		    this.$('step-3').hide();
		    this.$('step-4').show();
		  }.bind(this)
		});
	},
		
	userId: function() {
	  return Parameters.hash.substr(32) || 0; // 0 Ensures Resource.set generates 'Not Found'
	}
});

Controller.DashboardReservations = Class.create(Controller.Base, {
	addReservation: function(event) {
	  window.location = '/#dashboard/reservations/reservation';
	},
	
	authenticated: function() {
	  return Session.user.listingId;
	},
	
	beforeUnload: function() {
	  this.resizeObserver.stop();
	  this.contactObserver.stop();
	  $('Base.top').setStyle({ 'min-width': '' });
	},
	
	delighten: function(event, id) {
	  var element = event.element();
	  this.$(id).setStyle({ cursor: 'default' });
	  var grey = this.$(id).readAttribute('background-f3');
		this.$(id).removeClassName('background-e6');
		this.$(id).addClassName(grey ? 'background-f3' : '');
	},
	
	enlighten: function(event, id) {
	  var element = event.element();
	  var match = function(element) {
	    return ['[class="element"]', 'a', '[class="right s"]', 'select'].any(
	      function(selector) {
	        return element.match(selector);
	      }
	    );
	  };
	  if (this.isReservationColumn(element)) {
	    this.$(id).removeClassName('background-f3');
  	  this.$(id).addClassName('background-e6');
  	  this.$(id).setStyle({ cursor: 'pointer' });
  	}
	},
	
	evaluateTemplate: function(reservation) {
	  return this.template.evaluate({
	    'id': reservation.id,
			'time': reservation.time.substring(0,5),
			'name': this.getContactLink(reservation),
			'name-title': this.getContactTitle(reservation),
			'people': reservation.people,
			'tables': this.getTables(reservation),
			'tables-title': this.getTablesTitle(reservation),
			'notes': this.getNotes(reservation)
		});
	},
	
	extendReservations: function() {
	  this.reservations[this.date].each(
	    function(reservation, index) {
	      var contact = Model.Contact.get(reservation.contactId);
	      var seatingAreas = [], tables = [];
	      reservation.seatingAreaIds.each(
	        function(id) {
            var seatingArea = Model.SeatingArea.get(id);
            if (seatingArea) seatingAreas.push(seatingArea.name);
	        }.bind(this)
	      );
	      reservation.tableIds.each(
	        function(id) {
	          var table = Model.Table.get(id);
	          if (table) tables.push(table.name);
	        }.bind(this)
	      );
	      var notes = this.notes.findAll({ parentId: reservation.id });
	      this.reservations[this.date][index].contact = contact;
	      this.reservations[this.date][index].seatingAreas = seatingAreas;
	      this.reservations[this.date][index].tables = tables;
	      this.reservations[this.date][index].notes = notes;
	    }.bind(this)
	  );
	},
	
	getContactTitle: function(reservation) {
	  if (reservation.contact.title || reservation.contact.company) {
		  return [reservation.contact.company, reservation.contact.title].compress().join(' ');
		} else {
		  return reservation.contact.name || '';
		}
	},
	
	getContactLink: function(reservation) {
	  var name = (reservation.contact.name || '').truncate(50);
	  var href = '/#dashboard/contacts/contact/' + reservation.contact.id;
	  return Html.a({ href: href }, name);
	},
	
	getNotes: function(reservation) {
	  var noteCount = reservation.notes.length;
	  if (noteCount < 1) {
	    return '';
	  } else if (noteCount == 1) {
	    return '1 Note';
	  } else {
	    return noteCount + ' Notes';
	  }
	},
	
	getPerPage: function() {
	  var rows = ((this.height || 0) / 40).floor();
	  if (rows < 1) rows = 1;
	  return rows;
	},
	
	getTables: function(reservation) {
    if (reservation.tables.length > 5) {
      return (reservation.tables.slice(0, 5).join('  ') + '...').gsub(' ', '&nbsp;');
    } else if (reservation.tables.present()) {
      return reservation.tables.join('  ').gsub(' ', '&nbsp;');
    } else {
  	  return (reservation.seatingAreas.first() || '').truncate(22);
  	}
	},
	
	getTablesTitle: function(reservation) {
	  if (reservation.tables.present()) {
  	  var qualifier = reservation.tables.length == 1 ? 'Table ' : 'Tables ';
  	  return qualifier + reservation.tables.join(' ');
  	} else {
  	  return 'No Tables Allocated';
  	}
	},
	
	indexResources: function() {
	  this.contactNumbers = this.resources['contact-numbers'].index('contactId');
	  this.reservations = this.resources.reservations.index('date');
	  this.dates = Object.keys(this.reservations).sort();
	  this.notes = this.resources.notes.findAll({ parentType: 'Reservation' });
	},
			
  initialize: function() {
    $('Base.top').setStyle({ 'min-width': '894px' });
    
		this.page = 1;
    this.perPage = this.getPerPage();
    this.date = Controller.DashboardReservations.date || 'now'.toDateTime('Y-M-D');
    this.template = new Template(this.$('reservations').innerHTML);
    this.print = new Template(this.$('print-reservations').innerHTML);

		this.$('date').observe('mousedown', this.onMouseDownDate.bind(this));
		this.$('date').observe('mouseup', this.onMouseUpDate.bind(this));
		this.$('date').observe('change', this.onChangeDate.bind(this));
		this.$('add').observe('click', this.addReservation.bind(this));
				
		this.$('time').observe('change', this.reduce.bind(this));
		this.$('people').observe('change', this.reduce.bind(this));
		this.$('table').observe('change', this.reduce.bind(this));
		this.$('category').observe('change', this.reduce.bind(this));
		
		this.$('previous').observe('click', this.previous.bind(this));
		this.$('next').observe('click', this.next.bind(this));
		
    this.resources = {};
    // No need to refine notes or contact numbers by listingId as we search based
    // on parentId and contactId respectively and refining by listingId is slow.
    this.resources['contact-numbers'] = Model.ContactNumber.get();
    this.resources.notes = Model.Note.get();
    this.resources.reservations = Model.Reservation.get({
      listingId: Session.user.listingId
    });
    this.indexResources();

    this.setDate();
    this.elapsed();
    this.observeResize();
    this.resizeObserver = new PeriodicalExecuter(this.observeResize.bind(this), 0.005);
		this.contactObserver = new Form.Element.Observer(this.$('contact'), 0.08, this.reduce.bind(this));
	},
	
	insertPrint: function() {
	  var listing = Model.Listing.get(Session.user.listingId);
	  this.$('print-restaurant').update(listing.name + ' Reservations');
	  this.$('print-date').update(this.date.toDateTime('DAY d MONTH Y'));
	  this.$('print-reservations').update('');
	  var time = '';
	  this.reservations[this.date].order('time').each(
			function(reservation, index) {
			  if (['Cancelled', 'Dishonoured'].include(reservation.category)) return;
			  
			  if (time != reservation.time) {
			    time = reservation.time;
			    var timeOrBlank = time.substr(0, 5);
			    var html = '<tr style="height:20pt"><td></td><td></td></tr>';
			  } else {
			    var timeOrBlank = '';
			    var html = '';
			  }
			  
			  var number = '';
			  if (reservation.contact) {
			    number = (this.contactNumbers[reservation.contact.id] || {}).number;
			  }
			  
			  if (reservation.tables.present()) {
			    var allocation = reservation.tables.length == 1 ? 'Table ' : 'Tables ';
			    allocation += reservation.tables.join(', ');
			  } else {
			    var allocation = reservation.seatingAreas.join(', ');
			  }
			  
			  var notes = reservation.notes.collect(
			    function(note) {
			      var body = note.body;
			      if (!body.startsWith('Changed') && !body.startsWith('Accepted')) {
			        return body.strip();
			      } else {
			        return null;
			      }
			    }
			  ).compact();
			  var description = ((reservation.contact || {}).description || '').strip();
			  if (description.present()) notes.unshift(description);
			  if (reservation.category == 'Confirmed') notes.push('Confirmed');
			  
			  html += this.print.evaluate({
				  time: timeOrBlank,
				  name: (reservation.contact || {}).name,
				  number: number,
				  allocation: allocation,
				  people: reservation.people == 1 ? '1 Person' : reservation.people + ' People',
				  notes: notes.join('</span><span class="padding-left">')
				});
				this.$('print-reservations').insert(html);
			}.bind(this)
		);
	},
	
	insertReservations: function() {
	  this.extendReservations();
	  this.resetCounts();
		this.$('reservations').update('');
		this.reservations[this.date].order('time').each(
			function(reservation, index) {
			  var id = reservation.id;
				this.$('reservations').insert(this.evaluateTemplate(reservation));
				this.$(id).observe('mouseover', this.enlighten.bindAsEventListener(this, id));
				this.$(id).observe('mouseout', this.delighten.bindAsEventListener(this, id));
				this.$(id).observe('click', this.showReservation.bindAsEventListener(this, id));
    		this.$(id + '-category').observe('change', this.saveCategory.bindAsEventListener(this, id));
				this.$(id + '-category').setValue(reservation.category);
				this.updateCounts(reservation);
			}.bind(this)
		);
		this.reduce();
	},
	
	isMember: function(reservation, exception) {
	  var conditions = {
  	  from: reservation.time >= this.constraints.from,
      until: reservation.time <= this.constraints.until,
      contact: (reservation.contact.name || '').toLowerCase().include(this.constraints.contact),
      people: this.constraints.people ? reservation.people >= this.constraints.people : true,
      table: this.constraints.table ? reservation.tables.include(this.constraints.table) : true,
      category: this.constraints.category.include(reservation.category)
    };
    return Object.keys(conditions).all(
      function(key) {
        return key != exception ? conditions[key] : true;
      }
    );
	},
	
	isReservationColumn: function(element) {
	  return !['[class="element"]', 'a', '[class="right s"]', 'select'].any(
      function(selector) {
        return element.match(selector);
      }
    );
	},
	
	next: function(event) {
	  this.page = this.page == this.pages ? 1 : this.page + 1;
	  this.scroll();
	},
	
	observeResize: function() {
	  // Get coordinates for bottom of header:
	  var table = this.$('date').up('[class~="table"]');
	  var header = table.cumulativeOffset().top + table.getHeight() - 10 + 16;
	  // Get coordinates for top of footer:
	  var footer = this.$('time').cumulativeOffset().top - 16;
	  
	  // Red debugging markers:
    // if (!$('Base.marker1')) $('Base.top').insert('<div style="width:100px;height:10px;background:#FF0000;position:absolute" id="Base.marker1"></div>');
    // $('Base.marker1').setStyle({ left: '32px', top: header + 'px' });
    // if (!$('Base.marker2')) $('Base.top').insert('<div style="width:100px;height:10px;background:#FF0000;position:absolute" id="Base.marker2"></div>');
    // $('Base.marker2').setStyle({ left: '32px', top: footer + 'px' });
    
	  var height = -(header - footer);
	  if (this.ids && this.height != height) {
	    this.height = height;
      this.perPage = this.getPerPage();
      if (this.perPage == 1) {
        // Set 'padding-bottom' to prevent footer from rising further:
        this.$('reservations').setStyle({ 'padding-bottom': '51px' });
      } else {
        // Set 'padding-bottom' to default 'table-text':
        this.$('reservations').setStyle({ 'padding-bottom': '0px' });
      }
      this.pages = (this.ids.length / this.perPage).ceil();
      if (this.ids.empty()) this.pages = 1;
  	  this.scroll();
  	}
	},
	
	onChangeDate: function(event) {
    var element = this.$('date');
	  var date = element.getValue();

    if (this.date != date) this.page = 1;
    this.date = date;
    Controller.DashboardReservations.date = date;

    this.onMouseUpDate();
    
    this.insertReservations();
    this.insertPrint();
	},
	
	onMouseDownDate: function(event) {
	  // Revert text for previously selected date:
	  if (Object.isDefined(this.dateIndex)) {
	    var option = this.$('date').options[this.dateIndex];
      option.text = option.value.toDateTime('D DAY');
    }
	},
	
	onMouseUpDate: function(event) {
	  // Extend text for currently selected date:
    this.dateIndex = this.$('date').selectedIndex;
    var option = this.$('date').options[this.dateIndex];
    if (['Yesterday', 'Today', 'Tomorrow'].include(option.text)) {
      delete this.dateIndex;
      return;
    }
    option.text = option.value.toDateTime('Y MONTH D DAY');
    this.$('date').refreshSelect();
	},
	
	previous: function(event) {
	  this.page = this.page == 1 ? this.pages : this.page - 1;
	  this.scroll();
	},
	
	reduce: function(event) {
	  this.setConstraints();
	  this.setIds();
	  this.setPages(event);
	  this.setTime();
		this.setPeople();
		this.setTable();
		this.setCategories();
		// Avoid blank reservation screen:
		// Category may be constrained to Confirmed with one reservation showing.
		// Changing this reservation's status to Accepted would save and refresh.
		// No reservations would be present.
		// this.setCategories would be unable to setValue to Confirmed and default to blank.
		// Running the following again would then get it right.
		// Done like this to avoid recursive call to this.reduce.
		if (this.ids.empty()) {
		  this.setConstraints();
  	  this.setIds();
  	  this.setPages(event);
  	  this.setTime();
  		this.setPeople();
  		this.setTable();
  		this.setCategories();
		}
		this.setTotalPeople();
		this.scroll();
	},
	
	resetCounts: function() {
	  this.cancelled = 0;
		this.dishonored = 0;
		this.total = 0;
	},
	
	saveCategory: function(event, id) {
		var reservation = { id: id, category: event.element().getValue() };
		try {
  		Model.Reservation.set(reservation);
  		Notice.success('Your reservation category was successfully saved.');
  		this.$(id + '-notes').increment('Note');
  	} catch (error) {
  	  Notice.error(error);
  	}
	},
	
	scroll: function() {
	  if (this.page > this.pages) this.page = this.pages;
		var rows = this.$('reservations').childElements();
		rows.invoke('hide');
		var offset = (this.page - 1) * this.perPage;
		var limit = offset + this.perPage - 1;
		var grey = false;
		this.ids.each(
		  function(id, index) {
		    if (index < offset || index > limit) return;
		    var element = this.$(id);
		    if (grey) {
		      element.addClassName('background-f3');
		      element.writeAttribute({ 'background-f3': true });
		    } else {
		      element.removeClassName('background-f3');
		      element.writeAttribute({ 'background-f3': false });
		    }
		    element.show();
  			grey = !grey;
		  }.bind(this)
		);
		this.updateNavigation(offset, limit);
	},
	
	setCategories: function() {
	  var options = [{key: 'Accepted,Confirmed,Seated', value: 'Category'}];
	  var categories = this.reservations[this.date].inject([],
	    function(categories, reservation) {
	      if (this.isMember(reservation, 'category')) categories.push(reservation.category);
	      return categories;
	    }.bind(this)
	  );
	  var order = ['Accepted', 'Confirmed', 'Seated', 'Cancelled', 'Dishonored'];
	  categories = categories.unique().sortBy(
	    function(category) {
	      return order.indexOf(category);
	    }
	  );
	  categories.each(
	    function(category) {
	      options.push(category);
	    }
	  );
	  this.$('category').setValue({options: options, value: this.$F('category')});
	},
	
	setConstraints: function() {
	  var time = this.$F('time').split(',');
	  this.constraints = {
	    from: time.first(),
	    until: time.last(),
	    contact: this.$F('contact').toLowerCase(),
	    people: this.$F('people'),
	    table: this.$F('table'),
	    category: this.$F('category').split(',')
	  };
	  if (this.$('category').selectedIndex === 0) {
	    if (this.total - this.cancelled - this.dishonored === 0) {
	      this.constraints.category = ['Cancelled', 'Dishonored'];
	    }
	  }
	},
	
	setDate: function() {
		var options = [], value = null;
		var yesterday = 'yesterday'.toDateTime('Y-M-D');
		var today = 'today'.toDateTime('Y-M-D');
		var tomorrow = 'tomorrow'.toDateTime('Y-M-D');
		var last = this.dates.last();
		var shortcuts = [];
		this.dates.each(
			function(date) {
			  if (date < yesterday && date < last) return;
				options.push({ key: date, value: date.toDateTime('D DAY') });
				
				if (date == yesterday) shortcuts.push({ key: date, value: 'Yesterday' });
				if (date == today) shortcuts.push({ key: date, value: 'Today' });
				if (date == tomorrow) shortcuts.push({ key: date, value: 'Tomorrow' });
				
				if (date == this.date) value = date;
				if (!value && date > this.date) value = date;
			}.bind(this)
		);
		if (!value && options.last()) value = options.last().key;

    var html = '';
    shortcuts.each(
      function(option) {
        var selected = (option.key == value) ? ' selected="selected"' : '';
        if (selected) value = undefined;
		    html += '<option value="' + option.key + '"' + selected + '>' + option.value + '</option>';
      }
    );
    
		var group;
		options.each(
		  function(option) {
		    var month = option.key.substr(0, 7);
		    if (month != group) {
		      group = month;
		      if (html) html += '</optgroup>';
		      html += '<optgroup label="' + option.key.toDateTime('Y MONTH') + '">';
		    }
		    var selected = (option.key == value) ? ' selected="selected"' : '';
		    html += '<option value="' + option.key + '"' + selected + '>' + option.value + '</option>';
		  }
		);
		if (html) html += '</optgroup>';
		this.$('date').update(html).refreshSelect();
		this.onChangeDate();
	},
	
	setIds: function() {
    var reservations = this.reservations[this.date].findAll(
		  function(reservation) {
		    return this.isMember(reservation);
		  }.bind(this)
		);
		this.totalPeople = 0;
	  this.ids = reservations.collect(
		  function(reservation) {
		    this.totalPeople += reservation.people;
		    return reservation.id;
		  }.bind(this)
		);
	},
	
	setPages: function(event) {
	  this.pages = (this.ids.length / this.perPage).ceil();
		if (this.ids.length === 0) this.pages = 1;
		if (event) this.page = 1;
	},
	
	setPeople: function() {
	  var options = [{key: '', value: 'People'}], value = this.$F('people');
	  var people = this.reservations[this.date].inject([],
	    function(people, reservation) {
	      if (this.isMember(reservation, 'people')) people.push(reservation.people);
	      return people;
	    }.bind(this)
	  );
	  people = people.unique().sortNumeric();
	  if (value && !people.include(value) && (value * 1) < (people.last() * 1)) people.push(value);
	  people.sortNumeric().each(
	    function(people) {
	      options.push(people);
	    }
	  );
	  this.$('people').setValue({options: options, value: value});
	},
	
	setTable: function() {
	  var options = [{key: '', value: 'Table'}];
	  var tables = this.reservations[this.date].collect(
	    function(reservation) {
	      return reservation.tables;
	    }.bind(this)
	  );
	  var tables = this.reservations[this.date].inject([],
	    function(tables, reservation) {
	      if (this.isMember(reservation, 'table')) tables.push(reservation.tables);
	      return tables;
	    }.bind(this)
	  );
	  tables.flatten().unique().sortNumeric().each(
	    function(table) {
	      options.push(table);
	    }
	  );
	  this.$('table').setValue({options: options, value: this.$F('table')});
	},
	
	setTime: function() {
	  var options = [{ key: '00:00:00,23:30:00', value: 'Time' }];
	  var value = this.$F('time');
	  
	  var breakfast = false, lunch = false, dinner = false;
	  this.reservations[this.date].each(
	    function(reservation) {
        var time = reservation.time;
	      if (this.isMember(reservation, 'from')) {
	        if (time >= '17:00:00') return dinner = true;
	        if (time >= '12:00:00') return lunch = true;
	        return breakfast = true;
	      } else if (this.isMember(reservation, 'until')) {
	        if (time <= '11:30:00') return breakfast = true;
	        if (time <= '16:30:00') return lunch = true;
	        return dinner = true;
	      }
	    }.bind(this)
	  );
	  
	  if (breakfast) options.push({ key: '00:00:00,11:30:00', value: 'Breakfast' });
	  if (lunch) options.push({ key: '12:00:00,16:30:00', value: 'Lunch' });
	  if (dinner) options.push({ key: '17:00:00,23:30:00', value: 'Dinner' });

	  this.$('time').setValue({ options: options, value: value });
	},
	
	setTotalPeople: function() {
	  this.$('total-people').writeAttribute('title',
	    this.totalPeople == 1 ? '1 Person' : this.totalPeople + ' People'
	  );
	},
	
	showReservation: function(event, id) {
	  var element = event.element();
	  if (this.isReservationColumn(element)) {
  		window.location.hash = 'dashboard/reservations/reservation/' + id;
  	}
	},

	updateCounts: function(reservation) {
	  if (reservation.category == 'Cancelled') this.cancelled++;
		if (reservation.category == 'Dishonored') this.dishonored++;
		this.total++;
	},
	
	updateNavigation: function(offset, limit) {
    var total = this.ids.length;
    var from = offset + 1;
    var to = limit + 1 > total ? total : limit + 1;
    var context = total <= 1 ? '' : from + ' - ' + to + ' / ' + total;
    this.$('context').show().update(context + ' Reservation' + (total == 1 ? '' : 's'));
    this.$('previous').show().enable();
    this.$('next').show().enable();
    if (this.pages == 1) {
      this.$('context').hide();
      this.$('previous').hide();
      this.$('next').hide();
    } else if (this.page == 1) {
      this.$('previous').disable();
    } else if (this.page == this.pages) {
      this.$('next').disable();
    }
  }
});

Controller.OraclePosts = Class.create(Controller.Base, {  
  authenticated: function() {
    return [1, 77].include(Session.user.id);
  },
  
  initialize: function() {
    this.$('add').observe('click',
      function() {
        window.location.hash = 'oracle/posts/post';
      }
    );
    this.setInterface();
  },
  
  setInterface: function() {
    var template = this.$('posts').replaceContent();
    Model.Post.get().order('-created').each(
      function(post) {
        this.$('posts').insert(
          template.interpolate({
            title: post.title,
            date: post.created.toDateTime('month D Y')
          })
        );
        var anchor = this.$('posts').select('a').last();
        anchor.writeAttribute('href', '/#oracle/posts/post/' + post.id);
      }.bind(this)
    );
  }
});

Controller.Help = Class.create(Controller.Base, {  
  beforeUnload: function() {
    $('Base.help').update('Help'.toLink('/#help'));
  },
  
  initialize: function() {
    $('Base.help').update('Help');
    this.template = new Template(this.$('audience').innerHTML);
    this.setInterface();
  },
  
  setInterface: function() {
    var dasherize = function(string) {
      return string.toLowerCase().gsub(' ', '-');
    };
    
    var questions = Model.Question.get();
    
    var audiences = {};
    questions.each(
      function(question) {
        if (!audiences[question.audience]) audiences[question.audience] = {};
        audiences[question.audience][question.category] = true;
      }
    );
    
    this.$('audience').update('');
    Object.keys(audiences).sort().each(
      function(audience) {
        var elements = [];
        Object.keys(audiences[audience]).sort().each(
          function(category, index) {
            var uri = '/#help/' + dasherize(audience) + '.' + dasherize(category);
            elements.push(category.toLink(uri).toDiv(index === 0 ? 'left' : 'element'));
          }
        );
        this.$('audience').insert(this.template.evaluate({
          audience: audience,
          elements: elements.join('')
        }));
      }.bind(this)
    );
    
    this.elapsed();
  }
});

Controller.CancelReservation = Class.create(Controller.Base, {
  authenticated: function() {
    return Session.user.id;
  },
  
  cancelReservation: function() {
    try {
      Model.Reservation.set({
        id: Parameters.id,
        category: 'Cancelled'
      });
      window.location = '/#home';
      Notice.success('Your reservation was successfully cancelled.');
    } catch (error) {
      Notice.error(error);
    }
  },
  
  initialize: function() {
    this.$('button').observe('click', this.cancelReservation.bind(this));
    if (!Parameters.id) {
      this.pageNotFound();
    } else {
      this.setReservation();
    }
  },
  
  setReservation: function() {
    var reservation = Model.Reservation.get(Parameters.id);
    
    if (!reservation) {
      Notice.error('Please provide a valid reservation cancellation link.');
      return window.location = '/#home';
    }
    
    if (['Cancelled', 'Dishonored'].include(reservation.category)) {
      Notice.success('Your reservation has already been cancelled.');
      return window.location = '/#home';
    }
    
    var people = (reservation.people + ' person').inflect();
    var restaurant = Model.Listing.get(reservation.listingId).name;
    var time = reservation.time.substr(0, 5);
    var date = reservation.date.toDateTime('DAY D MONTH Y');
    var context = 'Your reservation is for #{people} at #{restaurant} at #{time} on #{date}.';
    
    this.$('confirmation').down('p').update(context.interpolate({
      people: people,
      restaurant: restaurant,
      time: time,
      date: date
    }));
  }
});

Controller.DashboardReservationsReservation = Class.create(Controller.Base, {  
  addNote: function() {
    try {
      Model.Note.set({
        id: Id.get(),
        listingId: Session.user.listingId,
        parentId: Parameters.id,
        parentType: 'Reservation',
        userId: Session.user.id,
        body: this.$F('history-note-body')
      });
      this.$('history-note-body').clear();
      this.setNotes();
      Notice.success('Your note was successfully added.');
    } catch (error) {
      Notice.error(error);
    }
  },
  
  attachContactObserver: function() {
    this.contactObserver = new PeriodicalExecuter(
			function() {
			  var contact = this.$F('contact-name');
			  if (contact != this.contact) {
			    this.contact = contact;
			    this.showContactResults();
			  }
			}.bind(this),
			0.02
		);
  },
  
  attachEditDeleteToNotes: function(notes) {
    notes.each(
      function(note) {
        new EditDelete(this.$('notes-' + note.id), {
          context: this,
          model: 'Note',
          id: note.id,
          afterUndo: function() {
            this.hideEmptyNoteDates();
          }.bind(this),
          afterUnset: function(element) {
            this.hideEmptyNoteDates();
          }.bind(this),
          beforeEdit: function(element) {
            var note = element.down('.element').innerHTML;
            element.next().down('textarea').setValue(note).focus();
          }.bind(this),
          afterSave: function(element, note) {
            element.down('.element').update(note.body);
          }.bind(this)
        });
      }.bind(this)
    );
  },
  
  authenticated: function() {
    return Session.user.listingId;
  },
  
  beforeUnload: function() {
    if (this.contactObserver) this.contactObserver.stop();
    // Capture state:
    var reservation = this.$('form').serialize();
    reservation.month = this.$F('month');
    reservation.seatingArea = this.$F('seating-area');
    reservation.tableIds = this.tableIds;
    var note = this.$F('note-body');
    var contact = {
      id: this.$F('contact-id'),
      name: this.$F('contact-name'),
      number: this.$F('contact-number'),
      email: this.$F('contact-email'),
      description: this.$F('contact-description')
    };
    Controller.DashboardReservationsReservation.flash = {
      reservation: reservation,
      note: note,
      contact: contact
    };
  },
  
  findResource: function(resource, id) {
    var element = this.resources[resource.plural()].find(
      function(element) {
        return element.id == id;
      }
    );
    return element || {};
  },
  
  formatUserName: function(name) {
    var names = (name || '').strip().split(' ').collect(
      function(name, index) {
        if (!name.blank() && index < 5) {
          return index === 0 ? name.truncate(7, '..') : name.substr(0, 1);
        } else {
          return null;
        }
      }
    );
    return names.compact().join(' ');
  },
  
  header: function() {
    var components = [
      'Dashboard'.toLink('/#dashboard'),
      'Reservations'.toLink('/#dashboard/reservations')
    ];
    components.push(Parameters.id ? 'Reservation' : 'Add Reservation');
    return components.join(' / ');
  },
  
  hideEmptyNoteDates: function() {
    this.$('notes').childElements().each(
      function(element) {
        if (!element.match('div')) return;
        var empty = element.childElements().all(
          function(child) {
            return !child.visible();
          }
        );
        if (empty) {
          element.previous('p').hide();
        } else {
          element.previous('p').show();
        }
      }
    );
  },
  
  initialize: function() {
    this.$('month').observe('change', this.onChangeReservation.bind(this));
    this.$('date').observe('change', this.onChangeReservation.bind(this));
    this.$('time').observe('change', this.onChangeReservation.bind(this));
    this.$('people').observe('change', this.onChangeReservation.bind(this));
    this.$('seating-area').observe('change', this.onChangeReservation.bind(this));
    this.$('contact-results').observe('change', this.onChangeContactResults.bind(this));
    this.$('add-contact-button').observe('click', this.showContactNumberEmail.bind(this));
    this.$('tables').observe('change', this.setTableIds.bind(this));
    this.tables = new AddRemove(this.$('tables'), {
      afterAdd: function(element) {
        if (!this.tableOptions) return;
        this.setTableIds();
        element.down('select').setValue({ options: this.tableOptions });
      }.bind(this),
      afterRemove: this.setTableIds.bind(this)
    });
    this.$('button').observe('click', this.saveReservation.bind(this));
    
    if (Parameters.id) {
      this.$('note-body').up('.row').hide();
      this.$('history').show();
    }
    
    this.setOpenAndContacts();
    
    this.setReservation();
    this.setContactResultOptions();
    this.attachContactObserver();
    if (Parameters.id) {
      this.reservation = Model.Reservation.get(Parameters.id);
      this.onChangeContactResults({}, this.reservation.contactId);
      var header = this.$F('contact-name').possessive() + ' Reservation';
      this.$('header').update(header).show();
      this.tableIds = this.reservation.tableIds;
      this.setTables();
      this.setNotes();
    }
    
    var flash = Controller.DashboardReservationsReservation.flash;
    var previous = History.get().last() || {};
    var returning = false;
    if (previous.controller == 'DashboardContactsContact') returning = true;
    if (previous.controller == 'DashboardContactsUpdate') returning = true;
    
    if (flash && returning && flash.reservation.id == Parameters.id) {
      this.open.constrain({
        month: flash.reservation.month,
        date: flash.reservation.date,
        time: flash.reservation.time,
        people: flash.reservation.people,
        seatingArea: flash.reservation.seatingArea
      });
      this.setReservation();
            
      this.$('note-body').setValue(flash.note);
      if (flash.contact.id) {
        this.onChangeContactResults({}, flash.contact.id);
      } else {
        this.$('contact-name').setValue(flash.contact.name);
        this.$('contact-number').setValue(flash.contact.number);
        this.$('contact-email').setValue(flash.contact.email);
        this.$('contact-description').setValue(flash.contact.description);
      }
      this.tableIds = flash.reservation.tableIds;
      this.setTables();
    } else {
      delete Controller.DashboardReservationsReservation.flash;
    }
    
    this.elapsed();
  },
  
  setOpenAndContacts: function() {
    this.open = new Open(Session.user.listingId, {
      hideNotices: function() {
        this.$('notice').up('.row').hide();
      }.bind(this),
      id: Parameters.id,
      onFailure: function(error) {
        Notice.error(error);
        this.$('form').hide();
      }.bind(this),
      mode: 'Internal',
      showNotices: function(notices) {
        this.$('notice').update(notices.join('<br>')).up('.row').show();
      }.bind(this)
    });
    
    this.contacts = Model.Contact.get({
      listingId: Session.user.listingId,
      visible: true,
      order: 'name'
    });
    this.contactNumbers = Model.ContactNumber.get({
      listingId: Session.user.listingId
    });
    this.contactEmails = Model.ContactEmail.get({
      listingId: Session.user.listingId
    });
  },
  
  // Idempotent.
  setNotes: function() {
    if (!this.note) {
      // Set event handlers for add note form:
      this.$('history-note-button').observe('click', this.addNote.bind(this));
      
      // Set note template:
      this.note = this.$('notes').replaceContent('');
    }

    // Set notes:    
    var notes = Model.Note.get({
      parentId: Parameters.id,
      parentType: 'Reservation'
    });
    
    new NoteHistory(this.$('notes'), {
      notes: notes,
      parentType: 'Reservation'
    });
  },
  
  minWidth: function() {
    return '880px';
  },
  
  onChangeContactResults: function(event, contactId) {
    // Enables setOpen to pass in a contactId and ride on this functionality:
    contactId = contactId || this.$F('contact-results');
    if (contactId) {
      var contact = Model.Contact.get(contactId);
      this.contact = contact.name;
      this.$('contact-name').setValue(contact.name);
      var uri = '/#dashboard/contacts/contact/' + contact.id;
      this.$('contact-more-information').show().down('a').writeAttribute('href', uri);
      this.showContactBasicInformation(contact);
      this.$('contact-results-and-add-contact-button').hide();
      this.$('contact-id').setValue(contact.id);
    }
  },
  
  onChangeReservation: function(event) {
    this.open.constrain({
      month: this.$F('month'),
      date: this.$F('date'),
      time: this.$F('time'),
      people: this.$F('people'),
      seatingArea: this.$F('seating-area')
    });
    this.setReservation();
    this.setTables();
  },
      
  saveReservation: function() {
    try {
      if (this.$F('contact-id')) {
        var contact = {
          id: this.$F('contact-id')
        };
      } else if (this.$('contact-number-and-email').visible()) {
        var contact = Model.Contact.set({
          id: Id.get(),
          listingId: Session.user.listingId,
          name: this.$F('contact-name'),
          description: this.$F('contact-description')
        });
        this.$('contact-id').setValue(contact.id);
        
        if (this.$F('contact-number')) {
          Model.ContactNumber.set({
            id: Id.get(),
            listingId: Session.user.listingId,
            contactId: contact.id,
            number: this.$F('contact-number'),
            category: 'Work'
          });
        }
        
        if (this.$F('contact-email')) {
          Model.ContactEmail.set({
            id: Id.get(),
            listingId: Session.user.listingId,
            contactId: contact.id,
            email: this.$F('contact-email'),
            category: 'Work'
          });
        }
      } else {
        var contact = {};
      }
      
      var reservation = this.$('form').serialize();
      reservation.contactId = contact.id;
      if (Parameters.id) {
        reservation.id = Parameters.id;
      } else {
        reservation.id = Id.get();
        reservation.listingId = Session.user.listingId;
        reservation.mode = 'Internal';
        reservation.category = 'Accepted';
      }
      var seatingAreaIds = [ this.$F('seating-area') ];
      if (seatingAreaIds.include('Multiple')) seatingAreaIds = [];
      reservation.seatingAreaIds = seatingAreaIds;
      reservation.tableIds = this.tableIds || [];
      if (reservation.tableIds.present()) {
        reservation.tableIds.each(
          function(id) {
            reservation.seatingAreaIds.push(Model.Table.get(id).seatingAreaId);
          }
        );
        reservation.seatingAreaIds = reservation.seatingAreaIds.unique();
      }

      Model.Reservation.set(reservation);
      
      if (this.$F('note-body')) {
        Model.Note.set({
          id: Id.get(),
          listingId: Session.user.listingId,
          parentId: reservation.id,
          parentType: 'Reservation',
          body: this.$F('note-body'),
          userId: Session.user.id
        });
      }
      
      this.setOpenAndContacts();
      
      if (Parameters.id) {
        var success = 'Your reservation was successfully saved.';
        this.setNotes();
      } else {
        this.$('form').show(); // May be hidden if Open encounters an error.
        this.$('time').selectedIndex = 0;
        this.$('people').selectedIndex = 0;
        this.$('seating-area').selectedIndex = 0;
        this.$('notice').up('.row').hide();
        this.$('note-body').clear();
        this.$('note-body').up('.row').show();

        this.$('contact-name').clear();
        this.$('contact-id').clear();
        this.$('contact-results-and-add-contact-button').hide();
        this.$('contact-status').hide();
        this.$('contact-more-information').hide();
        this.$('contact-basic-information').hide();
        this.$('contact-number-and-email').hide();
        this.$('contact-number').clear();
        this.$('contact-email').clear();

        this.$('tables').hide();
        this.tables.reset();
        
        this.open.constrain({
          month: this.$F('month'),
          date: this.$F('date'),
          time: this.$F('time'),
          people: this.$F('people'),
          seatingArea: this.$F('seating-area')
        });
        this.setReservation();
        var success = 'Your reservation was successfully added.';
        var href = '/#dashboard/reservations/reservation/' + reservation.id;
        var extra = Html.a({ href: href }, 'View');
      }
      Notice.success(success, extra);
    } catch (error) {
      Notice.error(error);
    }
  },
  
  setContactResultOptions: function() {
    var names = {};
    var duplicates = {};
    // this.contacts are already ordered by name.
    var options = this.contacts.inject([],
		  function(options, contact) {
	      if (!names[contact.name]) {
	        names[contact.name] = true;
	      } else {
	        duplicates[contact.name] = true;
	      }
	      options.push({ key: contact.id, value: contact.name });
		    return options;
		  }
		);
		// Differentiate duplicate options with contact numbers:
		this.contactResultOptions = options.inject([],
		  function(options, option) {
		    if (duplicates[option.value]) {
		      var contactNumber = this.contactNumbers.find(
		        function(contactNumber) {
		          return contactNumber.contactId == option.key;
		        }
		      );
		      if (contactNumber) option.value += ' - ' + contactNumber.number.without(/\+/);
		    }
		    options.push(option);
		    return options;
		  }.bind(this)
		);
  },
  
  setReservation: function() {
    this.$('month').setValue(this.open.months());
    this.$('date').setValue(this.open.dates());
    this.$('time').setValue(this.open.times());
    this.$('people').setValue(this.open.people());
    this.$('seating-area').setValue(this.open.seatingAreas());
    
    var date = this.$F('date');
    var time = this.$F('time');
    if (date && time) {
      var people = this.$F('people');
      var arrivalRate = this.open.resources.listing.arrivalRate;
      var capacityCanStillArrive = this.open.arrivalRates(date)[time] || arrivalRate;
      Log('Time: ' + time + ' Capacity To Fill Arrival Rate: ' + capacityCanStillArrive + ' People: ' + people);
      if (people && people > capacityCanStillArrive) {
        var brackets = (arrivalRate === 1 ? '1 person' : arrivalRate + ' people') + ' per half hour';
        var difference = people - capacityCanStillArrive;
        var exceeded = (difference === 1 ? '1 person' : difference + ' people');
        var day = date.toDateTime('DAY D MONTH Y');
        Notice.error('Your desired arrival rate of ' + brackets + ', for ' + time.substr(0,5) + ' ' + day + ', has been exceeded by ' + exceeded + '.');
      }
    }
  },
  
  // Called by onChangeReservation if seating area has a value:
  setTables: function() {
    if (this.$F('seating-area')) {
      this.$('tables').show();
    } else {
      this.$('tables').hide();
      return;
    }
    this.tableOptions = this.open.tables().options.sortBy(
      function(option) {
        return option.value * 1;
      }
    );
    var ids = this.tableIds || []; // Cache value of this.tableIds (see next line).
    var availableIds = this.tableOptions.pluck('key');
    this.tables.reset(); // Calls remove methods internally which update this.tableIds.
    var rejected = 0; // Keep track of rejected ids for indexing purposes.
    ids.each(
      function(id, index) {
        if (availableIds.exclude(id)) return rejected++;
        // Adjust index for rejected ids:
        if (index - rejected > 0) this.tables.add();
        this.tables.elements.last().down('select').setValue(id);
      }.bind(this)
    );
    this.tableIds = ids; // Restore value of this.tableIds.
  },
  
  // Called on changing any table select or on adding or removing a table:
  setTableIds: function(event) {
    this.tableIds = this.$('tables').select('select').inject([],
      function(tableIds, select) {
        var value = select.getValue();
        if (value && !tableIds.include(value)) tableIds.push(value);
        return tableIds;
      }
    );
  },
  
  showContactBasicInformation: function(contact) {
    this.$('contact-basic-information').update();
    var html = '&nbsp;'.toDiv('label xs');
    var present = false;
    this.contactNumbers.each(
      function(contactNumber, index) {
        if (contactNumber.contactId != contact.id) return;
        html += '<div class="element" title="' + contactNumber.category + '">';
        html += contactNumber.number + '</div>';
        present = true;
      }
    );
    this.contactEmails.each(
      function(contactEmail, index) {
        if (contactEmail.contactId != contact.id) return;
        html += '<div class="element" title="' + contactEmail.category + '">';
        var href = 'mailto:' + contactEmail.email;
        
        html += Html.a({ href: href }, contactEmail.email) + '</div>';
        present = true;
      }
    );
    if (!present) {
      var error = 'No phone number or email address could be found for ' + contact.name + '.';
      html += error.toDiv('element');
    }
    var uri = '/#dashboard/contacts/update/' + contact.id;
    html += 'Update'.toLink(uri).toDiv('element');
    html = html.toDiv('row');
    
    if (contact.description) {
      var about = '&nbsp;'.toDiv('label xs');
      about += ('<span class="highlight">' + contact.description + '</span>').toDiv('element');
      html += about.toDiv('row');
    }
    
    this.$('contact-basic-information').update(html);
    this.$('contact-basic-information').show();
  },
  
  // Called by clicking on add-contact-button.
  showContactNumberEmail: function() {
    this.$('contact-results-and-add-contact-button').hide();
    this.$('contact-number-and-email').show();
  },
  
  showContactResults: function() {
    this.$('contact-id').clear();
    //this.$('contact-number').clear();
    //this.$('contact-email').clear();
    this.$('contact-more-information').hide();
    this.$('contact-basic-information').hide();
    var name = this.contact.strip().toLowerCase();
    if (name.empty()) {
      this.$('contact-results-and-add-contact-button').hide();
      this.$('contact-number-and-email').hide();
    } else {
      var options = this.contactResultOptions.findAll(
        function(option) {
          return option.value.toLowerCase().include(name);
        }
      );
  		if (options.length === 0) {
        this.$('contact-results-and-add-contact-button').hide();
        this.$('contact-number-and-email').show();
  		} else {
  		  if (options.length === 1) {
    		  var caption = '1 Contact Found';
    		} else {
    		  var caption = options.length + ' Contacts Found';
    		}
    		options.unshift({ key: '', value: caption });
    		this.$('contact-results').setValue({options: options});
    		this.$('contact-results-and-add-contact-button').show();
    		this.$('contact-number-and-email').hide();
    	}
  	}
  },
  
  title: function() {
    return Parameters.id ? 'Reservation' : 'Add Reservation';
  }
});

Controller.DashboardSetupNotices = Class.create(Controller.Base, {  
  attachObservers: function(element) {
    var rows = element.select('select').inGroupsOf(3);
    rows.each(
      function(selects) {
        selects[0].observe('change', this.onChangeKey.bind(this));
      }.bind(this)
    );
  },
  
  authenticated: function() {
    return !!Session.user.listingId;
  },
  
  conditions: {
    date: [
      { key: '<', value: 'Before' },
      { key: '=', value: 'Is' },
      { key: '>', value: 'After (Inclusive)' }
    ],
    day: [
      { key: '=', value: 'Is' }
    ],
    time: [
      { key: '<', value: 'Before' },
      { key: '=', value: 'Is' },
      { key: '>', value: 'After (Inclusive)' }
    ],
    people: [
      { key: '<', value: 'Less Than' },
      { key: '=', value: 'Is' },
      { key: '>', value: 'More Than (Inclusive)' }
    ],
    seatingArea: [
      { key: '=', value: 'Is' }
    ]
  },
  
  header: '<a href="/#dashboard">Dashboard</a> / Setup',
  
  initialize: function() {
    this.notices = Model.Notice.get({
      listingId: Session.user.listingId,
      order: 'id'
    });
    this.seatingAreas = Model.SeatingArea.get({
      listingId: Session.user.listingId
    });
    this.setOptions();
    this.initializeAddRemove();
    this.$('button').observe('click',
      function(event) {
        try {
          this.noticeAddRemove.save();
          Notice.success('Your notices were successfully saved.');
        } catch (error) {
          Notice.error(error);
        }
      }.bind(this)
    );
    this.setAddRemove();
    this.elapsed();
  },
  
  initializeAddRemove: function() {
    this.noticeAddRemove = new AddRemove(this.$('notices'), {
      afterAdd: function(element) {
        this.attachObservers(element);
        element.select('input[type="radio"]').each(
          function(input) {
            new Caption(input.up().next());
          }
        );
      }.bind(this),
      model: 'Notice',
      parameters: { listingId: Session.user.listingId },
      present: ['body'],
      repeatLabels: true,
      representation: function(element) {
        var representation = element.down('form').serialize();
        var rows = element.select('select').inGroupsOf(3);
        var conditions = rows.inject([],
          function(conditions, selects) {
            if (selects[0].getValue()) {
              conditions.push(selects.invoke('getValue').join(''));
            }
            return conditions;
          }
        );
        representation.conditions = conditions.join();
        return representation;
      },
      rowIndex: 1 // Attaches AddRemove links to second row instead of first.
    });
  },
  
  minWidth: function() {
    return '820px';
  },
  
  onChangeKey: function(event) {
    var selects = event.element().up('.row').select('select');
    var key = selects[0].getValue();
    if (key) {
      selects[1].setValue({
        options: this.conditions[key],
        value: this.values[key].first()
      }).up().show();
      selects[2].setValue({
        options: this.options[key],
        value: this.values[key].last()
      }).up().show();
    } else {
      selects[1].up().hide();
      selects[2].up().hide();
    }
  },
  
  setAddRemove: function() {
    this.notices.each(
      function(notice, index) {
        if (index > 0) this.noticeAddRemove.add();
        var element = this.noticeAddRemove.elements.last();

        // Set id, listingId, body
        element.down('input[name="id"]').setValue(notice.id);
        element.down('input[name="listingId"]').setValue(notice.listingId);
        element.down('textarea[name="body"]').setValue(notice.body);
        
        // Set audience radio
        var radioIndex = ['Customers', 'Managers', 'Everyone'].indexOf(notice.audience);
        element.down('input[name="audience"]', radioIndex).checked = true;

        // Set conditions
        var rows = element.select('select').inGroupsOf(3);
        notice.conditions.split(',').each(
          function(condition, index) {
            if (index >= 3) return; // Only show first three conditions
            var matches = condition.match(/([a-z]+)(\<|=|\>)(.+)/i);
            var key = matches[1];
            var options = this.options[key];
            
            // Extend options
            if (key == 'date') {
              options = Options.extend(options, {
                key: matches[3],
                value: matches[3].toDateTime('D MONTH Y')
              });
            } else if (key == 'seatingArea') {
              var seatingArea = this.seatingAreas.find(matches[3]);
              if (!seatingArea) return;
              options = Options.extend(options, {
                key: matches[3],
                value: seatingArea.name
              });
            }
            
            rows[index][0].setValue(key);
            rows[index][1].setValue({ options: this.conditions[key], value: matches[2] }).up().show();
            rows[index][2].setValue({ options: options, value: matches[3] }).up().show();
          }.bind(this)
        );
      }.bind(this)
    );
  },
  
  setDateOptions: function() {
    this.options.date = Datetime.dates({
      format: 'D MONTH Y',
      limit: 365,
      options: true
    });
  },
  
  setDayOptions: function() {
    this.options.day = Datetime.dayNames;
  },
  
  setOptions: function() {
    this.options = {};
    this.setDateOptions();
    this.setDayOptions();
    this.setTimeOptions();
    this.setPeopleOptions();
    this.setSeatingAreaOptions();
  },
  
  setPeopleOptions: function() {
    this.options.people = [];
    (200).times(
      function(index) {
        var people = index + 1;
        var qualifier = people == 1 ? ' Person' : ' People';
        this.options.people.push({
          key: people,
          value: people + qualifier
        });
      }.bind(this)
    );
  },
  
  setSeatingAreaOptions: function() {
    this.options.seatingArea = this.seatingAreas.inject([],
      function(options, seatingArea) {
        options.push({
          key: seatingArea.id,
          value: seatingArea.name
        });
        return options;
      }.bind(this)
    );
  },
  
  setTimeOptions: function() {
    this.options.time = [];
    for (var index = 0; index < 24; index = index + 0.5) {
      var time = index.toTime();
      this.options.time.push({
        key: time,
        value: time.substr(0, 5)
      });
    }
  },
  
  title: 'Setup',
  
  values: {
    date: [ '=', undefined ],
    day: [ '=', 'Friday' ],
    time: [ '>', '21:00:00' ],
    people: [ '>', '8' ],
    seatingArea: [ '=', undefined ]
  }
});

Controller.DashboardSetupSeatingAreas = Class.create(Controller.Base, {  
  attachAddRemove: function() {
    this.seatingArea = new AddRemove(this.$('seating-areas'), {
      model: 'SeatingArea',
      parameters: { listingId: Session.user.listingId },
      present: ['name', 'description'],
      repeatLabels: true,
      required: true,
      unset: function(id) {
        var representation = { id: id, visible: false };
        Model.SeatingArea.set(representation);
      }
    });
  },
  
  authenticated: function() {
    return Session.user.listingId;
  },
    
  header: '<a href="/#dashboard">Dashboard</a> / Setup',
  
  initialize: function() {
    this.setSeatingAreaCapacity();
    this.$('button').observe('click', this.save.bind(this));
    this.attachAddRemove();
    var seatingAreas = Model.SeatingArea.get({
      listingId: Session.user.listingId,
      visible: true,
      order: 'name'
    });
    seatingAreas.each(
      function(seatingArea, index) {
        if (index > 0) this.seatingArea.add();
        var element = this.seatingArea.elements.last();
        element.down('input[name="id"]').setValue(seatingArea.id);
        element.down('input[name="name"]').setValue(seatingArea.name);
        element.down('select').setValue(seatingArea.capacity);
        element.down('textarea').setValue(seatingArea.description);
      }.bind(this)
    );
    this.elapsed();
  },
  
  save: function() {
    try {
      this.seatingArea.save();
      Notice.success('Your seating areas were successfully saved.');
    } catch (error) {
      Notice.error(error);
    }
  },
  
  setSeatingAreaCapacity: function() {
    var options = [{ key: '', value: 'Capacity' }];
    (200).times(
      function(index) {
        var key = index + 1;
        var value = key.toPaddedString(2);
        var qualifier = key == 1 ? ' Person' : ' People';
        options.push({ key: key, value: value + qualifier });
      }
    );
    this.$('seating-areas').down('select').setValue({ options: options });
  },
  
  title: 'Setup'
});

Controller.DashboardSetupTables = Class.create(Controller.Base, {  
  attachAddRemove: function() {
    this.tables = new AddRemove(this.$('tables'), {
      model: 'Table',
      parameters: { listingId: Session.user.listingId },
      present: ['name', 'capacity', 'seating-area-id'],
      afterAdd: function(element, addRemove) {
        var length = addRemove.elements.length;
        if (length > 1) {
          var previous = addRemove.elements[length - 2];
          var name = previous.down('input[name="name"]').getValue() * 1;
          var capacity = previous.down('select[name="capacity"]').getValue();
          var seatingArea = previous.down('select[name="seatingAreaId"]').getValue();
          if (name) element.down('input[name="name"]').setValue(name + 1).blur();
          element.down('select[name="capacity"]').setValue(capacity);
          element.down('select[name="seatingAreaId"]').setValue(seatingArea);
        }
      }.bind(this),
      unset: function(id) {
        var representation = { id: id, visible: false };
        Model.Table.set(representation);
      }
    });
  },
  
  authenticated: function() {
    return Session.user.listingId;
  },
  
  header: '<a href="/#dashboard">Dashboard</a> / Setup',
  
  initialize: function() {
    this.setTableCapacity();
    this.$('button').observe('click', this.save.bind(this));
    this.setSeatingAreas();
    this.attachAddRemove();
    var tables = Model.Table.get({
      listingId: Session.user.listingId,
      visible: true
    });
    tables = tables.sortBy(
      function(table) {
        return table.name * 1;
      }
    );
    var count = 0;
    tables.each(
      function(table) {
        if (!this.seatingAreas[table.seatingAreaId]) return;
        if (count > 0) this.tables.add();
        var element = this.tables.elements.last();
        element.down('input[name="id"]').setValue(table.id);
        element.down('input[name="name"]').setValue(table.name);
        element.down('select[name="capacity"]').setValue(table.capacity);
        element.down('select[name="seatingAreaId"]').setValue(table.seatingAreaId);
        count++;
      }.bind(this)
    );
    this.elapsed();
  },
  
  minWidth: function() {
    return '770px';
  },
  
  save: function() {
    try {
      this.tables.save();
      Notice.success('Your tables were successfully saved.');
    } catch (error) {
      Notice.error(error);
    }
  },
  
  setTableCapacity: function() {
    var options = [{key: '', value: 'Capacity'}];
    (50).times(
      function(index) {
        var key = index + 1;
        var value = key.toPaddedString(2);
        var qualifier = key == 1 ? ' Person' : ' People';
        options.push({
          key: key,
          value: value + qualifier
        });
      }
    );
    this.$('tables').down('select', 0).setValue({ options: options });
  },
  
  setSeatingAreas: function() {
    var options = [{ key: '', value: 'Seating Area' }];
    var seatingAreas = Model.SeatingArea.get({
      listingId: Session.user.listingId,
      visible: true,
      order: 'name'
    });
    seatingAreas.each(
      function(seatingArea) {
        options.push({
          key: seatingArea.id,
          value: seatingArea.name
        });
      }
    );
    this.seatingAreas = seatingAreas.index('id', true);
    this.$('tables').down('select', 1).setValue({options: options});
  },
  
  title: 'Setup'
});

Controller.OracleInterviewsInterview = Class.create(Controller.Base, {
  activate: function() {
    this.hide();
    this.getResources({ onSuccess: this.setInterface.bind(this) });
  },
  
  authenticated: function() {
    return [1, 77].include(Session.user.id);
  },
  
  collections: {
    'interviews, listings': {}
  },
    
  deactivate: function() {
    this.$('message').hide();
    this.$('form').clear();
    this.$('id').clear();
  },
    
  setInterface: function() {
    var options = ['Restaurant'];
    this.resources.listings.order('name').each(
      function(listing) {
        options.push({ key: listing.id, value: listing.name });
      }
    );
    this.$('listingId').setValue({ options: options });
    if (Parameters.id) {
      var interview = this.resources.interviews.find(Parameters.id);
      this.$('id').setValue(interview.id);
      this.$('userId').setValue(interview.userId);
      this.$('listingId').setValue(interview.listingId);
      this.$('title').setValue(interview.title);
      this.$('body').setValue(interview.body);
    } else {
      this.$('userId').setValue(Session.user.id);
    }
    this.show();
  },
  
  header: function() {
    var components = [
      'Oracle'.toLink('/#oracle'),
      'Interviews'.toLink('/#oracle/interviews')
    ];
    components.push(Parameters.id ? 'Interview' : 'Add Interview');
    return components.join(' / ');
  },
  
  initialize: function() {
    this.$('button').observe('click', this.save.bind(this));
  },
  
  save: function() {
    this.$('message').hide();
    this.$('button').loading('Saving');
    var id = this.$F('id');
    var resource = id ? 'interviews/' + id : 'interviews';
    var method = id ? 'Put' : 'Post';
    Resource.set(resource, {
      representation: this.$('form').serialize(),
      onSuccess: function(interview) {
        if (!Parameters.id) {
          this.$('form').clear();
          this.$('userId').setValue(Session.user.id);
          var success = 'Your interview was successfully added.</span><span class="padding-left">';
          success += 'View'.toLink('/#oracle/interviews/interview/' + interview.id);
        } else {
          var success = 'Your interview was successfully saved.';
        }
        this.$('message').success(success);
        this.$('button').loaded();
      }.bind(this),
      onFailure: function(error) {
        this.$('message').error(error);
        this.$('button').loaded();
      }.bind(this)
    });
  },
  
  title: function() {
    return Parameters.id ? 'Interview' : 'Add Interview';
  }
});

Controller.OracleListsList = Class.create(Controller.Base, {
  authenticated: function() {
    return [1, 77].include(Session.user.id);
  },
  
  collections: {
    'listings, lists': {}
  },
    
  header: function() {
    var components = [
      'Oracle'.toLink('/#oracle'),
      'Lists'.toLink('/#oracle/lists')
    ];
    components.push(Parameters.id ? 'List': 'Add List');
    return components.join(' / ');
  },
  
  initialize: function() {
    this.hide();
    this.$('button').observe('click', this.save.bind(this));
    this.getResources({ onSuccess: this.setInterface.bind(this) });
  },
  
  labelElements: function() {
    this.$('items').select('.table').each(
      function(table, index) {
        table.down('.label').update('Item ' + (index + 1) + ':');
      }
    );
  },
  
  save: function() {
    this.$('message').hide();
    this.$('button').loading('Saving');

    var representation = {
      userId: Session.user.id,
      title: this.$F('title'),
      description: this.$F('description'),
      listingIds: this.$('items').select('.select').invoke('getValue'),
      descriptions: this.$('items').select('.textarea').invoke('getValue')
    };

    Resource.set('lists/' + (this.$F('id') || Id.get()), {
      representation: representation,
      onSuccess: function(list) {
        this.$('id').setValue(list.id);
        this.$('message').success('Your list was successfully saved.');
        this.$('button').loaded();
      }.bind(this),
      onFailure: function(error) {
        this.$('message').error(error);
        this.$('button').loaded();
      }.bind(this)
    });
  },
  
  setInterface: function() {
    if (!this.items) {
      var options = [{ key: '', value: 'Restaurant' }];
      this.resources.listings.order('name').each(
        function(listing) {
          options.push({ key: listing.id, value: listing.name });
        }
      );
      this.$('items').down('select').setValue({ options: options });
      this.items = new AddRemove(this.$('items'), {
        afterAdd: this.labelElements.bind(this),
        afterRemove: this.labelElements.bind(this)
      });
    }
    if (Parameters.id) {
      var list = this.resources.lists.find(Parameters.id);
      if (!list) return this.pageNotFound();
      this.$('id').setValue(list.id);
      this.$('title').setValue(list.title);
      this.$('description').setValue(list.description);
      list.listingIds.each(
        function(listingId, index) {
          if (index > 0) this.items.add();
          var element = this.items.elements.last();
          element.down('select').setValue(listingId);
          element.down('textarea').setValue(list.descriptions[index]);
        }.bind(this)
      );
    }
    this.show();
  },
    
  title: function() {
    return Parameters.id ? 'List': 'Add List';
  }
});

Controller.OraclePostsPost = Class.create(Controller.Base, {  
  authenticated: function() {
    return [1, 77].include(Session.user.id);
  },
  
  header: function() {
    return [
      Html.a({ href: '/#oracle' }, 'Oracle'),
      Html.a({ href: '/#oracle/posts' }, 'Posts'),
      Parameters.id ? 'Post' : 'Add Post'
    ].join(' / ');
  },
  
  initialize: function() {
    this.$('button').observe('click', this.save.bind(this));
    if (Parameters.id) {
      var post = Model.Post.get(Parameters.id);
      this.$('id').setValue(post.id);
      this.$('user-id').setValue(post.userId);
      this.$('title').setValue(post.title);
      this.$('body').setValue(post.body);
    }
  },
  
  save: function() {
    var post = this.$('form').serialize();
    if (!post.id) {
      post.id = Id.get();
      post.userId = Session.user.id;
    }
    try {
      Model.Post.set(post);
      if (!Parameters.id) {
        this.$('id').clear();
        this.$('form').reset();
        var extra = Html.a({ href: '/#oracle/posts/post/' + post.id }, 'View');
        Notice.success('Your post was successfully added.', extra);
      } else {
        Notice.success('Your post was successfully saved.');
      }
    } catch (error) {
      Notice.error(error);
    }
  },
  
  title: function() {
    return Parameters.id ? 'Post' : 'Add Post';
  }
});

Controller.OracleQuestion = Class.create(Controller.Base, {  
  authenticated: function() {
    return [1, 77].include(Session.user.id);
  },
  
  header: function() {
    var components = [
      'Oracle'.toLink('/#oracle')
    ];
    components.push(Parameters.id ? 'Question' : 'Add Question');
    return components.join(' / ');
  },
  
  initialize: function() {
    this.hide();
    this.$('audience').observe('change', this.setCategoryHelper.bind(this));
    this.$('category').observe('keypress',
      function(event) {
        if (event.keyCode == Event.KEY_RETURN) this.save();
      }.bind(this)
    );
    this.$('category-helper').observe('change',
      function(event) {
        var category = this.$F('category-helper');
        if (category != 'Existing Category') this.$('category').setValue(category);
      }.bind(this)
    );
    this.$('button').observe('click', this.save.bind(this));
    this.setInterface();
  },
  
  save: function() {
    try {
      var question = this.$('form').serialize();
      question.id = question.id || Id.get();
      Model.Question.set(question);
      if (Parameters.id) {
        var success = 'Your question was successfully saved.';
      } else {
        var success = 'Your question was successfully added.';
        this.$('question').clear();
        this.$('answer').clear();
      }
      Notice.success(success);
    } catch (error) {
      Notice.error(error);
    }
  },
  
  setCategoryHelper: function() {
    var questions = Model.Question.get({ audience: this.$F('audience') });
    var options = questions.pluck('category').sort().unique();
    options.unshift('Existing Categories');
    this.$('category-helper').setValue({ options: options });
  },
  
  setInterface: function() {
    if (Parameters.id) {
      var question = Model.Question.get(Parameters.id);
      this.$('id').setValue(question.id);
      this.$('user-id').setValue(question.userId);
      this.$('audience').setValue(question.audience);
      this.$('question').setValue(question.question);
      this.$('answer').setValue(question.answer);
      this.$('category').setValue(question.category);
    } else {
      this.$('user-id').setValue(Session.user.id);
    }
    this.setCategoryHelper();
    this.show();
  },
  
  title: function() {
    return Parameters.id ? 'Question' : 'Add Question';
  }
});

Controller.Oracle = Class.create(Controller.Base, {
  attachTimer: function() {
    this.timer = new PeriodicalExecuter(
      function() {
        this.insertOracles();
      }.bind(this),
			10
		);
  },
  
  authenticated: function() {
    return [1, 77].include(Session.user.id);
  },
  
  beforeUnload: function() {
    this.timer.stop();
  },
  
  initialize: function() {
    this.template = new Template(this.$('oracles').innerHTML);
    this.$('body').observe('keypress',
      function(event) {
        if (event.keyCode == Event.KEY_RETURN) this.save();
      }.bind(this)
    );
    this.$('button').observe('click', this.save.bind(this));
    this.attachTimer();
    this.insertOracles();
    this.$('body').focus();
  },
  
  insertOracle: function(oracle) {
    var name = oracle.userName.split(' ').first();
    var time = oracle.created.toDateTime('h:m');
    this.$('oracles').insert({top: this.template.evaluate({
      meta: name + ' ' + time + ':',
      oracle: oracle.body.autoLink()
    })});
    var row = this.$('oracles').down('.row');
    if (row) {
      var userIds = [1, 77].without(Session.user.id);
      if (oracle.userId == Session.user.id) {
        var background = '#FFFFCC';
      } else {
        var index = userIds.indexOf(oracle.userId) % 2;
        var background = ['#FFFFFF', '#EEEEEE'][index];
      }
      row.setStyle({background: background});
    }
  },
  
  insertOracles: function() {
    this.$('oracles').update();
    var oracles = Model.Oracle.get({
      created: '~' + 'now'.toDateTime('Y-M-D'),
      order: 'created'
    });
    oracles.each(this.insertOracle.bind(this));
    if (oracles.present()) {
      this.$('oracles').show();
    } else {
      this.$('oracles').hide();
    }
  },
  
  save: function() {
    try {
      var oracle = Model.Oracle.set({
        id: Id.get(),
        groupId: 1, // Sexbyfood.
        userId: Session.user.id,
        body: this.$F('body')
      });
      this.$('body').clear().focus();
      this.insertOracle(oracle);
      this.$('oracles').show();
    } catch (error) {
      Notice.error(error);
    }
  }
});

Controller.SignInChangeEmailAddress = Class.create(Controller.Base, {
  initialize: function() {
		this.$('new-email-confirmation').observe('keypress',
		  function(event) {
		    if (event.keyCode == Event.KEY_RETURN) this.signIn();
		  }.bind(this)
		);
		this.$('button').observe('click', this.signIn.bind(this));
		this.$('existing-email').activate();
	},
	
	changeEmailAddress: function(password) {
	  var email = this.$F('new-email').strip().toLowerCase();
	  var a1 = email + ':' + Config.DigestAuthentication.realm + ':' + password;
	  var user = { id: Session.user.id, email: email, ha1: a1.md5() };
	  
	  try {
  	  Model.User.set(user, {
  		  onSuccess: function() {
  		    Session.set({
      		  email: email,
      		  password: password,
      		  onFailure: function(error) {
      		    Notice.error(error);
      		    this.$('button').loaded();
      		  }.bind(this),
      		  onProgress: function(percentage) {
      		    percentage = ((percentage / 2) + 50).round().toPaddedString(2);
      		    this.$('progress').update(percentage + '%');
      		  }.bind(this),
      		  onSuccess: function() {
              Notice.success('Your email address was successfully changed.');
              window.location.hash = 'home';
      		  }.bind(this)
      		});
  		  }.bind(this),
  		  onTimeout: function() {
  		    if (!this.timeout) {
  		      Notice.error('Please reconnect to the Internet.');
  		      this.timeout = true;
  		    }
  		  }.bind(this)
  		});
  	} catch (error) {
  	  Notice.error(error);
  	  this.$('button').loaded();
  	}
	},
	
	signIn: function() {
	  if (!navigator.onLine) {
	    return Notice.error('Please reconnect to the Internet.');
	  }
	  
	  if (this.$F('new-email') != this.$F('new-email-confirmation')) {
	    Notice.error('Please confirm your new email address.');
	    return;
	  }
	  
		this.$('button').loading('Saving');
		var password = this.$F('existing-password');

		Session.set({
		  email: this.$F('existing-email'),
		  password: password,
		  onFailure: function(error) {
		    Notice.error(error);
		    this.$('button').loaded();
		  }.bind(this),
		  onProgress: function(percentage) {
		    percentage = (percentage / 2).round().toPaddedString(2);
		    this.$('progress').update(percentage + '%');
		  }.bind(this),
		  onSuccess: this.changeEmailAddress.bind(this, password)
		});
	}
});

Controller.DashboardSetupGeneral = Class.create(Controller.Base, {  
  authenticated: function() {
    return Session.user.listingId;
  },
  
  initialize: function() {
    new SelectAdd(this.$('country'), {
      model: 'Country',
      afterAdd: this.setStates.bind(this),
      representation: function(element) {
        return {
          currencyId: 2,
          name: $F(element)
        };
      },
      option: function(country) {
        return {
          key: country.id,
          value: country.name
        };
      }
    });
    new SelectAdd(this.$('state'), {
      model: 'State',
      afterAdd: this.setCities.bind(this),
      representation: function(element) {
        return {
          countryId: this.$F('country'),
          name: $F(element)
        };
      }.bind(this),
      option: function(state) {
        return {
          key: state.id,
          value: state.name
        };
      }
    });
    new SelectAdd(this.$('city'), {
      model: 'City',
      afterAdd: this.setAreas.bind(this),
      representation: function(element) {
        return {
          stateId: this.$F('state'),
          name: $F(element)
        };
      }.bind(this),
      option: function(city) {
        return {
          key: city.id,
          value: city.name
        };
      }
    });
    new SelectAdd(this.$('area'), {
      model: 'Area',
      afterAdd: this.setSuburbs.bind(this),
      representation: function(element) {
        return {
          cityId: this.$F('city'),
          name: $F(element)
        };
      }.bind(this),
      option: function(area) {
        return {
          key: area.id,
          value: area.name
        };
      }
    });
    new SelectAdd(this.$('suburb'), {
      model: 'Suburb',
      representation: function(element) {
        return {
          cityId: this.$F('city'),
          areaId: this.$F('area'),
          name: $F(element)
        };
      }.bind(this),
      option: function(suburb) {
        return {
          key: suburb.id,
          value: suburb.name
        };
      }
    });
    
    this.$('country').observe('change', this.setStates.bind(this));
    this.$('state').observe('change', this.setCities.bind(this));
    this.$('city').observe('change', this.setAreas.bind(this));
    this.$('area').observe('change', this.setSuburbs.bind(this));

    this.coordinatesHelp = new Help({
      link: this.$('coordinates-help-link'),
      text: this.$('coordinates-help-text')
    });

    this.$('google-maps').observe('click',
      function() {
        var uri = this.getGoogleMapsUri();
        this.$('google-maps').writeAttribute('href', uri);
        return true;
      }.bind(this)
    );
    this.$('popup-code').observe('click',
      function() {
        this.$('popup-code').select();
      }.bind(this)
    );
		this.$('email').observe('keypress',
		  function(event) {
		    if (event.keyCode == Event.KEY_RETURN) this.save();
		  }.bind(this)
		);
		this.$('website').observe('keypress',
		  function(event) {
		    if (event.keyCode == Event.KEY_RETURN) this.save();
		  }.bind(this)
		);
		this.$('button').observe('click', this.save.bind(this));
    
    this.listing = Model.Listing.get(Session.user.listingId);
    this.states = Model.State.get().index('countryId');
    this.cities = Model.City.get().index('stateId');
    this.areas = Model.Area.get().index('cityId');
    this.suburbs = Model.Suburb.get(); // Don't index.
    
    this.setTextBoxes();
    this.setCountries();
    this.elapsed();
  },
  
  setCountries: function() {
    this.setLocale('country', Model.Country.get());
    this.setStates();
  },
  
  setStates: function() {
    var countryId = this.$F('country');
    this.resetState();
    if (countryId != 'all' && countryId != 'add') {
      var states = this.states[countryId] || [];
      this.setLocale('state', states);
      this.setCities();
    } else {
      this.resetCity();
      this.resetArea();
      this.resetSuburb();
    }
  },
  
  setCities: function() {
    var stateId = this.$F('state');
    this.resetCity();
    if (stateId != 'all' && stateId != 'add') {
      var cities = this.cities[stateId] || [];
      this.setLocale('city', cities);
      this.setAreas();
    } else {
      this.resetArea();
      this.resetSuburb();
    }
  },
  
  setAreas: function() {
    var cityId = this.$F('city');
    this.resetArea();
    if (cityId != 'all' && cityId != 'add') {
      var areas = this.areas[cityId] || [];
      this.setLocale('area', areas);
      this.setSuburbs();
    } else {
      this.resetSuburb();
    }
  },
  
  setSuburbs: function() {
    var cityId = this.$F('city');
    var areaId = this.$F('area');
    this.resetSuburb();
    if (areaId != 'all' && areaId != 'add') {
      var suburbs = this.suburbs.findAll(
        function(suburb) {
          return suburb.cityId == cityId && (areaId.integer() ? suburb.areaId == areaId : true);
        }
      );
      this.setLocale('suburb', suburbs);
    }
  },
  
  getGoogleMapsUri: function() {
    var name = this.$F('name').parenthesize();
    var latitude = this.$F('latitude');
    var longitude = this.$F('longitude');
    var country = this.getSelectText('country');
    var state = this.getSelectText('state');
    var city = this.getSelectText('city');
    var area = this.getSelectText('area');
    var suburb = this.getSelectText('suburb');
    var street = this.$F('street');
    if (latitude && longitude) {
      var coordinates = latitude + ',' + longitude;
      return 'http://maps.google.com/?q=' + encodeURIComponent(coordinates + ' ' + name);
    } else if (city || country) {
      var location = [street, city, state, country].compact().join(', ');
      return 'http://maps.google.com/?q=' + encodeURIComponent(location);
    } else {
      return 'http://maps.google.com';
    }
  },
  
  getSelectText: function(id) {
    var element = this.$(id);
    var text = element.options[element.selectedIndex].text;
    if (text.startsWith('Choose ') || text.startsWith('Add ')) text = null;
    return text;
  },
    
  resetState: function() {
    this.$('state').setValue('all').disable();
    this.$('state').up('.element').nextSiblings().invoke('hide');
  },
  
  resetCity: function() {
    this.$('city').setValue('all').disable();
    this.$('city').up('.element').nextSiblings().invoke('hide');
  },
  
  resetArea: function() {
    this.$('area').setValue('all').disable();
    this.$('area').up('.element').nextSiblings().invoke('hide');
  },
  
  resetSuburb: function() {
    this.$('suburb').setValue('all').disable();
    this.$('suburb').up('.element').nextSiblings().invoke('hide');
  },

  setLocale: function(locale, representation) {
    var options = [];
    options.push({'key': 'all', 'value': 'Choose ' + locale.capitalize()});
    if (Object.isArray(representation)) {
      representation.order('name').each(
        function(element) {
          options.push({'key': element.id, 'value': element.name});
        }
      );
    }
    if ((locale == 'area' || locale == 'suburb')) {
			options.push({'key': ' ', 'value': '-'});
		}
		options.push({'key': 'add', 'value': 'Add ' + locale.capitalize()});
		var value = this.listing[locale + 'Id'];
		if (value === null) value = ' ';
		this.$(locale).setValue({options: options, value: value}).enable();
  },
  
  setTextBoxes: function() {
    ['name', 'street', 'longitude', 'latitude', 'zip', 'phone', 'fax', 'email', 'website'].each(
			function(element) {
				this.$(element).setValue(this.listing[element]);
			}.bind(this)
		);
  },
  
  header: Html.a({ href: '#dashboard'}, 'Dashboard') + ' / Setup',
  
  save: function() {
    try {
      var listing = this.$('form').serialize();
      listing.id = Session.user.listingId;
      Model.Listing.set(listing);
      Notice.success('Your account was successfully saved.');
    } catch (error) {
      Notice.error(error);
    }
  },
  
  title: function() {
    return 'Setup';
  }
});

Controller.Simple = Class.create(Controller.Base, {
  initialize: function() {
    
  }
});

Controller.About = Class.create(Controller.Base, {});

Model.Reservation.addMethods({
  afterCreateSetAcceptedNote: function() {
    Model.Note.set({
      id: Id.get(),
      listingId: this.listingId,
      userId: Session.user.id,
      parentId: this.id,
      parentType: 'Reservation',
      body: 'Accepted.'
    });
  },
  
  afterUpdateSetChangeNote: function() {    
    var audit = {
      contactId: function(id) { return Model.Contact.get(id).name; },
      date: function(date) { return date.toDateTime('DAY d MONTH Y'); },
      time: function(time) { return time.substr(0, 5); },
      people: function(people) { return people; },
      category: function(category) { return category; }
    };
    var log = [];
    this._properties.each(
      function(property) {
        if (audit[property]) {
          var to = audit[property](this[property]);
          var from = audit[property](this._ancestor[property]);
          if (to != from) {
            if (property == 'contactId') property = 'contact';
            var entry = property + ' from ' + from + ' to ' + to;
            if (log.empty()) entry = 'changed ' + entry;
            log.push(entry);
          }
        }
      }.bind(this)
    );
    // Determine which tables were added to allocation:
    var added = this.tableIds.complement(this._ancestor.tableIds);
    added = added.collect(Model.Table.get).pluck('name');
    if (added.present()) {
      if (this._ancestor.tableIds.empty()) {
        log.push('allocated table'.s(added.length) + ' ' + added.sentence());
      } else {
        log.push('added table'.s(added.length) + ' ' + added.sentence() + ' to seating allocation');
      }
    }
    // Determine which tables were removed from allocation:
    var removed = this._ancestor.tableIds.complement(this.tableIds);
    removed = removed.collect(Model.Table.get).pluck('name');
    if (removed.present()) {
      if (added.empty()) {
        log.push('removed table'.s(removed.length) + ' ' + removed.sentence() + ' from seating allocation');
      } else {
        log.push('removed table'.s(removed.length) + ' ' + removed.sentence());
      }
    }
    // Set note if changes have been made:
    if (log.present()) {
      var body = log.sentence().upperCase(0) + '.';
      Model.Note.set({
        id: Id.get(),
        listingId: this.listingId,
        userId: Session.user.id,
        parentId: this.id,
        parentType: 'Reservation',
        body: body
      });
    }
  }
});

Object.extend(Number.prototype, {  
  toTime : function(overflow) {
    var hour = this.floor();
    var minute = ((this - hour) * 60).floor();
    var second = ((((this - hour) * 60) - minute) * 60).floor();
    if (overflow && hour >= 24) hour = 0;
    if (hour < 10) hour = '0' + hour;
    if (minute < 10) minute = '0' + minute;
    if (second < 10) second = '0' + second;
    return hour + ':' + minute + ':' + second;
  }
});

Object.extend(String.prototype, {
  append : function(phrase, separator) {
    return this + (!this.blank() ? (separator ? separator : ', ') : '') + phrase;
  },
  
  // Converts urls and email addresses into anchor tags.
  // Does not ignore existing anchor tags, i.e. not idempotent.
  autoLink : function() {
    var angle = [
      '&gt;',
      ' ;;ttgg&&'
    ];
    var website = [
      /https?:\/\/[^\s]+[\w\/=]{1}/i,
      '<a href="#{0}">#{0}</a>'
    ];
    var email = [
      /([\w\.!#\$%\-+.]+@[A-Z0-9\-]+(\.[A-Z0-9\-]+)+)/i,
      '<a href="mailto:#{0}">#{0}</a>'
    ];
    // Replace '&gt;' entity to help website regular expression:
    var string = this.gsub(angle[0], angle[1]);
    // Link website URIs:
    string = string.gsub(website[0], website[1]);
    // Link email URIs:
    string = string.gsub(email[0], email[1]);
    // Replace '&gt;' placemark with the real thing:
    string = string.gsub(angle[1], angle[0]);
    return string;
  },
  
  insertLinks : function() {
    return this.gsub(/https?:\/\/[^\s]+[^\.\s]+/i,
      function(matches) {
        return matches.first().toLink(matches.first());
      }
    ).gsub(/(^|[^=a-z0-9._%+-]+)([a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,6})/i,
      function(matches) {
        return matches[1] + matches[2].toLink(matches[2], {email : true});
      }
    );
  },
  
  parenthesize : function() {
    return this ? '(' + this + ')' : '';
  },
  
  removeQueryString : function() {
    return this.split('?').first();
  },
  
  removeRelativeHash : function() {
    return this.sub('/#', '');
  },
  
  toBookmarkId : function() {
    var uri = Parameters.uri.removeRelativeHash().removeQueryString() + '.' + this;
    var query = Parameters.uri.split('?')[1];
    return uri + (query ? '?' + query : '');
  },
  
  toH5 : function() {
    return this ? '<h5>' + this + '</h5>' : '';
  },
  
  toHours : function() {
    var hours = this.substring(0, 2) * 1;
    var minutes = this.substring(3, 5) * 1 / 60;
    var seconds = this.substring(6, 8) * 1 / 60 / 60;
    return hours + minutes + seconds;
  },
  
  toImage : function(options) {
    return ('<img src="#{src}"#{width}#{height}#{side}#{title}/>'.interpolate({
      'src' : this,
      'width' : (options && options.width ? ' width="' + options.width + '"' : ''),
      'height' : (options && options.height ? ' height="' + options.height + '"' : ''),
      'side' : (options && options.side ? ' width="' + options.side + '" height="' + options.side + '"' : ''),
      'title' : (options && options.title ? ' title="' + options.title + '"' : '')
    }));
  },
  
  toDiv : function(className) {
    if (this) {
      return '<div class="' + className + '">' + this + '</div>';
    } else {
      return '';
    }
  },
  
  toEmail : function(uri, options) {
    return this.toLink('mailto:' + uri, options);
  },
  
  toLink : function(uri, options) {
    if (this.present()) {
      return (uri ? '<a href="#{uri}"#{className}#{title}>#{text}</a>'.interpolate({
        'uri' : uri,
        'text' : this,
        'className' : options && options.className ? ' class="' + options.className + '"' : '',
        'title' : options && options.title ? ' title="' + options.title + '"' : ''
      }) : this);
    } else {
      return '';
    }
  },
  
  // Converts '2020-03-11' or '2020-03-11 07:30:00' to '2020-03'
  toMonth : function() {
    return this.substr(0, 7);
  },
  
  toP : function() {
    return this ? '<p>' + this.gsub(/\n\n/, '</p><p>').gsub(/\n/, '<br>') + '</p>' : '';
  }
});

// Do not extend Object.prototype as this prevents use of objects as
// key/value data structures. Attach static methods to Object instead.
Object.extend(Object, {
  present : function(object) {
    return Object.keys(object).all(
      function(key) {
        return object[key];
      }
    );
  }
});


Object.extend(Array.prototype, {
  compress : function() {
    return this.select(function(value) {
      return value !== null && value.present();
    });
  }  
});

Object.extend(Function.prototype, {
  repeat: function(interval) { // Milliseconds.
    return new PeriodicalExecuter(this, interval / 1000); // Seconds.
  }
});

Ajax.Responders.register({
  onCreate: function(request) {
    if (request.options.timeout) {
      request.timeoutId = window.setTimeout(
        function() {
          if ([1,2,3].include(request.transport.readyState)) {
            request.transport.abort();
            if (request.options.onTimeout) request.options.onTimeout();
          }
        }, request.options.timeout
      );
    }
  },
  
  onComplete: function(request) {
    if (request.timeoutId) window.clearTimeout(request.timeoutId);
  }
});

Session.afterSet = function(options) {
  options = options || {};
  
  // Set user listingId:
  var listing = Model.Listing.get({ userIds: [Session.user.id] }).first();
  if (listing) Session.user.listingId = listing.id;
  // Flush Session.user object to local storage.
  Session.flush();
  
  // Call onSuccess callback or redirect:
  if (options.onSuccess) {
    options.onSuccess();
  } else {
    var entry = History.get(true).reverse().find(
      function(entry) {
        var uri = entry.uri;
        return !uri.startsWith('/#sign-in') && !uri.startsWith('/#home');
      }
    );
    var uri = window.location;
    if (entry) {
      window.location = entry.uri;
    } else {
      window.location = '/#dashboard';
    }
    if (window.location == uri) {
      Dispatcher.classInstance.dispatch();
      History.pop();
    }
  }
};

Controller.Base.addMethods({
  afterInitializeStyleSelects: function() {
    $('Base.view').styleSelects();
  },
  
  afterInitializeUpdateNavigation: function() {
    var signOut = Html.a({ href: '', onclick: 'return false' }, 'Sign Out');
    if (Session.user.listingId) {
      $('Base.username').update(Session.user.email).show();
      $('Base.dashboard').show();
      $('Base.sign-in').hide();
      $('Base.sign-out').update(signOut).show();
      $('Base.search-query').show();
    } else {
      $('Base.username').hide();
      $('Base.dashboard').hide();
      $('Base.sign-in').show();
      $('Base.sign-out').hide();
      $('Base.search-query').hide();
    }
  },
  
  afterInitializeSetAssociateId: function() {
    if (Parameters.associateId) {
      var associateId = Parameters.associateId.without(/[^0-9]/);
      if (associateId) {
        Cookie.set('associateId', associateId, 1);
        Parameters.associateId = associateId;
      } else {
        delete Parameters.associateId;
      }
    } else {
      var associateId = Cookie.get('associateId');
      if (associateId) Parameters.associateId = associateId;
    }
  },
  
  afterInitializeSetTitleAndHeader: function() {
    this.setTitle(Object.isFunction(this.title) ? this.title() : this.title);
    this.setHeader(Object.isFunction(this.header) ? this.header() : this.header);
  },
  
  authenticateController: 'SignIn',
  
  authenticateUri: '/#sign-in',
  
  elapsed: function() {
    Log(this.className + ': ' + (Timestamp.get() - this.dispatched) + 'ms');
  },
  
  pageNotFound: function() {
    Parameters.controller = 'PageNotFound';
    Parameters.uri = '/#page-not-found';
    // this.dispatch is bound to Dispatcher class instance.
    // Pass 'true' to imply proxy execution:
    this.dispatch(true);
  },
  
  setHeader: function(header) {
    if (header) {
      $('Base.h1').update(header).show();
    } else {
      $('Base.h1').hide();
    }
  },

  setTitle: function(title) {
    document.title = (title ? title + ' - ' : '') + 'Sexbyfood';
  }
});

Element.addMethods({    
  // Sets element innerHTML to value or '' and returns original innerHTML.
  replaceContent: function(element, value) {
    element = $(element);
    var content = element.innerHTML;
    element.update(value || '');
    return content;
  },
  
  // setStyle does not set -moz-border-radius properly.
  setBorderRadius: function(element, value) {
    element = $(element);
    var style = element.readAttribute('style');
    style = '-moz-border-radius:' + value + ';' + style;
    style = '-webkit-border-radius:' + value + ';' + style;
    element.writeAttribute('style', style);
  }
});

Element.addMethods('p', {
  error: function(element, text){
    element = $(element);
    element.update('<span class="error">' + text + '</span>').show();
    return element;
  },
  
  success: function(element, text){
    element = $(element);
    element.update('<span class="success">' + text + '</span>').show();
    return element;
  }
});

Element.addMethods('div', {
  // Div is expected to be a row
  error: function(element, text){
    element = $(element);
    element.show().down('.element').update('<span class="error">' + text + '</span>');
    return element;
  },
  
  success: function(element, text){
    element = $(element);
    element.show().down('.element').update('<span class="success">' + text + '</span>');
    return element;
  }
});

Object.extend(Form.Methods, {
  clear: function(form) {
    form = $(form);
    form.getInputs('text').invoke('clear');
    form.select('textarea').invoke('clear');
    form.getInputs('password').invoke('clear');
    form.getInputs('hidden').invoke('clear');
    form.getInputs('radio').each(
      function(radio) {
        radio.checked = false;
      }
    );
    form.getInputs('checkbox').each(
      function(checkbox) {
        checkbox.checked = false;
      }
    );
    form.select('select').each(
      function(select) {
        select.selectedIndex = 0;
      }
    );
    return form;
  },
  
  serialize: function(form) {
    form = $(form);
    return Form.serializeElements(Form.getElements(form), true);
  }
});

Object.extend(Form.Element.Serializers, {
  select: function(element, object) {
    if (Object.isUndefined(object)) {
      return this[element.type == 'select-one' ? 'selectOne' : 'selectMany'](element);
    } else {
      var options = object.options, values = object.values || object.value || (!object.options ? object : null);
      if (options) {
        // Clear select options. Safari 4 (5530.17) cannot handle element.options.length = 0
        for (var i = element.options.length - 1; i >= 0; --i) {
          element.removeChild(element.options[i]);
        }
        for (var i = 0, length = options.length, option; i < length; ++i) {
          option = options[i];
          if (Object.isString(option) || Object.isNumber(option)) {
    				element.options[element.options.length] = new Option(option, option);
    			} else {
    				element.options[element.options.length] = new Option(option.value, option.key);
  				}
  			}
      }
      var option, value, single = !Object.isArray(values);
      for (var i = 0, length = element.length; i < length; ++i) {
        option = element.options[i];
        value = this.optionValue(option);
        if (single) {
          if (value == values) {
            option.selected = true;
            if (element.refreshSelect) element.refreshSelect();
            return;
          }
        } else {
          option.selected = values.include(value);
        }
      }
      if (element.refreshSelect) element.refreshSelect();
    }
  }
});

Object.extend(Form.Element.Methods, {
  clear: function(element) {
    element = $(element);
    element.value = '';
    return element;
  },
  
  loading: function(element, value) {
    element = $(element);
    if (element.readAttribute('cachedvalue') === null) {
      element.writeAttribute('cachedvalue', element.value);
    }
    if (value) element.value = value;
    element.disable().up().next().show();    
    return element;
  },
  
  loaded: function(element) {
    element = $(element);
    var value = element.readAttribute('cachedvalue');
    if (value !== null) element.value = value;
    element.enable().up().next().hide();
    return element;
  },
  
  getSelection: function(element) {
    element = $(element);
    var selection = {};
    if (document.selection) {
      var range1 = document.selection.createRange();
      var range2 = range1.duplicate();
      if (element.match('textarea')) {
        range2.moveToElementText(element);
        range2.setEndPoint('StartToStart', range1);
        selection.start = element.value.length - range2.text.length; 
        range2.setEndPoint('StartToEnd', range1);
        selection.end = element.value.length - range2.text.length;
      } else {
        selection.start = 0 - range2.moveStart('character', -100000);
        selection.end = selection.start + range1.text.length;
      }
    } else {
      selection.start = element.selectionStart + 0; // Return value not native getter.
      selection.end = element.selectionEnd + 0;
    }
    // IE sometimes returns negative values:
    if (selection.start < 0) selection = { start: 0, end: 0 };
    return selection;
  },
  
  setSelection: function(element, selection) {
    element = $(element);
    if (element.setSelectionRange) {
			element.focus();
			element.setSelectionRange(selection.start, selection.end);
		} else if (element.createTextRange) {
			var range = element.createTextRange();
			range.collapse(true);
			range.moveEnd('character', selection.end);
			range.moveStart('character', selection.start);
			range.select();
		}
		return element;
  }
});

Element.addMethods({
  insert: Element.insert.wrap(
    function(original, element, insertions) {
      var result = original(element, insertions);
      if (insertions.before) element = element.previous();
      if (insertions.after) element = element.next();
      element.styleSelects();
      return result;
    }
  ),
  
  refreshSelect: function(element) {
    element = $(element);
    var previous = element.previous(1);
    if (previous) previous.update(element.options[element.selectedIndex].text);
    return element;
  },
  
  replace: Element.replace.wrap(
    function(original, element, content) {
      var result = original(element, content);
      element.styleSelects();
      return result;
    }
  ),
  
  styleSelects: function(element) {
    element = $(element);
    var selects = $(element).select('select[class="select"]');
    selects.each(
      function(select) {
        var text = (select.options[select.selectedIndex] || {}).text;
        var previous = select.previous();
        // Idempotently attach style:
        if (!previous || !previous.match('div[class="select-button"]')) {
          var control = Html.div({ className: 'select-control' }, text);
          var button = Html.div({ className: 'select-button' }, '&or;');
          Element.insert(select, { before: control + button });
        }
        // Idempotently attach event handler:
        if (!select.retrieve('listening')) {
          select.observe('change',
            function(event) {
              event.element().refreshSelect();
            }
          );
          select.store('listening', true);
        }
      }
    );
    return element;
  },
  
  update: Element.update.wrap(
    function(original, element, content) {
      var result = original(element, content);
      element.styleSelects();
      return result;
    }
  )
});

ContentType.contentTypes = {
  'text/css' : 'css',
  'text/html' : 'html',
  'text/plain' : 'txt',
  'text/xml' : ['xml', 'rss'],

  'image/gif' : 'gif',
  'image/jpeg' : ['jpg', 'jpeg'],
  'image/png' : 'png',
  'image/tiff' : ['tiff', 'tif'],

  'application/json' : 'json',
  'application/pdf' : 'pdf',
  'application/psd': 'psd',
  'application/x-javascript' : 'js'
};

Config.DigestAuthentication.realm = 'www.sexbyfood.com';
Config.Http.timeout = (8).minutes();

Config.Controller.thumbnail = 'http://static2.sexbyfood.com/public/135';
Config.Database.adapter = 'LocalStorage';
Config.Router.routes = [
  ['HelpQuestions', 'help/([^/]+)', ['audience']],
  ['Simple', '(south-africa|spain)/([^/]+)/([^/]+)/([^/]+)/([^/]+)', ['country', 'state', 'city', 'area', 'suburb']],
  ['Simple', '(south-africa|spain)/([^/]+)/([^/]+)/([^/]+)', ['country', 'state', 'city', 'area']],
  ['Simple', '(south-africa|spain)/([^/]+)/([^/]+)', ['country', 'state', 'city']],
  ['Simple', '(south-africa|spain)/([^/]+)', ['country', 'state']],
  ['Simple', '(south-africa|spain)', ['country']],
  ['Simple', '(south-africa|spain)/([^/]+)/([^/]+)/([^/]+)/([^/]+)/([^/]+)', ['country', 'state', 'city', 'area', 'suburb', 'restaurant']],
  ['SignInChangePassword', 'sign-in/change-password/(.+)', ['hash']]
];
Config.WindowNameRequest.localResource = '/robots.txt';

var Client = true;
var Server = false;

Element.addMethods();

Event.observe(window, 'load',
  function() {
    Model.initialize();
    Session.initialize();
    Database.initialize({
      onFailure: Notice.error.bind(Notice),
      onProgress: Loading.setPercentage.bind(Loading),
      onSuccess: function() {
        Dispatcher.classInstance = new Dispatcher();
        if (window.Application) Application.classInstance = new Application();
      }
    });
  }
);

Event.observe(window, 'beforeunload',
  function() {
    var application = (window.Application || {}).classInstance;
    if (application && application.beforeUnload) application.beforeUnload();
  }
);

ApplicationCache.listen();