/*
---

name: Selectors

description: Adds advanced CSS-style querying capabilities for targeting HTML Elements. Includes pseudo selectors.

license: MIT-style license.

requires: Element

provides: Selectors

...
*/

Native.implement([Document, Element], {

  getElements: function(expression, nocash){
    expression = expression.split(',');
    var items, local = {};
    for (var i = 0, l = expression.length; i < l; i++){
      var selector = expression[i], elements = Selectors.Utils.search(this, selector, local);
      if (i != 0 && elements.item) elements = $A(elements);
      items = (i == 0) ? elements : (items.item) ? $A(items).concat(elements) : items.concat(elements);
    }
    return new Elements(items, {ddup: (expression.length > 1), cash: !nocash});
  }

});

Element.implement({

  match: function(selector){
    if (!selector || (selector == this)) return true;
    var tagid = Selectors.Utils.parseTagAndID(selector);
    var tag = tagid[0], id = tagid[1];
    if (!Selectors.Filters.byID(this, id) || !Selectors.Filters.byTag(this, tag)) return false;
    var parsed = Selectors.Utils.parseSelector(selector);
    return (parsed) ? Selectors.Utils.filter(this, parsed, {}) : true;
  }

});

var Selectors = {Cache: {nth: {}, parsed: {}}};

