<?php
/**
 * TComponent, TPropertyValue classes
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2008 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id$
 * @package System
 */

/**
 * TComponent class
 *
 * TComponent is the base class for all PRADO components.
 * TComponent implements the protocol of defining, using properties and events.
 *
 * A property is defined by a getter method, and/or a setter method.
 * Properties can be accessed in the way like accessing normal object members.
 * Reading or writing a property will cause the invocation of the corresponding
 * getter or setter method, e.g.,
 * <code>
 * $a=$this->Text;     // equivalent to $a=$this->getText();
 * $this->Text='abc';  // equivalent to $this->setText('abc');
 * </code>
 * The signatures of getter and setter methods are as follows,
 * <code>
 * // getter, defines a readable property 'Text'
 * function getText() { ... }
 * // setter, defines a writable property 'Text', with $value being the value to be set to the property
 * function setText($value) { ... }
 * </code>
 * Property names are case-insensitive. It is recommended that they are written
 * in the format of concatenated words, with the first letter of each word
 * capitalized (e.g. DisplayMode, ItemStyle).
 *
 * An event is defined by the presence of a method whose name starts with 'on'.
 * The event name is the method name and is thus case-insensitive.
 * An event can be attached with one or several methods (called event handlers).
 * An event can be raised by calling {@link raiseEvent} method, upon which
 * the attached event handlers will be invoked automatically in the order they
 * are attached to the event. Event handlers must have the following signature,
 * <code>
 * function eventHandlerFuncName($sender,$param) { ... }
 * </code>
 * where $sender refers to the object who is responsible for the raising of the event,
 * and $param refers to a structure that may contain event-specific information.
 * To raise an event (assuming named as 'Click') of a component, use
 * <code>
 * $component->raiseEvent('OnClick');
 * </code>
 * To attach an event handler to an event, use one of the following ways,
 * <code>
 * $component->OnClick=$callback;  // or $component->OnClick->add($callback);
 * $$component->attachEventHandler('OnClick',$callback);
 * </code>
 * The first two ways make use of the fact that $component->OnClick refers to
 * the event handler list {@link TList} for the 'OnClick' event.
 * The variable $callback contains the definition of the event handler that can
 * be either a string referring to a global function name, or an array whose
 * first element refers to an object and second element a method name/path that
 * is reachable by the object, e.g.
 * - 'buttonClicked' : buttonClicked($sender,$param);
 * - array($object,'buttonClicked') : $object->buttonClicked($sender,$param);
 * - array($object,'MainContent.SubmitButton.buttonClicked') :
 *   $object->MainContent->SubmitButton->buttonClicked($sender,$param);
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System
 * @since 3.0
 */
class TComponent
{
	/**
	 * @var array event handler lists
	 */
	private $_e=array();

	/**
	 * Returns a property value or an event handler list by property or event name.
	 * Do not call this method. This is a PHP magic method that we override
	 * to allow using the following syntax to read a property:
	 * <code>
	 * $value=$component->PropertyName;
	 * </code>
	 * and to obtain the event handler list for an event,
	 * <code>
	 * $eventHandlerList=$component->EventName;
	 * </code>
	 * @param string the property name or the event name
	 * @return mixed the property value or the event handler list
	 * @throws TInvalidOperationException if the property/event is not defined.
	 */
	public function __get($name)
	{
		$getter='get'.$name;
		if(method_exists($this,$getter))
		{
			// getting a property
			return $this->$getter();
		}
		else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
		{
			// getting an event (handler list)
			$name=strtolower($name);
			if(!isset($this->_e[$name]))
				$this->_e[$name]=new TList;
			return $this->_e[$name];
		}
		else
		{
			throw new TInvalidOperationException('component_property_undefined',get_class($this),$name);
		}
	}

