<?php

/*
Tag_Table abstracts the creation of complete tables with simple functionality and customization

DEMO USAGE CODE

class MyDemoObject {

	.....

	static function demo()
	{
		echo '<style>.test{color: blue;}</style>';
		$view = new Tag_Table;
		$view->onRowFormat('Tag_Table::demoFormatCallback');
		$view->setHeadings('First,Last,Phone');
		$view->setFooters(array('','','There are 2 People'));
		$view->setRows( array(
			array('first'=>'Joe','last'=>'Smith','phone'=>'777-777-7777'),
			array('first'=>'Kevin','last'=>'Thompson','address'=>'40 Arkham St, Gotham City','phone'=>'888-888-8888'),
			), 'first,last,phone');

		// Note, this call is optional, only used to be explicit when customizing formats for specific columns
		// setRows automatically sets this when you call it, so you don't "have to" call it if you're not
		// messing with things
		$view->setColumns(4);
		$view->columns[3]->addClass('test');

		$view->rows['even']->attr('style','color:red;');
		$view->render();
	}

	// Custom Callbacks MUST return the row
	static function demoFormatCallback($row)
	{
		if( $row['first'] == 'Joe' )
			$row['first'] = 'Joseph';
		$row[] = 'New Column Injection';
		return $row;
	}

*/

class Tag_Table extends Tag
{
	public $headings = array();
	public $columns = array();
	public $rows = array();

	protected $data_rows = array();		// The actual data which will be rendered
	protected $data_columns = array();	// The key/names for the columns which are NOT displayed, but output as hidden INPUT elements included with rows
	protected $extra_markup = array('before'=>'','after'=>'');
	protected $event_row_format = NULL;
	protected $row_order = NULL;
	protected $td_classes = array();
	protected $th_classes = array();

	function __construct($attributes=NULL)
	{
		parent::__construct('table',$attributes,TAG_CR | TAG_CLOSE);

		// Initialize Row Templates
		// This allows customization of the rows, based on a keyword rule
		// OR by numerical index of the row, meaning its possible to say
		// format the 201st row exactly like "this".
		$this->rows = array(
			'even' => new Tag('tr',array('class'=>'even'),TAG_CR | TAG_CLOSE),
			'odd' => new Tag('tr',array('class'=>'odd'),TAG_CR | TAG_CLOSE),
			'all' => new Tag('tr',NULL,TAG_CR | TAG_CLOSE),
			);
	}

	// Imports row values from a provided array of arrays
	// If filter is provided, then the keys in the filter will be imported only
	// ie
	// $x->setRows($rows, 'first_name,last_name,address');
	// or
	// $x->setRows($rows, array('first_name','last_name','address'));
	public function setRows( $rows, $filter=NULL )
	{
		// Make sure the filter is an array, and parse it into a keyset
		if( !is_null($filter) )
		{
			if( !is_array($filter) && strlen($filter) > 0 )
				$filter = explode(',',$filter);

			// Include Data Columns in the Filter
			$filter = array_merge($filter,$this->data_columns);

			// Store the filter in the row_order array, used later in ->render to control output
			$this->row_order = $filter;

			// Create an Associative Array of NULL values in the filter
			$filter = array_fill_keys($filter,NULL);
		}

		// Import the rows and filter them if specified
		$count = 0;
		if( is_array($rows) && count($rows) > 0 )
		{
			foreach($rows as $row)
			{
				// Optionally Filter the incoming values
				if( is_array($filter) )
					$row = array_intersect_key($row, $filter);

				// Get the Max Column Count
				if( count($row) > $count ) $count = count($row);

				// Append the new row, preserve the existing keys, assoc or otherwise
				$this->data_rows[] = $row;
			}

			// Initialize the Column Templates IF they haven't been explicitly initialized
			if( count($this->columns) == 0 )
				$this->setColumns($count);
		}
	}

	public function clearRows()
	{
		$this->data_rows = array();
	}

	// Sets up the DEFAULT templates for each table heading cell as it is rendered
	// Takes either an array of heading text names or,
	// a comma delimited string of names
	public function setHeadings($headings)
	{
		$this->headings = array();

		if( !is_array($headings) )
			$headings = explode(",", $headings);

		foreach($headings as $text)
			$this->headings[] = new Tag('th',array('text'=>$text),TAG_CR | TAG_CLOSE);
	}

	// Sets up the Default Column template objects
	// in the columns member array so that its possible to
	// customize formatting
	public function setColumns($count)
	{
		// Reset
		$this->columns = array();

		if( is_numeric($count) && $count > 0 )
		{
			for($i=1;$i<=$count;$i++)
				$this->columns[] = new Tag('td',array('text'=>''),TAG_CR | TAG_CLOSE);
			return;
		}
	}

	public function setFooters($footers)
	{
		$this->footers = array();

		if( !is_array($footers) )
			$footers = explode(",", $footers);

		foreach($footers as $text)
			$this->footers[] = new Tag('th',array('text'=>$text),TAG_CR | TAG_CLOSE);
	}

