<?php

namespace loyalsoft;

/*
 * File:MongoUtil.php
 * Function: MongoDB operation helper.
 * Author: gwang(wanggangzero@qq.com)
 * Version:
 *      1.0.0 Creat file. --gwang 2022.2.14
 */

/**
 * MongoDB辅助类
 * Author: gwang(wanggangzero@qq.com)
 */
class MongoUtil {

    /**
     * 负责与MongoDB连接, 管理
     * @var \MongoDB\Driver\Manager
     */
    private $mc;
    private $dbPrefix = "";

    /**
     * 返回一个新的option构造器,请使用链式写法追加所有option, 最后用GetOption()获取最终的option数组
     * @author gwang <wanggangzero@qq.com>
     * @return \loyalsoft\MongoQueryOptionBuilder
     */
    public static function QueryOptionBuilder() {
        return new MongoQueryOptionBuilder();
    }

    /**
     *
     * @param type $connStr
     * @return \MongoDB\Driver\Manager|false
     */
    public function conn($connStr, $dbPrefix = "") {
        try {
            $this->mc = new \MongoDB\Driver\Manager($connStr);
            $this->dbPrefix = $dbPrefix;
            return $this->mc;
        } catch (\Exception $e) {
            Clog::err($e->getMessage());
            return false;
        }
    }

    /**
     * 查询,返回的是光标,也可以用toArray()转换为数组使用
     * @author gwang<wanggangzero@qq.com>
     * @param string $collection 集合
     * @param array $filter 同过滤器语法
     * @param array $options 请利用QueryOptionBuilder()来辅助定制选项
     * @return \MongoDB\Driver\Cursor|false 访问光标或者false, 可以用foreach来访问Cursor
     */
    public function find($collection, $filter = [], $options = []) {
        if (empty($this->mc)) {
            return false;
        }
        try {
            $query = new \MongoDB\Driver\Query($filter, $options);
            $cursor = $this->mc->executeQuery($this->dbPrefix . '.' . $collection, $query);
            return $cursor;
        } catch (\Exception $ex) {
            CLog::err($ex->getMessage());
            return false;
        }
    }

    /**
     * 插入1条数据
     * @param string $collection 表名(db.collection)
     * @param array $addArr 待插入的数据,要求是数组
     * @return boolean
     */
    public function insert($collection, $addArr) {
        if (empty($addArr) || !is_array($addArr)) {
            return false;
        }
        if (empty($this->mc)) {
            return false;
        }
        try {
            $bulk = new \MongoDB\Driver\BulkWrite();
            $bulk->insert($addArr);
            $writeConcern = new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY, 6000);
            $result = $this->mc->executeBulkWrite($this->dbPrefix . '.' . $collection, $bulk, $writeConcern);
            if ($result->getInsertedCount()) {
                return true;
            }
        } catch (\Exception $e) {
            CLog::err($e->getMessage());                                        # 记录错误日志
        }
        return false;
    }

    /**
     * 删除
     * @param string $collection 表名
     * @param array $whereArr 筛选条件
     * @param bool $limit 是否仅删除第一条(默认false,删除所有匹配条目)
     * @return boolean
     */
    public function delete($collection, $whereArr, $limit = false) {
        if (empty($whereArr)) {
            return false;
        }
        $options = ['limit' => $limit ? 1 : 0];
        $conn = $this->mc;
        if (empty($conn)) {
            return false;
        }
        try {
            $bulk = new \MongoDB\Driver\BulkWrite();
            $bulk->delete($whereArr, $options);
            $writeConcern = new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY, 30000);
            $result = $conn->executeBulkWrite($this->dbPrefix . '.' . $collection, $bulk, $writeConcern);
            return $result->getDeletedCount();
        } catch (\Exception $e) {
            CLog::err($e->getMessage());                                        # 记录错误日志
        }
        return false;
    }

    /**
     * 更新
     * @param string $collection
     * @param array $whereArr 条件
     * @param array $newPropertiesArr 可以是一个新文档, 也可以是一个opration数组(eg. $set表达式), 还可以是一组update 管道(批量参考https://docs.mongodb.com/manual/reference/command/update/#update-with-an-aggregation-pipeline)
     * @param bool $upsert 当符合条件的文档不存在时是否作为一条新的文档插入(当新数据为文档时生效,operation不可以)
     * @param bool $multi 是否一次更新多个文档的值(当新数据为operation表达式时可以,否则不可以) 默认false
     * @return boolean
     */
    public function update($collection, $whereArr, $newPropertiesArr, $upsert = true, $multi = false) {
        if (empty($whereArr) || empty($newPropertiesArr)) {
            return false;
        }
        $options = ['multi' => $multi, 'upsert' => $upsert];
        $conn = $this->mc;
        if (empty($conn)) {
            return false;
        }
        try {
            $bulk = new \MongoDB\Driver\BulkWrite();
            $bulk->update($whereArr, $newPropertiesArr, $options);
            $writeConcern = new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY, 30000);
            $result = $conn->executeBulkWrite($this->dbPrefix . '.' . $collection, $bulk, $writeConcern);
            return $result->getUpsertedCount() + $result->getModifiedCount();
        } catch (\Exception $e) {
            CLog::err($e->getMessage());                                        # 记录错误日志
        }
        return false;
    }
}

