<?php

/**
 *--------------------------------------
 * model base
 *--------------------------------------
 * @project		: pfa
 * @author		: cblee
 * @created		: 2012-9-23
 * @copyright	: (c)2012 AsThis
 *--------------------------------------
 */
defined('PFA_PATH') or exit('Access Denied');

class Modl extends Pfa {
	const STATE_INSERT = 1;
	const STATE_UPDATE = 2;
	const STATE_BOTH = 3;

	protected $connection; // connect option
	protected $name = ''; // model name
	public $db = null; // current database operation object
	protected $dbName = ''; // database name
	protected $tablePrefix = ''; // database table prefix
	protected $tableSuffix = ''; // database table suffix
	protected $tableName = ''; // table name(not contain prefix and suffix)
	protected $trueTableName = ''; // true table name(contain prefix and suffix)
	protected $pk = 'id'; // default primary key
	protected $fields = array(); // fields information
	protected $data = array(); // data object
	protected $options = array(); // options
	protected $error = ''; // latest error
	protected $autoCheckFields = true; // whether auto check fields information
	protected $_validate = array(); // auto validate data. array(field, rule, message, type)
	protected $_auto = array(); // auto fill data. array('field', 'content', 'rule', 'condition', [extra])

	public function __construct($name = '', $connection = '') {
		/* get model name */
		if(!empty($name)) {
			$this->name = $name;
		}
		elseif(empty($this->name)) {
			$this->name = $this->get_modlName();
		}
		/* set table prefix and suffix */
		$this->tablePrefix = $this->tablePrefix ? $this->tablePrefix : C('DB.PREFIX');
		/* get database operation object */
		$this->db(0, empty($this->connection) ? $connection : $this->connection);

		if(!empty($this->name) && $this->autoCheckFields) {
			$this->check_tableInfo(); // check table fields
		}
	}

	/* get fields information and save cache */
	public function flush() {
		$fields = $this->db->get_fields($this->get_tableName());
		if(!$fields) {
			return false; // can't get fields information
		}
		$this->fields = array_keys($fields);
		$this->fields['_autoinc'] = false;
		foreach($fields as $key => $val) {
			$type[$key] = $val['type']; // record field type
			if($val['primary']) {
				$this->fields['_pk'] = $key;
				if($val['autoinc']) {
					$this->fields['_autoinc'] = true;
				}
			}
		}
		if(C('DB.FIELDTYPE_CHECK')) {
			$this->fields['_type'] = $type;
 			/* record fields type information */
		}
		if(C('DB.FIELDS_CACHE')) {
			F('_fields/_'.$this->name, $this->fields);
 			/* cache table fields information */
		}
	}

	/* get current data object name */
	public function get_modlName() {
		if(empty($this->name)) {
			$this->name = substr(get_class($this), 0, -4);
		}
		return $this->name;
	}

	/* get full table name */
	public function get_tableName() {
		if(empty($this->trueTableName)) {
			$tableName = !empty($this->tablePrefix) ? $this->tablePrefix : '';
			if(!empty($this->tableName)) {
				$tableName .= $this->tableName;
			}
			else {
				$tableName .= parse_name($this->name);
			}
			$tableName .= !empty($this->tableSuffix) ? $this->tableSuffix : '';
			$this->trueTableName = strtolower($tableName);
		}
		return (!empty($this->dbName) ? $this->dbName.'.' : '').$this->trueTableName;
	}

	/* get table field information */
	public function get_dbFields() {
		return $this->fields;
	}

	/* get primary key name */
	public function get_pk() {
		return isset($this->fields['_pk']) ? $this->fields['_pk'] : $this->pk;
	}

	/* get model error information */
	public function get_error() {
		return $this->error;
	}

	/* create and verify data object[$data], but do not save to database */
	public function create($data = '') {
		if(empty($data)) {
			$data = $_POST; // default get data from $_POST
		}
		elseif(is_object($data)) {
			$data = get_object_vars($data);
		}

		if(empty($data) || !is_array($data)) {
			$this->error = L('_DATA_TYPE_INVALID_');
			return false;
		}

		/* auto deal data */
		$type = !empty($data[$this->get_pk()]) ? self::STATE_UPDATE : self::STATE_INSERT;
		$this->_auto_deal($data, $type);

		/* auto verify data */
		if(!$this->_verify_data($data)) {
			return false;
		}

		/* verify data according fields */
		if($this->autoCheckFields) {
 			/* filter illegal field data */
			$vo = array();
			foreach($this->fields as $key => $name) {
				if(substr($key, 0, 1) == '_') {
					continue;
				}
				$val = isset($data[$name]) ? $data[$name] : null;
				/* remove invalid data */
				if(!is_null($val)) {
					$vo[$name] = $val;
				}
			}
			$data = $vo;
		}

		return $this->data = $data;
	}

