<?php
namespace KIF\Dao;

use KIF\Exception\DaoException;

use KIF\Verify;

use KIF\Exception\ParamsException;
use Exception;
use KIF\Db\MySQLi;
use KIF\Dao\SqlHelper;
use KIF\Core\Config;
use KIF\Cache\Memcached;
use KIF\Math\Math;

/**
 * 数据库操作的抽象类
 * @author gaoxiaogang@yoka.com
 *
 */
abstract class AbstractDao {
    /**
     * 数据表名
     * 该抽象类针对表的操作(如:delete、create、findBy)都是基于该属性。由继承的子类填充该值。
     * 好处:1、如果继承的子类只针对一个表,把tableName设为静态表名即可。
     * 2、如果继承的子类针对一类表操作(如根据某个唯一标志把相同结构的数据hash到多张表中),则针对不同表操作时,动态设置该值
     * @var string
     */
    protected $tableName;

    /**
     * 表的主键名。默认为id
     * @var string
     */
    protected $primaryKey = 'id';
    
    /**
     * 
     * 是否使用缓存
     * @var boolean
     */
    private $useCache = false;
    
    /**
     * 
     * 缓存失效时间 ,默认1小时
     * @var int
     */
    private $cache_expiration = 3600;
    
    /**
     * 
     * 存放这个数据表的缓存标志。
     * 目的:清空这个标志,能清空这张表的所有行记录缓存
     * @var string
     */
    private $tableCacheFlag;
    
    /**
     * 
     * 用于缓存表记录的memcache缓存集群标志
     * 优先使用config里的 memcached->dao_cache;如不存在,则使用 memcached->default;如仍不存在,则不启用缓存.
     * @var string
     */
    private $memcacheClusterFlag;

    /**
     *
     * 插入行为
     * @var string
     */
    const PARAM_CREATE_ACTION_INSERT = 'INSERT INTO';

    /**
     *
     * 插入,但重复时忽略
     * @var string
     */
    const PARAM_CREATE_ACTION_INSERT_IGNORE = 'INSERT IGNORE';

    /**
     *
     * 插入,但重复时完全replace
     * @var string
     */
    const PARAM_CREATE_ACTION_REPLACE = 'REPLACE INTO';

    /**
     *
     * 插入,但重复时自动转为update
     * @var string
     */
    const PARAM_CREATE_ACTION_ONDUPLICATE = 'ON DUPLICATE KEY UPDATE';

    /**
     * 数据库实例,可被动态切换到主库或从库
     *
     * @var KIF\Db\MySQLi
     */
    protected $db;
    
    /**
     * 
     * mysql集群标志
     * @var string
     */
    private $cluster_flag;

    /**
     * 
     * @param string $cluster_flag mysql集群标志
     * 
     */
    public function __construct($cluster_flag = 'default') {
    	$appConfig = Config::getInstance()->current();
    	$dbConfig = $appConfig['db'];
    	
    	if (!$dbConfig || !isset($dbConfig[$cluster_flag]) || !is_string($dbConfig[$cluster_flag])) {
    		throw new ParamsException("load config error:{$dbConfig}");
    	}
    	$this->cluster_flag = $cluster_flag;
    	
    	if (isset($appConfig['memcached']['dao_cache'])) {
    		$this->memcacheClusterFlag = 'dao_cache';
    	} elseif (isset($appConfig['memcached']['default'])) {
    		$this->memcacheClusterFlag = 'default';
    	} else {# 没有缓存配置,不启用缓存
    		$this->useCache(false);
    	}

    	$dsn = $dbConfig[$cluster_flag];
    	$this->db = new MySQLi($dsn);
    }

    /**
     *
     * 获取设置的主键名
     * @return string
     */
    public function getPrimaryKey() {
    	return $this->primaryKey;
    }

    /**
     * 设置表名
     * 动态设置表名是为了拆表
     * @param $tableName
     */
    public function setTableName($tableName) {
        $this->tableName = $tableName;
    }
    
    /**
     * 
     * 控制是否使用缓存。使用缓存,会将数据表的行记录缓存起来,极大的提高效率和高可用;同时也会在记录删除、更新时,清空对应的缓存。
     * @param boolean $useCache
     */
    public function useCache($useCache = true) {
    	$this->useCache = (bool) $useCache;
    }

