var Class = {

  create: function(source) {
    function klass() {
      this.initialize.apply(this, arguments);
    };
    
    mixin(klass, Class.Methods);
    mixin(klass.prototype, Object.Methods);
    mixin(klass.prototype, source || {});

    return klass;
  }
};

Class.Methods = {
  extend: function(source) {
    return Class.create(mixin(mixin({}, this.prototype), source));    
  }
};

Object.Methods = {
  initialize: function() {}
};

function mixin(object, source) {
  source = source || {};
  
  for (var id in source)
    if (typeof source[id] == 'function')
      object[id] = source[id];

  if (source.toString != Object.prototype.toString) // force IE to recognise when we override toString
    object.toString = source.toString;

  return object;
}

var errors;

var Component = Class.create({
  
  initialize: function(element, context) {
    this.window    = window;
    this.document  = element.ownerDocument;
    this.element   = element;
    this.context   = context;
    this.listeners = {};
    this.fields    = {};
    this.prepare();
  },
  
  // Initialization steps for when the container is found
  prepare: function() {
  },

  // Initialization steps for when all content has been registered
  run: function() {
  },
  
  getDefinition: function(name) {
    return this.klass.Components[name];
  },
  
  _handleFragment: function(fragment) {
    var callback, context = this.context.loadNode(fragment);
    
    for (var name in context.components)
      if (this[callback = 'insert' + name.capitalize()])
        this[callback](context.components[name]);
    
  },
  
  insert: function(object, reference) {
    var element;

    if (typeof object == 'string')
      element = document.loadElement(object);
    else
      element = object.element || object;
    
    if (reference)
      reference = reference.element || reference;

    this.element.insertBefore(element, reference);
    
    return this.context.insert(element);
  },
  
  notifyWithField: function(object) {
    var element = object.element || object;

    this.field = this.field || element;
    this.fields[element.name] = element;
  },
  
  notifyWithContext: function(context) {
    var component;
    
    for (var name in context.components) {
      component = context.components[name];
      
      if (component != this)
        this.notifyWithContainer(name, component);
    } 
    
    if (context.context)
      this.notifyWithContext(context.context);
  },
  
  notifyWithContainer: function(name, object, insertion) {
    return this.notifyWith(name, object, true, insertion);
  },
  
  notifyWithChild: function(name, object, insertion) {
    return this.notifyWith(name, object, false, insertion);
  },
  
  notifyWith: function(name, object, container, insertion) {
    var method = 'notifyWith' + name.capitalize();
    
    if (this[method]) {
      return this[method](object, container, insertion);
    } else if (typeof this[name] == 'undefined') {
      return this[name] = object;
    }
  },
  
  remove: function() {
    this.element.parentNode.removeChild(this.element);
  },
  
  handleError: function(object) { // if no handler defined, delegate upwards
    if (this.context.context) {
      return this.context.context.handleError(object);
    }
  },

  request: function() {
    if (arguments.length > 1) {
      var req = new AsyncRequest(arguments[0], arguments[1], arguments[2], this);
      
      req.send();
      return req;
    }
    
    var element = this[arguments[0]];

    switch (element.tagName.toLowerCase()) {
      case 'a':    return this.request('GET', element.href, {});
      case 'form': return this.request(element.method, element.action, {}); // todo: serialize!
    }
  },
    
  getName: function() {
    return this.context.getName(this);
  },
  
  addName: function(name, object) {
    var element = object.element || object;
    
    element.className = name + (element.className ? ' ' : '') + element.className;

    this.context.notifyWithChild(name, element);
    
    return element;
  },

  removeName: function(name) {
    var element;
    if (element = this[name]) {
      element.className = element.className.replace(name, '').replace(/\s+/, ' ').replace(/(^\s)|(\s$)/, '');
      delete this[name];
    }
  },
  
  setName: function(name, element) {
    this.removeName(name);
    this.addName(name, element);
  },
  
  serialize: function() {
    var o = {};
    
    for (var name in this.fields)
      o[name] = this.fields[name].value;
      
    return o;
  },

  registerListeners: function() {
    var component = this;
    
    for (var name in this.klass.listeners)
      if (this[name])
        for (var event in this.klass.listeners[name]) {
          this.listeners[name]        = this.listeners[name] || {};
          this.listeners[name][event] = this.createListener(this.klass.listeners[name][event]);
          
          if (this[name].addEventListener)
            this[name].addEventListener(event, this.listeners[name][event], false);
          else if (this[name].attachEvent)
            this[name].attachEvent('on' + event, this.listeners[name][event]);
        }
  },
  
  createListener: function(method) {
    var component = this, listener = function(event) {
      return component[method](event || window.event);
    };
    
    if (errors && errors.alert)
      return function(event) {
        try {
          listener(event);
        } catch (error) {
          errors.alert(
            new Component.Error(component, method, error.message || error.toString()) );
        }
      }
    else
      return listener;
  },
  
  unregisterListeners: function() {
    for (var name in this.listeners)
      for (var event in this.listeners[name])
        if (this[name].removeEventListener)
          this[name].removeEventListener(event, this.listeners[name][event], false);
        else if (this[name].detachEvent)
          this[name].detachEvent('on' + event, this.listeners[name][event]);

    this.listeners = {};
  },
  
  toString: function() {
    var object, elements = [], components = [];

    for (var id in this) {
      object = object = this[id];
      
      if (object) {
        if (object.nodeType == 1 && object != this.element)
          elements.push(id);
        if (object.initialize == Component.prototype.initialize && object != this.interface)
          components.push(id);        
      }
    }

    return elements.concat(components).join(' ');
  },
  
  toHTML: function() {
    return this.element.innerHTML;
  }
});

