/**
 * Copyright (c) 2007 Peter Michaux. All rights reserved.
 * petermichaux@gmail.com
 * http://forkjavascript.org
 * Code licensed under the MIT License:
 * http://dev.michaux.ca/svn/fork/trunk/public/javascripts/fork/MIT-LICENSE
 */

// CSS3 selectors http://www.w3.org/TR/2001/CR-css3-selectors-20011113/

// Supported CSS3 --------------------------------------
// 
// *
// E
// E#myid
// E.warning
// E[foo]
// E[foo="bar"]
// E[foo~="bar"]
// E[foo^="bar"]
// E[foo$="bar"]
// E[foo*="bar"]
// E:nth-child(n)
// E:nth-last-child(n)
// E:nth-of-type(n)
// E:nth-last-of-type(n)
// E:first-child
// E:last-child
// E:first-of-type
// E:last-of-type
// E:only-child
// E:only-of-type
// E:not(s)
// 
// E F
// E > F
// E + F
// E ~ F
// 
// Unsupported CSS3 --------------------------------------
// 
// E[hreflang|="en"]
// E:contains("foo")
// E:lang(fr)
// E:enabled
// E:disabled
// E:checked
// E:indeterminate
// E:empty
// E:root
// E:link
// E:visited
// E:target
// E:active
// E:hover
// E:focus
// E::first-line
// E::first-letter
// E::selection
// E::before
// E::after

var FORK = FORK || {};