	/* set data value */
	public function data($data) {
		if(is_object($data)) {
			$data = get_object_vars($data);
		}
		elseif(is_string($data)) {
			parse_str($data, $data);
		}
		elseif(!is_array($data)) {
			halt(L('_DATA_TYPE_INVALID_'));
		}
		$this->data = $data;
		return $this;
	}

	/* query one row */
	public function find($options = array()) {
		if(is_numeric($options) || is_string($options)) {
			$where[$this->get_pk()] = $options;
			$options = array();
			$options['where'] = $where;
		}
		$options['limit'] = 1; // always find one row
		$options = $this->parse_options($options); // parse option expression
		$resultSet = $this->db->select($options);
		if(false === $resultSet) {
			return false;
		}
		if(empty($resultSet)) {
			return '';
		}
		$this->data = $resultSet[0];
		return $this->data;
	}

	/* increase field value */
	public function field_inc($field, $condition = '', $step = 1) {
		return $this->expfield($field)->set_field($field, $field.'+'.$step, $condition);
	}

	/* decrease field value */
	public function field_dec($field, $condition = '', $step = 1) {
		return $this->expfield($field)->set_field($field, $field.'-'.$step, $condition);
	}

	/* set field, support use database field and method */
	public function set_field($field, $value, $condition = '') {
		if(empty($condition) && isset($this->options['where'])) {
			$condition = $this->options['where'];
		}
		$options['where'] = $condition;
		if(is_array($field)) {
			foreach($field as $key => $val) {
				$data[$val] = $value[$key];
			}
		}
		else {
			$data[$field] = $value;
		}
		return $this->update($data, $options);
	}

	/* get field value. $spea: field data delimiter */
	public function get_field($field, $condition = '', $sepa = null) {
		if(empty($condition) && isset($this->options['where'])) {
			$condition = $this->options['where'];
		}
		$options['where'] = $condition;
		$options['field'] = $field;
		$options = $this->parse_options($options);
		if(strpos($field, ',')) {
 			/* multi field */
			$resultSet = $this->db->select($options);
			if(!empty($resultSet)) {
				$_field = explode(',', $field);
				$field = array_keys($resultSet[0]);
				if($_field[0] == $_field[1]) {
					$field = array_merge(array($field[0]), $field);
				}
				$key = array_shift($field);
				$cols = array();
				foreach($resultSet as $result) {
					$name = $result[$key];
					if(1 == count($field)) {
						$cols[$name] = $result[$field[0]];
					}
					else {
						$cols[$name] = array();
						foreach($field as $val) {
							$cols[$name][$val] = $result[$val];
						}
						if(!is_null($sepa)) {
							$cols[$name] = implode($sepa, $cols[$name]);
						}
					}
				}
				return $cols;
			}
		}
		else {
			$options['limit'] = 1;
			$result = $this->db->select($options);
			if(!empty($result)) {
				return reset($result[0]);
			}
		}
		return '';
	}

	/* get latest insert ID */
	public function get_lastInsID() {
		return $this->db->lastInsID;
	}

	/* get latest SQL */
	public function get_lastSql() {
		return $this->db->get_lastSql();
	}

	/* build SQL */
	public function build_sql() {
		return '( '.$this->select(false).' )';
	}

	/* insert data */
	public function insert($data = '', $options = array(), $replace = false) {
		if(empty($data)) {
			if(!empty($this->data)) {
				$data = $this->data;
				$this->data = array(); // reset data
			}
			else {
				$this->error = L('_DATA_TYPE_INVALID_');
				return false;
			}
		}
		$data = $this->_deal_mq($data); // deal with magic quote
		$data = $this->facade($data); // deal with data
		$options = $this->parse_options($options); // parse database operation parameters
		$result = $this->db->insert($data, $options, $replace); // write data to database
		if(false !== $result) {
			$insertId = $this->get_lastInsID();
			if($insertId) {
				$data[$this->get_pk()] = $insertId; // return insert ID to increment primary key
				return $insertId;
			}
		}
		return $result;
	}

	/* replace data */
	public function replace($data = '', $options = array()) {
		return $this->insert($data, $options, true);
	}