Component.Error = Class.create({
  
  initialize: function(component, method, message) {
    this.component = component;
    this.method    = method;
    this.message   = message;
  },
  
  toString: function() {
    return this.component.getName() + '#' + this.method + ': ' + this.message;
  }
})

Component.ClassMethods = {
  
  // Create a new tree.
  // Creates a Context containing a single instance of this component, with subcomponents determined by klass.Definitions
  load: function(content) {
    var element, component, context;

    if (typeof content == 'string')
      element = document.loadElement(content);
    else
      element = content;
      
    if (!element || element.nodeType != 1)
      throw new Error('Content is not an Element or HTML string: ' + content);
    
    context   = new Context(element);
    component = new this(element, context);
    
    context.components.interface = component;
    
    context.load();
    context.run();
    
    if (window.attachEvent)
      window.attachEvent('onunload', function(event) { context.unload() });
    
    return component;
  },
  
  define: function(name, source) {
    return this.Components[name] = Component.create(source);
  }
};

// Extend the Component class and build a registerListeners methods based on the methods provided:
Component.create = function(source) {
  var klass = mixin( Component.extend(source), Component.ClassMethods );
  
  klass.prototype.klass = klass;
  klass.listeners = Component.matchListeners(source);
  
  klass.Components = {};
    
  return klass;
};

Component.matchListeners = function(source) {
  var element, event, matches, listeners = {};

  for (var id in source) {
    if (typeof source[id] == 'function') {
      if (matches = id.match(Component.Listener)) {
        event   = matches[1].toLowerCase();
        element = matches[2].uncapitalize();
        
        listeners[element] = listeners[element] || {};
        listeners[element][event] = id;
      }
    }
  }
  return listeners;
};

var Events = 'abort beforeunload blur change click dblclick error focus keydown keypress keyup load mousedown mousemove mouseout mouseover mouseup reset resize select submit unload'.split(' ');

Component.Listener = new RegExp('^on(' + Events.join('|') + ')(\w+)$', 'i');
Component.Listener = /^on(abort|beforeunload|blur|change|click|dblclick|error|focus|keydown|keypress|keyup|load|mousedown|mousemove|mouseout|mouseover|mouseup|reset|resize|select|submit|unload)(\w+)$/i;

