<?php
/**
 * TListControl class file
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id$
 * @package System.Web.UI.WebControls
 */

/**
 * Includes the supporting classes
 */
Prado::using('System.Web.UI.WebControls.TDataBoundControl');
Prado::using('System.Web.UI.WebControls.TListItem');
Prado::using('System.Collections.TAttributeCollection');
Prado::using('System.Util.TDataFieldAccessor');


/**
 * TListControl class
 *
 * TListControl is a base class for list controls, such as {@link TListBox},
 * {@link TDropDownList}, {@link TCheckBoxList}, etc.
 * It manages the items and their status in a list control.
 * It also implements how the items can be populated from template and
 * data source.
 *
 * The property {@link getItems} returns a list of the items in the control.
 * To specify or determine which item is selected, use the
 * {@link getSelectedIndex SelectedIndex} property that indicates the zero-based
 * index of the selected item in the item list. You may also use
 * {@link getSelectedItem SelectedItem} and {@link getSelectedValue SelectedValue}
 * to get the selected item and its value. For multiple selection lists
 * (such as {@link TCheckBoxList} and {@link TListBox}), property
 * {@link getSelectedIndices SelectedIndices} is useful.
 *
 * TListControl implements {@link setAutoPostBack AutoPostBack} which allows
 * a list control to postback the page if the selections of the list items are changed.
 * The {@link setCausesValidation CausesValidation} and {@link setValidationGroup ValidationGroup}
 * properties may be used to specify that validation be performed when auto postback occurs.
 *
 * There are three ways to populate the items in a list control: from template,
 * using {@link setDataSource DataSource} and using {@link setDataSourceID DataSourceID}.
 * The latter two are covered in {@link TDataBoundControl}. To specify items via
 * template, using the following template syntax:
 * <code>
 * <com:TListControl>
 *   <com:TListItem Value="xxx" Text="yyy" >
 *   <com:TListItem Value="xxx" Text="yyy" Selected="true" >
 *   <com:TListItem Value="xxx" Text="yyy" >
 * </com:TListControl>
 * </code>
 *
 * When {@link setDataSource DataSource} or {@link setDataSourceID DataSourceID}
 * is used to populate list items, the {@link setDataTextField DataTextField} and
 * {@link setDataValueField DataValueField} properties are used to specify which
 * columns of the data will be used to populate the text and value of the items.
 * For example, if a data source is as follows,
 * <code>
 * $dataSource=array(
 *    array('name'=>'John', 'age'=>31),
 *    array('name'=>'Cary', 'age'=>28),
 *    array('name'=>'Rose', 'age'=>35),
 * );
 * </code>
 * setting {@link setDataTextField DataTextField} and {@link setDataValueField DataValueField}
 * to 'name' and 'age' will make the first item's text be 'John', value be 31,
 * the second item's text be 'Cary', value be 28, and so on.
 * The {@link setDataTextFormatString DataTextFormatString} property may be further
 * used to format how the item should be displayed. See {@link formatDataValue()}
 * for an explanation of the format string.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System.Web.UI.WebControls
 * @since 3.0
 */
abstract class TListControl extends TDataBoundControl
{
	/**
	 * @var TListItemCollection item list
	 */
	private $_items=null;
	/**
	 * @var boolean whether items are restored from viewstate
	 */
	private $_stateLoaded=false;
	/**
	 * @var mixed the following selection variables are used
	 * to keep selections when Items are not available
	 */
	private $_cachedSelectedIndex=-1;
	private $_cachedSelectedValue=null;
	private $_cachedSelectedIndices=null;
	private $_cachedSelectedValues=null;

	/**
	 * @return string tag name of the list control
	 */
	protected function getTagName()
	{
		return 'select';
	}

	/**
	 * Adds attributes to renderer.
	 * @param THtmlWriter the renderer
	 */
	protected function addAttributesToRender($writer)
	{
		$page=$this->getPage();
		$page->ensureRenderInForm($this);
		if($this->getIsMultiSelect())
			$writer->addAttribute('multiple','multiple');
		if($this->getEnabled(true) && $this->getAutoPostBack() && $page->getClientSupportsJavaScript())
		{
			$writer->addAttribute('id',$this->getClientID());
			$this->getPage()->getClientScript()->registerPostBackControl($this->getClientClassName(),$this->getPostBackOptions());
		}
		if(!$this->getEnabled(true) && $this->getEnabled())
			$writer->addAttribute('disabled','disabled');
		parent::addAttributesToRender($writer);
	}