	/**
	 * Sets value of a component property.
	 * Do not call this method. This is a PHP magic method that we override
	 * to allow using the following syntax to set a property or attach an event handler.
	 * <code>
	 * $this->PropertyName=$value;
	 * $this->EventName=$handler;
	 * </code>
	 * @param string the property name or event name
	 * @param mixed the property value or event handler
	 * @throws TInvalidOperationException If the property is not defined or read-only.
	 */
	public function __set($name,$value)
	{
		$setter='set'.$name;
		if(method_exists($this,$setter))
		{
			$this->$setter($value);
		}
		else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
		{
			$this->attachEventHandler($name,$value);
		}
		else if(method_exists($this,'get'.$name))
		{
			throw new TInvalidOperationException('component_property_readonly',get_class($this),$name);
		}
		else
		{
			throw new TInvalidOperationException('component_property_undefined',get_class($this),$name);
		}
	}

	/**
	 * Determines whether a property is defined.
	 * A property is defined if there is a getter or setter method
	 * defined in the class. Note, property names are case-insensitive.
	 * @param string the property name
	 * @return boolean whether the property is defined
	 */
	public function hasProperty($name)
	{
		return method_exists($this,'get'.$name) || method_exists($this,'set'.$name);
	}

	/**
	 * Determines whether a property can be read.
	 * A property can be read if the class has a getter method
	 * for the property name. Note, property name is case-insensitive.
	 * @param string the property name
	 * @return boolean whether the property can be read
	 */
	public function canGetProperty($name)
	{
		return method_exists($this,'get'.$name);
	}

	/**
	 * Determines whether a property can be set.
	 * A property can be written if the class has a setter method
	 * for the property name. Note, property name is case-insensitive.
	 * @param string the property name
	 * @return boolean whether the property can be written
	 */
	public function canSetProperty($name)
	{
		return method_exists($this,'set'.$name);
	}

	/**
	 * Evaluates a property path.
	 * A property path is a sequence of property names concatenated by '.' character.
	 * For example, 'Parent.Page' refers to the 'Page' property of the component's
	 * 'Parent' property value (which should be a component also).
	 * @param string property path
	 * @return mixed the property path value
	 */
	public function getSubProperty($path)
	{
		$object=$this;
		foreach(explode('.',$path) as $property)
			$object=$object->$property;
		return $object;
	}

	/**
	 * Sets a value to a property path.
	 * A property path is a sequence of property names concatenated by '.' character.
	 * For example, 'Parent.Page' refers to the 'Page' property of the component's
	 * 'Parent' property value (which should be a component also).
	 * @param string property path
	 * @param mixed the property path value
	 */
	public function setSubProperty($path,$value)
	{
		$object=$this;
		if(($pos=strrpos($path,'.'))===false)
			$property=$path;
		else
		{
			$object=$this->getSubProperty(substr($path,0,$pos));
			$property=substr($path,$pos+1);
		}
		$object->$property=$value;
	}

	/**
	 * Determines whether an event is defined.
	 * An event is defined if the class has a method whose name is the event name prefixed with 'on'.
	 * Note, event name is case-insensitive.
	 * @param string the event name
	 * @return boolean
	 */
	public function hasEvent($name)
	{
		return strncasecmp($name,'on',2)===0 && method_exists($this,$name);
	}

	/**
	 * @return boolean whether an event has been attached one or several handlers
	 */
	public function hasEventHandler($name)
	{
		$name=strtolower($name);
		return isset($this->_e[$name]) && $this->_e[$name]->getCount()>0;
	}

	/**
	 * Returns the list of attached event handlers for an event.
	 * @return TList list of attached event handlers for an event
	 * @throws TInvalidOperationException if the event is not defined
	 */
	public function getEventHandlers($name)
	{
		if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
		{
			$name=strtolower($name);
			if(!isset($this->_e[$name]))
				$this->_e[$name]=new TList;
			return $this->_e[$name];
		}
		else
			throw new TInvalidOperationException('component_event_undefined',get_class($this),$name);
	}