var Context = Class.create({

  initialize: function(element, context) {
    this.element    = element;
    this.components = {};
    this.context    = context;
    this.contexts   = [];
  },
  
  load: function() {
    for (var i = 0; i < this.element.childNodes.length; i++)
      this.loadNode(this.element.childNodes[i], false);
    
    if (this.context)
      for (var name in this.components)
        this.components[name].notifyWithContext(this.context);
  },
  
  run: function() {
    var component;
    
    for (var name in this.components) {
      component = this.components[name];
      
      component.run();
      component.registerListeners();
    }
    
    for (var i = 0; i < this.contexts.length; i++)
      this.contexts[i].run();
  },
  
  unload: function() {
    for (var name in this.components) {
      this.components[name].unregisterListeners();
      this.components[name] = null;
    }
    
    for (var i = 0; i < this.contexts.length; i++)
      this.contexts[i].unload();
    
    this.components = null;
    this.contexts   = null;
  },
  
  insert: function(content) {
    if (typeof content == 'string')
      content = document.loadElement(content);
    
    var contexts = this.loadNode(content, true);

    for (var i = 0; i < contexts.length; i++)
      contexts[i].run();
    
    if (contexts[0])
      return contexts[0].components;
  },
  
  loadNode: function(node, insertion) {
    var contexts = [];
    
    if ((node.nodeType == 1)  &&
        (context = this.loadElement(node, insertion)) ) {
      
      this.contexts.push(context);
      
      contexts.push(context);
      context.load();
    } else {
      for (var i = 0; i < node.childNodes.length; i++)
        contexts = contexts.concat(this.loadNode(node.childNodes[i], insertion));
    }    
    return contexts;
  },
  
  // loadNode: function(node, insertion) {
  //   var context;
  //   
  //   if ((node.nodeType == 1)  &&
  //       (context = this.loadElement(node, insertion)) ) {
  //     this.contexts.push(context);
  //     context.load();
  //     return context;
  //   } else {
  //     for (var i = 0; i < node.childNodes.length; i++)
  //       this.loadNode(node.childNodes[i], insertion);
  //   }
  // },
  
  loadElement: function(element, insertion) {    
    var context, names;
    
    if (element.className)
      names = element.className.replace(/^\s*/, '').replace(/\s*$/, '').split(/\s+/);
    
    names = names || [];
    
    if (element.id)
      names.unshift(element.id);
    
    for (var component, name, i = 0; i < names.length; i++) {
      name = names[i];
      
      if (definition = this.getDefinition(name)) {
        context = context || new Context(element, this);
        
        component = new definition(element, context);
        context.components[name] = component;
        
        this.notifyWithChild(name, component, insertion);
      } else {
        this.notifyWithChild(name, element, insertion);
      }
    }
    
    return context;
  },
  
  notifyWithChild: function(name, object, insertion) {
    for (var id in this.components)
      this.components[id].notifyWithChild(name, object, insertion);
    if (this.context)
      this.context.notifyWithChild(name, object, insertion);
  },
  
  getName: function(component) {
    for (var name in this.components)
      if (this.components[name] == component)
        return name;
  },
  
  handleError: function(object) {
    for (var id in this.components) {
      if (this.components[id].handleError) {
        return this.components[id].handleError(object);
      }
    }
    return this.context.handleError(object);
  },
    
  getDefinition: function(name) {
    var definition;
    
    for (var id in this.components)
      if (definition = this.components[id].getDefinition(name))
        return definition;

    if (this.context)
      return this.context.getDefinition(name);
  }
});

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

