<?php
define('TAG_CR', 1);
define('TAG_CLOSE', 2);

/**
 * TAGS Library Base Class
 *
 * This library is a simple standalone HTML tags library for building object abstration on HTML DOM
 */
class Tag
{
	protected $attributes = array();
	protected $tagname = '';
	protected $children = array();
	protected $text = NULL;
	protected $flags = 0;
	protected $close_tag = false;		// Explicitly end the tag with a closing tag "</script>"
	protected $cr = false;				// End tag rendering with a carriage return (cleaner markup)

	const CR = 1;
	const SELF_CLOSE = 2;
	const CLOSE = 2;

	public function __construct($tagname, $attributes=NULL, $flags=0)
	{
		$this->tagname = $tagname;
		$this->attributes = ( is_array($attributes) ? array_merge($this->attributes, $attributes) : array() );
		if( is_array($attributes) && array_key_exists('text', $attributes) )
		{
			$this->text = $attributes['text'];
			unset($this->attributes['text']);
		}
		else
		{
			$this->text = NULL;
		}
		$this->cr = (bool)($flags & TAG_CR);
		$this->close_tag = (bool)($flags & TAG_CLOSE);
	}

	public function openTag()
	{
		$html = '<'.$this->tagname.' '.$this->getAttributeString();
		if( $this->close_tag || count($this->children) > 0 )
			$html .= '>';
		else
			$html .= ' />';
		return $html;
	}

	public function closeTag()
	{
		$html = '';

		if( count($this->children) > 0 || !is_null($this->text) || $this->close_tag)
			$html = ($this->cr?"\n":'').'</'.$this->tagname.'>';

		if( $this->attr('id') && $this->close_tag )
			$html .= '<!--'.$this->attr('id').'-->';

		if( $this->cr )
			$html .= "\n";

		return $html;
	}

	public function text($text=NULL)
	{
		if( is_null($text) )
			return $this->text;
		else
			$this->text = $text;
	}

	public function attr($name, $value=NULL)
	{
		if( is_null($value) ) // NULL Value Provided, "get" request
		{
			if( $name == 'text' )
				return $this->text;
			else
			{
				if( array_key_exists($name, $this->attributes) )
					return $this->attributes[$name];
				else
					return NULL;
			}
		}
		else // Non-Null Value Provided, "set" request
		{
			if( $value === '' && $name != 'value' )
			{
				// Remove the attribute entirely if set to "empty string",
				// Except in the case of the value attribute, which can and should be blank on occasion
				if( array_key_exists($name, $this->attributes) )
					unset($this->attributes[$name]);
			}
			elseif( $name == 'text' )
			{
				// Set the text property, NOT an attribute when using 'text'
				$this->text = $value;
			}
			else
			{
				// Set the attribute // default functionality
				$this->attributes[$name] = $value;
			}
		}
	}

	// Returns a copy of the local attributes
	public function getAttributes()
	{
		return $this->attributes;
	}

	/** Adds the provided child object
	 * 
	 * Can use multiple forms of parameters
	 * addChild( array(child-objects) )
	 * addChild( child1,child2,child3 )
	 * addChild( child1 )
	 */
	public function addChild()
	{
		foreach( func_get_args() as $arg )
		{
			if( is_array($arg) )
			{
				foreach($arg as $obj)
					$this->children[] = $obj;
			}
			else
			{
				$this->children[] = $arg;
			}
		}
		return (count($this->children) - 1);
	}

	public function &getChildren()
	{
		return $this->children;
	}

	public function removeChild($index)
	{
		if( array_key_exists($index, $this->children) )
			unset($this->children[$index]);
	}

	public function getChildIndexByName($name)
	{
		foreach($this->children as $index => $child)
		{
			if( is_object($child) && method_exists($child, 'attr') && $child->attr('name') == $name )
				return $index;
		}
	}

	public function getChildIndexById($id)
	{
		foreach($this->children as $index => $child)
		{
			if( is_object($child) && method_exists($child, 'attr') && $child->attr('id') == $id )
				return $index;
		}
	}

	protected function getAttributeString()
	{
		$html = array();
		foreach($this->attributes as $k => $v)
		{
			if( is_numeric($k) ) // Assume a keyword property, like "multiple" on the new HTML5 file input
				$html[] = $v;
			else
				$html[] = sprintf('%s="%s"', $k, $v);
		}
		return implode(" ", $html);
	}

	// Compatibility function for use with other established patterns
	public function getHtml()
	{
		$html = $this->openTag();
		$html .= ($this->cr?"\n":'');
		$html .= $this->renderChildren();
		if( !is_null($this->text) )
			$html .= $this->text;
		$html .= $this->closeTag();
		return $html;
	}

