/*  --- BEHAVE ---
 *  This script implements a function document.getElementsBySelector(selector)
 *  which allows the selection of dom elements via CSS 2 (plus CSS 3
 *  attribute selectors).
 *  There are a few limitations, though:
 *  - Simple selectors and selector combinators (+ and >) have to be
 *    seperated by exactly one space. (I.e. "div > p" is ok, but not "div>p".)
 *  - Attribute values may only contain letters and digits.
 *  - Only :first-child pseudo class in implemented.
 *  This script is inspired by Simon Willison's selector.js (which also
 *  has above limitations and a few more), but has a totally differnt
 *  design. This script should be faster than version 0.4 of Willison's
 *  script.
 *  The basic idea is, that a selector defines a series of producers and
 *  predicates that create the node selection. Here, a producer has node, a
 *  predicate, and function. According to the node, it retrieves further
 *  nodes. For each node n of these nodes it calls the function with n if
 *  the predicate is true for n. The given function can be again a producer
 *  or a collector (at the end of this chain). This way, the collector
 *  will get called for all nodes that are specified by the CSS selector.
 *  License: BSD, i.e., free to use at own risk, no warranty whatsoever,
 *  please give credit where credit is due. See file LICENSE for details.
 *  Verion 0.1 - Christian A. Haselbach
 */

