﻿var Class = {

  create: function(methods) {
    var klass = function() {
      this.initialize.apply(this, arguments);
    };
    klass.prototype = methods;
    return klass;
  }
};

Function.prototype.withErrorAlerting = function(objectId, methodId) {
  return function() {
    try {
      return this.apply(this, arguments);
    } catch(error) {
      alert('Error in ' + objectId + '#' + methodId + ': ' + (error.message || error));
    }
  };
};

// From prototype
Function.prototype.bindAsEventListener = function(object) {
  var __method = this;
  return function(event) {
    return __method.call(object, event || window.event);
  }
};

String.prototype.capitalize = function() {
  return this.charAt(0).toUpperCase() + this.substring(1);
};

String.prototype.uncapitalize = function() {
  return this.charAt(0).toLowerCase() + this.substring(1);
};

document.loadFragment = function(text) {
  var fragment = document.createDocumentFragment(), container = document.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);
  
  return fragment;
};

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'
  }  
};

var Component = {
    
  klasses: {},
  
  // Catch errors and alert the class, listener and description.
  alertErrors: false,
  
  create: function(name, methods, alertErrors) {
    if (this.klasses[name]) {
      throw new Error("'" + name + "' has already been defined");
    } else {
      var klass = function(container) {
        this.klass   = name;
        this.element = container;
        
        this._children = [];
        this._parents  = [];
      };
      
      for (var prop in Component.Methods) {
        klass.prototype[prop] = Component.Methods[prop];
      }
      for (var prop in methods) {
        klass.prototype[prop] = methods[prop];
      }
      if (alertErrors) {
        for (var prop in klass.prototype) {
          if (typeof klass.prototype[prop] == 'function') {
            klass.prototype[prop] = klass.prototype[prop].withErrorAlerting(name, prop);
          }
        }
      }
      
      klass.listeners = klass.prototype.matchListeners();

      return Component.klasses[name] = klass;
    }
  },
  
  instancesForDocument: function() {
    return Component.instancesFor(document);
  },
  
  instancesFor: function(node) {
    return (new Component.Initializer(node, Component.klasses)).components;
  }
};

Component.Methods = {
  
  matchListeners: function() {
    var matches, listeners = [];

    for (var prop in this) {
      if (typeof this[prop] == 'function') {
        if (matches = prop.match(/^on(abort|blur|change|click|dblclick|error|focus|keydown|keypress|keyup|load|mousedown|mousemove|mouseout|mouseover|mouseup|reset|resize|select|submit|unload)(\w+)$/i)) {
          listeners.push([matches[2].charAt(0).toLowerCase() + matches[2].substring(1), matches[1].toLowerCase(), prop]);
        }
      }
    }
    
    return listeners;
  },
    
  assignWithClass: function(name, element) {
    if (!element.className.match(new RegExp("(^|\\s)" + name + "(\\s|$)"))) {
      element.className += (element.className ? ' ' : '') + name;
    }
    return this[name] = element;
  },
  
  unassignClass: function(name) {
    var element;
    if (element = this[name]) {
      element.className = element.className.replace(name, '').replace(/\s+/, ' ').replace(/(^\s)|(\s$)/, '');
      return this[name] = null;
    }
  },
  
  request: function(method, url, parameters) {
    new AsynchronousRequest(method, url, parameters, this);
  },
    
  respondToFragment: function(fragment) {
    this.respondToComponents( Component.instancesFor(fragment) );
  },
  
  respondToComponents: function(components) {
    if (components[this.klass]) {
      this.replace(components[this.klass][0]);
    } else {
      var callback;
      
      for (prop in components) {      
        callback = 'add' + prop.charAt(0).toUpperCase() + prop.substring(1);

        if (this[callback]) {
          for (var i = 0; i < components[prop].length; i++) {
            this[callback](components[prop][i]);
          }
        }
      }
    }
  },
  
  registerListeners: function() {
    var instanceListeners = [], klassListeners = Component.klasses[this.klass].listeners;
    
    for (var listener, i = 0; i < klassListeners.length; i++) {
      if (listener = this.registerListener(klassListeners[i])) {
        instanceListeners.push(listener);
      }
    }

    return instanceListeners;
  },
  
  registerListener: function(listener) {
    var target = this[listener[0]], type = listener[1], callback = this[listener[2]].bindAsEventListener(this);
    
    if (target) {
      target = target.element || target;
      
      if (target.addEventListener) {
        target.addEventListener(type, callback, false);
      } else {
        target.attachEvent('on' + type, callback);
      }
      return listener.concat(callback);
    }
  },

  unregisterListeners: function() {
    if (this.listeners) {
      for (var listener, i = 0; i < this.listeners.length; i++) {
        listener = this.listeners[i];
        listener[0].removeEventListener(listener[1], listener[3], false);
      }
      this.listeners = [];
    }
  },
  
  registerObject: function(name, object) {
    var callback  = 'register' + name.capitalize();
      
    if (this[callback]) {
      this[callback](object)
    } else {
      this[name] = this[name] || object
    }
  }
};