String.prototype.uncapitalize = function() {
  return this.charAt(0).toLowerCase() + this.substring(1);
};
var AsyncRequest = Class.create({
  
  initialize: function(method, url, parameters, handler) {

    this.url     = url;
    this.handler = handler || {};
    this.headers = {};
    
    this.transport  = this.getTransport();
    
    this.parameters = {};
    for (prop in parameters) this.parameters[prop] = parameters[prop];
    
    if (!method.match(/GET|POST/i)) {
      this.parameters._method = method;
      method = 'POST';
    }
    
    this.method = method.toUpperCase();
    
    var queryString = this.compileQueryString(this.parameters);;
    
    if (this.method == 'GET') {
      if (queryString.length > 0) {
        this.url += '?' + queryString;
      }
      this.queryString = '';
    } else {
      this.queryString = queryString;
    }
  },
  
  dispatchResponse: function(status, text, contentType) {
    contentType = contentType || 'text/html';
    
    var content;

    if (text && text != ' ') {
      if (contentType.match(/html/i)) {
        content = document.loadElement(text);
      } else if (contentType.match(/json/i)) {
        content = eval('(' + text + ')');
      }  
    }

    if (status >= 500) {
      if (this.handler.debug) {
        return this.handler.debug(content);
      }
    } else if (status >= 400) {
      if (this.handler.handleError) {
        return this.handler.handleError(content);
      }
    } else if (status == 0 || (status >= 200 && status < 300)) {
      
      if (content) {
        
        if (content.nodeType >= 0 && this.handler.handleElement) {
          
          return this.handler.handleElement(content);
        } else if (this.handler.update) {
          return this.handler.update(content);
        }
      } else if ((this.parameters._method || this.method) == 'DELETE') {
        if (this.handler.remove) {
          return this.handler.remove();
        }
      }
    }

    if (this.handler.handleResponse)
      return this.handler.handleResponse(status, text, contentType);
  },
  
  send: function() {
    if (!this.transport) {
      return;
    }
    
    var request = this, transport = this.transport;
    
    transport.open(request.method.toUpperCase(), request.url, true);

    transport.onreadystatechange = function() {
      if (transport.readyState == 4) {
        request.dispatchResponse(transport.status, transport.responseText, transport.getResponseHeader('Content-Type'));

        //transport.onreadystatechange = null;
      }
    }
    
    var headers = {
      'X-Requested-With':  'XMLHttpRequest', // keep compatibility with Ajax in Rails
      'Content-type':      'application/x-www-form-urlencoded',
      'Accept':            'text/html, application/json, text/xml, */*',
      'If-Modified-Since': 'Thu, 1 Jan 1970 00:00:00 GMT' // Stop IE7 caching
    };
    
    for (var prop in headers) {
      transport.setRequestHeader(prop, headers[prop]);
    }

    transport.send(this.queryString);
  },
  
  compileQueryString: function(parameters) {
    var parts = [];
    for (prop in parameters) {
      parts.push(encodeURIComponent(prop) + '=' + encodeURIComponent(parameters[prop]));
    }
    return parts.join('&');
  },
  
  getKeyValues: function(object) {
    var pairs = [];
  },
  
  getTransport: function() {
    try {
      try {
        return new ActiveXObject('Msxml2.XMLHTTP')
      } catch(error) {
        try {
          return new ActiveXObject('Microsoft.XMLHTTP')
        } catch(error) {
          return new XMLHttpRequest()
        }
      }
    } catch(error) {
      return null;
    }
  }
});

document.buildElement = function() {
  var element = this.createElement(arguments[0]);
  
  for (var i = 1; i < arguments.length; i++)
    element.appendChild( typeof arguments[i] == 'string' ?
      this.loadFragment(arguments[i]) : this.buildElement.apply(document, arguments[i]) );
  
  return element;
};

document.loadFragment = function(text) {
  var fragment = this.createDocumentFragment(), container = this.createElement('div');
  
  var containingTags = [];
  
  var matches = text.match(/^\s*<(li|td|tr|tbody)/i);
  
  if (matches)
    containingTags = Tags.containersFor(matches[1].toLowerCase());
  
  for (var i = 0; i < containingTags.length; i++) text = "<" + containingTags[i] + ">" + text + "</" + containingTags[i] + ">";
  container.innerHTML = text;
  for (var i = 0; i < containingTags.length; i++) container = container.firstChild;
  
  for (var i = container.childNodes.length - 1; i > -1; i--) fragment.insertBefore(container.childNodes[i], fragment.firstChild);
  
  if (!fragment.firstChild && text.length > 0)
    fragment.appendChild(this.createTextNode(text));

  return fragment;
};

