/**
 * DOMDom, easy DOM element creation
 *
 * @version 1.0
 * @author  Zach Leatherman (zachleatherman@gmail.com)
 * @license BSD License
 * @package DOMDom
 * @link    http://www.zachleat.com/web/
 */
var DOMDom = function()
{
    var ADAPTER = {
        setStyle: YAHOO.util.Dom.setStyle,
        addClass: YAHOO.util.Dom.addClass,
        isString: YAHOO.lang.isString,
        isArray: YAHOO.lang.isArray,
        isNumber: YAHOO.lang.isNumber,
        isObject: YAHOO.lang.isObject,
        get: YAHOO.util.Dom.get
    };

    var USE_FRAGMENTS = true;
    var TEXT_NODE_CHAR = '#'; // BadgerFish JSON convention uses a '$' to indicate a text node

    var MATCHERS = {
        // in style declarations, {}, quotes are not required for values
        'style': /([\w<>-]+)\=(?:['"])?([\w<>\s-#\(\)%]+)(?:["'])?(?:\,)?/g,
        // in attributes, quotes _are_ required for values and you can't nest them, ie: '"' or "'"
        'attribute': /(?:\[)?(?:@)?((?:[\w<>-]+\:)?(?:[\w<>-]+))\=(?:['"])([^'"]+)(?:["'])(?:[(?:,)|(?:\])])/g,
        'node': /((?:[\w<>-]+\:)?(?:[\w<>-]+))(\#[\w<>-]+)?((?:\.[\w<>\+-]+)*)?(\{.*\})?(\[.*\])?/g,
        //'variable': /(\{\$\w+\})/,
        'variable': /(<\$\w+>)/
    };

    var MODE_APPEND = 0;
    var MODE_UNSHIFT = 1; // insert at the beginning
    var MODE_INSERT = 2; // argument is the index before which you wish you insert your nodes
    var MODE_REPLACE = 3; // get rid of any child content and replace with your nodes

    /* Create an element handling name and type attribute bug in Internet Explorer 6 */
    function createElement( node, name, type )
    {
        var element;
        try {
            element = document.createElement( '<' + node + ( name ? ' name="'+name+'"' : '' ) + ( type ? ' type="'+type+'"' : '' ) + '>' );
        } catch (e) {}
        if( !element || !element.name || !element.type )
        {
            element = document.createElement( node );
            if( name ) element.setAttribute( 'name', name );
            if( type ) element.setAttribute( 'type', type );
        }
        return element;
    }

    function setStyle( elements, styles /* string with multiple style declarations: border:0;height:90px */ )
    {
        var split = styles.split( ';' );
        for( var j = 0; j < split.length; j++ )
        {
            var keyvalue = split[ j ].split( ':' );
            ADAPTER.setStyle( elements, keyvalue[ 0 ], keyvalue[ 1 ] );
        }
    }

    function addClass( elements, classNames /* array */ )
    {
        for( var j = 0; j < classNames.length; j++ )
        {
            ADAPTER.addClass( elements, classNames[ j ] );
        }
    }

    function setAttribute( node, name, value )
    {
        // name and type attributes must be set at create time, use createElement (for IE )
        var corrections = {
            'accesskey': 'accessKey',
            'usemap': 'useMap',
            'maxlength': 'maxLength',
            'frameborder': 'frameBorder'
        };
        var preadded = {
            'name': '',
            'type': ''
        };
        var lName = name.toLowerCase();
        if( lName == 'style' )
        {
            setStyle( node, value );
        } else if( lName == 'class' ) {
            addClass( node, value.split( ' ' ) );
        } else if( lName == 'for' ) {
            node.htmlFor = value;
        } else if( lName == 'checked' && ( value == 'checked' || value == 'true' ) ) {
            node.checked = true;
            node.defaultChecked = true;
        } else if( ADAPTER.isString( preadded[ lName ] ) && node.getAttribute( lName ) ) {
            // do nothing
        } else if( lName.substr( 0, 2 ) == 'on' ) { // ideally we would use YAHOO.util.Event.addListener, but who sets these as strings anyway?
            node[ lName ] = value;
        } else {
            if( corrections[ lName ] ) node.setAttribute( corrections[ lName ], value );
            else node.setAttribute( lName, value );
        }
    }

    function appendChildren( parentNode, /* Array */children )
    {
        for( var j = 0; j < children.length; j++ )
        {
            if( ADAPTER.isArray( children[ j ] ) )
                appendChildren( parentNode, children[ j ] );
            else
                parentNode.appendChild( children[ j ] );
        };
    }

    // depth first search to the first leaf
    function getLeaf( parentNode )
    {
        var max = 20, j = 0;
        while( parentNode.firstChild != null && j < max )
        {
            parentNode = parentNode.firstChild;
            j++;
        }
        if( j == max ) throw 'Infinite Loop Protection called (' + max + ' loops for getLeaf)';
        return parentNode;
    }

    function getNonWhitespaceNodes( parentNode )
    {
        var children = [];
        for( var j = 0; j < parentNode.childNodes.length; j++ )
        {
            if( !isWhitespaceNode( parentNode.childNodes[ j ] ) ) children.push( parentNode.childNodes[ j ] );
        }
        return children;
    }

    // Mozilla considers whitespace as a text node
    // http://developer.mozilla.org/en/docs/Whitespace_in_the_DOM
    function isWhitespaceNode( node )
    {
        return node.nodeType == 8 || ( ( node.nodeType == 3 ) && !( /[^\t\n\r ]/.test( node.data ) ) );
    };

    function execNodeFragment( node, parent, mode, argument )
    {
        if( parent )
        {
            if( parent.insertAdjacentHTML )
            {
                switch( mode )
                {
                    case MODE_APPEND:
                    case MODE_REPLACE:
                        parent.insertAdjacentHTML( 'beforeEnd', node );
                        return [ parent.lastChild ]; // warning: will return only the last root level node
                        break;
                    case MODE_UNSHIFT:
                        parent.insertAdjacentHTML( 'afterBegin', node );
                        return [ parent.firstChild ]; // warning: will return only the first root level node
                        break;
                    case MODE_INSERT:
                        throw 'INSERT mode is not supported for HTML Fragments in execNodeFragment';
                    default:
                        throw 'Unknown mode for execNodeFragment';
                }
            } else {
                var range = parent.ownerDocument.createRange();
                switch( mode )
                {
                    case MODE_APPEND:
                    case MODE_REPLACE:
                        if( parent.lastChild )
                        {
                            range.setStartAfter( parent.lastChild );
                            frag = range.createContextualFragment( node );
                            parent.appendChild( frag );
                        } else {
                            parent.innerHTML = node;
                        }
                        return [ parent.lastChild ]; // warning: will return only the last root level node
                    case MODE_UNSHIFT:
                        if( parent.firstChild )
                        {
                            range.setStartBefore( parent.firstChild );
                            frag = range.createContextualFragment( node );
                            parent.insertBefore( frag, parent.firstChild );
                        } else {
                            parent.innerHTML = node;
                        }
                        return [ parent.firstChild ]; // warning: will return only the first root level node
                    case MODE_INSERT:
                        throw 'INSERT mode is not supported for HTML Fragments in execNodeFragment';
                    default:
                        throw 'Unknown mode for execNodeFragment';
                }
            }
        } else {
            var unparent = document.createElement( 'div' );
            if( unparent.insertAdjacentHTML )
            {
                unparent.insertAdjacentHTML( 'afterBegin', node );
            } else {
                unparent.innerHTML = node;
            }
            var child = unparent.firstChild;
            unparent = null;
            return [ child ]; // warning: will return only the first root level node
        }
    };

    function getObject( obj )
    {
        if( ADAPTER.isString( obj ) )
        {
            return [ parseString( obj ) ];
        } else {
            var arg = [];
            for( var key in obj )
            {
                var value = obj[ key ];
                var isTextNode = ADAPTER.isObject( value ) && value.nodeName != null && value.nodeName.toLowerCase() == '#text';
                var isNode = !isTextNode && ADAPTER.isObject( value ) && value.nodeName != null;
                if( !ADAPTER.isNumber( parseInt( key ) ) )
                {
                    var parentNode;
                    if( !USE_FRAGMENTS )
                    {
                        parentNode = parseString( key );
                        var leaf = getLeaf( parentNode );
                        if( isTextNode || isNode )
                            leaf.appendChild( value );
                        else /* parentNode =  */
                            appendChildren( leaf, getObject( value ) );
                    } else {
                        //@TODO handle raw elements
                        if( isTextNode || isNode )
                            throw 'Raw DOM nodes are not yet supported with HTML Fragments.  Try setting USE_FRAGMENTS to false.';
                        else
                            parentNode = parseString( key, getObject( value ) );
                    }

                    arg[ arg.length ] = parentNode;
                } else {
                    if( isTextNode || isNode )
                        arg[ arg.length ] =  value;
                    else
                        arg = arg.concat( getObject( value ) );
                }
            }
            return arg;
        }
    }

    function parseString( str, children )
    {
        if( str.substr( 0, 1 ) == TEXT_NODE_CHAR )
        {
            if( !USE_FRAGMENTS ) return document.createTextNode( str.substr( 1 ) );
            else return str.substr( 1 );
        }

        if( USE_FRAGMENTS ) var output = [], outputLen = 0, openNodes = [], openNodesLen = 0;
        var topLevelNode, parentNode, node;
        var executed = false;
        RegExp.lastIndex = 0;
        var reg = new RegExp( MATCHERS.node );
        while( ( node = reg.exec( str ) ) != null )
        {
            if( USE_FRAGMENTS ) var classStr = '', styleStr = '';
            executed = true;
            var current;
            var attributeList = {};
            if( node[ 5 ] )
            {
                var attributes;
                RegExp.lastIndex = 0;
                var attReg = new RegExp( MATCHERS.attribute );
                while( ( attributes = attReg.exec( node[ 5 ] ) ) != null )
                {
                    attributeList[ attributes[ 1 ].toLowerCase() ] = attributes[ 2 ];
                }
            }
            if( !USE_FRAGMENTS )
            {
                if( attributeList[ 'name' ] || attributeList[ 'type' ] )
                {
                    current = createElement( node[ 1 ], attributeList[ 'name' ], attributeList[ 'type' ] );
                } else {
                    current = document.createElement( node[ 1 ] );
                }
            } else {
                output[ outputLen++ ] = '<' + node[ 1 ];
            }

            if( node[ 2 ] )
            {
                if( !USE_FRAGMENTS ) current.setAttribute( 'id', node[ 2 ].substr( 1 ) );
                else output[ outputLen++ ] = ' id="' + node[ 2 ].substr( 1 ) + '"';
            }
            if( node[ 3 ] )
            {
                if( !USE_FRAGMENTS ) addClass( current, node[ 3 ].substr( 1 ).split( '.' ) );
                else classStr += node[ 3 ].substr( 1 ).split( '.' ).join( ' ' );
            }
            for( var j in attributeList )
            {
                if( !USE_FRAGMENTS )
                {
                    setAttribute( current, j, attributeList[ j ] );
                } else {
                    if( j == 'class' ) classStr += ' ' + attributeList[ j ];
                    else if( j == 'style' ) styleStr += attributeList[ j ] + ( attributeList[ j ].substr( attributeList[ j ].length - 1 ) == ';' ? '' : ';' );
                    else output[ outputLen++ ] = ' ' + j + '="' + attributeList[ j ] + '"';
                }
            }
            if( node[ 4 ] )
            {
                var styles = [];
                var len = 0;
                var style;
                RegExp.lastIndex = 0;
                var attReg = new RegExp( MATCHERS.style );
                while( ( style = attReg.exec( node[ 4 ] ) ) != null )
                {
                    styles[len++] = style[ 1 ].toLowerCase() + ':' + style[ 2 ];
                }
                if( !USE_FRAGMENTS ) setStyle( current, styles.join( ';' ) );
                else styleStr += styles.join( ';' );
            }

            if( !USE_FRAGMENTS )
            {
                if( parentNode != null ) // we've looped once already
                {
                    parentNode.appendChild( current );
                } else {
                    topLevelNode = current;
                }
                parentNode = current;
            } else {
                if( classStr != '' ) output[ outputLen++ ] = ' class="' + classStr + '"';
                if( styleStr != '' ) output[ outputLen++ ] = ' style="' + styleStr + '"';
                // @TODO check to see if no child nodes, then />
                output[ outputLen++ ] = '>';
                openNodes[ openNodesLen++ ] = node[ 1 ];
            }
        }
        if( !executed )
            throw 'Could not parse node string (' + str + ') in parseString';

        if( !USE_FRAGMENTS )
        {
            return topLevelNode;
        } else {
            if( children )
            {
                for( var j = 0; j < children.length; j++ )
                {
                    output[ outputLen++ ] = children[ j ];
                }
            }
            for( var j = openNodes.length - 1; j >= 0; j-- )
            {
                output[ outputLen++ ] = '</' + openNodes[ j ] + '>';
            }
            return output.join( '' );
        }
    }

    /* values is the Array containing the values for the loop. */
    function compiledExecute( str, parent, values, mode, argument )
    {

        if( mode == null ) throw new Error( 'Invalid mode in compiledExecute' );
        
        if( parent )
        {
            if( ADAPTER.isString( parent ) ) parent = ADAPTER.get( parent );
            if( mode == MODE_REPLACE )
                parent.innerHTML = '';
        }

        var split = str.split( MATCHERS.variable );
        if( split.length > 1 )
        {
            for( var j = 1; j < split.length; j+=2 ) // every odd index should be a variable to replace
            {
                var index = split[ j ].substr( 2, split[ j ].length - 3 );
                if( ADAPTER.isNumber( parseInt( index, 10 ) ) )
                    index = parseInt( index, 10 );

                if( index != null && values && values[ index ] != null )
                {
                    split[ j ] = values[ index ];
                }
            }
        }
        return execNodeFragment( split.join( '' ), parent, mode, argument );
    }

    function elementExecute( obj, parent, mode, argument ) // #idString, .classString, [@att1="value",att2="value"] @ is optional
    {
        if( mode == null ) throw new Error( 'Invalid mode in elementExecute.' );

        var nodes = getObject( obj );

        if( parent )
        {
            if( ADAPTER.isString( parent ) ) parent = ADAPTER.get( parent );
            if( mode == MODE_REPLACE )
                parent.innerHTML = '';
        }

        if( parent && !USE_FRAGMENTS )
        {
            for( var j = 0; j < nodes.length; j++ )
            {
                switch( mode )
                {
                    case MODE_APPEND:
                    case MODE_REPLACE:
                        parent.appendChild( nodes[ j ] );
                        break;
                    case MODE_UNSHIFT:
                        parent.insertBefore( nodes[ j ], parent.firstChild );
                        break;
                    case MODE_INSERT:
                        //var childNodes = Ext.query( '*', parent ); // gets rid of those pesky \n nodes
                        var childNodes = getNonWhitespaceNodes( parent );
                        if( !ADAPTER.isNumber( argument ) || argument >= childNodes.length )
                        {
                            throw 'Incorrect index for INSERT mode in DOMDom.element';
                        }
                        parent.insertBefore( nodes[ j ], childNodes[ argument ] );
                        break;
                    default:
                        throw 'Unknown mode for DOMDom.element';
                }
            }
        } else if( USE_FRAGMENTS ) {
            return execNodeFragment( nodes.join( '' ), parent, mode );
        }
        return nodes;
    }

    return {
        element: function( obj, parent, values ) // #idString, .classString, [@att1="value",att2="value"] @ is optional
        {
            if( values != null ) return compiledExecute( obj, parent, values, MODE_APPEND );
            return elementExecute( obj, parent, MODE_APPEND );
        },
        append: function( obj, parent, values )
        {
            if( values != null ) return compiledExecute( obj, parent, values, MODE_APPEND );
            return elementExecute( obj, parent, MODE_APPEND );
        },
        replace: function( obj, parent, values )
        {
            if( values != null ) return compiledExecute( obj, parent, values, MODE_REPLACE );
            return elementExecute( obj, parent, MODE_REPLACE );
        },
        unshift: function( obj, parent, values )
        {
            if( values != null ) return compiledExecute( obj, parent, values, MODE_UNSHIFT );
            return elementExecute( obj, parent, MODE_UNSHIFT );
        },
        insert: function( obj, parent, valuesOrIndex ) //insert doesn't work with compiled yet
        {
            if( valuesOrIndex == null )
            {
                return DOMDom.unshift( obj, parent );
            } else {
                if( ADAPTER.isObject( valuesOrIndex ) )
                    return compiledExecute( obj, parent, values, MODE_UNSHIFT );
                else if( ADAPTER.isNumber( valuesOrIndex ) )
                    return elementExecute( obj, parent, MODE_INSERT, valuesOrIndex );
                else
                    throw new Error( 'Invalid argument passed to DOMDom.insert' );
            }
        },
        compile: function( obj )
        {
            return getObject( obj ).join( '' );
        }
    };
}();