/*--------------------------------------------------------------------------*/

Component.Initializer = Class.create({
  
  initialize: function(node, klasses) {
    this.klasses    = klasses;
    this.components = {};
    
    this.traverseNode(node, []);
    
    this.initializeComponents();
  },
  
  traverseNode: function(node, ancestors) {
    var classNames, object, components = [];
    
    if (node.tagName) {
      if (node.className) {
        names = node.className.split(/\s+/);

        for (var name, i = 0; i < names.length; i++) {
          name   = names[i];
          object = node;

          if (this.klasses[name]) {
            object = new this.klasses[name](node);
            components.push(object);
            
            this.components[name] = this.components[name] || [];
            this.components[name].push(object);
          }
          
          for (var component, j = ancestors.length - 1; j > -1 ; j--) {
            component = ancestors[j];
            
            component.registerObject(name, object);
            component._children.push(name);
            
            if (object.registerObject) {
              object.registerObject(component.klass, component);
              object._parents.push(component.klass);
            }
          }
        }
      }
    }
    
    for (var i = 0; i < node.childNodes.length; i++) {
      this.traverseNode(node.childNodes[i], ancestors.concat(components));
    }
  },
    
  initializeComponents: function() {
    for (var name in this.components) {
      
      for (var component, i = 0; i < this.components[name].length; i++) {
        component = this.components[name][i];
        
        if (component.initialize) {
          component.initialize();
        }
        component.listeners = component.registerListeners();
      }
    }
  }
});

/*--------------------------------------------------------------------------*/

var AsynchronousRequest = Class.create({
  
  initialize: function(method, url, parameters, component) {

    this.url        = url;
    this.parameters = {};
    this.component  = component;
    this.headers    = {};
    
    this.transport  = this.getTransport();
    
    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;
    }
    
    this.request();
  },
  
  respondToContent: function(status, text, contentType) {
    if (status == 500) {

    } else if (status == 0 || (status >= 200 && status < 300)) {
      if (!contentType || contentType.match(/html/i)) {
        if (text && text != ' ') {
          this.component.respondToFragment(document.loadFragment(text));
        } else if ((this.parameters._method || this.method) == 'DELETE') {
          if (this.component.remove) {
            this.component.remove();
          }
        }
      } else if (contentType.match(/json/i)) {
        if (this.component.update) {
          this.component.update(eval('(' + text + ')'));
        }
      }
    }
  },
  
  request: 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.respondToContent(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('&');
  },
  
  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;
    }
  }
});

/*--------------------------------------------------------------------------*/

(function(i) {
  var u = navigator.userAgent.toLowerCase();
  var ie = /*@cc_on!@*/false;
  if (/webkit/.test(u)) {
    // safari
    timeout = setTimeout(function(){
			if ( document.readyState == "loaded" || 
				document.readyState == "complete" ) {
				i();
			} else {
			  setTimeout(arguments.callee,10);
			}
		}, 10); 
  } else if ((/mozilla/.test(u) && !/(compatible)/.test(u)) ||
             (/opera/.test(u))) {
    // opera/moz
    document.addEventListener("DOMContentLoaded",i,false);
  } else if (ie) {
    // IE
    (function (){ 
      var tempNode = document.createElement('document:ready'); 
      try {
        tempNode.doScroll('left'); 
        i(); 
        tempNode = null; 
      } catch(e) { 
        setTimeout(arguments.callee, 0); 
      } 
    })();
  } else {
    window.onload = i;
  }
})(Component.instancesForDocument);