document.loadElement = function(text) {
  var node = this.loadFragment(text).firstChild;
  
  do {
    if (node.nodeType == 1)
      return node.parentNode.removeChild(node);
  } while (node = node.nextSibling);
};

var Tags = {
  containersFor: function(tag) {
    tag = tag.toLowerCase();
    
    var containingTags = [], containingTag = Tags.containers[tag];
    if (containingTag) {
      containingTags = Tags.containersFor(containingTag);
      containingTags.unshift(containingTag);
    }
    return containingTags;
  },
  
  containers: {
    li:    'ul',
    td:    'tr',
    tr:    'tbody',
    tbody: 'table'
  }  
};

document.loadEvents = [];

mixin(document, {
  
  register: function(callback) {
    this.loadEvents.push(callback);
  },

  run: function() {
    for (var callback, i = 0; i < this.loadEvents.length; i++) {
      callback = this.loadEvents[i];

      if (errors && errors.alert) {
        try {
          callback.call(this);
        } catch (error) {
         errors.alert(error);
        }
      } else {
        callback.call(this);
      }
    }
  }
});

var userAgent = navigator.userAgent.toLowerCase();
var ie = /*@cc_on!@*/false;

if (/webkit/.test(userAgent)) {
  var timeout = setTimeout(function() {
    if (document.readyState == "loaded" || document.readyState == "complete" ) {
      document.run();
    } else {
      setTimeout(arguments.callee, 10);
    }
  }, 10); 
} else if ((/mozilla/.test(userAgent) && !/(compatible)/.test(userAgent)) || (/opera/.test(userAgent))) {
  document.addEventListener("DOMContentLoaded", document.run, false);
} else if (ie) {
  (function () { 
    var tempNode = document.createElement('document:ready'); 
    try {
      tempNode.doScroll('left'); 
      document.run();
      tempNode = null; 
    } catch(e) {
      setTimeout(arguments.callee, 0); 
    } 
  })();
}
var assert = function(description) {
  var context = {}, assertion = arguments[ arguments.length - 1 ];
  
  if (arguments.length > 2)
    arguments[1].call(context);
  
  // if (typeof assertion == 'function') {
  //   assertions.wait(description);
  //   
  //   var interval = setInterval(function() {
  //     if (!assertion.call(context))
  //       return;
  //     
  //     clearInterval(interval);
  //     assertions.pass(description);
  //   }, 100);
  // } else {
    
    
    var result;
    
    if (typeof assertion == 'function')
      result = assertion.call(context);
    else
      result = assertion;
    
    if (result === false)
      assertions.fail(description);
    else
      assertions.pass(description);
  //}
};

var assertions = {
  passed:  [],
  failed:  [],
  waiting: [],
  
  pass: function(description) {
    this.stopWaiting(description);
    this.passed.push(description);
    this.alert();
  },
  
  fail: function(description) {
    this.stopWaiting(description);
    this.failed.push(description);
    this.alert();
  },
  
  wait: function(description) {
    this.waiting.push(description);
    this.alert();
  },
  
  alert: function() {
    this.count = this.passed.length + this.failed.length + this.waiting.length;
    
    if (this.report)
      this.report.alert(this);
  },
  
  stopWaiting: function(description) {
    for (var i = 0; i < this.waiting.length; i++)
      if (this.waiting[i] == description)
        return this.waiting.splice(i, 1);
  }
};


// From mootools
var Cookie = {
	set: function(key, value, options){
		options = mixin({
			domain: false,
			path: '/',
			duration: 365
		}, options || {});
		value = escape(value);
		if (options.domain) value += "; domain=" + options.domain;
		if (options.path) value += "; path=" + options.path;
		if (options.duration){
			var date = new Date();
			date.setTime(date.getTime() + (options.duration * 86400000));
			value += "; expires=" + date.toGMTString();
		}
		document.cookie = key + "=" + value;
	},

	get: function(key){
		var value = document.cookie.match('(?:^|;)\\s*'+key+'=([^;]*)');
		return value ? unescape(value[1]) : false;
	},

	remove: function(key){
		this.set(key, '', {duration: -1});
	}
};