	/**
	 * Attaches an event handler to an event.
	 *
	 * The handler must be a valid PHP callback, i.e., a string referring to
	 * a global function name, or an array containing two elements with
	 * the first element being an object and the second element a method name
	 * of the object. In Prado, you can also use method path to refer to
	 * an event handler. For example, array($object,'Parent.buttonClicked')
	 * uses a method path that refers to the method $object->Parent->buttonClicked(...).
	 *
	 * The event handler must be of the following signature,
	 * <code>
	 * function handlerName($sender,$param) {}
	 * </code>
	 * where $sender represents the object that raises the event,
	 * and $param is the event parameter.
	 *
	 * This is a convenient method to add an event handler.
	 * It is equivalent to {@link getEventHandlers}($name)->add($handler).
	 * For complete management of event handlers, use {@link getEventHandlers}
	 * to get the event handler list first, and then do various
	 * {@link TList} operations to append, insert or remove
	 * event handlers. You may also do these operations like
	 * getting and setting properties, e.g.,
	 * <code>
	 * $component->OnClick[]=array($object,'buttonClicked');
	 * $component->OnClick->insertAt(0,array($object,'buttonClicked'));
	 * </code>
	 * which are equivalent to the following
	 * <code>
	 * $component->getEventHandlers('OnClick')->add(array($object,'buttonClicked'));
	 * $component->getEventHandlers('OnClick')->insertAt(0,array($object,'buttonClicked'));
	 * </code>
	 *
	 * @param string the event name
	 * @param callback the event handler
	 * @throws TInvalidOperationException if the event does not exist
	 */
	public function attachEventHandler($name,$handler)
	{
		$this->getEventHandlers($name)->add($handler);
	}

	/**
	 * Detaches an existing event handler.
	 * This method is the opposite of {@link attachEventHandler}.
	 * @param string event name
	 * @param callback the event handler to be removed
	 * @return boolean if the removal is successful
	 */
	public function detachEventHandler($name,$handler)
	{
		if($this->hasEventHandler($name))
		{
			try
			{
				$this->getEventHandlers($name)->remove($handler);
				return true;
			}
			catch(Exception $e)
			{
			}
		}
		return false;
	}

	/**
	 * Raises an event.
	 * This method represents the happening of an event and will
	 * invoke all attached event handlers for the event.
	 * @param string the event name
	 * @param mixed the event sender object
	 * @param TEventParameter the event parameter
	 * @throws TInvalidOperationException if the event is undefined
	 * @throws TInvalidDataValueException If an event handler is invalid
	 */
	public function raiseEvent($name,$sender,$param)
	{
		$name=strtolower($name);
		if(isset($this->_e[$name]))
		{
			foreach($this->_e[$name] as $handler)
			{
				if(is_string($handler))
				{
					if(($pos=strrpos($handler,'.'))!==false)
					{
						$object=$this->getSubProperty(substr($handler,0,$pos));
						$method=substr($handler,$pos+1);
						if(method_exists($object,$method))
							$object->$method($sender,$param);
						else
							throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,$handler);
					}
					else
						call_user_func($handler,$sender,$param);
				}
				else if(is_callable($handler,true))
				{
					// an array: 0 - object, 1 - method name/path
					list($object,$method)=$handler;
					if(is_string($object))	// static method call
						call_user_func($handler,$sender,$param);
					else
					{
						if(($pos=strrpos($method,'.'))!==false)
						{
							$object=$this->getSubProperty(substr($method,0,$pos));
							$method=substr($method,$pos+1);
						}
						if(method_exists($object,$method))
							$object->$method($sender,$param);
						else
							throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,$handler[1]);
					}
				}
				else
					throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,gettype($handler));
			}
		}
		else if(!$this->hasEvent($name))
			throw new TInvalidOperationException('component_event_undefined',get_class($this),$name);
	}

	/**
	 * Evaluates a PHP expression in the context of this control.
	 * @return mixed the expression result
	 * @throws TInvalidOperationException if the expression is invalid
	 */
	public function evaluateExpression($expression)
	{
		try
		{
			if(eval("\$result=$expression;")===false)
				throw new Exception('');
			return $result;
		}
		catch(Exception $e)
		{
			throw new TInvalidOperationException('component_expression_invalid',get_class($this),$expression,$e->getMessage());
		}
	}

	/**
	 * Evaluates a list of PHP statements.
	 * @param string PHP statements
	 * @return string content echoed or printed by the PHP statements
	 * @throws TInvalidOperationException if the statements are invalid
	 */
	public function evaluateStatements($statements)
	{
		try
		{
			ob_start();
			if(eval($statements)===false)
				throw new Exception('');
			$content=ob_get_contents();
			ob_end_clean();
			return $content;
		}
		catch(Exception $e)
		{
			throw new TInvalidOperationException('component_statements_invalid',get_class($this),$statements,$e->getMessage());
		}
	}

	/**
	 * This method is invoked after the component is instantiated by a template.
	 * When this method is invoked, the component's properties have been initialized.
	 * The default implementation of this method will invoke
	 * the potential parent component's {@link addParsedObject}.
	 * This method can be overridden.
	 * @param TComponent potential parent of this control
	 * @see addParsedObject
	 */
	public function createdOnTemplate($parent)
	{
		$parent->addParsedObject($this);
	}

	/**
	 * Processes an object that is created during parsing template.
	 * The object can be either a component or a static text string.
	 * This method can be overridden to customize the handling of newly created objects in template.
	 * Only framework developers and control developers should use this method.
	 * @param string|TComponent text string or component parsed and instantiated in template
	 * @see createdOnTemplate
	 */
	public function addParsedObject($object)
	{
	}
}