	/**
	 * Gets the name of the javascript class responsible for performing postback for this control.
	 * Derived classes may override this method and return customized js class names.
	 * @return string the javascript class name
	 */
	protected function getClientClassName()
	{
		return 'Prado.WebUI.TListControl';
	}

	/**
	 * @return array postback options for JS postback code
	 */
	protected function getPostBackOptions()
	{
		$options['ID'] = $this->getClientID();
		$options['CausesValidation'] = $this->getCausesValidation();
		$options['ValidationGroup'] = $this->getValidationGroup();
		$options['EventTarget'] = $this->getUniqueID();
		return $options;
	}

	/**
	 * Adds object parsed from template to the control.
	 * This method adds only {@link TListItem} objects into the {@link getItems Items} collection.
	 * All other objects are ignored.
	 * @param mixed object parsed from template
	 */
	public function addParsedObject($object)
	{
		// Do not add items from template if items are loaded from viewstate
		if(!$this->_stateLoaded && ($object instanceof TListItem))
		{
			$index=$this->getItems()->add($object);
			if(($this->_cachedSelectedValue!==null && $this->_cachedSelectedValue===$object->getValue()) || ($this->_cachedSelectedIndex===$index))
			{
				$object->setSelected(true);
				$this->_cachedSelectedValue=null;
				$this->_cachedSelectedIndex=-1;
			}
		}
	}

	/**
	 * Performs databinding to populate list items from data source.
	 * This method is invoked by dataBind().
	 * You may override this function to provide your own way of data population.
	 * @param Traversable the data
	 */
	protected function performDataBinding($data)
	{
		$items=$this->getItems();
		if(!$this->getAppendDataBoundItems())
			$items->clear();
		$textField=$this->getDataTextField();
		if($textField==='')
			$textField=0;
		$valueField=$this->getDataValueField();
		if($valueField==='')
			$valueField=1;
		$textFormat=$this->getDataTextFormatString();
		$groupField=$this->getDataGroupField();
		foreach($data as $key=>$object)
		{
			$item=$items->createListItem();
			if(is_array($object) || is_object($object))
			{
				$text=TDataFieldAccessor::getDataFieldValue($object,$textField);
				$value=TDataFieldAccessor::getDataFieldValue($object,$valueField);
				$item->setValue($value);
				if($groupField!=='')
					$item->setAttribute('Group',TDataFieldAccessor::getDataFieldValue($object,$groupField));
			}
			else
			{
				$text=$object;
				$item->setValue("$key");
			}
			$item->setText($this->formatDataValue($textFormat,$text));
		}
		// SelectedValue or SelectedIndex may be set before databinding
		// so we make them be effective now
		if($this->_cachedSelectedValue!==null)
		{
			$this->setSelectedValue($this->_cachedSelectedValue);
			$this->resetCachedSelections();
		}
		else if($this->_cachedSelectedIndex!==-1)
		{
			$this->setSelectedIndex($this->_cachedSelectedIndex);
			$this->resetCachedSelections();
		}
		else if($this->_cachedSelectedValues!==null)
		{
			$this->setSelectedValues($this->_cachedSelectedValues);
			$this->resetCachedSelections();
		}
		else if($this->_cachedSelectedIndices!==null)
		{
			$this->setSelectedIndices($this->_cachedSelectedIndices);
			$this->resetCachedSelections();
		}
	}

	private function resetCachedSelections()
	{
		$this->_cachedSelectedValue=null;
		$this->_cachedSelectedIndex=-1;
		$this->_cachedSelectedValues=null;
		$this->_cachedSelectedIndices=null;
	}

	/**
	 * Creates a collection object to hold list items.
	 * This method may be overriden to create a customized collection.
	 * @return TListItemCollection the collection object
	 */
	protected function createListItemCollection()
	{
		return new TListItemCollection;
	}

