<?php<liu21st@gmail.com>namespace Think\Model;
use Think\Model;
class AdvModel extends Model {
    protected $optimLock        =   'lock_version';
    protected $returnType       =   'array';
    protected $blobFields       =   array();
    protected $blobValues       =   null;
    protected $serializeField   =   array();
    protected $readonlyField    =   array();
    protected $_filter          =   array();
    protected $partition        =   array();
    public function __construct($name='',$tablePrefix='',$connection='') {
        if('' !== $name || is_subclass_of($this,'AdvModel') ){
                   }else{
                       $this->autoCheckFields = false;
        }
        parent::__construct($name,$tablePrefix,$connection);
    }
    
    public function __call($method,$args) {
        if(strtolower(substr($method,0,3))=='top'){
                       $count = substr($method,3);
            array_unshift($args,$count);
            return call_user_func_array(array(&$this, 'topN'), $args);
        }else{
            return parent::__call($method,$args);
        }
    }
    
     protected function _facade($data) {
               $data = $this->serializeField($data);
        return parent::_facade($data);
     }
       protected function _after_find(&$result,$options='') {
               $this->checkSerializeField($result);
               $this->getBlobFields($result);
               $result   =  $this->getFilterFields($result);
               $this->cacheLockVersion($result);
    }
       protected function _after_select(&$resultSet,$options='') {
               $resultSet   =  $this->checkListSerializeField($resultSet);
               $resultSet   =  $this->getListBlobFields($resultSet);
               $resultSet   =  $this->getFilterListFields($resultSet);
    }
       protected function _before_insert(&$data,$options='') {
               $data = $this->recordLockVersion($data);
               $data = $this->checkBlobFields($data);
               $data = $this->setFilterFields($data);
    }
    protected function _after_insert($data,$options) {
               $this->saveBlobFields($data);
    }
       protected function _before_update(&$data,$options='') {
               $pk     =   $this->getPK();
        if(isset($options['where'][$pk])){
            $id     =   $options['where'][$pk];   
            if(!$this->checkLockVersion($id,$data)) {
                return false;
            }
        }
               $data = $this->checkBlobFields($data);
               $data = $this->checkReadonlyField($data);
               $data = $this->setFilterFields($data);
    }
    protected function _after_update($data,$options) {
               $this->saveBlobFields($data);
    }
    protected function _after_delete($data,$options) {
               $this->delBlobFields($data);
    }
    
    protected function recordLockVersion($data) {
               if($this->optimLock && !isset($data[$this->optimLock]) ) {
            if(in_array($this->optimLock,$this->fields,true)) {
                $data[$this->optimLock]  =   0;
            }
        }
        return $data;
    }
    
    protected function cacheLockVersion($data) {
        if($this->optimLock) {
            if(isset($data[$this->optimLock]) && isset($data[$this->getPk()])) {
                               $_SESSION[$this->name.'_'.$data[$this->getPk()].'_lock_version']    =   $data[$this->optimLock];
            }
        }
    }
    
    protected function checkLockVersion($id,&$data) {
               $identify   = $this->name.'_'.$id.'_lock_version';
        if($this->optimLock && isset($_SESSION[$identify])) {
            $lock_version = $_SESSION[$identify];
            $vo   =  $this->field($this->optimLock)->find($id);
            $_SESSION[$identify]     =   $lock_version;
            $curr_version = $vo[$this->optimLock];
            if(isset($curr_version)) {
                if($curr_version>0 && $lock_version != $curr_version) {
                                       $this->error = L('_RECORD_HAS_UPDATE_');
                    return false;
                }else{
                                       $save_version = $data[$this->optimLock];
                    if($save_version != $lock_version+1) {
                        $data[$this->optimLock]  =   $lock_version+1;
                    }
                    $_SESSION[$identify]     =   $lock_version+1;
                }
            }
        }
        return true;
    }
    
    public function topN($count,$options=array()) {
        $options['limit'] =  $count;
        return $this->select($options);
    }
    
    public function getN($position=0,$options=array()) {
        if($position>=0) {            $options['limit'] = $position.',1';
            $list   =  $this->select($options);
            return $list?$list[0]:false;
        }else{            $list   =  $this->select($options);
            return $list?$list[count($list)-abs($position)]:false;
        }
    }
    
    public function first($options=array()) {
        return $this->getN(0,$options);
    }
    
    public function last($options=array()) {
        return $this->getN(-1,$options);
    }
    
