<?php

namespace Api\PhpUtils\Mongo;

use MongoDB\Client;
use MongoDB\Driver\Manager;
use MongoDB\Driver\ReadConcern;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern;

/**
 * MongoDB基类
 * 
 * 1. MongoDB配置信息在yaconf的mongo.ini中，configIndex, databaseName, collectionName 在子类中设置
 * 
 * 2. writeConcern, readConcern, readPreference, typeMap 是可以从MongoDB\Driver\Manager继承的，
 *    可以参考扩展API文档修改
 *    $options = [
 *           'readConcern' => new ReadConcern(ReadConcern::LOCAL),
 *           'readPreference' => new ReadPreference(ReadPreference::RP_SECONDARY_PREFERRED),
 *           'typeMap' => ['root' => 'array'],
 *           'writeConcern' => new WriteConcern(WriteConcern::MAJORITY),
 *       ];
 * @see https://www.php.net/manual/zh/book.mongodb.php
 * 
 * 3. 提供常用的CURD方法，共他方法会调用 __call()方法
 * 
 * 4. MongoDB建议对重要的数据使用 {w: “marjority”} 的选项 （WriteConcern::MAJORITY）。
 *    可以保证数据在复制到多数节点后才返回成功结果。使用该机制可以有效防止数据回滚的发生。
 *    另外你可以使用 {j:1} （可以和 w:”majrotiy” 结合使用）来指定数据必须在写入WAL日志之后才向应用返回成功确认。这个会导致写入性能有所下降，但是对于重要的数据可以考虑使用
 * 
 * 5. 异常的捕获与处理 及 getMatchedCount(), getModifiedCount(), getUpsertedCount() 在业务中按需获取，写入retry（1s，2s，4s...）
 * 
 * 6. 根据镜像中php的monogodb扩展版本选择使用1.4版本 
 * @see https://docs.mongodb.com/drivers/php/       驱动兼容对照表
 * @see https://docs.mongodb.com/php-library/v1.4/  官方文档
 */
abstract class MongoBase
{
    private static $instance = null;
    private $client = null;
    private $database = null;
    private $collection = null;
    const W = 1; // 要求进行写入确认
    const WTIMEOUTMS = 1000; // 写入关注超时时间
    const MAXTIMEMS = 100; //最大执行时间，毫秒

    /**
     * 获取子类实例
     * @return object
     */
    public static function getInstance()
    {
        if (empty(self::$instance)) {
            self::$instance = new static();
        }
        return self::$instance;
    }

    /**
     * 根据配置，构建实例
     */
    final private function __construct()
    {
        $conf = config('mongo', $this->getConfigIndex());
        if (!empty($conf)) {
            $uriOptions = [];
            $driverOptions = [];
            if (isset($conf['uriOptions'])) {
                $uriOptions = $conf['uriOptions'];
                //某些配置项值必须是数字
                foreach ($uriOptions as $key => $value) {
                    $keys = ['connectTimeoutMS', 'socketTimeoutMS', 'wTimeoutMS', 'w'];
                    if (in_array($key, $keys)) {
                        $uriOptions[$key] = intval($value);
                    }
                }
            }
            if (isset($conf['driverOptions'])) {
                $driverOptions = $conf['driverOptions'];
            }

            //初始化客户端
            //兼容多个复制集的情况（rs_interact1.mongo.int.yidian-inc.com:27017,rs_interact2.mongo.int.yidian-inc.com:27017,rs_interact3.mongo.int.yidian-inc.com:27017）
            if (strpos($conf['host'], ':') !== false) {
                $this->client = new Client(
                    "mongodb://{$conf['host']}",
                    $uriOptions,
                    $driverOptions
                );
            }else{
                $this->client = new Client(
                    "mongodb://{$conf['host']}:{$conf['port']}",
                    $uriOptions,
                    $driverOptions
                );
            }
            $this->database = $this->client->selectDatabase($this->getDatabaseName());
            $this->collection = $this->database->selectCollection($this->getCollectionName(), $this->getCollectionOptions());
        }
    }

    /**
     * 不允许clone
     *
     * @return void
     */
    private function __clone(){}

