<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2010 http://topthink.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace Think\Model;

use Think\Model;

/**
 * MongoModel模型类
 * 实现了ODM和ActiveRecords模式
 */
class MongoModel extends Model
{
    // 主键类型
    const TYPE_OBJECT = 1;

    const TYPE_INT = 2;

    const TYPE_STRING = 3;
    // 主键名称
    protected $pk = '_id';
    // _id 类型 1 Object 采用MongoId对象 2 Int 整形 支持自动增长 3 String 字符串Hash
    protected $_idType = self::TYPE_OBJECT;
    // 主键是否自增
    protected $_autoinc = true;
    // Mongo默认关闭字段检测 可以动态追加字段
    protected $autoCheckFields = false;
    // 链操作方法列表
    protected $methods = array(
        'table',
        'order',
        'auto',
        'filter',
        'validate'
    );

    /**
     * 利用__call方法实现一些特殊的Model方法
     * 
     * @access public
     * @param string $method
     *            方法名称
     * @param array $args
     *            调用参数
     * @return mixed
     */
    public function __call($method, $args)
    {
        if (in_array(strtolower($method), $this->methods, true)) {
            // 连贯操作的实现
            $this->options[strtolower($method)] = $args[0];
            
            return $this;
        } elseif (strtolower(substr($method, 0, 5)) == 'getby') {
            // 根据某个字段获取记录
            $field = parse_name(substr($method, 5));
            $where[$field] = $args[0];
            
            return $this->where($where)->find();
        } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') {
            // 根据某个字段获取记录的某个值
            $name = parse_name(substr($method, 10));
            $where[$name] = $args[0];
            
            return $this->where($where)->getField($args[1]);
        } else {
            E(__CLASS__ . ':' . $method . L('_METHOD_NOT_EXIST_'));
            
            return;
        }
    }

    /**
     * 获取字段信息并缓存 主键和自增信息直接配置
     * 
     * @access public
     * @return void
     */
    public function flush()
    {
        // 缓存不存在则查询数据表信息
        $fields = $this->db->getFields();
        if (! $fields) { // 暂时没有数据无法获取字段信息 下次查询
            return false;
        }
        $this->fields = array_keys($fields);
        foreach ($fields as $key => $val) {
            // 记录字段类型
            $type[$key] = $val['type'];
        }
        // 记录字段类型信息
        if (C('DB_FIELDTYPE_CHECK')) {
            $this->fields['_type'] = $type;
        }
        // 2008-3-7 增加缓存开关控制
        if (C('DB_FIELDS_CACHE')) {
            // 永久缓存数据表信息
            $db = $this->dbName ? $this->dbName : C('DB_NAME');
            F('_fields/' . $db . '.' . $this->name, $this->fields);
        }
    }
    
    // 写入数据前的回调方法 包括新增和更新
    protected function _before_write(&$data)
    {
        $pk = $this->getPk();
        // 根据主键类型处理主键数据
        if (isset($data[$pk]) && $this->_idType == self::TYPE_OBJECT) {
            $data[$pk] = new \MongoId($data[$pk]);
        }
    }

    /**
     * count统计 配合where连贯操作
     * 
     * @access public
     * @return integer
     */
    public function count()
    {
        // 分析表达式
        $options = $this->_parseOptions();
        
        return $this->db->count($options);
    }

    /**
     * 获取唯一值
     * 
     * @access public
     * @return array | false
     */
    public function distinct($field, $where = array())
    {
        // 分析表达式
        $this->options = $this->_parseOptions();
        $this->options['where'] = array_merge((array) $this->options['where'], $where);
        $command = array(
            "distinct" => $this->options['table'],
            "key" => $field,
            "query" => $this->options['where']
        );
        $result = $this->command($command);
        
        return isset($result['values']) ? $result['values'] : false;
    }

    /**
     * 获取下一ID 用于自动增长型
     * 
     * @access public
     * @param string $pk
     *            字段名 默认为主键
     * @return mixed
     */
    public function getMongoNextId($pk = '')
    {
        if (empty($pk)) {
            $pk = $this->getPk();
        }
        
        return $this->db->getMongoNextId($pk);
    }

    /**
     * 新增数据
     * 
     * @access public
     * @param mixed $data
     *            数据
     * @param array $options
     *            表达式
     * @param boolean $replace
     *            是否replace
     * @return mixed
     */
    public function add($data = '', $options = array(), $replace = false)
    {
        if (empty($data)) {
            // 没有传递数据,获取当前数据对象的值
            if (! empty($this->data)) {
                $data = $this->data;
                // 重置数据
                $this->data = array();
            } else {
                $this->error = L('_DATA_TYPE_INVALID_');
                
                return false;
            }
        }
        // 分析表达式
        $options = $this->_parseOptions($options);
        // 数据处理
        $data = $this->_facade($data);
        if (false === $this->_before_insert($data, $options)) {
            return false;
        }
        // 写入数据到数据库
        $result = $this->db->insert($data, $options, $replace);
        if (false !== $result) {
            $this->_after_insert($data, $options);
            if (isset($data[$this->getPk()])) {
                return $data[$this->getPk()];
            }
        }
        
        return $result;
    }
    
    // 插入数据前的回调方法
    protected function _before_insert(&$data, $options)
    {
        // 写入数据到数据库
        if ($this->_autoinc && $this->_idType == self::TYPE_INT) { // 主键自动增长
            $pk = $this->getPk();
            if (! isset($data[$pk])) {
                $data[$pk] = $this->db->getMongoNextId($pk);
            }
        }
    }

    public function clear()
    {
        return $this->db->clear();
    }
    
    // 查询成功后的回调方法
    protected function _after_select(&$resultSet, $options)
    {
        array_walk($resultSet, array(
            $this,
            'checkMongoId'
        ));
    }

    /**
     * 获取MongoId
     * 
     * @access protected
     * @param array $result
     *            返回数据
     * @return array
     */
    protected function checkMongoId(&$result)
    {
        if (is_object($result['_id'])) {
            $result['_id'] = $result['_id']->__toString();
        }
        
        return $result;
    }
    
    // 表达式过滤回调方法
    protected function _options_filter(&$options)
    {
        $id = $this->getPk();
        if (isset($options['where'][$id]) && is_scalar($options['where'][$id]) && $this->_idType == self::TYPE_OBJECT) {
            $options['where'][$id] = new \MongoId($options['where'][$id]);
        }
    }

    /**
     * 查询数据
     * 
     * @access public
     * @param mixed $options
     *            表达式参数
     * @return mixed
     */
    public function find($options = array())
    {
        if (is_numeric($options) || is_string($options)) {
            $id = $this->getPk();
            $where[$id] = $options;
            $options = array();
            $options['where'] = $where;
        }
        // 分析表达式
        $options = $this->_parseOptions($options);
        $result = $this->db->find($options);
        if (false === $result) {
            return false;
        }
        if (empty($result)) { // 查询结果为空
            return null;
        } else {
            $this->checkMongoId($result);
        }
        $this->data = $result;
        $this->_after_find($this->data, $options);
        
        return $this->data;
    }

    /**
     * 字段值增长
     * 
     * @access public
     * @param string $field
     *            字段名
     * @param integer $step
     *            增长值
     * @return boolean
     */
    public function setInc($field, $step = 1)
    {
        return $this->setField($field, array(
            'inc',
            $step
        ));
    }

    /**
     * 字段值减少
     * 
     * @access public
     * @param string $field
     *            字段名
     * @param integer $step
     *            减少值
     * @return boolean
     */
    public function setDec($field, $step = 1)
    {
        return $this->setField($field, array(
            'inc',
            '-' . $step
        ));
    }

    /**
     * 获取一条记录的某个字段值
     * 
     * @access public
     * @param string $field
     *            字段名
     * @param string $spea
     *            字段数据间隔符号
     * @return mixed
     */
    public function getField($field, $sepa = null)
    {
        $options['field'] = $field;
        $options = $this->_parseOptions($options);
        if (strpos($field, ',')) { // 多字段
            if (is_numeric($sepa)) { // 限定数量
                $options['limit'] = $sepa;
                $sepa = null; // 重置为null 返回数组
            }
            $resultSet = $this->db->select($options);
            if (! empty($resultSet)) {
                $_field = explode(',', $field);
                $field = array_keys($resultSet[0]);
                $key = array_shift($field);
                $key2 = array_shift($field);
                $cols = array();
                $count = count($_field);
                foreach ($resultSet as $result) {
                    $name = $result[$key];
                    if (2 == $count) {
                        $cols[$name] = $result[$key2];
                    } else {
                        $cols[$name] = is_null($sepa) ? $result : implode($sepa, $result);
                    }
                }
                
                return $cols;
            }
        } else {
            // 返回数据个数
            if (true !== $sepa) { // 当sepa指定为true的时候 返回所有数据
                $options['limit'] = is_numeric($sepa) ? $sepa : 1;
            } // 查找符合的记录
            $result = $this->db->select($options);
            if (! empty($result)) {
                if (1 == $options['limit']) {
                    return reset($result)[$field];
                }
                foreach ($result as $val) {
                    $array[] = $val[$field];
                }
                
                return $array;
            }
        }
        
        return null;
    }

    /**
     * 执行Mongo指令
     * 
     * @access public
     * @param array $command
     *            指令
     * @return mixed
     */
    public function command($command, $options = array())
    {
        $options = $this->_parseOptions($options);
        
        return $this->db->command($command, $options);
    }

    /**
     * 执行MongoCode
     * 
     * @access public
     * @param string $code
     *            MongoCode
     * @param array $args
     *            参数
     * @return mixed
     */
    public function mongoCode($code, $args = array())
    {
        return $this->db->execute($code, $args);
    }
    
    // 数据库切换后回调方法
    protected function _after_db()
    {
        // 切换Collection
        $this->db->switchCollection($this->getTableName(), $this->dbName ? $this->dbName : C('db_name'));
    }

    /**
     * 得到完整的数据表名 Mongo表名不带dbName
     * 
     * @access public
     * @return string
     */
    public function getTableName()
    {
        if (empty($this->trueTableName)) {
            $tableName = ! empty($this->tablePrefix) ? $this->tablePrefix : '';
            if (! empty($this->tableName)) {
                $tableName .= $this->tableName;
            } else {
                $tableName .= parse_name($this->name);
            }
            $this->trueTableName = strtolower($tableName);
        }
        
        return $this->trueTableName;
    }

    /**
     * 分组查询
     * 
     * @access public
     * @return string
     */
    public function group($key, $init, $reduce, $option = array())
    {
        $option = $this->_parseOptions($option);
        // 合并查询条件
        if (isset($option['where'])) {
            $option['condition'] = array_merge((array) $option['condition'], $option['where']);
        }
        
        return $this->db->group($key, $init, $reduce, $option);
    }

    /**
     * 返回Mongo运行错误信息
     * 
     * @access public
     * @return json
     */
    public function getLastError()
    {
        return $this->db->command(array(
            'getLastError' => 1
        ));
    }

    /**
     * 返回指定集合的统计信息,包括数据大小、已分配的存储空间和索引的大小
     * 
     * @access public
     * @return json
     */
    public function status()
    {
        $option = $this->_parseOptions();
        
        return $this->db->command(array(
            'collStats' => $option['table']
        ));
    }

    /**
     * 取得当前数据库的对象
     * 
     * @access public
     * @return object
     */
    public function getDB()
    {
        return $this->db->getDB();
    }

    /**
     * 取得集合对象,可以进行创建索引等查询
     * 
     * @access public
     * @return object
     */
    public function getCollection()
    {
        return $this->db->getCollection();
    }
}