<?php
/**
 * TDataGatewayCommand, TDataGatewayEventParameter and TDataGatewayResultEventParameter class file.
 *
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2008 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id$
 * @package System.Data.DataGateway
 */

/**
 * TDataGatewayCommand is command builder and executor class for
 * TTableGateway and TActiveRecordGateway.
 *
 * TDataGatewayCommand builds the TDbCommand for TTableGateway
 * and TActiveRecordGateway commands such as find(), update(), insert(), etc,
 * using the TDbCommandBuilder classes (database specific TDbCommandBuilder
 * classes are used).
 *
 * Once the command is built and the query parameters are binded, the
 * {@link OnCreateCommand} event is raised. Event handlers for the OnCreateCommand
 * event should not alter the Command property nor the Criteria property of the
 * TDataGatewayEventParameter.
 *
 * TDataGatewayCommand excutes the TDbCommands and returns the result obtained from the
 * database (returned value depends on the method executed). The
 * {@link OnExecuteCommand} event is raised after the command is executed and resulting
 * data is set in the TDataGatewayResultEventParameter object's Result property.
 *
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
 * @version $Id$
 * @package System.Data.DataGateway
 * @since 3.1
 */
class TDataGatewayCommand extends TComponent
{
	private $_builder;

	/**
	 * @param TDbCommandBuilder database specific database command builder.
	 */
	public function __construct($builder)
	{
		$this->_builder = $builder;
	}

	/**
	 * @return TDbTableInfo
	 */
	public function getTableInfo()
	{
		return $this->_builder->getTableInfo();
	}

	/**
	 * @return TDbConnection
	 */
	public function getDbConnection()
	{
		return $this->_builder->getDbConnection();
	}

	/**
	 * @return TDbCommandBuilder
	 */
	public function getBuilder()
	{
		return $this->_builder;
	}

	/**
	 * Executes a delete command.
	 * @param TSqlCriteria delete conditions and parameters.
	 * @return integer number of records affected.
	 */
	public function delete($criteria)
	{
		$where = $criteria->getCondition();
		$parameters = $criteria->getParameters()->toArray();
		$command = $this->getBuilder()->createDeleteCommand($where, $parameters);
		$this->onCreateCommand($command,$criteria);
		$command->prepare();
		return $command->execute();
	}

	/**
	 * Updates the table with new data.
	 * @param array date for update.
	 * @param TSqlCriteria update conditions and parameters.
	 * @return integer number of records affected.
	 */
	public function update($data, $criteria)
	{
		$where = $criteria->getCondition();
		$parameters = $criteria->getParameters()->toArray();
		$command = $this->getBuilder()->createUpdateCommand($data,$where, $parameters);
		$this->onCreateCommand($command,$criteria);
		$command->prepare();
		return $this->onExecuteCommand($command, $command->execute());
	}

	/**
	 * @param array update for update
	 * @param array primary key-value name pairs.
	 * @return integer number of records affected.
	 */
	public function updateByPk($data, $keys)
	{
		list($where, $parameters) = $this->getPrimaryKeyCondition((array)$keys);
		return $this->update($data, new TSqlCriteria($where, $parameters));
	}

	/**
	 * Find one record matching the critera.
	 * @param TSqlCriteria find conditions and parameters.
	 * @return array matching record.
	 */
	public function find($criteria)
	{
		$command = $this->getFindCommand($criteria);
		return $this->onExecuteCommand($command, $command->queryRow());
	}

	/**
	 * Find one or more matching records.
	 * @param TSqlCriteria $criteria
	 * @return TDbDataReader record reader.
	 */
	public function findAll($criteria)
	{
		$command = $this->getFindCommand($criteria);
		return $this->onExecuteCommand($command, $command->query());
	}

	/**
	 * Build the find command from the criteria. Limit, Offset and Ordering are applied if applicable.
	 * @param TSqlCriteria $criteria
	 * @return TDbCommand.
	 */
	protected function getFindCommand($criteria)
	{
		if($criteria===null)
			return $this->getBuilder()->createFindCommand();
		$where = $criteria->getCondition();
		$parameters = $criteria->getParameters()->toArray();
		$ordering = $criteria->getOrdersBy();
		$limit = $criteria->getLimit();
		$offset = $criteria->getOffset();
		$command = $this->getBuilder()->createFindCommand($where,$parameters,$ordering,$limit,$offset);
		$this->onCreateCommand($command, $criteria);
		return $command;
	}