    /**
     * 解析条件
     * @param array|string $condition
     * @throws Exception parameter errors
     * @return string
     */
    public function parseCondition($condition = null) {
        if(empty($condition)) return '';
        
        // 如果条件语句是字符串 直接拼接
        if(is_string($condition)){
        	return "
            WHERE ". $condition ." ";
        }
        // 如果不是字符串 强制传递数组 如果不是数组抛异常
        if(!is_array($condition)){
        	throw new Exception('parameter errors');
        }
		$condition = $this->quote($condition);
        $where = array();
        foreach($condition as $tmpK => $tmpV) {
        	# 避免条件字段重复,支持二维数组。比如$conditon = array(
        	# 	array(
        	#		'publish_time'	=> SqlHelper::addCompareOperator(">=", $s_ts),
        	#	),
        	#	array(
        	#		'publish_time'	=> SqlHelper::addCompareOperator("<", $e_ts),
        	#	),
        	# );
        	# 以前只能这样变相支持:$condition = array(
        	# 	'publish_time'	=> SqlHelper::addCompareOperator(">=", $s_ts),
        	#	'`publish_time`'	=> SqlHelper::addCompareOperator("<", $e_ts),
        	# );
        	if (Verify::naturalNumber($tmpK)) {
        		list($key, $val) = each($tmpV);
        	} else {
        		$key = $tmpK;
        		$val = $tmpV;
        	}
        	
        	if (strpos($key, '`') === false // 没用使用 ` 字符,比如 `status`
        		&& strpos($key, '(') === false) // 也不含有括号(即不是函数),比如 last_insert_id(status)
        	{
        		$key = "`{$key}`";
        	}
            if(is_scalar($val)) {
                $where[] = "{$key} " . SqlHelper::explodeCompareOperator($val);
            } elseif(is_array($val)) {
                $where[] = "{$key} IN (".join(',', $val).")";
            } else {
                throw new Exception('parameter errors');
            }
        }
        return "
            WHERE ".join(' && ', $where)."
        ";
    }

    /**
     * 根据条件查找主键id列表集
     * 做这个方法,是出于架构上的考虑。只取出主键id集,然后通过主键id集才去取数据(内存或数据库)
     * @param array|String $condition 条件
     * @param string | int $limit 指定分页
     * @param string $order 指定排序
     * @return array 如:array(123, 124)。无值时返回空数组
     */
    public function findIdsBy($condition = null, $limit = null, $order = null) {
        $result = $this->findBy($condition, $this->primaryKey, $limit, $this->primaryKey, $order);
        if (! $result) {
            return array();
        }

        return array_keys($result);
    }

    /**
     * 查找的底层方法。对查找只提供有限支持,太复杂的查询请手动sql
     * final 让该方法禁止被继承
     * 1、根据条件查找,多条件只支持 与(&&),不支持或之类的
     * 2、支持limit
     * 3、group by
     * @param array|string $condition
     * @param string $returnAssociateKey 如果指定该指,返回的值不再以0,1,2为key,而是以其对应的值为key
     * @param string | int $limit
     * @param string $selectCols 要获取的列。语法:id, uid ,默认为 *
     * @param string $order 指定排序
     * @return array
     */
    final public function findBy($condition = null, $returnAssociateKey = null, $limit = null, $selectCols = '*', $order = null) {
        $where = $this->parseCondition($condition);
        if (!isset($limit) || !preg_match('#^(?:\d+\s*,\s*)?\d+$#', $limit)) {
            $strLimit = ' ';
        } else {
            $strLimit = " LIMIT $limit ";
        }

        $strOrder = '';
        if(!empty($order)) {
            $strOrder = " ORDER BY {$order} ";
        }

        if(!isset($selectCols)) {
            $selectCols = '*';
        }

        $sql = "SELECT {$selectCols} FROM {$this->tableName}
            {$where}
            {$strOrder}
            {$strLimit}
            ;
        ";
        return $this->db->fetchAll($sql, $returnAssociateKey);
    }