/**
 * TEnumerable class.
 * TEnumerable is the base class for all enumerable types.
 * To define an enumerable type, extend TEnumberable and define string constants.
 * Each constant represents an enumerable value.
 * The constant name must be the same as the constant value.
 * For example,
 * <code>
 * class TTextAlign extends TEnumerable
 * {
 *     const Left='Left';
 *     const Right='Right';
 * }
 * </code>
 * Then, one can use the enumerable values such as TTextAlign::Left and
 * TTextAlign::Right.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System
 * @since 3.0
 */
class TEnumerable
{
}

/**
 * TPropertyValue class
 *
 * TPropertyValue is a utility class that provides static methods
 * to convert component property values to specific types.
 *
 * TPropertyValue is commonly used in component setter methods to ensure
 * the new property value is of specific type.
 * For example, a boolean-typed property setter method would be as follows,
 * <code>
 * function setPropertyName($value) {
 *     $value=TPropertyValue::ensureBoolean($value);
 *     // $value is now of boolean type
 * }
 * </code>
 *
 * Properties can be of the following types with specific type conversion rules:
 * - string: a boolean value will be converted to 'true' or 'false'.
 * - boolean: string 'true' (case-insensitive) will be converted to true,
 *            string 'false' (case-insensitive) will be converted to false.
 * - integer
 * - float
 * - array: string starting with '(' and ending with ')' will be considered as
 *          as an array expression and will be evaluated. Otherwise, an array
 *          with the value to be ensured is returned.
 * - object
 * - enum: enumerable type, represented by an array of strings.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System
 * @since 3.0
 */
class TPropertyValue
{
	/**
	 * Converts a value to boolean type.
	 * Note, string 'true' (case-insensitive) will be converted to true,
	 * string 'false' (case-insensitive) will be converted to false.
	 * If a string represents a non-zero number, it will be treated as true.
	 * @param mixed the value to be converted.
	 * @return boolean
	 */
	public static function ensureBoolean($value)
	{
		if (is_string($value))
			return strcasecmp($value,'true')==0 || $value!=0;
		else
			return (boolean)$value;
	}

	/**
	 * Converts a value to string type.
	 * Note, a boolean value will be converted to 'true' if it is true
	 * and 'false' if it is false.
	 * @param mixed the value to be converted.
	 * @return string
	 */
	public static function ensureString($value)
	{
		if (is_bool($value))
			return $value?'true':'false';
		else
			return (string)$value;
	}