	/* update data */
	public function update($data = '', $options = array()) {
		if(empty($data)) {
			if(!empty($this->data)) {
				$data = $this->data;
				$this->data = array(); // reset data
			}
			else {
				$this->error = L('_DATA_TYPE_INVALID_');
				return false;
			}
		}
		$data = $this->_deal_mq($data); // deal with magic quote
		$data = $this->facade($data); // deal with data
		$options = $this->parse_options($options); // parse database operation parameters
		if(!isset($options['where'])) {
			/* if the primary key data exists, set it as automatically updated conditions */
			if(isset($data[$this->get_pk()])) {
				$pk = $this->get_pk();
				$where[$pk] = $data[$pk];
				$options['where'] = $where;
				$pkValue = $data[$pk];
				unset($data[$pk]);
			}
			else {
				$this->error = L('_OPERATION_WRONG_');
 				/* do not execute if no update condition. */
				return false;
			}
		}
		$result = $this->db->update($data, $options);
		if(false !== $result) {
			if(isset($pkValue)) {
				$data[$pk] = $pkValue;
			}
		}
		return $result;
	}

	/* delete data */
	public function delete($options = array()) {
		if(empty($options) && empty($this->options)) {
			/* if condition is empty, delete current data object row */
			if(!empty($this->data) && isset($this->data[$this->get_pk()])) {
				return $this->delete($this->data[$this->get_pk()]);
			}
			return false;
		}
		if(is_numeric($options) || is_string($options)) {
			/* delete according primary key */
			$pk = $this->get_pk();
			if(strpos($options, ',')) {
				$where[$pk] = array('IN', $options);
			}
			else {
				$where[$pk] = $options;
				$pkValue = $options;
			}
			$options = array();
			$options['where'] = $where;
		}
		$options = $this->parse_options($options); // parse database operation parameters
		$result = $this->db->delete($options);
		if(false !== $result) {
			$data = array();
			if(isset($pkValue)) {
				$data[$pk] = $pkValue;
			}
		}
		return $result; // return number of rows that have been deleted
	}

	/* select data */
	public function select($options = array()) {
		if(is_string($options) || is_numeric($options)) {
			/* select according primary key */
			$pk = $this->get_pk();
			if(strpos($options, ',')) {
				$where[$pk] = array('IN', $options);
			}
			else {
				$where[$pk] = $options;
			}
			$options = array();
			$options['where'] = $where;
		}
		elseif(false === $options){ // for sub select, return SQL
			$options['fetch_sql'] = true;
		}

		$options = $this->parse_options($options); // parse database operation parameters
		$resultSet = $this->db->select($options);
		if(false === $resultSet) {
			return false;
		}
		if(empty($resultSet)) {
			return ''; // query result empty
		}
		return $resultSet;
	}

	/* SQL query */
	public function query($sql, $parse = false) {
		if(!empty($sql)) {
			if($parse) {
				$sql = preg_replace_callback("/__([0-9A-Z_-]+)__/sU", array('Modl', '_get_tableName'), $sql);
			}
			return $this->db->query($sql);
		}
		return false;
	}

	/* execute SQL */
	public function execute($sql, $parse = false) {
		if(!empty($sql)) {
			if($parse) {
				$sql = preg_replace_callback("/__([0-9A-Z_-]+)__/sU", array('Modl', '_get_tableName'), $sql);
			}
			return $this->db->execute($sql);
		}
		return false;
	}

	/* start transaction */
	public function start_trans() {
		$this->commit();
		$this->db->startTrans();
		return;
	}

	/* commit transaction */
	public function commit() {
		return $this->db->commit();
	}

	/* transaction rollback */
	public function rollback() {
		return $this->db->rollback();
	}

