/**
 * 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 = {
		isString: YAHOO.lang.isString,
		isArray: YAHOO.lang.isArray,
		isNumber: YAHOO.lang.isNumber,
		isObject: YAHOO.lang.isObject,
		get: YAHOO.util.Dom.get
	};

	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+>)/
	};

	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

	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;
                    //@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 )
		{
			return str.substr( 1 );
		}

		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 )
		{
			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 ];
				}
			}
			output[ outputLen++ ] = '<' + node[ 1 ];
			
			if( node[ 2 ] )
			{
				output[ outputLen++ ] = ' id="' + node[ 2 ].substr( 1 ) + '"';
			}
			if( node[ 3 ] )
			{
				classStr += node[ 3 ].substr( 1 ).split( '.' ).join( ' ' );
			}
			for( var j in attributeList )
			{
                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 ];
				}
				styleStr += styles.join( ';' );
			}

            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( 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' );

		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 = '';
		}

		return execNodeFragment( nodes.join( '' ), parent, mode );
	}

	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( '' );
		}
	};
}();

