/*
 * A behavioral JavaScript application framework. (Documentation a-comin)
 * Adam Bones (adam@boxpop.co.uk)
 */

var Responder = {
  klasses:   [],
  instances: [],

  debug: false,
  
  find: function(container) {
    return Responder.instances.detect(function(responder) {
      return responder.container == container;
    });
  },

  create: function() {
    var klass = Class.create();

    Object.extend(klass.prototype, Responder.Methods);
    Responder.klasses.push([klass, $A(arguments)]);

    return klass;
  },

  onDOMContentLoaded: function(event) {
    Responder.info('Load Responders ...');
    Responder.initializeFor(document.body);
    Responder.info('... done');
    Event.observe(document.body, 'DOMNodeInserted', Responder.onDOMNodeInserted);
  },

  onDOMNodeInserted: function(event) {
    if (event.target.nodeType == 1) {
      Responder.initializeFor(event.target);
    }
  },

  initializeFor: function(element) {
    element = $(element);
    var klass, containerSelectors, containers, object;

    for (var i = 0; i < Responder.klasses.length; i++) {
      klass = Responder.klasses[i][0], containerSelectors = Responder.klasses[i][1];

      for (var j = 0; j < containerSelectors.length; j++) {
        containers = element.getElementsBySelector(containerSelectors[j]);
        if (element.match(containerSelectors[j])) {
          containers.push(element);
        }
        containers.each(function(container) {
          Responder.instances.push(new klass(container, klass.Selectors || {}));
        });
      }
    }
  },

  error: function(message) {
    if (this.debug) alert(message);
  },

  info: function(message) {
    if (this.debug) alert(message);
  }
};

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

Object.extend(String.prototype, {
  unCapitalize: function() {
    if (this.length > 0) {
      return this.charAt(0).toLowerCase() + this.substring(1);
    }
  }
});

Event.Names = $w('abort blur change click dblclick error focus keydown keypress keyup load mousedown mousemove mouseout mouseover mouseup reset resize select submit unload');

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

Responder.Methods = {
  klass: Responder,

  initialize: function(container, selectors) {
    this.assignPropertiesFromSelectors(container, selectors);
    this.observe.apply(this, this.elementNames());
  },

  elementNames: function() {
    var names = [];
    for (prop in this) {
      if (Node.element(this[prop])) {
        names.push(prop);
      }
    }
    return names;
  },

  assignPropertiesFromSelectors: function(container, selectors) {
    Object.extend(this, this.properties(container, selectors));
  },

  properties: function(container, selectors) {
    var properties = { container: container };

    for (name in selectors) {
      element = container.getElementsBySelector(selectors[name]).first();
      if (element) {
        properties[name] = element;
      }
    }
    return properties;
  },

  observe: function() {
    this.observersFor($A(arguments)).each(function(observer) {
      //this[observer.method] = this[observer.method].bind(this);
      var method            = this[observer.method];
      this[observer.method] = function(event) {
        try {
          method.call(this, event);
        } catch (error) {
          Responder.error('[' + observer.method + '] ' + (error.message || error));
        }
      }.bind(this);
      observer.element.observe(observer.name, this[observer.method]);
    }.bind(this));
  },

  stopObserving: function() {
    this.observersFor($A(arguments)).each(function(observer) {
      observer.element.stopObserving(observer.name, this[observer.method]);
    }.bind(this));
  },

  observersFor: function(props) {
    var observers = [];
    //var regex = new RegExp('^on(\\w+)(' + props.join('|') + ')$', 'i');
    var regex = new RegExp('^on(' + Event.Names.join('|') + ')(' + props.join('|') + ')$', 'i');
    var matches, element, name;

    for (prop in this) {
      matches = prop.match(regex);
      if (matches) {
        name    = matches[1].toLowerCase();
        element = this[matches[2].unCapitalize()];
        if (element) {
          //observers.push([element, name, this[prop]]);
          observers.push({
            element: element,
            name:    name,
            method:  prop
          });
        }
      }
    }
    return observers;
  },
  
  assignElementsByClassName: function(container) {
    var element, elements = container.getElementsByTagName('*');
    for (var i = 0; i < elements.length; i++) {
      element = elements[i];
      if (element.className) {
        this[element.className] = $(element);
      }
    }
  }
};

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