	/* $data[$val[0]]:$value, $val[1]:$rule, $val[3]:$type, $val[4]:$args */
	public function check($data, $val) {
		switch($val[3]) {
			case 'function':
			case 'callback':
				$args = isset($val[4]) ? (array)$val[4] : array();
				array_unshift($args, $data[$val[0]]);
				return ('function' == $val[3]
					? call_user_func_array($val[1], $args)
					: call_user_func_array(array(&$this, $val[1]), $args));
			case 'in':
 				/* verify whether value is in a range. use array or string delimited by ','. */
				$range = is_array($val[1]) ? $val[1] : explode(',', $val[1]);
				return in_array($data[$val[0]], $range);
			case 'between':
 				/* verify whether value is between a range */
				list($min, $max) = explode(',', $val[1]);
				return $data[$val[0]] >= $min && $data[$val[0]] <= $max;
			case 'equal':
 				/* verify whether equal some value */
				return $data[$val[0]] == $val[1];
			case 'confirm':
				return $data[$val[0]] == $data[$val[1]];
			case 'unique':
				$where = array();
				if(is_string($val[0]) && strpos($val[0], ',')) {
					$val[0] = explode(',', $val[0]);
				}
				if(is_array($val[0])) {
					foreach($val[0] as $field) {
						$where[$field] =  $data[$field];
					}
				}
				else{
					$where[$val[0]] = $data[$val[0]];
				}
				if(!empty($data[$this->get_pk()])) { // for update check
					$where[$this->get_pk()] = array('NEQ', $data[$this->get_pk()]);
				}
				if($this->field($this->get_pk())->where($where)->find()) {
					return false;
				}
				return true;
			case 'regex':
			default:
 				/* use regular validation data */
				return $this->regex($data[$val[0]], $val[1]);
		}
	}

	/* use regular validation data */
	public function regex($value, $rule) {
		$validate = array(
			'require' => '/\S+/',
			'email' => '/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/',
			'url' => '/^http:\/\/[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$/',
			'currency' => '/^\d+(\.\d+)?$/',
			'number' => '/^\d+$/',
			'zip' => '/^[1-9]\d{5}$/',
			'integer' => '/^[-\+]?\d+$/',
			'double' => '/^[-\+]?\d+(\.\d+)?$/',
			'english' => '/^[A-Za-z]+$/',
			);
		/* check whether there is a built-in regular expression */
		if(isset($validate[strtolower($rule)])) {
			$rule = $validate[strtolower($rule)];
		}
		return preg_match($rule, $value) === 1;
	}

	/* set data object value */
	public function __set($name, $value) {
		$this->data[$name] = $value;
	}

	/* get data object value */
	public function __get($name) {
		return isset($this->data[$name]) ? $this->data[$name] : '';
	}

	/* check data object value */
	public function __isset($name) {
		return isset($this->data[$name]);
	}

	/* unset data object value */
	public function __unset($name) {
		unset($this->data[$name]);
	}

	/* use __call to achieve some special model method */
	public function __call($method, $args) {
		if(in_array(strtolower($method), array('table', 'field', 'where', 'order', 'limit','page', 'alias', 'having', 'group', 'lock', 'distinct'), true)) {
			/* achieve coherent operation */
			$this->options[strtolower($method)] = $args[0];
			return $this;
		}
		elseif('expfield' == strtolower($method)) { /* set expression field */
			$expfield = $args[0];
			if(is_string($expfield)) {
				$expfield = explode(',', $expfield);
			}
			$this->options['expfield'] = $expfield;
			return $this;
		}
		elseif(in_array(strtolower($method), array('count', 'sum', 'min', 'max', 'avg'), true)) {
			/* achieve statistics query */
			$field = isset($args[0]) ? $args[0] : '*';
			return $this->get_field(strtoupper($method).'('.$field.') AS pfa_'.$method);
		}
		elseif(strtolower(substr($method, 0, 6)) == 'getby_') {
			/* query according some field */
			$field = parse_name(substr($method, 6));
			$where[$field] = $args[0];
			return $this->where($where)->find();
		}
		else {
			echo ('['.__CLASS__.']'.L('_METHOD_INEXISTENCE_', null, array('method' => $method)));
			return;
		}
	}

	/* set query SQL join */
	public function join($join) {
		if(is_array($join)) {
			$this->options['join'] = $join;
		}
		else {
			$this->options['join'][] = $join;
		}
		return $this;
	}

	/* set property */
	public function set_property($name, $value) {
		if(property_exists($this, $name)) {
			$this->$name = $value;
		}
		return $this;
	}

	/* parse database operation parameters */
	protected function parse_options($options = array()) {
		if(is_array($options)) {
			$options = array_merge($this->options, $options);
		}
		$this->options = array();
 		/* clear sql expression option after query */
		if(!isset($options['table'])) {
			$options['table'] = $this->get_tableName(); // auto get table name
		}
		if(!empty($options['alias'])) {
			$options['table'] .= ' '.$options['alias'];
		}
		/* verify field type */
		if(C('DB.FIELDTYPE_CHECK')) {
			if(isset($options['where']) && is_array($options['where'])) {
				/* check field type of array query condition */
				foreach($options['where'] as $key => $val) {
					if(in_array($key, $this->fields, true) && is_scalar($val)) {
						$this->parse_type($options['where'], $key);
					}
				}
			}
		}
		return $options;
	}