	/**
	 * Saves items into viewstate.
	 * This method is invoked right before control state is to be saved.
	 */
	public function saveState()
	{
		parent::saveState();
		if($this->_items)
			$this->setViewState('Items',$this->_items->saveState(),null);
		else
			$this->clearViewState('Items');
	}

	/**
	 * Loads items from viewstate.
	 * This method is invoked right after control state is loaded.
	 */
	public function loadState()
	{
		parent::loadState();
		$this->_stateLoaded=true;
		if(!$this->getIsDataBound())
		{
			$this->_items=$this->createListItemCollection();
			$this->_items->loadState($this->getViewState('Items',null));
		}
		$this->clearViewState('Items');
	}

	/**
	 * @return boolean whether this is a multiselect control. Defaults to false.
	 */
	protected function getIsMultiSelect()
	{
		return false;
	}

	/**
	 * @return boolean whether performing databind should append items or clear the existing ones. Defaults to false.
	 */
	public function getAppendDataBoundItems()
	{
		return $this->getViewState('AppendDataBoundItems',false);
	}

	/**
	 * @param boolean whether performing databind should append items or clear the existing ones.
	 */
	public function setAppendDataBoundItems($value)
	{
		$this->setViewState('AppendDataBoundItems',TPropertyValue::ensureBoolean($value),false);
	}

	/**
	 * @return boolean a value indicating whether an automatic postback to the server
     * will occur whenever the user makes change to the list control and then tabs out of it.
     * Defaults to false.
	 */
	public function getAutoPostBack()
	{
		return $this->getViewState('AutoPostBack',false);
	}

	/**
	 * Sets the value indicating if postback automatically.
	 * An automatic postback to the server will occur whenever the user
	 * makes change to the list control and then tabs out of it.
	 * @param boolean the value indicating if postback automatically
	 */
	public function setAutoPostBack($value)
	{
		$this->setViewState('AutoPostBack',TPropertyValue::ensureBoolean($value),false);
	}

	/**
	 * @return boolean whether postback event trigger by this list control will cause input validation, default is true.
	 */
	public function getCausesValidation()
	{
		return $this->getViewState('CausesValidation',true);
	}

	/**
	 * @param boolean whether postback event trigger by this list control will cause input validation.
	 */
	public function setCausesValidation($value)
	{
		$this->setViewState('CausesValidation',TPropertyValue::ensureBoolean($value),true);
	}

	/**
	 * @return string the field of the data source that provides the text content of the list items.
	 */
	public function getDataTextField()
	{
		return $this->getViewState('DataTextField','');
	}

	/**
	 * @param string the field of the data source that provides the text content of the list items.
	 */
	public function setDataTextField($value)
	{
		$this->setViewState('DataTextField',$value,'');
	}

	/**
	 * @return string the formatting string used to control how data bound to the list control is displayed.
	 */
	public function getDataTextFormatString()
	{
		return $this->getViewState('DataTextFormatString','');
	}

	/**
	 * Sets data text format string.
	 * The format string is used in {@link TDataValueFormatter::format()} to format the Text property value
	 * of each item in the list control.
	 * @param string the formatting string used to control how data bound to the list control is displayed.
	 * @see TDataValueFormatter::format()
	 */
	public function setDataTextFormatString($value)
	{
		$this->setViewState('DataTextFormatString',$value,'');
	}

	/**
	 * @return string the field of the data source that provides the value of each list item.
	 */
	public function getDataValueField()
	{
		return $this->getViewState('DataValueField','');
	}

	/**
	 * @param string the field of the data source that provides the value of each list item.
	 */
	public function setDataValueField($value)
	{
		$this->setViewState('DataValueField',$value,'');
	}

	/**
	 * @return string the field of the data source that provides the label of the list item groups
	 */
	public function getDataGroupField()
	{
		return $this->getViewState('DataGroupField','');
	}

	/**
	 * @param string the field of the data source that provides the label of the list item groups
	 */
	public function setDataGroupField($value)
	{
		$this->setViewState('DataGroupField',$value,'');
	}