	/**
	 * Converts a value to integer type.
	 * @param mixed the value to be converted.
	 * @return integer
	 */
	public static function ensureInteger($value)
	{
		return (integer)$value;
	}

	/**
	 * Converts a value to float type.
	 * @param mixed the value to be converted.
	 * @return float
	 */
	public static function ensureFloat($value)
	{
		return (float)$value;
	}

	/**
	 * Converts a value to array type. If the value is a string and it is
	 * in the form (a,b,c) then an array consisting of each of the elements
	 * will be returned. If the value is a string and it is not in this form
	 * then an array consisting of just the string will be returned. If the value
	 * is not a string then
	 * @param mixed the value to be converted.
	 * @return array
	 */
	public static function ensureArray($value)
	{
		if(is_string($value))
		{
			$value = trim($value);
			$len = strlen($value);
			if ($len >= 2 && $value[0] == '(' && $value[$len-1] == ')')
			{
				eval('$array=array'.$value.';');
				return $array;
			}
			else
				return $len>0?array($value):array();
		}
		else
			return (array)$value;
	}

	/**
	 * Converts a value to object type.
	 * @param mixed the value to be converted.
	 * @return object
	 */
	public static function ensureObject($value)
	{
		return (object)$value;
	}

	/**
	 * Converts a value to enum type.
	 *
	 * This method checks if the value is of the specified enumerable type.
	 * A value is a valid enumerable value if it is equal to the name of a constant
	 * in the specified enumerable type (class).
	 * For more details about enumerable, see {@link TEnumerable}.
	 *
	 * For backward compatibility, this method also supports sanity
	 * check of a string value to see if it is among the given list of strings.
	 * @param mixed the value to be converted.
	 * @param mixed class name of the enumerable type, or array of valid enumeration values. If this is not an array,
	 * the method considers its parameters are of variable length, and the second till the last parameters are enumeration values.
	 * @return string the valid enumeration value
	 * @throws TInvalidDataValueException if the original value is not in the string array.
	 */
	public static function ensureEnum($value,$enums)
	{
		static $types=array();
		if(func_num_args()===2 && is_string($enums))
		{
			if(!isset($types[$enums]))
				$types[$enums]=new ReflectionClass($enums);
			if($types[$enums]->hasConstant($value))
				return $value;
			else
				throw new TInvalidDataValueException(
					'propertyvalue_enumvalue_invalid',$value,
						implode(' | ',$types[$enums]->getConstants()));
		}
		else if(!is_array($enums))
		{
			$enums=func_get_args();
			array_shift($enums);
		}
		if(in_array($value,$enums,true))
			return $value;
		else
			throw new TInvalidDataValueException('propertyvalue_enumvalue_invalid',$value,implode(' | ',$enums));
	}
}

/**
 * TEventParameter class.
 * TEventParameter is the base class for all event parameter classes.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System
 * @since 3.0
 */
class TEventParameter extends TComponent
{
}

/**
 * TComponentReflection class.
 *
 * TComponentReflection provides functionalities to inspect the public/protected
 * properties, events and methods defined in a class.
 *
 * The following code displays the properties and events defined in {@link TDataGrid},
 * <code>
 *   $reflection=new TComponentReflection('TDataGrid');
 *   Prado::varDump($reflection->getProperties());
 *   Prado::varDump($reflection->getEvents());
 * </code>
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System
 * @since 3.0
 */
class TComponentReflection extends TComponent
{
	private $_className;
	private $_properties=array();
	private $_events=array();
	private $_methods=array();

	/**
	 * Constructor.
	 * @param object|string the component instance or the class name
	 * @throws TInvalidDataTypeException if the object is not a component
	 */
	public function __construct($component)
	{
		if(is_string($component) && class_exists($component,false))
			$this->_className=$component;
		else if(is_object($component))
			$this->_className=get_class($component);
		else
			throw new TInvalidDataTypeException('componentreflection_class_invalid');
		$this->reflect();
	}