	/* toggle current database connect */
	protected function db($linkNum, $config = '') {
		static $_db = array();
		if(!isset($_db[$linkNum])) {
			if(!empty($config) && false === strpos($config, '/')) {
				$config = C($config);
			}
			$_db[$linkNum] = get_instance('Db', '', 'get_db', $config);
		}
		elseif(null === $config) {
			$_db[$linkNum]->close(); // close database connect
			unset($_db[$linkNum]);
			return;
		}
		$this->db = $_db[$linkNum];
 		/* toggle current database connect */
		return $this;
	}

	/* check table information */
	protected function check_tableInfo() {
		if(empty($this->fields)) {
			if(C('DB.FIELDS_CACHE')) {
				$this->fields = F('_fields/_'.$this->name);
				if(!$this->fields) {
					$this->flush();
				}
			}
			else {
				$this->flush();
			}
		}
	}

	/* deal with data saved to database */
	protected function facade($data) {
		if(!empty($this->fields)) {
			foreach($data as $key => $val) {
				if(!in_array($key, $this->fields, true)) {
 					/* remove non-data field */
					unset($data[$key]);
				}
				elseif(C('DB.FIELDTYPE_CHECK') && is_scalar($val) && !in_array($key, $this->options['expfield'])) {
					$this->parse_type($data, $key); // data type verify and conversion
				}
			}
		}
		return $data;
	}

	/* data type verify and conversion */
	protected function parse_type(&$data, $key) {
		$fieldType = strtolower($this->fields['_type'][$key]);
		if(false === strpos($fieldType, 'bigint') && false !== strpos($fieldType, 'int')) {
			$data[$key] = intval($data[$key]);
		}
		elseif(false !== strpos($fieldType, 'float') || false !== strpos($fieldType, 'double')) {
			$data[$key] = floatval($data[$key]);
		}
		elseif(false !== strpos($fieldType, 'bool')) {
			$data[$key] = (bool)$data[$key];
		}
	}

	/* auto deal data */
	private function _auto_deal(&$data, $type) {
		if(!empty($this->_auto)) {
			foreach ($this->_auto as $auto) {
				// array('field', 'content', 'rule', 'condition', [extra])
				if(empty($auto[3])) {
					$auto[3] = self::STATE_INSERT; // default is for insert
				}

				if($auto[3] == $type or $auto[3] == self::STATE_BOTH) {
					switch($auto[2]) {
						case 'function':
						case 'callback':
							$args = isset($auto[4]) ? (array)$auto[4] : array();
							if(isset($data[$auto[0]])) {
								array_unshift($args, $data[$auto[0]]);
							}
							$data[$auto[0]] = ('function' == $auto[2]
								? call_user_func_array($auto[1], $args)
								: call_user_func_array(array(&$this,$auto[1]), $args));
							break;
						case 'field': // use other field
							$data[$auto[0]] = $data[$auto[1]];
							break;
						case 'string':
						default: // default use string
							$data[$auto[0]] = $auto[1];
					}
					if(false === $data[$auto[0]] ) {
						unset($data[$auto[0]]);
					}
				}
			}
		}
		return $data;
	}

	/* verify data */
	private function _verify_data($data) {
		$this->error = array();
		if(!empty($this->_validate)) {
			foreach($this->_validate as $val) {
				/* array(field, rule, message, type, function args) */
				$val[2] = isset($val[2]) ? $val[2] : L('_DATA_TYPE_INVALID_');
				$val[3] = isset($val[3]) ? strtolower($val[3]) : 'regex';
				$val[4] = isset($val[4]) ? $val[4] : array();
				if(isset($data[$val[0]])) {
					if(false === $this->check($data, $val)) {
						$this->error = $val[2];
						return false;
					}
				}
			}
		}
		return true;
	}

	/* deal with MAGIC_QUOTES_GPC */
	private function _deal_mq($data) {
		if(is_array($data)) {
			foreach($data as $k => $v) {
				$data[$k] = $this->_deal_mq($v);
			}
		}
		elseif(is_string($data)) {
			$data = (MAGIC_QUOTES_GPC ? stripslashes($data) : $data);
		}
		return $data;
	}

	private static function _get_tableName($matches) {
		return C("DB.PREFIX").strtolower($matches[1]).C("DB.SUFFIX");
	}
}

?>