    public function fetchOne($condition = null, $selectCols = '*', $order = null) {
        $result = self::findBy($condition, null, 1, $selectCols, $order);
        if (!$result) {
        	return false;
        }
        return array_pop($result);
    }

    /**
     * 获取所有
     * @param string $order 排序方式。默认以主键倒序;有效的格式:"create_time desc"
     * @param string $limit 该参数用于支持分页,默认为不使用分页。格式 "offset, length"
     * @return array
     */
    public function getsAll($order = null, $limit = null) {
        $ids = self::getsAllIds($order, $limit);
        return $this->gets($ids);
    }

    /**
     * 获取所有id列表集
     * @param string $order 排序方式。默认以主键倒序;有效的格式:"create_time desc"
     * @param string $limit 该参数用于支持分页,默认为不使用分页。格式 "offset, length"
     * @return array
     */
    public function getsAllIds($order = null, $limit = null) {
    	if (is_null($order)) {
            $order = "{$this->primaryKey} desc";
        }
        if (!is_string($order)) {
            throw new Exception('$order 必须是字符串或null');
        }

        $condition = null;
        $ids = self::findIdsBy($condition, $limit, $order);
        return $ids;
    }

    /**
     * 获取指定$id
     * @param int | string $id
     * @return false | array
     */
    public function get($id) {
        if (Verify::int($id)) {
            if ($id < 1) {
                return false;
            }
        } elseif (is_string($id)) {
            if (strlen($id) == 0) {
                return false;
            }
        } else {
            return false;
        }

        $result = self::gets(array($id));// 调用该类自身的gets方法,而不是继承者的gets方法!
        if (!$result) {
            return false;
        }
        return array_pop($result);
    }

    /**
     * 批量获取信息
     * @param array $ids id组成的数组
     * @return array 无结果时返回空数组
     */
    public function gets(array $ids) {
    	$return = array();
        if (empty($ids)) {
            return $return;
        }
        $ids = array_unique($ids);
        
    	# 从缓存取 
        $cache_return = self::getsFromCache($ids);
        $non_cache_ids = array_diff($ids, array_keys($cache_return));
        if (!$non_cache_ids) {
        	return $cache_return;
        }
        
        # 从DB取
        $db_return = self::getsFromDB($non_cache_ids);
        if ($db_return) {
        	self::setToCache($db_return);
        }
        foreach ($ids as $id) {
        	if (isset($cache_return[$id])) {
        		$return[$id] = $cache_return[$id];
        		continue;
        	}
        	
        	if (isset($db_return[$id])) {
        		$return[$id] = $db_return[$id];
        		continue;
        	}
        }
        
        return $return;
    }
    
    /**
     * 
     * 从后端数据库取结果
     * @param array $ids
     * @return array 格式:array(
     * 		(int) id => (mixed) value,
     * 		... ,
     * )
     * 
     */
    private function getsFromDB(array $ids) {
    	$return = array();
    	$condition = array(
            $this->primaryKey => $ids,
        );
        $result = $this->findBy($condition, $this->primaryKey);
        if (!$result) {
            return $return;
        }

        foreach ($ids as $id) {
        	$id = (string) $id;// php的数组下标是int整时,如果超出了操作系统平台的最大有符号正整数后,会取不到值。转成字符串型,解决这个bug
            if (array_key_exists($id, $result)) {
                $return[$id] = $result[$id];
            }
        }
        return $return;
    }
    