	/**
	 * @param mixed primary key value, or composite key values as array.
	 * @return array matching record.
	 */
	public function findByPk($keys)
	{
		list($where, $parameters) = $this->getPrimaryKeyCondition((array)$keys);
		$command = $this->getBuilder()->createFindCommand($where, $parameters);
		$this->onCreateCommand($command, new TSqlCriteria($where,$parameters));
		return $this->onExecuteCommand($command, $command->queryRow());
	}

	/**
	 * @param array multiple primary key values or composite value arrays
	 * @return TDbDataReader record reader.
	 */
	public function findAllByPk($keys)
	{
		$where = $this->getCompositeKeyCondition((array)$keys);
		$command = $this->getBuilder()->createFindCommand($where);
		$this->onCreateCommand($command, new TSqlCriteria($where,$keys));
		return $this->onExecuteCommand($command,$command->query());
	}

	public function findAllByIndex($criteria,$fields,$values)
	{
		$index = $this->getIndexKeyCondition($this->getTableInfo(),$fields,$values);
		if(strlen($where = $criteria->getCondition())>0)
			$criteria->setCondition("({$index}) AND ({$where})");
		else
			$criteria->setCondition($index);
		$command = $this->getFindCommand($criteria);
		$this->onCreateCommand($command, $criteria);
		return $this->onExecuteCommand($command,$command->query());
	}

	/**
	 * @param array multiple primary key values or composite value arrays
	 * @return integer number of rows affected.
	 */
	public function deleteByPk($keys)
	{
		$where = $this->getCompositeKeyCondition((array)$keys);
		$command = $this->getBuilder()->createDeleteCommand($where);
		$this->onCreateCommand($command, new TSqlCriteria($where,$keys));
		$command->prepare();
		return $this->onExecuteCommand($command,$command->execute());
	}

	public function getIndexKeyCondition($table,$fields,$values)
	{
		if (!count($values))
			return 'FALSE';
		$columns = array();
		$tableName = $table->getTableFullName();
		foreach($fields as $field)
			$columns[] = $tableName.'.'.$table->getColumn($field)->getColumnName();
		return '('.implode(', ',$columns).') IN '.$this->quoteTuple($values);
	}

	/**
	 * Construct a "pk IN ('key1', 'key2', ...)" criteria.
	 * @param array values for IN predicate
	 * @param string SQL string for primary keys IN a list.
	 */
	protected function getCompositeKeyCondition($values)
	{
		$primary = $this->getTableInfo()->getPrimaryKeys();
		$count = count($primary);
		if($count===0)
		{
			throw new TDbException('dbtablegateway_no_primary_key_found',
				$this->getTableInfo()->getTableFullName());
		}
		if(!is_array($values) || count($values) === 0)
		{
			throw new TDbException('dbtablegateway_missing_pk_values',
				$this->getTableInfo()->getTableFullName());
		}
		if($count>1 && !is_array($values[0]))
			$values = array($values);
		if($count > 1 && count($values[0]) !== $count)
		{
			throw new TDbException('dbtablegateway_pk_value_count_mismatch',
				$this->getTableInfo()->getTableFullName());
		}
		return $this->getIndexKeyCondition($this->getTableInfo(),$primary, $values);
	}

	/**
	 * @param TDbConnection database connection.
	 * @param array values
	 * @return string quoted recursive tuple values, e.g. "('val1', 'val2')".
	 */
	protected function quoteTuple($array)
	{
		$conn = $this->getDbConnection();
		$data = array();
		foreach($array as $k=>$v)
			$data[] = is_array($v) ? $this->quoteTuple($v) : $conn->quoteString($v);
		return '('.implode(', ', $data).')';
	}