/**
 * 查询选项构造器
 * @author gwang <wanggangzero@qq.com>
 */
class MongoQueryOptionBuilder {

    /**
     * 内部对象
     */
    private $option = array();

    /**
     * 返回最终的 options
     * @return array
     */
    public function GetOption() {
        return $this->option;
    }

    /**
     * 参照MySQL的 explain
     * @param bool $yes
     * @return MongoQueryOptionBuilder
     */
    public function explain($yes = true) {
        $this->option[__FUNCTION__] = $yes;
        return $this;
    }

    /**
     * 限定返回值数量(0=无限制)
     * @param int $limit 默认 0
     * @return MongoQueryOptionBuilder
     */
    public function limit($limit = 0) {
        $this->option[__FUNCTION__] = $limit;
        return $this;
    }

    /**
     * 限定返回值跳过前n个文档(索引从0开始)
     * @param int $n 默认值0
     * @return MongoQueryOptionBuilder
     */
    public function skip($n = 0) {
        $this->option[__FUNCTION__] = $n;
        return $this;
    }

    /**
     * project
     * @param array $arr 假如MQL中为{$project:{key:1,name:"$value.baseInfo.name"}}
     *      则此处['key'=>1,'name'=>'$value.baseInfo.name'] 注意如果包含$符使用单引号
     * @return MongoQueryOptionBuilder
     */
    public function projection($arr = array()) {
        $this->option[__FUNCTION__] = $arr;
        return $this;
    }

    /**
     * 必须与 tailable awaitData 结合使用.
     * 设定正整数值可以让服务器阻塞对应的毫秒数(没有数据的前提下,有数据自然直接返回)
     * @param int $n
     * @return MongoQueryOptionBuilder
     */
    public function maxAwaitTimeMS($n = 0) {
        $this->option[__FUNCTION__] = $n;
        return $this;
    }

    /**
     * 只返回索引
     * @param bool $yes 默认值false, 当设为true时,你应该了解会返回什么.
     * @return $this
     */
    public function returnKey($yes = false) {
        $this->option[__FUNCTION__] = $yes;
        return $this;
    }

    /**
     * The sort specification for the ordering of the results.
     *    Falls back to the deprecated modifier if not specified. "$orderby"
     * @param array $arr 假如MQL中为{$sort:{"value.baseInfo.level":-1}则对应["value.baseInfo.level"=>-1]
     * @return MongoQueryOptionBuilder
     */
    public function sort($arr = array()) {
        $this->option[__FUNCTION__] = $arr;
        return $this;
    }

// <editor-fold defaultstate="collapsed" desc="不常用或已经废弃的">