(function() {
   
    // a unique id for nested whiles so el doesn't get overwritten
    var u = 0;
        
    // str is a portion of a selector that follows a '(' and the matching ')' must be found
    // ')' characters inside strings need to be ignored and nested parens need to be considered
    //
    // for :not() and perhaps eventually for :contains()
    function splitOnCloser(s) {
        var i=0, // the position of the current character under scrutiny
            c, // the current character under scrutiny 
            p=1, // nesting depth of parens
            d=0; // boolean. Answers: In a string?
            
        while (c = s.charAt(i)) { // c = str[i]; // works also?    
            if (c=='"') {
                d = !d; // entering or exiting a string
            }
            else if (c=='\\' && d) {
                i++; // skip the escaped character in a string
            }
            else if (c=='(' && !d) {
                p++; // entering a pair of parens
            }
            else if (c==')' && !d) {
                p--; // exiting a pair of parens
            }
            
            if (!p) {
                break; // the paren nesting count is zero so we found the closing paren
            }
            i++;
        }
        // p is the index of the closing paren
        return [s.substring(0,i),s.substring(i+1)];
    }
        
    // Compiles a complex selector (the "s" argument) like "div:first-child > span.fooClass"
    // The "j" argument is a string of JavaScript code and is wrapped by the compiled code.
    function compileSelector(s, j) {
        s=s.replace(/^\s*|\s*$/g,''); // trim leading and trailing whitespace
        var m; // the matches of each regular expression
        
        outerWhile: while (s) {
            u++;
        
            // *
            if (m = s.match(/^\*(.*)/)) {
              //  nothing to do. This branch prevents syntax error and also short circuits this if-else structure
            }
            // div
            else if (m = s.match(/^([\w-]+)(.*)/)) {
                // for non-HTML pages remove the ".toUpperCase()" or use [tagName="DiV"] for a case-sensitive match
                j = 'if (e.tagName==="' + m[1].toUpperCase() + '"){' + j + '}'; // for HTML with uppercase
                t = m[1];
            }
            // #idFoo
            else if (m =s.match(/^#([\w-]+)(.*)/)) {
                j = 'if (e.id&&e.id==="' + m[1] + '"){' + j + '}';
            }
            // .classFoo
            else if (m = s.match(/^\.([\w-]+)(.*)/)) {
                j = 'if (e.className&&e.className.match(/(?:^|\\s+)' + m[1] + '(?:\\s+|$)/)){' + j + '}'; // TODO faster not using RegExp
            }
            // [attr] [attr=value] [attr="value"] [attr*="value"] and ~=, ^=, $=
            else if (m = s.match(/^\[([\w-]+)(~|\^|\*|\$)?(\=)?"?([^"\]]+)?"?\](.*)/)) { //" this comment for broken syntax highlighter
                m[1] = (m[1]=='class' ? 'className' : m[1]);
                j = 'if (e.'+m[1]+(m[3]?'&&e.'+m[1]+'.match(/'+(!m[2]||m[2]=='^'?'^':'')+(m[2]=='~'?'(^|\\s+)':'')
                    +m[4]+
                    (m[2]=='~'?'($|\\s+)':'')+(!m[2]||m[2]=='$'?'$':'')+'/)':'')+'){' + j + '}';
            }
            
            // COMBINATORS ====================================================

            else if (m = s.match(/^\s*\+\s*(.*)/)) {
                j = 'while(e=e.previousSibling){if(e.nodeType==1){break;}}if(e){' + j + '}';
            }
            else if (m = s.match(/^\s*~\s*(.*)/)) {
                j = 'var e'+(u)+'=e;while(e'+u+'=e'+u+'.previousSibling){if(e'+u+'.nodeType==1){e=e'+u+';' + j + '}}';
            }
            else if (m = s.match(/^\s*>\s*(.*)/)) {
                j = 'if(e=e.parentNode){' + j + '}';
            }
            else if (m = s.match(/^(\s+)(.*)/)) {
                j = 'var e'+(u)+'=e;while(e'+u+'=e'+u+'.parentNode){e=e'+u+';' + j + '}';
            }
            
            // PSEUDOS ========================================================

            // :not()
            else if (m = s.match(/^\:not\((.*)/)) {
                m = splitOnCloser(m[1]);
                // Determine start and end before call to compileSelector because
                // call to compile selector will adjust the "u" variable and the
                // use of "u" needs to match on both sides.
                var start = 'var e'+u+'=e;',
                    end = 'e=e'+u+';';
                j = start + compileSelector(m[0],'return false;') + end + j;
            }

            // :first-child, :last-child, :only-child, :first-child-of-type, :last-child-of-type, :only-child-of-type
            else if (m = s.match(/^\:(first|last|only)(-child)?(-of-type)?(.*)/)) {
                // TRICKY Well, at least it's compact. Wait until you get to the next branch. :)
                var start = 'var pass=true, el=e;'+(m[3]?'var tn=e.tagName;':'')+'while(el=el.',
                    end = 'Sibling){if(el.nodeType==1'+(m[3]?'&&el.tagName === tn':'')+'){pass=false;break;}}if(pass){',
                    prev = start + 'previous' + end,
                    next = start + 'next' + end;
                j = ((m[1]=='only'|| m[1]=='first')?prev:'') + ((m[1]=='only'|| m[1]=='last')?next:'') + j + (m[1]=='only'?'}}':'}');
            }
            
            // :nth-child(), :nth-last-child-of-type() and all other combinations
            // arguments can be even, odd, any combintation of an+b
            else if (m = s.match(/^\:nth(-last)?(-child)?(-of-type)?\((even|odd|[^\)]*)\)(.*)/)) {
                var a,b,temp;
                if (m[4]=='even') {
                    a=2;
                    b=0;
                }
                else if (m[4]=='odd') {
                    a=2;
                    b=1;
                }
                else { // assumes correct "an+b" format
                    a = m[4].match(/^n/) ? 1 : ((temp = m[4].match(/^(-?\d{1,})n/)) ? parseInt(temp[1],10) : 0);
                    b = (temp = m[4].match(/(-?\d{1,})$/)) ? parseInt(temp[1],10) :  0;
                }
                
                // TRICKY <--- obviously
                j = 'var count=0, el=e;'+
                     (m[3]?'var tn=e.tagName;':'') + // for -of-type
                    'if (batch != e.parentNode._FORKbatch) {'+ // technique thanks to Jack Slocum (Ext DomQuery) who thanks John Resig (jQuery)
                        'var els = e.parentNode.childNodes;' +
                        (m[1] ? 'for(var i'+u+'=els.length;i'+u+'--;){' // backwards for -last
                              : 'for(var i'+u+'=0,len'+u+'=els.length;i'+u+'<len'+u+';i'+u+'++){') + // forwards for without -last
                            'if(els[i'+u+'].nodeType==1'+(m[3] ?'&&els[i'+u+'].tagName === tn':'')+'){' + // second conditional for -of-type
                                'var c = (++count)-(' + b + ');' +
                                (a?'c=c%('+a+');' : '') +
                                // Sin of all Sins: Augmenting an object I don't own. Gasp!
                                // Thanks to JavaScript's single-threadedness
                                // this won't be clobbered while I'm using it.
                                'els[i'+u+']._FORKflag = (c===0 ? true : false);'+
                            '}' +
                        '}' +
                        'e.parentNode._FORKbatch = batch;' +
                    '}' +
                    'if(e._FORKflag){' + j + '}';
            }
            // ================================================================
            else {
                /* BEGIN EXTENSIBLE */
                // Another file can supply the FORK.find.ext function.
                // If FORK.find.ext doesn't exist then throws an error
                // which is really a good syntax error on the selector.
                // var r=FORK.match.ext(s,j);
                // s = r.s;
                // j = r.j;
                // continue outerWhile;
                /* END EXTENSIBLE */
                // if using extensible stuff above then don't need this next line
                throw new Error('FORK.match: syntax error, unknown selector rule');
            }
            s = m[m.length-1];
        }
        return j;
    }

    function compileSelectorGroup(sg) {
        var ss = sg.split(','), // array of selectors
            j = '', // will accumlate the core of the generated JavaScript function
            i, // loop index
            f; // the function being generated

        f = 'function(element){';

                for (i=0; i<ss.length; i++) {
                    f += 'var batch=(new Date()).getTime()+"_"+Math.random(),' +
                         'e = element;' +
                         compileSelector(ss[i], 'return true;'); // selectors are relative to root
                }
        
        f +=    'return false;' +
             '}';
        
        // beautify this with http://elfz.laacz.lv/beautify/
        // console.log(f);
        return eval(f);
    }

    var cfs = {}; // cache of compiled functions
    FORK.match = function(sg, el) { // sg is a selectorGroup (ie comma separated list of selectors)
        if (!cfs[sg]) cfs[sg] = compileSelectorGroup(sg);
        return cfs[sg](el);
    };
    
})();