    /**
     * 
     * 获取表缓存的标志。后续这个表里每行的缓存,都会关联上这个表缓存标志。
     * 所以只要清空这个标志,即可使整个表记录的缓存失效。
     * @Exception DaoException::CACHE_SET_TABLE_FLAG_ERROR
     * @Exception DaoException::CACHE_SERVICE_UNAVAILABLE
     * @return String
     */
    public function getTableCacheFlag() {
    	if ($this->tableCacheFlag) {
    		return $this->tableCacheFlag;
    	}
    	
    	$dbconfig = Config::getInstance()->get('db');
    	$cacheKey_elements = array(
    		serialize($dbconfig[$this->cluster_flag]),
    		$this->tableName,
    		'table_cache_flag',
    	);
    	$key = Math::md5_16(join($cacheKey_elements, '::'));
    	$objMemcached = new Memcached($this->memcacheClusterFlag);
    	$tmpGetResult = $objMemcached->get($key);
    	if ($tmpGetResult) {
    		$this->tableCacheFlag = $tmpGetResult;
    		return $this->tableCacheFlag;
    	}
    	
    	if (is_null($tmpGetResult)) {// 缓存不存在,建立
    		$tmpVal = microtime(true);
    		if ($objMemcached->set($key, $tmpVal, 0)) {// 永不过期
    			$this->tableCacheFlag = $tmpVal;
    			return $this->tableCacheFlag;
    		}
    		
    		# TODO 设置缓存失败的处理
    		throw new DaoException(DaoException::CACHE_SET_TABLE_FLAG_ERROR);
    	}
    	
    	if (!$tmpGetResult) {
    		# TODO memcache服务不可用的处理
    		throw new DaoException(DaoException::CACHE_SERVICE_UNAVAILABLE);
    	}
    }
    
    /**
     * 
     * 获取指定ids集对应的缓存key集
     * @param array $ids  array(13343, 9939);
     * @Exception DaoException::CACHE_SET_TABLE_FLAG_ERROR
     * @Exception DaoException::CACHE_SERVICE_UNAVAILABLE
     * @return array 格式:array(
     *  (int) id => (string) cacheKey,
     *  ... ,
     * 
     * )
     */
    public function getsCacheKeys(array $ids) {
    	$tableCacheFlag = $this->getTableCacheFlag();
    	$cacheKeys = array();
    	$dbconfig = Config::getInstance()->get('db');
    	foreach ($ids as $id) {
    		$cacheKey_elements = array(
    			$tableCacheFlag,
    			serialize($dbconfig[$this->cluster_flag]),
    			$this->tableName,
    			$id,
    			'row_cache',// 这是针对数据库记录行的缓存
    		);
    		$cacheKeys[$id] = Math::md5_16(join($cacheKey_elements, '::'));
    	}
    	
    	return $cacheKeys;
    }
    
    /**
     * 
     * 从缓存里取数据表对应的行记录
     * @param array $ids
     * @return array 只返回缓存里有的值,如果缓存里没有任何值,则返回空数组
     */
    private function getsFromCache(array $ids) {
    	$return = array();
    	if (!$this->useCache) {
    		return $return;
    	}
    	try {
    		$cacheKeys = self::getsCacheKeys($ids);
    	} catch (DaoException $e) {
    		return $return;
    	}
    	
    	$objMemcached = new Memcached($this->memcacheClusterFlag);
    	$cacheVals = $objMemcached->gets($cacheKeys);
    	
    	$cache_return = array_combine(array_keys($cacheKeys), $cacheVals);
    	foreach ($cacheVals as $tmpK => $tmpV) {
    		if (is_null($tmpV)) {
    			continue;
    		}
    		
    		$return[array_search($tmpK, $cacheKeys)] = $tmpV;
    	}
    	
    	return $return;
    }
    
    /**
     * 
     * 从缓存里将$id对应的行记录删除
     * @param int $id
     * @throws Exception
     * @return Boolean
     */
    private function deleteFromCache($id) {
    	if (!$this->useCache) {
    		return false;
    	}
    	$objMemcached = new Memcached($this->memcacheClusterFlag);
    	
    	if (Verify::unsignedInt($id)) {
	    	try {
	    		$cacheKeys = self::getsCacheKeys(array($id));
	    	} catch (DaoException $e) {
	    		return false;
	    	}
    		return $objMemcached->delete(array_pop($cacheKeys));
    	}
    	
    	if (is_array($id) && !empty($id)) {
    		try {
	    		$cacheKeys = self::getsCacheKeys($id);
	    	} catch (DaoException $e) {
	    		return false;
	    	}
	    	
    		if (count($cacheKeys) == 1) {
    			return $objMemcached->delete(array_pop($cacheKeys));
    		}
    		return $objMemcached->deletes($cacheKeys);
    	}
    	
    	return false;
    }
    