// Mini effects library, adapted from moo.fx
// See http://moofx.mad4milk.net/

// Transitions (c) 2003 Robert Penner (http://www.robertpenner.com/easing/), BSD License.

Transitions = {
 linear: function(t, b, c, d)    { return c*t/d + b; },
 sineInOut: function(t, b, c, d) { return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; }
};

var Effect = Class.create({

  setOptions: function(options){
    options = options || {};
    this.options = {};
   
    var defaults = {
      onStart:    function(){},
      onComplete: function(){},
      transition: Transitions.sineInOut,
      duration:   350,
      unit:       'px',
      wait:       true,
      fps:        50
    };
   
    for (var prop in defaults) {
      this.options[prop] = options[prop] || defaults[prop];
    }
  },

 step: function(){
   var time = new Date().getTime(), object = this;
   if (time < this.time + this.options.duration){
     this.cTime = time - this.time;
     this.setNow();
   } else {
     setTimeout(function() {
       object.options.onComplete(object.element);
     }, 10);
     this.clearTimer();
     this.now = this.to;
   }
   this.increase();
 },

 setNow: function(){
   this.now = this.compute(this.from, this.to);
 },

 compute: function(from, to){
   var change = to - from;
   return this.options.transition(this.cTime, from, change, this.options.duration);
 },

 clearTimer: function(){
   clearInterval(this.timer);
   this.timer = null;
   return this;
 },

 _start: function(from, to){
   var object = this;
   if (!this.options.wait) this.clearTimer();
   if (this.timer) return;
   setTimeout(function() {
     object.options.onStart(object.element);
   }, 10);
   this.from = from;
   this.to = to;
   this.time = new Date().getTime();
   this.timer = setInterval(function() {
     object.step();
   }, Math.round(1000/this.options.fps));
   return this;
 },

 custom: function(from, to){
   return this._start(from, to);
 },

 set: function(to){
   this.now = to;
   this.increase();
   return this;
 },

 hide: function(){
   return this.set(0);
 },

 setStyle: function(e, p, v){
   if (p == 'opacity'){
     //if (v == 0 && e.style.visibility != "hidden") e.style.visibility = "hidden";
     //else if (e.style.visibility != "visible") e.style.visibility = "visible";
     if (window.ActiveXObject) e.style.filter = "alpha(opacity=" + v*100 + ")";
     e.style.opacity = v;
   } else e.style[p] = v+this.options.unit;
 }

});

var Effects = {};

Effects.Opacity = Effect.extend({

 initialize: function(element, options){
   this.element = element;
   this.now     = 1;   

   this.setOptions(options);
 },

 toggle: function(){
   if (this.now > 0) return this.custom(1, 0);
   else return this.custom(0, 1);
 },

 show: function(){
   return this.set(1);
 },
 
 increase: function(){
   this.setStyle(this.element, 'opacity', this.now);
 }
});

var IE6 = /MSIE (5|6|7)/.test(navigator.userAgent);

var Appearance = Effects.Opacity.extend({

  initialize: function(element) {
    this.now     = 1;   
    this.element = element;

    this.setOptions();    
    
    if (IE6)
      return this.options.onComplete(this.element);
    
    this.setStyle(element, 'opacity', 0);
    this.custom(0, 1);
  }
});

var FadeRemove = Effects.Opacity.extend({

  initialize: function(element, callback) {    
    this.now     = 1;
    this.element = element;
    this.setOptions({
      onComplete: function(element) {
        element.parentNode.removeChild(element);
        if (callback) callback(element);
      }
    });
    
    if (IE6)
      this.options.onComplete(this.element);  
    else
      this.custom(1, 0);
  }
});