	/**
	 * Create the condition and parameters for find by primary.
	 * @param array primary key values
	 * @return array tuple($where, $parameters)
	 */
	protected function getPrimaryKeyCondition($values)
	{
		$primary = $this->getTableInfo()->getPrimaryKeys();
		if(count($primary)===0)
		{
			throw new TDbException('dbtablegateway_no_primary_key_found',
				$this->getTableInfo()->getTableFullName());
		}
		$criteria=array();
		$bindings=array();
		$i = 0;
		foreach($primary as $key)
		{
			$column = $this->getTableInfo()->getColumn($key)->getColumnName();
			$criteria[] = $column.' = :'.$key;
			$bindings[$key] = isset($values[$key])?$values[$key]:$values[$i++];
		}
		return array(implode(' AND ', $criteria), $bindings);
	}

	/**
	 * Find one matching records for arbituary SQL.
	 * @param TSqlCriteria $criteria
	 * @return TDbDataReader record reader.
	 */
	public function findBySql($criteria)
	{
		$command = $this->getSqlCommand($criteria);
		return $this->onExecuteCommand($command, $command->queryRow());
	}

	/**
	 * Find zero or more matching records for arbituary SQL.
	 * @param TSqlCriteria $criteria
	 * @return TDbDataReader record reader.
	 */
	public function findAllBySql($criteria)
	{
		$command = $this->getSqlCommand($criteria);
		return $this->onExecuteCommand($command, $command->query());
	}

	/**
	 * Build sql command from the criteria. Limit, Offset and Ordering are applied if applicable.
	 * @param TSqlCriteria $criteria
	 * @return TDbCommand command corresponding to the criteria.
	 */
	protected function getSqlCommand($criteria)
	{
		$sql = $criteria->getCondition();
		$ordering = $criteria->getOrdersBy();
		$limit = $criteria->getLimit();
		$offset = $criteria->getOffset();
		if(count($ordering) > 0)
			$sql = $this->getBuilder()->applyOrdering($sql, $ordering);
		if($limit>=0 || $offset>=0)
			$sql = $this->getBuilder()->applyLimitOffset($sql, $limit, $offset);
		$command = $this->getBuilder()->createCommand($sql);
		$this->getBuilder()->bindArrayValues($command, $criteria->getParameters()->toArray());
		$this->onCreateCommand($command, $criteria);
		return $command;
	}

	/**
	 * @param TSqlCriteria $criteria
	 * @return integer number of records.
	 */
	public function count($criteria)
	{
		if($criteria===null)
			return (int)$this->getBuilder()->createCountCommand()->queryScalar();
		$where = $criteria->getCondition();
		$parameters = $criteria->getParameters()->toArray();
		$ordering = $criteria->getOrdersBy();
		$limit = $criteria->getLimit();
		$offset = $criteria->getOffset();
		$command = $this->getBuilder()->createCountCommand($where,$parameters,$ordering,$limit,$offset);
		$this->onCreateCommand($command, $criteria);
		return $this->onExecuteCommand($command, (int)$command->queryScalar());
	}

	/**
	 * Inserts a new record into the table. Each array key must
	 * correspond to a column name in the table unless a null value is permitted.
	 * @param array new record data.
	 * @return mixed last insert id if one column contains a serial or sequence,
	 * otherwise true if command executes successfully and affected 1 or more rows.
	 */
	public function insert($data)
	{
		$command=$this->getBuilder()->createInsertCommand($data);
		$this->onCreateCommand($command, new TSqlCriteria(null,$data));
		$command->prepare();
		if($this->onExecuteCommand($command, $command->execute()) > 0)
		{
			$value = $this->getLastInsertId();
			return $value !== null ? $value : true;
		}
		return false;
	}

	/**
	 * Iterate through all the columns and returns the last insert id of the
	 * first column that has a sequence or serial.
	 * @return mixed last insert id, null if none is found.
	 */
	public function getLastInsertID()
	{
		return $this->getBuilder()->getLastInsertID();
	}

	/**
	 * @param string __call method name
	 * @param string criteria conditions
	 * @param array method arguments
	 * @return TActiveRecordCriteria criteria created from the method name and its arguments.
	 */
	public function createCriteriaFromString($method, $condition, $args)
	{
		$fields = $this->extractMatchingConditions($method, $condition);
		$args=count($args) === 1 && is_array($args[0]) ? $args[0] : $args;
		if(count($fields)>count($args))
		{
			throw new TDbException('dbtablegateway_mismatch_args_exception',
				$method,count($fields),count($args));
		}
		return new TSqlCriteria(implode(' ',$fields), $args);
	}