    /**
     * 
     * 清除缓存的这张表的所有记录
     * @return Boolean
     */
    public function deleteAllCaches() {
    	if (!$this->useCache) {
    		return false;
    	}
    	
    	try {
    		$tableCacheFlag = $this->getTableCacheFlag();
    	} catch (DaoException $e) {
    		return false;
    	}
    	
    	$objMemcached = new Memcached($this->memcacheClusterFlag);
    	return $objMemcached->delete($tableCacheFlag);
    }
    
    /**
     * 
     * 将gets出来的结果集,存到缓存
     * @param array $results
     * @return boolean
     */
    private function setToCache(array $results) {
    	if (!$this->useCache) {
    		return false;
    	}
    	if (!$results) {
    		return false;
    	}
    	try {
    		$cacheKeys = self::getsCacheKeys(array_keys($results));
    	} catch (DaoException $e) {
    		return false;
    	}
    	$objMemcached = new Memcached($this->memcacheClusterFlag);
    	return $objMemcached->sets(array_combine($cacheKeys, $results), $this->cache_expiration);
    }

    /**
     * 创建一条记录
     * @param array $tableInfo 待插入的数据
     * @param boolean $isAutoIncrement 操作成功时,如果该值为true,返回最后插入的id;否则返回true
     * @return boolean | int
     */
    private function _create(array $tableInfo, $isAutoIncrement = true, $action = self::PARAM_CREATE_ACTION_INSERT) {
        if(empty($tableInfo)) return false;

        switch($action) {
            case self::PARAM_CREATE_ACTION_INSERT :
            case self::PARAM_CREATE_ACTION_INSERT_IGNORE :
            case self::PARAM_CREATE_ACTION_REPLACE :
                break;
            default:
                throw new Exception('error insert action');
        }

        $sql = "{$action} {$this->tableName}
            SET
        ";
        $sqlSets = '';
        $tableInfo = $this->quote($tableInfo);
        foreach($tableInfo as $key => $val) {
            if($sqlSets != '') $sqlSets .= ' ,';
            $sqlSets .= "
               `{$key}` = {$val}
            ";
        }
        $sql .= $sqlSets;

        if($this->db->query($sql)) {
            if($isAutoIncrement) {
                $id = $this->db->insertId();
                
                # 清空这条记录的缓存
                self::deleteFromCache($id);
                
                return $id > 0 ? $id : true;
            } else {
                return true;
            }
        }

        return false;
    }

    /**
     * 创建一条记录,如果重复,则替换
     * @param array $tableInfo 待插入的数据
     * @param boolean $isAutoIncrement 操作成功时,如果该值为true,返回最后插入的id;否则返回true
     * @return boolean | int
     */
    public function replace(array $tableInfo, $isAutoIncrement = true) {
        return $this->_create($tableInfo, $isAutoIncrement, self::PARAM_CREATE_ACTION_REPLACE);
    }

    /**
     * 创建一条记录
     * @param array $tableInfo 待插入的数据
     * @param boolean $isAutoIncrement 操作成功时,如果该值为true,返回最后插入的id;否则返回true
     * @return boolean | int
     */
    public function create(array $tableInfo, $isAutoIncrement = true) {
        return $this->_create($tableInfo, $isAutoIncrement, self::PARAM_CREATE_ACTION_INSERT);
    }

    /**
     * 创建一条记录,如果重复,则忽略
     * @param array $tableInfo 待插入的数据
     * @param boolean $isAutoIncrement 操作成功时,如果该值为true,返回最后插入的id;否则返回true
     * @return boolean | int PS:$isAutoIncrement = true时:1、如果插入了,返回自动id值;2、如果已存在,返回true。
     */
    public function insertIgnore(array $tableInfo, $isAutoIncrement = true) {
        return $this->_create($tableInfo, $isAutoIncrement, self::PARAM_CREATE_ACTION_INSERT_IGNORE);
    }