    public function returnResult($data,$type='') {
        if('' === $type)
            $type = $this->returnType;
        switch($type) {
            case 'array' :  return $data;
            case 'object':  return (object)$data;
            default:// 允许用户自定义返回类型
                if(class_exists($type))
                    return new $type($data);
                else
                    E(L('_CLASS_NOT_EXIST_').':'.$type);
        }
    }
    
    protected function getFilterFields(&$result) {
        if(!empty($this->_filter)) {
            foreach ($this->_filter as $field=>$filter){
                if(isset($result[$field])) {
                    $fun  =  $filter[1];
                    if(!empty($fun)) {
                        if(isset($filter[2]) && $filter[2]){
                                                       $result[$field]  =  call_user_func($fun,$result);
                        }else{
                                                       $result[$field]  =  call_user_func($fun,$result[$field]);
                        }
                    }
                }
            }
        }
        return $result;
    }
    protected function getFilterListFields(&$resultSet) {
        if(!empty($this->_filter)) {
            foreach ($resultSet as $key=>$result)
                $resultSet[$key]  =  $this->getFilterFields($result);
        }
        return $resultSet;
    }
    
    protected function setFilterFields($data) {
        if(!empty($this->_filter)) {
            foreach ($this->_filter as $field=>$filter){
                if(isset($data[$field])) {
                    $fun              =  $filter[0];
                    if(!empty($fun)) {
                        if(isset($filter[2]) && $filter[2]) {
                                                       $data[$field]   =  call_user_func($fun,$data);
                        }else{
                                                       $data[$field]   =  call_user_func($fun,$data[$field]);
                        }
                    }
                }
            }
        }
        return $data;
    }
    
    protected function returnResultSet(&$resultSet,$type='') {
        foreach ($resultSet as $key=>$data)
            $resultSet[$key]  =  $this->returnResult($data,$type);
        return $resultSet;
    }
    protected function checkBlobFields(&$data) {
               if(!empty($this->blobFields)) {
            foreach ($this->blobFields as $field){
                if(isset($data[$field])) {
                    if(isset($data[$this->getPk()]))
                        $this->blobValues[$this->name.'/'.$data[$this->getPk()].'_'.$field] =   $data[$field];
                    else
                        $this->blobValues[$this->name.'/@?id@_'.$field] =   $data[$field];
                    unset($data[$field]);
                }
            }
        }
        return $data;
    }
    
    protected function getListBlobFields(&$resultSet,$field='') {
        if(!empty($this->blobFields)) {
            foreach ($resultSet as $key=>$result){
                $result =   $this->getBlobFields($result,$field);
                $resultSet[$key]    =   $result;
            }
        }
        return $resultSet;
    }
    
    protected function getBlobFields(&$data,$field='') {
        if(!empty($this->blobFields)) {
            $pk =   $this->getPk();
            $id =   $data[$pk];
            if(empty($field)) {
                foreach ($this->blobFields as $field){
                    $identify   =   $this->name.'/'.$id.'_'.$field;
                    $data[$field]   =   F($identify);
                }
                return $data;
            }else{
                $identify   =   $this->name.'/'.$id.'_'.$field;
                return F($identify);
            }
        }
    }
    
    protected function saveBlobFields(&$data) {
        if(!empty($this->blobFields)) {
            foreach ($this->blobValues as $key=>$val){
                if(strpos($key,'@?id@'))
                    $key    =   str_replace('@?id@',$data[$this->getPk()],$key);
                F($key,$val);
            }
        }
    }
    
    protected function delBlobFields(&$data,$field='') {
        if(!empty($this->blobFields)) {
            $pk =   $this->getPk();
            $id =   $data[$pk];
            if(empty($field)) {
                foreach ($this->blobFields as $field){
                    $identify   =   $this->name.'/'.$id.'_'.$field;
                    F($identify,null);
                }
            }else{
                $identify   =   $this->name.'/'.$id.'_'.$field;
                F($identify,null);
            }
        }
    }
    