	private function isPropertyMethod($method)
	{
		$methodName=$method->getName();
		return $method->getNumberOfRequiredParameters()===0
				&& strncasecmp($methodName,'get',3)===0
				&& isset($methodName[3]);
	}

	private function isEventMethod($method)
	{
		$methodName=$method->getName();
		return strncasecmp($methodName,'on',2)===0
				&& isset($methodName[2]);
	}

	private function reflect()
	{
		$class=new TReflectionClass($this->_className);
		$properties=array();
		$events=array();
		$methods=array();
		$isComponent=is_subclass_of($this->_className,'TComponent') || strcasecmp($this->_className,'TComponent')===0;
		foreach($class->getMethods() as $method)
		{
			if($method->isPublic() || $method->isProtected())
			{
				$methodName=$method->getName();
				if(!$method->isStatic() && $isComponent)
				{
					if($this->isPropertyMethod($method))
						$properties[substr($methodName,3)]=$method;
					else if($this->isEventMethod($method))
					{
						$methodName[0]='O';
						$events[$methodName]=$method;
					}
				}
				if(strncmp($methodName,'__',2)!==0)
					$methods[$methodName]=$method;
			}
		}
		$reserved=array();
		ksort($properties);
		foreach($properties as $name=>$method)
		{
			$this->_properties[$name]=array(
				'type'=>$this->determinePropertyType($method),
				'readonly'=>!$class->hasMethod('set'.$name),
				'protected'=>$method->isProtected(),
				'class'=>$method->getDeclaringClass()->getName(),
				'comments'=>$method->getDocComment()
			);
			$reserved['get'.strtolower($name)]=1;
			$reserved['set'.strtolower($name)]=1;
		}
		ksort($events);
		foreach($events as $name=>$method)
		{
			$this->_events[$name]=array(
				'class'=>$method->getDeclaringClass()->getName(),
				'protected'=>$method->isProtected(),
				'comments'=>$method->getDocComment()
			);
			$reserved[strtolower($name)]=1;
		}
		ksort($methods);
		foreach($methods as $name=>$method)
		{
			if(!isset($reserved[strtolower($name)]))
				$this->_methods[$name]=array(
					'class'=>$method->getDeclaringClass()->getName(),
					'protected'=>$method->isProtected(),
					'static'=>$method->isStatic(),
					'comments'=>$method->getDocComment()
				);
		}
	}

	/**
	 * Determines the property type.
	 * This method uses the doc comment to determine the property type.
	 * @param ReflectionMethod
	 * @return string the property type, '{unknown}' if type cannot be determined from comment
	 */
	protected function determinePropertyType($method)
	{
		$comment=$method->getDocComment();
		if(preg_match('/@return\\s+(.*?)\\s+/',$comment,$matches))
			return $matches[1];
		else
			return '{unknown}';
	}

	/**
	 * @return string class name of the component
	 */
	public function getClassName()
	{
		return $this->_className;
	}

	/**
	 * @return array list of component properties. Array keys are property names.
	 * Each array element is of the following structure:
	 * [type]=>property type,
	 * [readonly]=>whether the property is read-only,
	 * [protected]=>whether the method is protected or not
	 * [class]=>the class where the property is inherited from,
	 * [comments]=>comments	associated with the property.
	 */
	public function getProperties()
	{
		return $this->_properties;
	}

	/**
	 * @return array list of component events. Array keys are event names.
	 * Each array element is of the following structure:
	 * [protected]=>whether the event is protected or not
	 * [class]=>the class where the event is inherited from.
	 * [comments]=>comments associated with the event.
	 */
	public function getEvents()
	{
		return $this->_events;
	}

	/**
	 * @return array list of public/protected methods. Array keys are method names.
	 * Each array element is of the following structure:
	 * [protected]=>whether the method is protected or not
	 * [static]=>whether the method is static or not
	 * [class]=>the class where the property is inherited from,
	 * [comments]=>comments associated with the event.
	 */
	public function getMethods()
	{
		return $this->_methods;
	}
}

?>