    /**
     *
     * 插入一条记录,如果重复,自动转为更新语句
     * @param array $tableInfo
     * @param array $onDuplicate 如果重复时,需要更新的信息。如果不指定,则使用$tableInfo的值,即认为要全部更新
     * @return int | Boolean
     * 		   int:只要存在,无论之前记录是否存在,都会返回记录的id;
     * 		   true:执行成功,但获取记录id时失败;
     * 		   false:执行失败
     */
    public function insertDuplicate(array $tableInfo, array $onDuplicate = array()) {
    	if (!$tableInfo) {
    		return false;
    	}
    	$tmpArrKeys = array();
		foreach ($tableInfo as $tmpKey => $tmpV) {
			$tmpArrKeys[] = "`{$tmpKey}`";
		}
		$sql = "INSERT INTO {$this->tableName} (" . join(', ', $tmpArrKeys). ") VALUES ";

        $tmpArrValues = array();
        $new_tableInfo = $this->quote($tableInfo);
        foreach ($new_tableInfo as $tmpKey => $tmpV) {
			$tmpArrValues[] = $tmpV;
        }
        $sql .= " ( " . join(', ', $tmpArrValues) . " ) ";

        $sql .= "
            ON DUPLICATE KEY UPDATE
        ";
        $tmpArrDps = array();
        if (empty($onDuplicate)) {
        	$onDuplicate = $tableInfo;
        }

        $new_onDuplicate = $this->quote($onDuplicate);
		foreach ($new_onDuplicate as $tmpKey => $tmpV) {
			$tmpArrDps[] = " `{$tmpKey}` = {$tmpV} ";
		}
		$sql .= join(', ', $tmpArrDps);

		if (!$this->db->query($sql)) {
			return false;
		}

		$id = $this->db->insertId();
		
		# 清空这条记录的缓存
        self::deleteFromCache($id);
        
        return $id > 0 ? $id : true;
    }

    /**
     * 根据条件更新指定数据
     * @param array $tableInfo 待更新的数据(与数据库字段对应的数据)
     * @param array $condition 条件(与数据库字段对应的数据)
     * @return boolean
     */
    public function update(array $tableInfo, array $condition) {
        if(empty($tableInfo)) return false;

        $sql = "UPDATE {$this->tableName}
            SET
        ";
        $sqlSets = '';
        foreach($tableInfo as $key => $val) {
            if($sqlSets != '') $sqlSets .= ' ,';
            $sqlSets .= "
               `{$key}` = {$this->quote($val)}
            ";
        }
        $sql .= $sqlSets;

        $where = $this->parseCondition($condition);
        $sql .= "
            {$where}
            ;
        ";

        $tmpQueryResult = $this->db->query($sql);
        
        if ($tmpQueryResult) {
        	if (isset($condition[$this->primaryKey])) {
        		# 清空这条记录的缓存,条件里有可能是一批ids
        		self::deleteFromCache($condition[$this->primaryKey]);
        	} else {
        		$this->deleteAllCaches();
        	}
        }
        
        return $tmpQueryResult;
    }

    /**
     * 根据条件删除数据
     * @param Array|String $condition 条件
     * @return boolean
     */
    public function delete($condition) {
        $where = $this->parseCondition($condition);

        $sql = "DELETE FROM {$this->tableName}
            {$where}
            ;
        ";
           
        $tmpQueryResult = $this->db->query($sql);
        
        if ($tmpQueryResult) {
        	if (isset($condition[$this->primaryKey])) {
        		# 清空这条记录的缓存,条件里有可能是一批ids
        		self::deleteFromCache($condition[$this->primaryKey]);
        	} else {
        		$this->deleteAllCaches();
        	}
        }
        
        return $tmpQueryResult;
    }

    /**
     * 转义数据
     * @param mixed $data
     */
    public function quote($data) {
        return SqlHelper::escape($data, true);
    }

    /**
     * 判断给定的值是否是有效的自增主键值
     * @param mixid $pk
     * @return boolean
     */
    static protected function isValidPK($pk) {
        return SqlHelper::isValidPK($pk);
    }

    /**
     * 判断给定数组的某个key的值,是否是有效的自增主键值
     * @param array $arr
     * @param mixid $key
     * @return boolean
     */
    static protected function isValidPKWithArray(array $arr, $key) {
        return SqlHelper::isValidPKWithArray($arr, $key);
    }

    /**
     * 获取指定条件的记录数
     * @return int
     */
    public function totals($condtion = null){
        $result = $this->findBy($condtion, null, null, "count(*)");
        if (!$result) {
            return 0;
        }
        return (int) array_pop(array_pop($result));
    }
}