Selectors.RegExps = {
  id: (/#([\w-]+)/),
  tag: (/^(\w+|\*)/),
  quick: (/^(\w+|\*)$/),
  splitter: (/\s*([+>~\s])\s*([a-zA-Z#.*:\[])/g),
  combined: (/\.([\w-]+)|\[(\w+)(?:([!*^$~|]?=)(["']?)([^\4]*?)\4)?\]|:([\w-]+)(?:\(["']?(.*?)?["']?\)|$)/g)
};

Selectors.Utils = {

  chk: function(item, uniques){
    if (!uniques) return true;
    var uid = $uid(item);
    if (!uniques[uid]) return uniques[uid] = true;
    return false;
  },

  parseNthArgument: function(argument){
    if (Selectors.Cache.nth[argument]) return Selectors.Cache.nth[argument];
    var parsed = argument.match(/^([+-]?\d*)?([a-z]+)?([+-]?\d*)?$/);
    if (!parsed) return false;
    var inta = parseInt(parsed[1], 10);
    var a = (inta || inta === 0) ? inta : 1;
    var special = parsed[2] || false;
    var b = parseInt(parsed[3], 10) || 0;
    if (a != 0){
      b--;
      while (b < 1) b += a;
      while (b >= a) b -= a;
    } else {
      a = b;
      special = 'index';
    }
    switch (special){
      case 'n': parsed = {a: a, b: b, special: 'n'}; break;
      case 'odd': parsed = {a: 2, b: 0, special: 'n'}; break;
      case 'even': parsed = {a: 2, b: 1, special: 'n'}; break;
      case 'first': parsed = {a: 0, special: 'index'}; break;
      case 'last': parsed = {special: 'last-child'}; break;
      case 'only': parsed = {special: 'only-child'}; break;
      default: parsed = {a: (a - 1), special: 'index'};
    }

    return Selectors.Cache.nth[argument] = parsed;
  },

  parseSelector: function(selector){
    if (Selectors.Cache.parsed[selector]) return Selectors.Cache.parsed[selector];
    var m, parsed = {classes: [], pseudos: [], attributes: []};
    while ((m = Selectors.RegExps.combined.exec(selector))){
      var cn = m[1], an = m[2], ao = m[3], av = m[5], pn = m[6], pa = m[7];
      if (cn){
        parsed.classes.push(cn);
      } else if (pn){
        var parser = Selectors.Pseudo.get(pn);
        if (parser) parsed.pseudos.push({parser: parser, argument: pa});
        else parsed.attributes.push({name: pn, operator: '=', value: pa});
      } else if (an){
        parsed.attributes.push({name: an, operator: ao, value: av});
      }
    }
    if (!parsed.classes.length) delete parsed.classes;
    if (!parsed.attributes.length) delete parsed.attributes;
    if (!parsed.pseudos.length) delete parsed.pseudos;
    if (!parsed.classes && !parsed.attributes && !parsed.pseudos) parsed = null;
    return Selectors.Cache.parsed[selector] = parsed;
  },

  parseTagAndID: function(selector){
    var tag = selector.match(Selectors.RegExps.tag);
    var id = selector.match(Selectors.RegExps.id);
    return [(tag) ? tag[1] : '*', (id) ? id[1] : false];
  },

  filter: function(item, parsed, local){
    var i;
    if (parsed.classes){
      for (i = parsed.classes.length; i--; i){
        var cn = parsed.classes[i];
        if (!Selectors.Filters.byClass(item, cn)) return false;
      }
    }
    if (parsed.attributes){
      for (i = parsed.attributes.length; i--; i){
        var att = parsed.attributes[i];
        if (!Selectors.Filters.byAttribute(item, att.name, att.operator, att.value)) return false;
      }
    }
    if (parsed.pseudos){
      for (i = parsed.pseudos.length; i--; i){
        var psd = parsed.pseudos[i];
        if (!Selectors.Filters.byPseudo(item, psd.parser, psd.argument, local)) return false;
      }
    }
    return true;
  },

  getByTagAndID: function(ctx, tag, id){
    if (id){
      var item = (ctx.getElementById) ? ctx.getElementById(id, true) : Element.getElementById(ctx, id, true);
      return (item && Selectors.Filters.byTag(item, tag)) ? [item] : [];
    } else {
      return ctx.getElementsByTagName(tag);
    }
  },

  search: function(self, expression, local){
    var splitters = [];

    var selectors = expression.trim().replace(Selectors.RegExps.splitter, function(m0, m1, m2){
      splitters.push(m1);
      return ':)' + m2;
    }).split(':)');

    var items, filtered, item;

    for (var i = 0, l = selectors.length; i < l; i++){

      var selector = selectors[i];

      if (i == 0 && Selectors.RegExps.quick.test(selector)){
        items = self.getElementsByTagName(selector);
        continue;
      }

      var splitter = splitters[i - 1];

      var tagid = Selectors.Utils.parseTagAndID(selector);
      var tag = tagid[0], id = tagid[1];

      if (i == 0){
        items = Selectors.Utils.getByTagAndID(self, tag, id);
      } else {
        var uniques = {}, found = [];
        for (var j = 0, k = items.length; j < k; j++) found = Selectors.Getters[splitter](found, items[j], tag, id, uniques);
        items = found;
      }

      var parsed = Selectors.Utils.parseSelector(selector);

      if (parsed){
        filtered = [];
        for (var m = 0, n = items.length; m < n; m++){
          item = items[m];
          if (Selectors.Utils.filter(item, parsed, local)) filtered.push(item);
        }
        items = filtered;
      }

    }

    return items;

  }

};

Selectors.Getters = {

  ' ': function(found, self, tag, id, uniques){
    var items = Selectors.Utils.getByTagAndID(self, tag, id);
    for (var i = 0, l = items.length; i < l; i++){
      var item = items[i];
      if (Selectors.Utils.chk(item, uniques)) found.push(item);
    }
    return found;
  },

  '>': function(found, self, tag, id, uniques){
    var children = Selectors.Utils.getByTagAndID(self, tag, id);
    for (var i = 0, l = children.length; i < l; i++){
      var child = children[i];
      if (child.parentNode == self && Selectors.Utils.chk(child, uniques)) found.push(child);
    }
    return found;
  },

  '+': function(found, self, tag, id, uniques){
    while ((self = self.nextSibling)){
      if (self.nodeType == 1){
        if (Selectors.Utils.chk(self, uniques) && Selectors.Filters.byTag(self, tag) && Selectors.Filters.byID(self, id)) found.push(self);
        break;
      }
    }
    return found;
  },

  '~': function(found, self, tag, id, uniques){
    while ((self = self.nextSibling)){
      if (self.nodeType == 1){
        if (!Selectors.Utils.chk(self, uniques)) break;
        if (Selectors.Filters.byTag(self, tag) && Selectors.Filters.byID(self, id)) found.push(self);
      }
    }
    return found;
  }

};

Selectors.Filters = {

  byTag: function(self, tag){
    return (tag == '*' || (self.tagName && self.tagName.toLowerCase() == tag));
  },

  byID: function(self, id){
    return (!id || (self.id && self.id == id));
  },

  byClass: function(self, klass){
    return (self.className && self.className.contains && self.className.contains(klass, ' '));
  },

  byPseudo: function(self, parser, argument, local){
    return parser.call(self, argument, local);
  },

  byAttribute: function(self, name, operator, value){
    var result = Element.prototype.getProperty.call(self, name);
    if (!result) return (operator == '!=');
    if (!operator || value == undefined) return true;
    switch (operator){
      case '=': return (result == value);
      case '*=': return (result.contains(value));
      case '^=': return (result.substr(0, value.length) == value);
      case '$=': return (result.substr(result.length - value.length) == value);
      case '!=': return (result != value);
      case '~=': return result.contains(value, ' ');
      case '|=': return result.contains(value, '-');
    }
    return false;
  }

};

Selectors.Pseudo = new Hash({

  // w3c pseudo selectors

  checked: function(){
    return this.checked;
  },

  empty: function(){
    return !(this.innerText || this.textContent || '').length;
  },

  not: function(selector){
    return !Element.match(this, selector);
  },

  contains: function(text){
    return (this.innerText || this.textContent || '').contains(text);
  },

  'first-child': function(){
    return Selectors.Pseudo.index.call(this, 0);
  },

  'last-child': function(){
    var element = this;
    while ((element = element.nextSibling)){
      if (element.nodeType == 1) return false;
    }
    return true;
  },

  'only-child': function(){
    var prev = this;
    while ((prev = prev.previousSibling)){
      if (prev.nodeType == 1) return false;
    }
    var next = this;
    while ((next = next.nextSibling)){
      if (next.nodeType == 1) return false;
    }
    return true;
  },

  'nth-child': function(argument, local){
    argument = (argument == undefined) ? 'n' : argument;
    var parsed = Selectors.Utils.parseNthArgument(argument);
    if (parsed.special != 'n') return Selectors.Pseudo[parsed.special].call(this, parsed.a, local);
    var count = 0;
    local.positions = local.positions || {};
    var uid = $uid(this);
    if (!local.positions[uid]){
      var self = this;
      while ((self = self.previousSibling)){
        if (self.nodeType != 1) continue;
        count ++;
        var position = local.positions[$uid(self)];
        if (position != undefined){
          count = position + count;
          break;
        }
      }
      local.positions[uid] = count;
    }
    return (local.positions[uid] % parsed.a == parsed.b);
  },

  // custom pseudo selectors

  index: function(index){
    var element = this, count = 0;
    while ((element = element.previousSibling)){
      if (element.nodeType == 1 && ++count > index) return false;
    }
    return (count == index);
  },

  even: function(argument, local){
    return Selectors.Pseudo['nth-child'].call(this, '2n+1', local);
  },

  odd: function(argument, local){
    return Selectors.Pseudo['nth-child'].call(this, '2n', local);
  },

  selected: function(){
    return this.selected;
  },

  enabled: function(){
    return (this.disabled === false);
  }

});