// Selector with specializer
var SelectorRegExp = new RegExp(/^([a-z0-9_-]*|\*)((\.|#|:)([a-z0-9_-]+))?$/i);
// Selector with attributes.
var SelectorAttRegExp = new RegExp(/^([a-z0-9_-]*|\*)\[(.*)\]$/i);
// Attribute specification.
var AttRegExp = new RegExp(/^([a-z0-9_-]*|\*)((=|~=|\|=|\*=|\$=|\^=)\"?([a-z0-9_-]*)\"?)?$/i);

function falseP(e) { return false }
function trueP(e) { return true }

// Creates a function that returns true iff the node has the given tag.
function makeTagNameP(tagname) {  
  tagname = tagname.toUpperCase();
  return function(node) { return tagname==node.nodeName }
}
// Creates a function that returns true iff the node has the given id.
function makeElementIdP(elementId) {
  return function(node) { return elementId==node.id }
}
// Creates a funtion that return true iff the node has the given class.
function makeElementClassP(className) {
  var classRegExp = new RegExp('\\b'+className+'\\b');
  return function(node) {
    return node.className && node.className.match(classRegExp)
  }
}
// Creates a funtion that returns true iff the pseudo class is satisfied.
function makePseudoClassP(pseudoClass) {
  switch(pseudoClass) {
    case 'first-child':
      return function(node) {
        return getFirstElementNode(node.parentNode.firstChild) == node
      }
  }
  return trueP;
}
// Creates a function that matches a regExp against a given attribute o a node.
function makeAttRegExpP(attrName, regExp) {
  return function(e) { return e.getAttribute(attrName).match(regExp) }
}
// Creates a function that returns true iff its given funtion return true.
// Arguments are either two functions with one input or an array of
// function with one input argument.
function andP(filter1, filter2) {
  if (filter2) return function(e) { return filter1(e) && filter2(e) }
  return function(e) {
    for (var i=filter1.length-1; i>=0; i--) {
      if (!(filter1[i](e))) return false;
    }
    return true;
  }
}
// Creates a function that returns true iff the node has the given attribute.
function makeAttP(attr) {
  var attrParts = attr.match(AttRegExp);
  var attrName = attrParts[1];
  var attrOp = attrParts[3];
  if (!attrOp) return function(e) { return e.getAttribute(attrName) }
  var attrValue = attrParts[4];
  switch (attrOp) {
    case '=':
      return function(e) { return e.getAttribute(attrName) == attrValue };
    case '~=':
      return makeAttRegExpP(attrName, new RegExp('\\b'+attrValue+'\\b'));
    case '|=':
      return makeAttRegExpP(attrName, new RegExp('^'+attrValue+'(-|$)'));
    case '*=':
      return makeAttRegExpP(attrName, new RegExp(attrValue));
    case '^=':
      return makeAttRegExpP(attrName, new RegExp('^'+attrValue));
    case '$=':
      return makeAttRegExpP(attrName, new RegExp(attrValue+'$'));
  }
  return trueP;
}

function makeSelectorTokenP(selector) {
  var selectorParts = selector.match(SelectorRegExp);
  if (selectorParts) {
    var tagname = selectorParts[1] == '*' ? null : selectorParts[1];
    var category = selectorParts[3];
    var specifier = selectorParts[4];
    var tagFunction;
    var categoryFunction;
    if (!category) return [tagname,null];
    switch (category) {
      case '.': return [tagname,makeElementClassP(specifier)];
      case '#': return [tagname,makeElementIdP(specifier)];
      case ':': return [tagname,makePseudoClassP(specifier)];
    }
    return;
  }
  selectorParts = selector.match(SelectorAttRegExp);
  if (!selectorParts) return;
  var tagname = selectorParts[1] == '*' ? null : selectorParts[1];
  var attrs = selectorParts[2].split('][');
  for (var i=attrs.length-1; i>=0; i--) {
    attrs[i]=makeAttP(attrs[i]);
  }
  return [tagname,attrs.length==1 ? attrs[0] : andP(attrs)];
}

// Creates a produuer that gets all descendants of node with tag tagname.
function makeDescendantsByTagNameProducer(tagname) {
  if (!tagname) {
    if (document.all) {
      return function(node, fun) {
        var nodes = node.all;
        for (var i=nodes.length-1; i>=0; i--) fun(nodes[i]);
      }
    }
    tagname = '*';
  }
  return function(node, fun) {
    var nodes = node.getElementsByTagName(tagname);
    for (var i=nodes.length-1; i>=0; i--) fun(nodes[i]);
  }
}

// Creates a producer that gets all childs of a node with tag tagname.
function makeChildsByTagNameProducer(tagname) {
  if (tagname) {
    var predicate = makeTagNameP(tagname);
    return function(node, fun) {
      var nodes = node.childNodes;
      for (var i=nodes.length-1; i>=0; i--) {
        var curNode = nodes[i];
        if (predicate(curNode)) fun(curNode);
      }
    }
  }
  return function(node, fun) {
    var nodes = node.childNodes;
    for (var i=nodes.length-1; i>=0; i--) fun(nodes[i]); 
  }
}

// Returns the node or the first sibling of a node which is an element node.
function getFirstElementNode(node) {
  while (node && node.nodeType != 1) node=node.nextSibling;
  return node;
}

// Returns a prodcuer that gets the adjacent node of if it has the tag tagname.
function makeAdjacentByTagNameProducer(tagname) {
  if (tagname) {
    var predicate = makeTagNameP(tagname);
    return function(node, fun) {
      var nextNode = getFirstElementNode(node.nextSibling);
      if (nextNode && predicate(nextNode)) fun(nextNode)
    }
  }
  return function(node, fun) {
    var nextNode = getFirstElementNode(node.nextSibling);
    if (nextNode) fun(nextNode);
  }
}

function makeSelectorTokenPs(tokens, tokenPos, filterPos, lastToken) {
  if (tokenPos >= tokens.length) {
    tokens.length=filterPos;
  } else {
    var curToken = tokens[tokenPos];
    if (curToken=='+' || curToken=='>') {
      makeSelectorTokenPs(tokens, tokenPos+1, filterPos, curToken)
    } else {
      var filter = makeSelectorTokenP(curToken);
      var producer;
      switch (lastToken) {
        case '>':
          filter.push(makeChildsByTagNameProducer(filter[0]));
          break;
        case '+':
          filter.push(makeAdjacentByTagNameProducer(filter[0]));
          break;
        default:
          filter.push(makeDescendantsByTagNameProducer(filter[0]));
      }
      tokens[filterPos] = filter;
      makeSelectorTokenPs(tokens, tokenPos+1, filterPos+1, null)
    }
  }
}

function processSel(node, predicates, pos, finalFun) {
  if (pos >= predicates.length) return finalFun(node);
  var token=predicates[pos];
  var nextPos=pos+1;
  var predicate=token[1];
  token[2](node,
           predicate ?
             function(e) {
               if (predicate(e)) processSel(e, predicates, nextPos, finalFun)
             } :
             function(e) {
               processSel(e, predicates, nextPos, finalFun)
             });
}

function callForElementsBySelector(selector, node, fun) {
  var tokens = selector.split(' ');
  makeSelectorTokenPs(tokens, 0, 0, null);
  processSel(node, tokens, 0, fun);
}

// For compatibility with selector.js
document.getElementsBySelector = function(selector) {
  var found = new Array();
  callForElementsBySelector(selector, document, function(e) {found.push(e)});
  return found;
}

function registerEventHandler(node, event, fun) {
  var oldFun = node[event];
  node[event] = oldFun ? function(e) {oldFun(e); return fun(e)} : fun;
}

function registerRules(rules, registerFun) {
  if (!registerFun) registerFun = registerEventHandler;
  registerEventHandler(window, 'onload', function() {
    for (var selector in rules) {
      var events = rules[selector];
      callForElementsBySelector(selector, document,
        function(node) {
          for (var ev in events) registerFun(node, ev, events[ev]);
        });
    }
  })
}