    public function setLazyInc($field,$step=1,$lazyTime=0) {
        $condition   =  $this->options['where'];
        if(empty($condition)) {            return false;
        }
        if($lazyTime>0)             $guid =  md5($this->name.'_'.$field.'_'.serialize($condition));
            $step = $this->lazyWrite($guid,$step,$lazyTime);
            if(false === $step ) return true;        }
        return $this->setField($field,array('exp',$field.'+'.$step));
    }
    
    public function setLazyDec($field,$step=1,$lazyTime=0) {
        $condition   =  $this->options['where'];
        if(empty($condition)) {            return false;
        }
        if($lazyTime>0)             $guid =  md5($this->name.'_'.$field.'_'.serialize($condition));
            $step = $this->lazyWrite($guid,$step,$lazyTime);
            if(false === $step ) return true;        }
        return $this->setField($field,array('exp',$field.'-'.$step));
    }
    
    protected function lazyWrite($guid,$step,$lazyTime) {
        if(false !== ($value = F($guid))) {            if(time()>F($guid.'_time')+$lazyTime) {
                               F($guid,NULL);
                F($guid.'_time',NULL);
                return $value+$step;
            }else{
                               F($guid,$value+$step);
                return false;
            }
        }else{            F($guid,$step);
                       F($guid.'_time',time());
            return false;
        }
    }
    
     protected function serializeField(&$data) {
               if(!empty($this->serializeField)) {
                       foreach ($this->serializeField as $key=>$val){
                if(empty($data[$key])) {
                    $serialize  =   array();
                    foreach ($val as $name){
                        if(isset($data[$name])) {
                            $serialize[$name]   =   $data[$name];
                            unset($data[$name]);
                        }
                    }
                    if(!empty($serialize)) {
                        $data[$key] =   serialize($serialize);
                    }
                }
            }
        }
        return $data;
     }
       protected function checkSerializeField(&$result) {
               if(!empty($this->serializeField)) {
            foreach ($this->serializeField as $key=>$val){
                if(isset($result[$key])) {
                    $serialize   =   unserialize($result[$key]);
                    foreach ($serialize as $name=>$value)
                        $result[$name]  =   $value;
                    unset($serialize,$result[$key]);
                }
            }
        }
        return $result;
    }
       protected function checkListSerializeField(&$resultSet) {
               if(!empty($this->serializeField)) {
            foreach ($this->serializeField as $key=>$val){
                foreach ($resultSet as $k=>$result){
                    if(isset($result[$key])) {
                        $serialize   =   unserialize($result[$key]);
                        foreach ($serialize as $name=>$value)
                            $result[$name]  =   $value;
                        unset($serialize,$result[$key]);
                        $resultSet[$k] =   $result;
                    }
                }
            }
        }
        return $resultSet;
    }
    
    protected function checkReadonlyField(&$data) {
        if(!empty($this->readonlyField)) {
            foreach ($this->readonlyField as $key=>$field){
                if(isset($data[$field]))
                    unset($data[$field]);
            }
        }
        return $data;
    }
    
    public function patchQuery($sql=array()) {
        if(!is_array($sql)) return false;
               $this->startTrans();
        try{
            foreach ($sql as $_sql){
                $result   =  $this->execute($_sql);
                if(false === $result) {
                                       $this->rollback();
                    return false;
                }
            }
                       $this->commit();
        } catch (ThinkException $e) {
            $this->rollback();
        }
        return true;
    }
    
    public function getPartitionTableName($data=array()) {
               if(isset($data[$this->partition['field']])) {
            $field   =   $data[$this->partition['field']];
            switch($this->partition['type']) {
                case 'id':
                                       $step    =   $this->partition['expr'];
                    $seq    =   floor($field / $step)+1;
                    break;
                case 'year':
                                       if(!is_numeric($field)) {
                        $field   =   strtotime($field);
                    }
                    $seq    =   date('Y',$field)-$this->partition['expr']+1;
                    break;
                case 'mod':
                                       $seq    =   ($field % $this->partition['num'])+1;
                    break;
                case 'md5':
                                       $seq    =   (ord(substr(md5($field),0,1)) % $this->partition['num'])+1;
                    break;
                default :
                    if(function_exists($this->partition['type'])) {
                                               $fun    =   $this->partition['type'];
                        $seq    =   (ord(substr($fun($field),0,1)) % $this->partition['num'])+1;
                    }else{
                                               $seq    =   (ord($field{0}) % $this->partition['num'])+1;
                    }
            }
            return $this->getTableName().'_'.$seq;
        }else{
                                  $tableName  =   array();
            for($i=0;$i<$this->partition['num'];$i++)
                $tableName[] = 'SELECT * FROM '.$this->getTableName().'_'.($i+1);
            $tableName = '( '.implode(" UNION ",$tableName).') AS '.$this->name;
            return $tableName;
        }
    }
}