	/**
	 * Calculates the AND/OR condition from dynamic method substrings using
	 * table meta data, allows for any AND-OR combinations.
	 * @param string dynamic method name
	 * @param string dynamic method search criteria
	 * @return array search condition substrings
	 */
	protected function extractMatchingConditions($method, $condition)
	{
		$table = $this->getTableInfo();
		$columns = $table->getLowerCaseColumnNames();
		$regexp = '/('.implode('|', array_keys($columns)).')(and|_and_|or|_or_)?/i';
		$matches = array();
		if(!preg_match_all($regexp, strtolower($condition), $matches,PREG_SET_ORDER))
		{
			throw new TDbException('dbtablegateway_mismatch_column_name',
				$method, implode(', ', $columns), $table->getTableFullName());
		}

		$fields = array();
		foreach($matches as $match)
		{
			$key = $columns[$match[1]];
			$column = $table->getColumn($key)->getColumnName();
			$sql = $column . ' = ? ';
			if(count($match) > 2)
				$sql .= strtoupper(str_replace('_', '', $match[2]));
			$fields[] = $sql;
		}
		return $fields;
	}

	/**
	 * Raised when a command is prepared and parameter binding is completed.
	 * The parameter object is TDataGatewayEventParameter of which the
	 * {@link TDataGatewayEventParameter::getCommand Command} property can be
	 * inspected to obtain the sql query to be executed.
	 * @param TDataGatewayCommand originator $sender
	 * @param TDataGatewayEventParameter
	 */
	public function onCreateCommand($command, $criteria)
	{
		$this->raiseEvent('OnCreateCommand', $this, new TDataGatewayEventParameter($command,$criteria));
	}

	/**
	 * Raised when a command is executed and the result from the database was returned.
	 * The parameter object is TDataGatewayResultEventParameter of which the
	 * {@link TDataGatewayEventParameter::getResult Result} property contains
	 * the data return from the database. The data returned can be changed
	 * by setting the {@link TDataGatewayEventParameter::setResult Result} property.
	 * @param TDataGatewayCommand originator $sender
	 * @param TDataGatewayResultEventParameter
	 */
	public function onExecuteCommand($command, $result)
	{
		$parameter = new TDataGatewayResultEventParameter($command, $result);
		$this->raiseEvent('OnExecuteCommand', $this, $parameter);
		return $parameter->getResult();
	}
}

/**
 * TDataGatewayEventParameter class contains the TDbCommand to be executed as
 * well as the criteria object.
 *
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
 * @version $Id$
 * @package System.Data.DataGateway
 * @since 3.1
 */
class TDataGatewayEventParameter extends TEventParameter
{
	private $_command;
	private $_criteria;

	public function __construct($command,$criteria)
	{
		$this->_command=$command;
		$this->_criteria=$criteria;
	}

	/**
	 * The database command to be executed. Do not rebind the parameters or change
	 * the sql query string.
	 * @return TDbCommand command to be executed.
	 */
	public function getCommand()
	{
		return $this->_command;
	}

	/**
	 * @return TSqlCriteria criteria used to bind the sql query parameters.
	 */
	public function getCriteria()
	{
		return $this->_criteria;
	}
}

/**
 * TDataGatewayResultEventParameter contains the TDbCommand executed and the resulting
 * data returned from the database. The data can be changed by changing the
 * {@link setResult Result} property.
 *
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
 * @version $Id$
 * @package System.Data.DataGateway
 * @since 3.1
 */
class TDataGatewayResultEventParameter extends TEventParameter
{
	private $_command;
	private $_result;

	public function __construct($command,$result)
	{
		$this->_command=$command;
		$this->_result=$result;
	}

	/**
	 * @return TDbCommand database command executed.
	 */
	public function getCommand()
	{
		return $this->_command;
	}

	/**
	 * @return mixed result returned from executing the command.
	 */
	public function getResult()
	{
		return $this->_result;
	}

	/**
	 * @param mixed change the result returned by the gateway.
	 */
	public function setResult($value)
	{
		$this->_result=$value;
	}
}

?>