	/**
	 * @return integer the number of items in the list control
	 */
	public function getItemCount()
	{
		return $this->_items?$this->_items->getCount():0;
	}

	/**
	 * @return boolean whether the list control contains any items.
	 */
	public function getHasItems()
	{
		return ($this->_items && $this->_items->getCount()>0);
	}

	/**
	 * @return TListItemCollection the item collection
	 */
	public function getItems()
	{
		if(!$this->_items)
			$this->_items=$this->createListItemCollection();
		return $this->_items;
	}

	/**
	 * @return integer the index (zero-based) of the item being selected, -1 if no item is selected.
	 */
	public function getSelectedIndex()
	{
		if($this->_items)
		{
			$n=$this->_items->getCount();
			for($i=0;$i<$n;++$i)
				if($this->_items->itemAt($i)->getSelected())
					return $i;
		}
		return -1;
	}

	/**
	 * @param integer the index (zero-based) of the item to be selected
	 */
	public function setSelectedIndex($index)
	{
		if(($index=TPropertyValue::ensureInteger($index))<0)
			$index=-1;
		if($this->_items)
		{
			$this->clearSelection();
			if($index>=0 && $index<$this->_items->getCount())
				$this->_items->itemAt($index)->setSelected(true);
			else if($index!==-1)
				throw new TInvalidDataValueException('listcontrol_selectedindex_invalid',get_class($this),$index);
		}
		$this->_cachedSelectedIndex=$index;
	}

	/**
	 * @return array list of index of items that are selected
	 */
	public function getSelectedIndices()
	{
		$selections=array();
		if($this->_items)
		{
			$n=$this->_items->getCount();
			for($i=0;$i<$n;++$i)
				if($this->_items->itemAt($i)->getSelected())
					$selections[]=$i;
		}
		return $selections;
	}

	/**
	 * @param array list of index of items to be selected
	 */
	public function setSelectedIndices($indices)
	{
		if($this->getIsMultiSelect())
		{
			if($this->_items)
			{
				$this->clearSelection();
				$n=$this->_items->getCount();
				foreach($indices as $index)
				{
					if($index>=0 && $index<$n)
						$this->_items->itemAt($index)->setSelected(true);
				}
			}
			$this->_cachedSelectedIndices=$indices;
		}
		else
			throw new TNotSupportedException('listcontrol_multiselect_unsupported',get_class($this));
	}

	/**
	 * @return TListItem|null the selected item with the lowest cardinal index, null if no item is selected.
	 */
	public function getSelectedItem()
	{
		if(($index=$this->getSelectedIndex())>=0)
			return $this->_items->itemAt($index);
		else
			return null;
	}

	/**
	 * @return string the value of the selected item with the lowest cardinal index, empty if no selection
	 */
	public function getSelectedValue()
	{
		$index=$this->getSelectedIndex();
		return $index>=0?$this->getItems()->itemAt($index)->getValue():'';
	}

	/**
	 * Sets selection by item value.
	 * Existing selections will be cleared if the item value is found in the item collection.
	 * Note, if the value is null, existing selections will also be cleared.
	 * @param string the value of the item to be selected.
	 */
	public function setSelectedValue($value)
    {
	    if($this->_items)
	    {
		    if($value===null)
		    	$this->clearSelection();
		    else if(($item=$this->_items->findItemByValue($value))!==null)
	    	{
		    	$this->clearSelection();
		    	$item->setSelected(true);
	    	}
	    	else
	    		throw new TInvalidDataValueException('listcontrol_selectedvalue_invalid',get_class($this),$value);
    	}
    	$this->_cachedSelectedValue=$value;
    }


	/**
	 * @return array list of the selected item values (strings)
	 */
	public function getSelectedValues()
	{
		$values=array();
		if($this->_items)
		{
			foreach($this->_items as $item)
			{
				if($item->getSelected())
					$values[]=$item->getValue();
			}
		}
		return $values;
	}