    /**
     * 不了解, 先不要使用
     * @deprecated since version 0
     * @param type $yes
     * @return $this
     */
    public function allowDiskUse($yes = true) {
        $this->option[__FUNCTION__] = $yes;
        return $this;
    }

    /**
     * 不了解, 先不要使用
     * @deprecated since version 0
     * @param type $yes
     * @return $this
     */
    public function allowPartialResults($yes = true) {
        $this->option[__FUNCTION__] = $yes;
        return $this;
    }

    /**
     * 不了解, 先不要使用
     * @deprecated since version 0
     * @param type $yes
     * @return $this
     */
    public function awaitData($yes = true) {
        $this->option[__FUNCTION__] = $yes;
        return $this;
    }

    /**
     * 返回值第一批的文档数,默认值为101,设置为0表示将建立光标,但不返回任何文档
     * @deprecated since version 0 不懂,先不要用
     * @param type $size
     * @return MongoQueryOptionBuilder
     */
    public function batchSize($size = 101) {
        $this->option[__FUNCTION__] = $size;
        return $this;
    }

    /**
     * 语言设置
     * @deprecated since version 0 这个太复杂还没研究,先不要用
     * @param array|object $collation
     * @return MongoQueryOptionBuilder
     */
    public function collation($collation = array()) {
        $this->option[__FUNCTION__] = $collation;
        return $this;
    }

    /**
     * 注释
     * @param type $msg
     * @return MongoQueryOptionBuilder
     */
    public function comment($msg = "默认注释") {
        $this->option[__FUNCTION__] = $msg;
        return $this;
    }

    /**
     * 尽最大努力下载数据,(从3.2+已经不再支持)
     * @deprecated since version mongodb 3.2+
     * @param type $yes
     * @return MongoQueryOptionBuilder
     */
    public function exhaust($yes = true) {
        $this->option[__FUNCTION__] = $yes;
        return $this;
    }

    /**
     * 不懂, 先不要用
     * @deprecated since version 0  在某个index的上限(不包含上限值)
     * @param array $arr
     * @return MongoQueryOptionBuilder
     */
    public function max($arr = array()) {
        $this->option[__FUNCTION__] = $arr;
        return $this;
    }

    /**
     * 不懂,先不要用
     * @deprecated since version 0 在某个index的下限(包含下限值)
     * @param array $arr
     * @return MongoQueryOptionBuilder
     */
    public function min($arr = array()) {
        $this->option[__FUNCTION__] = $arr;
        return $this;
    }

    /**
     * @deprecated since version 0 已经废弃
     * @param int $i
     * @throws \Exception
     */
    public function maxScan($i = 0) {
        throw new \Exception("Deprecated!");
    }

    /**
     * Determines whether to close the cursor after the first batch. Defaults to false.
     * @param bool $yes
     * @deprecated since version 0 不放心, 先不要用
     * @return MongoQueryOptionBuilder
     */
    public function singleBatch($yes = false) {
        $this->option[__FUNCTION__] = $yes;
        return $this;
    }

    /**
     * Determines whether to return the record identifier for each document.
     * If true, adds a top-level field to the returned documents. "$recordId"
     * @deprecated since version 0 这个感觉没啥用,也没测试过,先不要用
     * @param bool $yes
     * @return $this
     */
    public function showRecordId($yes = false) {
        $this->option[__FUNCTION__] = $yes;
        return $this;
    }

    /**
     * Returns a tailable cursor for a capped collection.
     * @deprecated since version 0 不懂,先不要用
     * @param type $yes
     * @return MongoQueryOptionBuilder
     */
    public function tailable($yes = false) {
        $this->option[__FUNCTION__] = $yes;
        return $this;
    }

// </editor-fold>
}