	// Primary rendering function.
	public function render($return=false)
	{
		if( $return )
			return $this->getHtml();
		else
			echo $this->getHtml();
	}

	public function renderChildren()
	{
		$html = '';
		if( count($this->children) > 0 )
		{
			foreach($this->children as $child)
			{
				if( $child instanceof Tag )
				{
					$html .= $child->render(true);
				}
				else
					$html .= $child;
			}
		}
		return $html;
	}

	// Renders all instances of Tag based objects found in the subject array
	// returns a copy of the subject array with all qualified objects replaced
	// with rendered markup strings.
	static function renderTags($subject)
	{
		foreach($subject as $i => $node)
		{
			if( is_array($node) )
				$subject[$i] = self::renderTags($node);
			else
			if( is_object($node) && $node instanceof Tag && method_exists($node, 'render') )
				$subject[$i] = $node->render(true);
		}
		return $subject;
	}

	// Sets the 'value' attribute of every Tag based object found in the $subject array
	// matching by the 'name' attribute of the element in the $values array which should
	// be an assoc array
	static function setTagValues($subject, $values)
	{
		if( !is_array($values) )
			throw new Exception('The provided $values parameter is NOT an array.');

		foreach($subject as $i => $node)
		{
			if( is_array($node) )
			{
				$subject[$i] = self::setTagValues($node, $values);
			}
			elseif( is_object($node) )
			{
				if( $node instanceof Tag )
				{
					if( $node instanceof Tag_Field && $node->field instanceof Tag && array_key_exists($node->field->attr('name'), $values) )
						$node->field->attr('value', $values[$node->field->attr('name')]);
					elseif( $node instanceof Tag && array_key_exists($node->attr('name'), $values) )
						$node->attr('value', $values[$node->attr('name')]);
				}
			}
		}
		return $subject;
	}

	/**
	 * Retrieves all of the 'value' attributes from a collection of tag instances
	 * and returns them as an assoc array using the 'name' or 'id' attributes as keys
	 * @param array $subject Array of Tag instances or sub instances
	 * @return associative array of data
	 */
	static function getTagValues($instances)
	{
		if( !is_array($instances) )
			throw new Exception('The provided $instances parameter is NOT an array.');

		$values = array();
		foreach($instances as $i => $node)
		{
			if( is_array($node) )
			{
				$values = array_merge($values, self::getTagValues($node));
			}
			elseif( $node instanceof Tag )
			{
				if( $node instanceof Tag_Field && $node->field instanceof Tag )
				{
					$name = $node->field->attr('name');
					$id = $node->field->attr('id');
					$key = ($name?$name:$id);
					$key = ($key?$key:count($values));
					$values[$key] = $node->field->attr('value');
				}
				else
				{
					$name = $node->attr('name');
					$id = $node->attr('id');
					$key = ($name?$name:$id);
					$key = ($key?$key:count($values));
					$values[$key] = $node->attr('value');
				}
			}
		}
		
		return $values;
	}

	// Utility function for stripping non-alphanumeric characters out of names/id's etc.
	// Ensures that the string contains ONLY letters, or numbers, nothing like brackets, etc.
	static function stripId($id)
	{
		return preg_replace("[^A-Za-z0-9\_\-]", "", $id);
		//return ereg_replace("[^A-Za-z0-9\_\-]", "", $id );
	}

	// Similar to jQuery, abstracts the process of adding and removing CSS classes
	// from Tags
	function addClass($classname)
	{
		$classes = explode(' ', $this->attr('class'));

		$new_classes = array();
		if( !is_array($classname) && !empty($classname) )
			$new_classes = explode(' ',$classname);
		elseif( empty($classname) )
			return;
		elseif( is_array($classname) )
			$new_classes = $classname;
		else
			throw new Exception('The provided classname was invalid');

		foreach($new_classes as $class)
		{
			$i = array_search($class, $classes);
			if( $i === false )
				$classes[] = $class;
		}

		// Classes should appear only once
		$classes = array_unique($classes);

		$this->attr('class', trim(implode(' ', $classes)));
	}

	// Similar to jQuery, abstracts the process of adding and removing CSS classes
	// from Tags
	function removeClass($classname=NULL)
	{
		if( is_null($classname) )
		{
			$this->attr('class','');
			return;
		}
		$classes = explode(' ', $this->attr('class'));
		$i = array_search($classname, $classes);
		if( $i !== false )
			unset($classes[$i]);
		$this->attr('class', implode(' ', $classes));
	}

	// Handles HTML5 data- attributes explicitly
	public function data($name, $value=NULL)
	{
		$name = 'data-'.$name;
		if( is_null($value) && isset($this->attributes[$name]) )
			return $this->attributes[$name];
		else
			$this->attributes[$name] = $value;
	}	
}