	/**
	 * @param array list of the selected item values
	 */
	public function setSelectedValues($values)
	{
		if($this->getIsMultiSelect())
		{
			if($this->_items)
			{
				$this->clearSelection();
				$lookup=array();
				foreach($this->_items as $item)
					$lookup[$item->getValue()]=$item;
				foreach($values as $value)
				{
					if(isset($lookup["$value"]))
						$lookup["$value"]->setSelected(true);
			    	else
			    		throw new TInvalidDataValueException('listcontrol_selectedvalue_invalid',get_class($this),$value);
				}
			}
			$this->_cachedSelectedValues=$values;
		}
		else
			throw new TNotSupportedException('listcontrol_multiselect_unsupported',get_class($this));
	}

    /**
     * @return string selected value
     */
    public function getText()
    {
	    return $this->getSelectedValue();
    }

    /**
     * @param string value to be selected
     */
    public function setText($value)
    {
	    $this->setSelectedValue($value);
    }

    /**
     * Clears all existing selections.
     */
    public function clearSelection()
    {
	    if($this->_items)
	    {
		    foreach($this->_items as $item)
		    	$item->setSelected(false);
	    }
    }

	/**
	 * @return string the group of validators which the list control causes validation upon postback
	 */
	public function getValidationGroup()
	{
		return $this->getViewState('ValidationGroup','');
	}

	/**
	 * @param string the group of validators which the list control causes validation upon postback
	 */
	public function setValidationGroup($value)
	{
		$this->setViewState('ValidationGroup',$value,'');
	}

	/**
	 * Raises OnSelectedIndexChanged event when selection is changed.
	 * This method is invoked when the list control has its selection changed
	 * by end-users.
	 * @param TEventParameter event parameter
	 */
	public function onSelectedIndexChanged($param)
	{
		$this->raiseEvent('OnSelectedIndexChanged',$this,$param);
		$this->onTextChanged($param);
	}

	/**
	 * Raises OnTextChanged event when selection is changed.
	 * This method is invoked when the list control has its selection changed
	 * by end-users.
	 * @param TEventParameter event parameter
	 */
	public function onTextChanged($param)
	{
		$this->raiseEvent('OnTextChanged',$this,$param);
	}

	/**
	 * Renders body content of the list control.
	 * This method renders items contained in the list control as the body content.
	 * @param THtmlWriter writer
	 */
	public function renderContents($writer)
	{
		if($this->_items)
		{
			$writer->writeLine();
			$previousGroup=null;
			foreach($this->_items as $item)
			{
				if($item->getEnabled())
				{
					if($item->getHasAttributes())
					{
						$group=$item->getAttributes()->remove('Group');
						if($group!==$previousGroup)
						{
							if($previousGroup!==null)
							{
								$writer->renderEndTag();
								$writer->writeLine();
								$previousGroup=null;
							}
							if($group!==null)
							{
								$writer->addAttribute('label',$group);
								$writer->renderBeginTag('optgroup');
								$writer->writeLine();
								$previousGroup=$group;
							}
						}
						foreach($item->getAttributes() as $name=>$value)
							$writer->addAttribute($name,$value);
					}
					else if($previousGroup!==null)
					{
						$writer->renderEndTag();
						$writer->writeLine();
						$previousGroup=null;
					}
					if($item->getSelected())
						$writer->addAttribute('selected','selected');
					$writer->addAttribute('value',$item->getValue());
					$writer->renderBeginTag('option');
					$writer->write(THttpUtility::htmlEncode($item->getText()));
					$writer->renderEndTag();
					$writer->writeLine();
				}
			}
			if($previousGroup!==null)
			{
				$writer->renderEndTag();
				$writer->writeLine();
			}
		}
	}

	/**
	 * Formats the text value according to a format string.
	 * If the format string is empty, the original value is converted into
	 * a string and returned.
	 * If the format string starts with '#', the string is treated as a PHP expression
	 * within which the token '{0}' is translated with the data value to be formated.
	 * Otherwise, the format string and the data value are passed
	 * as the first and second parameters in {@link sprintf}.
	 * @param string format string
	 * @param mixed the data to be formatted
	 * @return string the formatted result
	 */
	protected function formatDataValue($formatString,$value)
	{
		if($formatString==='')
			return TPropertyValue::ensureString($value);
		else if($formatString[0]==='#')
		{
			$expression=strtr(substr($formatString,1),array('{0}'=>'$value'));
			try
			{
				if(eval("\$result=$expression;")===false)
					throw new Exception('');
				return $result;
			}
			catch(Exception $e)
			{
				throw new TInvalidDataValueException('listcontrol_expression_invalid',get_class($this),$expression,$e->getMessage());
			}
		}
		else
			return sprintf($formatString,$value);
	}
}