	public function render($return=false)
	{
		// Applies a callback function to every row object in the rows array, allowing customization
		// from outside the instance's scope
		$this->formatRows();

		// Check to see if columns have been defined
		// IF NOT, then we have to setup defaults now
		if( count($this->columns) <= 0 )
			$this->setColumns(count($this->columns));

		// Setup Containers
		$thead = new Tag('thead',NULL,TAG_CR | TAG_CLOSE);
		$tfoot = new Tag('tfoot',NULL,TAG_CR | TAG_CLOSE);
		$tbody = new Tag('tbody',NULL,TAG_CR | TAG_CLOSE);

		// Render head
		$tr = new Tag('tr',NULL,TAG_CR | TAG_CLOSE);
		if( isset($this->headings) && is_array($this->headings) && count($this->headings) > 0 )
		{
			$tr->addChild( $this->headings );
			$thead->addChild( $tr );
		}

		// Render foot
		$tr = new Tag('tr',NULL,TAG_CR | TAG_CLOSE);
		if( isset($this->footers) && is_array($this->footers) && count($this->footers) > 0 )
		{
			$tr->addChild( $this->footers );
			$tfoot->addChild( $tr );
		}

		// Render body
		$row_count = 0;
		foreach($this->data_rows as $x => $row)
		{
			$row_count++;

			// Determine what template to use for the TR tag instance
			if( array_key_exists($x,$this->rows) ) // Check for a Direct Numerical Index Match
			{
				$tr = clone $this->rows[$x];
			}
			elseif( array_key_exists('all',$this->rows) ) // Check for the 'all' keyword, which overrides even and odd
			{
				$tr = clone $this->rows['all'];
			}

			// Import attributes depending on EVEN / ODD from the even/odd templates
			$tpl = ( $row_count % 2 != 0 ? 'odd' : 'even' );

			// Import Attribs from the odd or even template
			foreach( $this->rows[$tpl]->getAttributes() as $attrib => $value )
					$tr->attr($attrib, $value);

			// Re-Order the Row into the desired format using the row_order var, which is derived from the filter provided in setRows()
			if( !is_null($this->row_order) )
			{
				$tmp = array();
				foreach( $this->row_order as $key )
					$tmp[$key] = $row[$key];
				$row = $tmp;
			}

			// Append cells to the row
			foreach(array_keys($row) as $i => $key)
			{
				// Check to see if the column should be displayed or output as a hidden field
				if( array_search($key, $this->data_columns) !== false || array_search($i, $this->data_columns,true) !== false )
				{
					$tag = new Tag('input',array('type'=>'hidden','name'=>$key,'value'=>$row[$key]));
					$td = new Tag('td',array('style'=>'display:none;'));
					$td->addChild($tag);
					$tr->addChild($td);
				}
				else // Normal Column , Render it
				{
					if( array_key_exists($i,$this->columns) )
					{
						$td = clone $this->columns[$i];
					}
					else
					{
						$td = new Tag('td');
					}
					$td->text( $row[$key] );
					$tr->addChild($td);
				}
			}
			$tbody->addChild($tr);
		}

		$this->addChild($thead);
		$this->addChild($tfoot);
		$this->addChild($tbody);

		return parent::render($return);
	}

	// Iterates all rows and runs any bound handlers
	// The callback will be passed an object, with a data array attached for the fields;
	// Note, event_row_format supports array syntax for use with call_user_func to support object methods
	// eg. array($object,'memberFunction')
	public function formatRows()
	{
		if( is_null($this->event_row_format) ) return;

		// Call the callback function for each row in the set
		foreach(array_keys($this->data_rows) as $i)
		{
			// Call the defined callback, must be standalone function in current scope OR static
			if( is_array($this->event_row_format) )
			{
				// Assume Object Instance Included in parameter
				$this->data_rows[$i] = call_user_func($this->event_row_format, $this->data_rows[$i]);
			}
			else
			{
				$this->data_rows[$i] = call_user_func($this->event_row_format, $this->data_rows[$i]);
			}
		}
	}

	// Binds a callback used to format each row conditionally
	public function onRowFormat($handler)
	{
		if( is_array($handler) && count($handler) != 2 )
			throw new InvalidArgumentException('onRowFormat has been given an array with more than 2 elements. It supports (object,function) syntax only for use with method functions');
		$this->event_row_format = $handler;
	}

	// Depreciated Alias to onRowFormat
	public function bindRowFormat($handler)
	{
		$this->onRowFormat($handler);
	}

	public function addMarkupBefore($markup)
	{
		$this->extra_markup['before'] .= $markup;
	}

	public function addMarkupAfter($markup)
	{
		$this->extra_markup['after'] .= $markup;
	}

	// Private implementation of array_impose, keeps it within the object/ No dependency
	// Imposes the values in the 2nd parameter wherever the keys of the 2nd parameter
	// match the values in the 1st parameter.
	static function impose($keys, $values)
	{
		foreach($keys as $i => $key)
		{
			if( array_key_exists($key, $values) )
			{
				$keys[$i] = $values[$key];
			}
		}
		return $keys;
	}

	function setDataColumn($column_keys)
	{
		if( empty($column_keys) )
			throw new Exception("Cannot set a data column without providing an array key/fieldname");

		if( !is_array($column_keys) )
			$column_keys = array($column_keys);

		foreach($column_keys as $key)
		{
			if( array_search($key, $this->data_columns) === false )
			{
				$this->data_columns[] = $key;
				break;
			}
		}
	}
}