    /**
     * 允许调用Libery自己的方法
     *
     * @param string $method Missing method name
     * @param array  $args   Method arguments
     *
     * @return mixed
     * @throws UnsupportedException if options are not supported by the selected server
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function __call($method, array $args)
    {
        if (!$this->collection) {
            return false;
        }
        
        return call_user_func_array([$this->collection, $method], $args);
    }

    /**
     * 获取mongode配置，由子类实现
     * @return string
     */
    abstract protected function getConfigIndex();

    /**
     * 获取数据库名，由子类实现
     * @return string
     */
    abstract protected function getDatabaseName();

    /**
     * 获取表名，由子类实现
     * @return string
     */
    abstract protected function getCollectionName();

    /**
     * Get Client instance
     *
     * @return object
     */
    public function getClient() 
    {
        return $this->client;
    }

    /**
     * Get Database instance
     *
     * @return object
     */
    public function getDatabase() 
    {
        return $this->database;
    }

    /**
     * Get Collection instance
     *
     * @return object
     */
    public function getCollection() 
    {
        return $this->collection;
    }

    /**
     * 集合的初始化参数
     *
     * 可以继承初始化配置文件中，默认的，或都子类中的
     * 
     * @param array $collectionOptions
     * @return array
     */
    protected function getCollectionOptions(array $collectionOptions = []) 
    {
        $collectionOptions += [
            'readConcern' => $this->client->getreadConcern() ?: new ReadConcern(ReadConcern::LOCAL), //3.2之前的服务器版本不支持此功能
            'readPreference' => $this->client->getReadPreference() ?: new ReadPreference(ReadPreference::RP_SECONDARY_PREFERRED),
            'typeMap' => ['root' => 'array', 'document' => 'array'],
            'writeConcern' => $this->client->getWriteConcern() ?: new WriteConcern(self::W, 3 * self::WTIMEOUTMS),
        ];
        
        return $collectionOptions;
    }