/**
 * TListItemCollection class.
 *
 * TListItemCollection maintains a list of {@link TListItem} for {@link TListControl}.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System.Web.UI.WebControls
 * @since 3.0
 */
class TListItemCollection extends TList
{
	/**
	 * Creates a list item object.
	 * This method may be overriden to provide a customized list item object.
	 * @param integer index where the newly created item is to be inserted at.
	 * If -1, the item will be appended to the end.
	 * @return TListItem list item object
	 */
	public function createListItem($index=-1)
	{
		$item=new TListItem;
		if($index<0)
			$this->add($item);
		else
			$this->insertAt($index,$item);
		return $item;
	}

	/**
	 * Inserts an item into the collection.
	 * @param integer the location where the item will be inserted.
	 * The current item at the place and the following ones will be moved backward.
	 * @param TListItem the item to be inserted.
	 * @throws TInvalidDataTypeException if the item being inserted is neither a string nor TListItem
	 */
	public function insertAt($index,$item)
	{
		if($item instanceof TListItem)
			parent::insertAt($index,$item);
		else if(is_string($item))
		{
			$item=$this->createListItem($index);
			$item->setText($item);
		}
		else
			throw new TInvalidDataTypeException('listitemcollection_item_invalid',get_class($this));
	}

	/**
	 * Finds the lowest cardinal index of the item whose value is the one being looked for.
	 * @param string the value to be looked for
	 * @param boolean whether to look for disabled items also
	 * @return integer the index of the item found, -1 if not found.
	 */
	public function findIndexByValue($value,$includeDisabled=true)
	{
		$value=TPropertyValue::ensureString($value);
		$index=0;
		foreach($this as $item)
		{
			if($item->getValue()===$value && ($includeDisabled || $item->getEnabled()))
				return $index;
			$index++;
		}
		return -1;
	}

	/**
	 * Finds the lowest cardinal index of the item whose text is the one being looked for.
	 * @param string the text to be looked for
	 * @param boolean whether to look for disabled items also
	 * @return integer the index of the item found, -1 if not found.
	 */
	public function findIndexByText($text,$includeDisabled=true)
	{
		$text=TPropertyValue::ensureString($text);
		$index=0;
		foreach($this as $item)
		{
			if($item->getText()===$text && ($includeDisabled || $item->getEnabled()))
				return $index;
			$index++;
		}
		return -1;
	}

	/**
	 * Finds the item whose value is the one being looked for.
	 * @param string the value to be looked for
	 * @param boolean whether to look for disabled items also
	 * @return TListItem the item found, null if not found.
	 */
	public function findItemByValue($value,$includeDisabled=true)
	{
		if(($index=$this->findIndexByValue($value,$includeDisabled))>=0)
			return $this->itemAt($index);
		else
			return null;
	}

	/**
	 * Finds the item whose text is the one being looked for.
	 * @param string the text to be looked for
	 * @param boolean whether to look for disabled items also
	 * @return TListItem the item found, null if not found.
	 */
	public function findItemByText($text,$includeDisabled=true)
	{
		if(($index=$this->findIndexByText($text,$includeDisabled))>=0)
			return $this->itemAt($index);
		else
			return null;
	}

	/**
	 * Loads state into every item in the collection.
	 * This method should only be used by framework and control developers.
	 * @param array|null state to be loaded.
	 */
	public function loadState($state)
	{
		$this->clear();
		if($state!==null)
			$this->copyFrom($state);
	}

	/**
	 * Saves state of items.
	 * This method should only be used by framework and control developers.
	 * @return array|null the saved state
	 */
	public function saveState()
	{
		return ($this->getCount()>0) ? $this->toArray() : null;
	}
}

?>