Responder.RequestHijacker = {};
Responder.RequestHijacker.Methods = {
  hijackLink: function(prop) {
    this[('onClick-' + prop).camelize()] = function(event) {
      Event.stop(event);
      new Ajax.Request(this[prop].href, { onSuccess: this.successCallbackFor(prop) });
    }.bind(this);
  },

  hijackForm: function(prop) {
    this[('onSubmit-' + prop).camelize()] = function(event) {
      Event.stop(event);
      new Ajax.Request(this[prop].action, { parameters: Form.serialize(this[prop]), onSuccess: this.successCallbackFor(prop) });
    }.bind(this);
  },

  successCallbackFor: function(prop) {
    return function(response) {
      var method = ('on-' + prop).camelize() + 'Success';
      if (this[method]) {
        this[method](document.createDocumentFragmentFromHTML(response.responseText));
      }
    }.bind(this);
  }
}

Object.extend(Responder.Methods, Responder.RequestHijacker.Methods);

Object.extend(Event, {
  dispatch: function() {
    var args = $A(arguments), event = args.first();
    if (this.observers) {
      event.target = $(event.target);
      [event.target].concat(event.target.ancestors()).each(function(element) { // bubble from target to root
        this.observers.each(function(observer) {
          if (observer[0] == element && observer[1] == event.type) {
            observer[2].apply(element, args);
          }
        });
      }.bind(this));
    }
  }
});

Object.extend(Form, {
  submitToIFrame: function(form, callback) {
    var iframe = Form.target(form), observer = function(event) {
      var document = this.contentDocument || this.contentWindow.document;
      if (document && document.body) {
        callback(document.body.innerHTML);
      };
      Event.stopObserving(iframe, 'load', observer);
    }
    Event.observe(iframe, 'load', observer);
    form.submit();
  },

  target: function(form) {
    var iframe, iframes = document.getElementsByTagName('iframe');
    var i = 0;
    do {
      iframe = iframes[i++];
    } while (i < iframes.length && iframe.name != form.target)
    return iframe;
  }
});

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

Node = {
  element: function(node, name) {
    if (Node.isA(node, 1) && (!name || node.tagName.toLowerCase() == name.toLowerCase())) {
      return node;
    }
  },

  isA: function(node, type) {
    return node && node.nodeType == type;
  },

  contains: function(node, container) {
    return Node.range(container, container.nextSibling).detect(function(subnode) {
      return subnode == node;
    });
  },

  text: function(node) {
    return Node.isA(node, 3) ? node : null; //  && node.data.length > 0;
  },

  range: function(start, end) {
    return new Node.Range(start, end);
  },

  last: function(node) {
    return node.lastChild ? Node.last(node.lastChild) : node;
  },

  next: function(node) {
    var _node = node.firstChild;
    while (node && !_node) {
      _node = node.nextSibling;
      node = node.parentNode;
    }
    return _node;
  }
};

Node.Range = Class.create();
Node.Range.prototype = {
  initialize: function(start, end, filter) {
    this.start = start;
    this.end = end;
    this.filter = filter || function() { return true };
  },

  _each: function(iterator) {
    var node = this.start;
    while (node != this.end) {
      if (this.filter(node)) {
        iterator(node);
      }
      node = Node.next(node);
    }
  }
};

Object.extend(Node.Range.prototype, Enumerable);

Object.extend(Element, {

  // dl/dd/dt
  Containers: {
    li:    'ul', // ol ??
    td:    'tr',
    tr:    'tbody',
    tbody: 'table'
  },

  containers: function(tag) {
    tag = tag.toLowerCase();
    var containers = [], container = Element.Containers[tag];
    if (container) {
      containers = [container].concat(Element.containers(container));
    }
    return containers;
  },

  first: function(node) {
    return $(Element.range(node).detect(Prototype.K));
  },

  next: function(node) {
    if (!node) return;
    node = Node.next(node);
    return Node.element(node) || Element.next(node);
  },

  range: function(start, end) {
    return new Node.Range(start, end, function(node) {
      return Node.element(node);
    });
  }
});

document.createDocumentFragmentFromHTML = function(html) {
  var fragment = document.createDocumentFragment(), matches = html.match(/[^<]*<(\w+)[^>]*>/i);
  if (matches) {
    var tag        = matches[1];
    var container  = document.createElement('div');
    var containers = Element.containers(tag);

    containers.each(function(tag) {
      html = "<" + tag + ">" + html + "</" + tag + ">";
    });
    container.innerHTML = html;
    (containers.length).times(function() {
      container = container.firstChild;
    });

    for (var i = container.childNodes.length - 1; i > -1; i--) {
      fragment.insertBefore(container.childNodes[i], fragment.firstChild);
    };
  } else {
    fragment.appendChild(document.createTextNode(html));
  }

  return fragment;
};

Element.addMethods({
  hasCommonClassName: function(element, refElement) {
    element    = $(element);
    refElement = $(refElement);
    return element.classNames().detect(function(name) {
      return refElement.hasClassName(name);
    });
  }
});