    /**
     * Inserts one document.
     *
     * @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-insertOne/
     * @param array|object $document The document to insert
     * @param array        $options  Command options
     * @return InsertOneResult
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function insertOne($document, array $options = [])
    {
        $options += [
            'writeConcern' => new WriteConcern(self::W, 3 * self::WTIMEOUTMS), // W=1要求进行写入确认, 超时时间3000毫秒
        ];
        return $this->collection->insertOne($document, $options);
    }

    /**
     * Inserts multiple documents.
     *
     * @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-insertMany/
     * @param array[]|object[] $documents The documents to insert
     * @param array            $options   Command options
     * @return InsertManyResult
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function insertMany(array $documents, array $options = [])
    {
        $options += [
            'ordered' => true, // 默认值为true, 当单个写入失败时，该操作将停止而不执行剩余的写入操作并引发异常
            'writeConcern' => new WriteConcern(self::W, 5 * self::WTIMEOUTMS),
        ];
        return $this->collection->insertMany($documents, $options);
    }
    
    /**
     * Deletes at most one document matching the filter.
     *
     * @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-deleteOne/
     * @param array|object $filter  Query by which to delete documents
     * @param array        $options Command options
     * @return DeleteResult
     * @throws UnsupportedException if options are not supported by the selected server
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function deleteOne($filter, array $options = [])
    {
        $options += [
            'writeConcern' => new WriteConcern(self::W, 3 * self::WTIMEOUTMS),
        ];
        return $this->collection->deleteOne($filter, $options);
    }
    
    /**
     * Deletes all documents matching the filter.
     *
     * @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-deleteMany/
     * @param array|object $filter  Query by which to delete documents
     * @param array        $options Command options
     * @return DeleteResult
     * @throws UnsupportedException if options are not supported by the selected server
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function deleteMany($filter, array $options = [])
    {
        $options += [
            'writeConcern' => new WriteConcern(self::W, 5 * self::WTIMEOUTMS), // W=1要求进行写入确认, 超时时间3000毫秒
        ];
        return $this->collection->deleteMany($filter, $options);
    }

    /**
     * Finds a single document matching the query.
     *
     * @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-findOne/
     * @param array|object $filter  Query by which to filter documents
     * @param array        $options Additional options
     * @return array|object|null
     * @throws UnsupportedException if options are not supported by the selected server
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function findOne($filter = [], array $options = [])
    {
        $options += [
            'maxTimeMS' => 3 * self::MAXTIMEMS,
        ];
        return $this->collection->findOne($filter, $options);
    }
    
    /**
     * Finds documents matching the query.
     *
     * @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-find/
     * @param array|object $filter  Query by which to filter documents
     * @param array        $options Additional options
     * @return array
     * @throws UnsupportedException if options are not supported by the selected server
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function find($filter = [], array $options = [])
    {
        $options += [
            'limit' => 1000,
            'maxTimeMS' => 10 * self::MAXTIMEMS,
        ];
        
        $cursor = $this->collection->find($filter, $options);
        return $cursor->toArray();
    }
    
    /**
     * Finds a single document and updates it, returning either the original or
     * the updated document.
     *
     * The document to return may be null if no document matched the filter. By
     * default, the original document is returned. Specify
     * FindOneAndUpdate::RETURN_DOCUMENT_AFTER for the "returnDocument" option
     * to return the updated document.
     *
     * @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-findOneAndDelete/
     * @param array|object $filter  Query by which to filter documents
     * @param array|object $update  Update to apply to the matched document
     * @param array        $options Command options
     * @return array|object|null
     * @throws UnexpectedValueException if the command response was malformed
     * @throws UnsupportedException if options are not supported by the selected server
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function findOneAndUpdate($filter, $update, array $options = [])
    {
        $options += [
            'upsert' => true, // 如果设置为true，则在没有文档符合查询条件时创建一个新文档
            'maxTimeMS' => 5 * self::MAXTIMEMS,
            'returnDocument' => \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_BEFORE, //返回更新之前文档
        ];
        
        return $this->collection->findOneAndUpdate($filter, $update, $options);
    }

    /**
     * Finds a single document and deletes it, returning the original.
     *
     * The document to return may be null if no document matched the filter.
     *
     * @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-findOneAndDelete/
     * @param array|object $filter  Query by which to filter documents
     * @param array        $options Command options
     * @return array|object|null
     * @throws UnexpectedValueException if the command response was malformed
     * @throws UnsupportedException if options are not supported by the selected server
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function findOneAndDelete($filter, array $options = [])
    {
        $options += [
            'maxTimeMS' => 5 * self::MAXTIMEMS,
        ];
        return $this->collection->findOneAndDelete($filter, $options);
    }

    /**
     * Updates at most one document matching the filter.
     *
     * @see http://docs.mongodb.org/manual/reference/command/update/
     * @param array|object $filter  Query by which to filter documents
     * @param array|object $update  Update to apply to the matched document
     * @param array        $options Command options
     * @return UpdateResult
     * @throws UnsupportedException if options are not supported by the selected server
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function updateOne($filter, $update, array $options = [])
    {
        $options += [
            'upsert' => true, // 如果设置为true，则在没有文档符合查询条件时创建一个新文档
        ];
        return $this->collection->updateOne($filter, $update, $options);
    }
    
    /**
     * Updates all documents matching the filter.
     *
     * @see http://docs.mongodb.org/manual/reference/command/update/
     * @param array|object $filter  Query by which to filter documents
     * @param array|object $update  Update to apply to the matched documents
     * @param array        $options Command options
     * @return UpdateResult
     * @throws UnsupportedException if options are not supported by the selected server
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function updateMany($filter, $update, array $options = [])
    {
        $options += [
            'upsert' => true, // 如果设置为true，则在没有文档符合查询条件时创建一个新文档
        ];
        return $this->collection->updateMany($filter, $update, $options);
    }

    /**
     * Gets the number of documents matching the filter.
     *
     * @param array|object $filter  Query by which to filter documents
     * @param array        $options Command options
     * @return integer
     * @throws UnexpectedValueException if the command response was malformed
     * @throws UnsupportedException if options are not supported by the selected server
     * @throws InvalidArgumentException for parameter/option parsing errors
     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
     */
    public function countDocuments($filter = [], array $options = [])
    {
        $options += [
            'maxTimeMS' => 10 * self::MAXTIMEMS, // count操作尽量少做
        ];
        return $this->collection->countDocuments($filter, $options);
    }
}
