/*
 * Copyright 2014-2017 MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifdef HAVE_CONFIG_H
#	include "config.h"
#endif

/* External libs */
#include "bson.h"
#include "mongoc.h"


/* PHP Core stuff */
#include <php.h>
#include <php_ini.h>
#include <ext/standard/info.h>
#include <ext/standard/file.h>
#include <Zend/zend_hash.h>
#include <Zend/zend_interfaces.h>
#include <Zend/zend_exceptions.h>
#include <ext/spl/spl_iterators.h>
#include <ext/spl/spl_exceptions.h>
#include <ext/standard/php_var.h>

#if PHP_VERSION_ID >= 70000
# include <Zend/zend_smart_str.h>
#else
# include <ext/standard/php_smart_str.h>
#endif

/* getpid() */
#if HAVE_UNISTD_H
# include <unistd.h>
#endif
#ifdef PHP_WIN32
# include <process.h>
#endif

/* Stream wrapper */
#include <main/php_streams.h>
#include <main/php_network.h>
/* Debug log writing */
#include <main/php_open_temporary_file.h>
/* For formating timestamp in the log */
#include <ext/date/php_date.h>
/* String manipulation */
#include <Zend/zend_string.h>
/* PHP array helpers */
#include "php_array_api.h"

/* Our Compatability header */
#include "phongo_compat.h"

/* Our stuffz */
#include "php_phongo.h"
#include "php_bson.h"
#include "src/BSON/functions.h"
#include "src/MongoDB/Monitoring/functions.h"

#undef MONGOC_LOG_DOMAIN
#define MONGOC_LOG_DOMAIN "PHONGO"

#define PHONGO_DEBUG_INI "mongodb.debug"
#define PHONGO_DEBUG_INI_DEFAULT ""

ZEND_DECLARE_MODULE_GLOBALS(mongodb)
#if PHP_VERSION_ID >= 70000
#if defined(ZTS) && defined(COMPILE_DL_MONGODB)
	ZEND_TSRMLS_CACHE_DEFINE();
#endif
#endif

/* Declare zend_class_entry dependencies, which are initialized in MINIT */
zend_class_entry *php_phongo_date_immutable_ce;
zend_class_entry *php_phongo_json_serializable_ce;

php_phongo_server_description_type_map_t
php_phongo_server_description_type_map[PHONGO_SERVER_DESCRIPTION_TYPES] = {
	{ PHONGO_SERVER_UNKNOWN, "Unknown" },
	{ PHONGO_SERVER_STANDALONE, "Standalone" },
	{ PHONGO_SERVER_MONGOS, "Mongos" },
	{ PHONGO_SERVER_POSSIBLE_PRIMARY, "PossiblePrimary" },
	{ PHONGO_SERVER_RS_PRIMARY, "RSPrimary" },
	{ PHONGO_SERVER_RS_SECONDARY, "RSSecondary" },
	{ PHONGO_SERVER_RS_ARBITER, "RSArbiter" },
	{ PHONGO_SERVER_RS_OTHER, "RSOther" },
	{ PHONGO_SERVER_RS_GHOST, "RSGhost" },
};

/* {{{ phongo_std_object_handlers */
zend_object_handlers phongo_std_object_handlers;

zend_object_handlers *phongo_get_std_object_handlers(void)
{
	return &phongo_std_object_handlers;
}
/* }}} */

/* Forward declarations */
static bool phongo_split_namespace(const char *namespace, char **dbname, char **cname);

/* {{{ Error reporting and logging */
zend_class_entry* phongo_exception_from_phongo_domain(php_phongo_error_domain_t domain)
{
	switch (domain) {
		case PHONGO_ERROR_INVALID_ARGUMENT:
			return php_phongo_invalidargumentexception_ce;
		case PHONGO_ERROR_LOGIC:
			return php_phongo_logicexception_ce;
		case PHONGO_ERROR_RUNTIME:
			return php_phongo_runtimeexception_ce;
		case PHONGO_ERROR_UNEXPECTED_VALUE:
			return php_phongo_unexpectedvalueexception_ce;
		case PHONGO_ERROR_MONGOC_FAILED:
			return php_phongo_runtimeexception_ce;
		case PHONGO_ERROR_WRITE_FAILED:
			return php_phongo_bulkwriteexception_ce;
		case PHONGO_ERROR_CONNECTION_FAILED:
			return php_phongo_connectionexception_ce;
	}

	MONGOC_ERROR("Resolving unknown phongo error domain: %d", domain);
	return php_phongo_runtimeexception_ce;
}
zend_class_entry* phongo_exception_from_mongoc_domain(uint32_t /* mongoc_error_domain_t */ domain, uint32_t /* mongoc_error_code_t */ code)
{
	switch(code) {
		case 50: /* ExceededTimeLimit */
			return php_phongo_executiontimeoutexception_ce;
		case MONGOC_ERROR_STREAM_SOCKET:
		case MONGOC_ERROR_SERVER_SELECTION_FAILURE:
			return php_phongo_connectiontimeoutexception_ce;
		case MONGOC_ERROR_CLIENT_AUTHENTICATE:
			return php_phongo_authenticationexception_ce;

		case MONGOC_ERROR_COMMAND_INVALID_ARG:
			return php_phongo_invalidargumentexception_ce;

		case MONGOC_ERROR_STREAM_INVALID_TYPE:
		case MONGOC_ERROR_STREAM_INVALID_STATE:
		case MONGOC_ERROR_STREAM_NAME_RESOLUTION:
		case MONGOC_ERROR_STREAM_CONNECT:
		case MONGOC_ERROR_STREAM_NOT_ESTABLISHED:
			return php_phongo_connectionexception_ce;
		case MONGOC_ERROR_CLIENT_NOT_READY:
		case MONGOC_ERROR_CLIENT_TOO_BIG:
		case MONGOC_ERROR_CLIENT_TOO_SMALL:
		case MONGOC_ERROR_CLIENT_GETNONCE:
		case MONGOC_ERROR_CLIENT_NO_ACCEPTABLE_PEER:
		case MONGOC_ERROR_CLIENT_IN_EXHAUST:
		case MONGOC_ERROR_PROTOCOL_INVALID_REPLY:
		case MONGOC_ERROR_PROTOCOL_BAD_WIRE_VERSION:
		case MONGOC_ERROR_CURSOR_INVALID_CURSOR:
		case MONGOC_ERROR_QUERY_FAILURE:
		/*case MONGOC_ERROR_PROTOCOL_ERROR:*/
		case MONGOC_ERROR_BSON_INVALID:
		case MONGOC_ERROR_MATCHER_INVALID:
		case MONGOC_ERROR_NAMESPACE_INVALID:
		case MONGOC_ERROR_COLLECTION_INSERT_FAILED:
		case MONGOC_ERROR_GRIDFS_INVALID_FILENAME:
		case MONGOC_ERROR_QUERY_COMMAND_NOT_FOUND:
		case MONGOC_ERROR_QUERY_NOT_TAILABLE:
			return php_phongo_runtimeexception_ce;
	}
	switch (domain) {
		case MONGOC_ERROR_CLIENT:
		case MONGOC_ERROR_STREAM:
		case MONGOC_ERROR_PROTOCOL:
		case MONGOC_ERROR_CURSOR:
		case MONGOC_ERROR_QUERY:
		case MONGOC_ERROR_INSERT:
		case MONGOC_ERROR_SASL:
		case MONGOC_ERROR_BSON:
		case MONGOC_ERROR_MATCHER:
		case MONGOC_ERROR_NAMESPACE:
		case MONGOC_ERROR_COMMAND:
		case MONGOC_ERROR_COLLECTION:
		case MONGOC_ERROR_GRIDFS:
			/* FIXME: We don't have the Exceptions mocked yet.. */
#if 0
			return phongo_ce_mongo_connection_exception;
#endif
		default:
			return php_phongo_runtimeexception_ce;
	}
}
void phongo_throw_exception(php_phongo_error_domain_t domain TSRMLS_DC, const char *format, ...)
{
	va_list args;
	char *message;
	int message_len;

	va_start(args, format);
	message_len = vspprintf(&message, 0, format, args);
	zend_throw_exception(phongo_exception_from_phongo_domain(domain), message, 0 TSRMLS_CC);
	efree(message);
	va_end(args);
}
void phongo_throw_exception_from_bson_error_t(bson_error_t *error TSRMLS_DC)
{
	zend_throw_exception(phongo_exception_from_mongoc_domain(error->domain, error->code), error->message, error->code TSRMLS_CC);
}
static void php_phongo_log(mongoc_log_level_t log_level, const char *log_domain, const char *message, void *user_data)
{
	phongo_char *dt;

	PHONGO_TSRMLS_FETCH_FROM_CTX(user_data);
	(void)user_data;

	dt = php_format_date((char *) ZEND_STRL("Y-m-d\\TH:i:sP"), time(NULL), 0 TSRMLS_CC);

	fprintf(MONGODB_G(debug_fd), "[%s] %10s: %-8s> %s\n", ZSTR_VAL(dt), log_domain, mongoc_log_level_str(log_level), message);
	fflush(MONGODB_G(debug_fd));
	efree(dt);
}

/* }}} */

/* {{{ Init objects */
static void phongo_cursor_init(zval *return_value, mongoc_client_t *client, mongoc_cursor_t *cursor, zval *readPreference TSRMLS_DC) /* {{{ */
{
	php_phongo_cursor_t *intern;

	object_init_ex(return_value, php_phongo_cursor_ce);

	intern = Z_CURSOR_OBJ_P(return_value);
	intern->cursor = cursor;
	intern->server_id = mongoc_cursor_get_hint(cursor);
	intern->client = client;

	if (readPreference) {
#if PHP_VERSION_ID >= 70000
		ZVAL_ZVAL(&intern->read_preference, readPreference, 1, 0);
#else
		Z_ADDREF_P(readPreference);
		intern->read_preference = readPreference;
#endif
	}
} /* }}} */

static void phongo_cursor_init_for_command(zval *return_value, mongoc_client_t *client, mongoc_cursor_t *cursor, const char *db, zval *command, zval *readPreference TSRMLS_DC) /* {{{ */
{
	php_phongo_cursor_t *intern;

	phongo_cursor_init(return_value, client, cursor, readPreference TSRMLS_CC);
	intern = Z_CURSOR_OBJ_P(return_value);

	intern->database = estrdup(db);

#if PHP_VERSION_ID >= 70000
	ZVAL_ZVAL(&intern->command, command, 1, 0);
#else
	Z_ADDREF_P(command);
	intern->command = command;
#endif
} /* }}} */

static void phongo_cursor_init_for_query(zval *return_value, mongoc_client_t *client, mongoc_cursor_t *cursor, const char *namespace, zval *query, zval *readPreference TSRMLS_DC) /* {{{ */
{
	php_phongo_cursor_t *intern;

	phongo_cursor_init(return_value, client, cursor, readPreference TSRMLS_CC);
	intern = Z_CURSOR_OBJ_P(return_value);

	/* namespace has already been validated by phongo_execute_query() */
	phongo_split_namespace(namespace, &intern->database, &intern->collection);

#if PHP_VERSION_ID >= 70000
	ZVAL_ZVAL(&intern->query, query, 1, 0);
#else
	Z_ADDREF_P(query);
	intern->query = query;
#endif
} /* }}} */

void phongo_server_init(zval *return_value, mongoc_client_t *client, int server_id TSRMLS_DC) /* {{{ */
{
	php_phongo_server_t *server;

	object_init_ex(return_value, php_phongo_server_ce);

	server = Z_SERVER_OBJ_P(return_value);
	server->server_id = server_id;
	server->client = client;
}
/* }}} */

void phongo_readconcern_init(zval *return_value, const mongoc_read_concern_t *read_concern TSRMLS_DC) /* {{{ */
{
	php_phongo_readconcern_t *intern;

	object_init_ex(return_value, php_phongo_readconcern_ce);

	intern = Z_READCONCERN_OBJ_P(return_value);
	intern->read_concern = mongoc_read_concern_copy(read_concern);
}
/* }}} */

void phongo_readpreference_init(zval *return_value, const mongoc_read_prefs_t *read_prefs TSRMLS_DC) /* {{{ */
{
	php_phongo_readpreference_t *intern;

	object_init_ex(return_value, php_phongo_readpreference_ce);

	intern = Z_READPREFERENCE_OBJ_P(return_value);
	intern->read_preference = mongoc_read_prefs_copy(read_prefs);
}
/* }}} */

void phongo_writeconcern_init(zval *return_value, const mongoc_write_concern_t *write_concern TSRMLS_DC) /* {{{ */
{
	php_phongo_writeconcern_t *intern;

	object_init_ex(return_value, php_phongo_writeconcern_ce);

	intern = Z_WRITECONCERN_OBJ_P(return_value);
	intern->write_concern = mongoc_write_concern_copy(write_concern);
}
/* }}} */

zend_bool phongo_writeconcernerror_init(zval *return_value, bson_t *bson TSRMLS_DC) /* {{{ */
{
	bson_iter_t iter;
	php_phongo_writeconcernerror_t *intern;

	object_init_ex(return_value, php_phongo_writeconcernerror_ce);

	intern = Z_WRITECONCERNERROR_OBJ_P(return_value);

	if (bson_iter_init_find(&iter, bson, "code") && BSON_ITER_HOLDS_INT32(&iter)) {
		intern->code = bson_iter_int32(&iter);
	}

	if (bson_iter_init_find(&iter, bson, "errmsg") && BSON_ITER_HOLDS_UTF8(&iter)) {
		uint32_t errmsg_len;
		const char *err_msg = bson_iter_utf8(&iter, &errmsg_len);

		intern->message = estrndup(err_msg, errmsg_len);
	}

	if (bson_iter_init_find(&iter, bson, "errInfo") && BSON_ITER_HOLDS_DOCUMENT(&iter)) {
		uint32_t       len;
		const uint8_t *data = NULL;

		bson_iter_document(&iter, &len, &data);

		if (!php_phongo_bson_to_zval(data, len, &intern->info)) {
			zval_ptr_dtor(&intern->info);
			ZVAL_UNDEF(&intern->info);

			return false;
		}
	}

	return true;
} /* }}} */

zend_bool phongo_writeerror_init(zval *return_value, bson_t *bson TSRMLS_DC) /* {{{ */
{
	bson_iter_t iter;
	php_phongo_writeerror_t *intern;

	object_init_ex(return_value, php_phongo_writeerror_ce);

	intern = Z_WRITEERROR_OBJ_P(return_value);

	if (bson_iter_init_find(&iter, bson, "code") && BSON_ITER_HOLDS_INT32(&iter)) {
		intern->code = bson_iter_int32(&iter);
	}

	if (bson_iter_init_find(&iter, bson, "errmsg") && BSON_ITER_HOLDS_UTF8(&iter)) {
		uint32_t errmsg_len;
		const char *err_msg = bson_iter_utf8(&iter, &errmsg_len);

		intern->message = estrndup(err_msg, errmsg_len);
	}

	if (bson_iter_init_find(&iter, bson, "errInfo") && BSON_ITER_HOLDS_DOCUMENT(&iter)) {
		uint32_t       len;
		const uint8_t *data = NULL;

		bson_iter_document(&iter, &len, &data);

		if (!php_phongo_bson_to_zval(data, len, &intern->info)) {
			zval_ptr_dtor(&intern->info);
			ZVAL_UNDEF(&intern->info);

			return false;
		}
	}

	if (bson_iter_init_find(&iter, bson, "index") && BSON_ITER_HOLDS_INT32(&iter)) {
		intern->index = bson_iter_int32(&iter);
	}

	return true;
} /* }}} */

static php_phongo_writeresult_t *phongo_writeresult_init(zval *return_value, bson_t *reply, mongoc_client_t *client, int server_id TSRMLS_DC) /* {{{ */
{
	php_phongo_writeresult_t *writeresult;

	object_init_ex(return_value, php_phongo_writeresult_ce);

	writeresult = Z_WRITERESULT_OBJ_P(return_value);
	writeresult->reply     = bson_copy(reply);
	writeresult->server_id = server_id;
	writeresult->client = client;

	return writeresult;
} /* }}} */
/* }}} */

/* {{{ CRUD */
/* Splits a namespace name into the database and collection names, allocated with estrdup. */
static bool phongo_split_namespace(const char *namespace, char **dbname, char **cname) /* {{{ */
{
	char *dot = strchr(namespace, '.');

	if (!dot) {
		return false;
	}

	if (cname) {
		*cname = estrdup(namespace + (dot - namespace) + 1);
	}
	if (dbname) {
		*dbname = estrndup(namespace, dot - namespace);
	}

	return true;
} /* }}} */

mongoc_bulk_operation_t *phongo_bulkwrite_init(zend_bool ordered) { /* {{{ */
	return mongoc_bulk_operation_new(ordered);
} /* }}} */

bool phongo_execute_write(mongoc_client_t *client, const char *namespace, php_phongo_bulkwrite_t  *bulk_write, const mongoc_write_concern_t *write_concern, int server_id, zval *return_value, int return_value_used TSRMLS_DC) /* {{{ */
{
	bson_error_t error;
	int success;
	bson_t reply = BSON_INITIALIZER;
	mongoc_bulk_operation_t *bulk = bulk_write->bulk;
	php_phongo_writeresult_t *writeresult;

	if (bulk_write->executed) {
		phongo_throw_exception(PHONGO_ERROR_WRITE_FAILED TSRMLS_CC, "BulkWrite objects may only be executed once and this instance has already been executed");
		return false;
	}

	if (!phongo_split_namespace(namespace, &bulk_write->database, &bulk_write->collection)) {
		phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "%s: %s", "Invalid namespace provided", namespace);
		return false;
	}

	mongoc_bulk_operation_set_database(bulk, bulk_write->database);
	mongoc_bulk_operation_set_collection(bulk, bulk_write->collection);
	mongoc_bulk_operation_set_client(bulk, client);

	/* If a write concern was not specified, libmongoc will use the client's
	 * write concern; however, we should still fetch it for the write result. */
	if (write_concern) {
		mongoc_bulk_operation_set_write_concern(bulk, write_concern);
	} else {
		write_concern = mongoc_client_get_write_concern(client);
	}

	if (server_id > 0) {
		mongoc_bulk_operation_set_hint(bulk, server_id);
	}

	success = mongoc_bulk_operation_execute(bulk, &reply, &error);
	bulk_write->executed = true;

	/* Write succeeded and the user doesn't care for the results */
	if (success && !return_value_used) {
		bson_destroy(&reply);
		return true;
	}

	/* Check for connection related exceptions */
	if (EG(exception)) {
		bson_destroy(&reply);
		return false;
	}

	writeresult = phongo_writeresult_init(return_value, &reply, client, mongoc_bulk_operation_get_hint(bulk) TSRMLS_CC);
	writeresult->write_concern = mongoc_write_concern_copy(write_concern);

	/* The Write failed */
	if (!success) {
		if ((error.domain == MONGOC_ERROR_COMMAND && error.code != MONGOC_ERROR_COMMAND_INVALID_ARG) ||
		    error.domain == MONGOC_ERROR_WRITE_CONCERN) {
			phongo_throw_exception(PHONGO_ERROR_WRITE_FAILED TSRMLS_CC, "%s", error.message);
			phongo_add_exception_prop(ZEND_STRL("writeResult"), return_value TSRMLS_CC);
		} else {
			phongo_throw_exception_from_bson_error_t(&error TSRMLS_CC);
		}
	}

	bson_destroy(&reply);

	return success;
} /* }}} */

/* Advance the cursor and return whether there is an error. On error, the cursor
 * will be destroyed and an exception will be thrown. */
static bool phongo_advance_cursor_and_check_for_error(mongoc_cursor_t *cursor TSRMLS_DC)
{
	const bson_t *doc;

	if (!mongoc_cursor_next(cursor, &doc)) {
		bson_error_t error;

		/* Check for connection related exceptions */
		if (EG(exception)) {
			mongoc_cursor_destroy(cursor);
			return false;
		}

		/* Could simply be no docs, which is not an error */
		if (mongoc_cursor_error(cursor, &error)) {
			phongo_throw_exception_from_bson_error_t(&error TSRMLS_CC);
			mongoc_cursor_destroy(cursor);
			return false;
		}
	}

	return true;
}

int phongo_execute_query(mongoc_client_t *client, const char *namespace, zval *zquery, zval *zreadPreference, int server_id, zval *return_value, int return_value_used TSRMLS_DC) /* {{{ */
{
	const php_phongo_query_t *query;
	mongoc_cursor_t *cursor;
	char *dbname;
	char *collname;
	mongoc_collection_t *collection;

	if (!phongo_split_namespace(namespace, &dbname, &collname)) {
		phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "%s: %s", "Invalid namespace provided", namespace);
		return false;
	}
	collection = mongoc_client_get_collection(client, dbname, collname);
	efree(dbname);
	efree(collname);

	query = Z_QUERY_OBJ_P(zquery);

	if (query->read_concern) {
		mongoc_collection_set_read_concern(collection, query->read_concern);
	}

	cursor = mongoc_collection_find_with_opts(collection, query->filter, query->opts, phongo_read_preference_from_zval(zreadPreference TSRMLS_CC));
	mongoc_collection_destroy(collection);

	if (server_id > 0 && !mongoc_cursor_set_hint(cursor, server_id)) {
		phongo_throw_exception(PHONGO_ERROR_MONGOC_FAILED TSRMLS_CC, "%s", "Could not set cursor server_id");
		return false;
	}

	/* maxAwaitTimeMS must be set before the cursor is sent */
	if (query->max_await_time_ms) {
		mongoc_cursor_set_max_await_time_ms(cursor, query->max_await_time_ms);
	}

	if (!phongo_advance_cursor_and_check_for_error(cursor TSRMLS_CC)) {
		return false;
	}

	if (!return_value_used) {
		mongoc_cursor_destroy(cursor);
		return true;
	}

	phongo_cursor_init_for_query(return_value, client, cursor, namespace, zquery, zreadPreference TSRMLS_CC);
	return true;
} /* }}} */

int phongo_execute_command(mongoc_client_t *client, const char *db, zval *zcommand, zval *zreadPreference, int server_id, zval *return_value, int return_value_used TSRMLS_DC) /* {{{ */
{
	const php_phongo_command_t *command;
	mongoc_cursor_t *cursor;
	bson_iter_t iter;

	command = Z_COMMAND_OBJ_P(zcommand);

	cursor = mongoc_client_command(client, db, MONGOC_QUERY_NONE, 0, 1, 0, command->bson, NULL, phongo_read_preference_from_zval(zreadPreference TSRMLS_CC));

	if (server_id > 0 && !mongoc_cursor_set_hint(cursor, server_id)) {
		phongo_throw_exception(PHONGO_ERROR_MONGOC_FAILED TSRMLS_CC, "%s", "Could not set cursor server_id");
		return false;
	}

	if (!phongo_advance_cursor_and_check_for_error(cursor TSRMLS_CC)) {
		return false;
	}

	if (!return_value_used) {
		mongoc_cursor_destroy(cursor);
		return true;
	}

	if (bson_iter_init_find(&iter, mongoc_cursor_current(cursor), "cursor") && BSON_ITER_HOLDS_DOCUMENT(&iter)) {
		mongoc_cursor_t *cmd_cursor;

		/* According to mongoc_cursor_new_from_command_reply(), the reply bson_t
		 * is ultimately destroyed on both success and failure. Use bson_copy()
		 * to create a writable copy of the const bson_t we fetched above. */
		cmd_cursor = mongoc_cursor_new_from_command_reply(client, bson_copy(mongoc_cursor_current(cursor)), mongoc_cursor_get_hint(cursor));
		mongoc_cursor_destroy(cursor);

		if (!phongo_advance_cursor_and_check_for_error(cmd_cursor TSRMLS_CC)) {
			return false;
		}

		phongo_cursor_init_for_command(return_value, client, cmd_cursor, db, zcommand, zreadPreference TSRMLS_CC);
		return true;
	}

	phongo_cursor_init_for_command(return_value, client, cursor, db, zcommand, zreadPreference TSRMLS_CC);
	return true;
} /* }}} */

/* }}} */

/* {{{ mongoc types from from_zval */
const mongoc_write_concern_t* phongo_write_concern_from_zval(zval *zwrite_concern TSRMLS_DC) /* {{{ */
{
	if (zwrite_concern) {
		php_phongo_writeconcern_t *intern = Z_WRITECONCERN_OBJ_P(zwrite_concern);

		if (intern) {
			return intern->write_concern;
		}
	}

	return NULL;
} /* }}} */

const mongoc_read_concern_t* phongo_read_concern_from_zval(zval *zread_concern TSRMLS_DC) /* {{{ */
{
	if (zread_concern) {
		php_phongo_readconcern_t *intern = Z_READCONCERN_OBJ_P(zread_concern);

		if (intern) {
			return intern->read_concern;
		}
	}

	return NULL;
} /* }}} */

const mongoc_read_prefs_t* phongo_read_preference_from_zval(zval *zread_preference TSRMLS_DC) /* {{{ */
{
	if (zread_preference) {
		php_phongo_readpreference_t *intern = Z_READPREFERENCE_OBJ_P(zread_preference);

		if (intern) {
			return intern->read_preference;
		}
	}

	return NULL;
} /* }}} */
/* }}} */

/* {{{ phongo zval from mongoc types */
void php_phongo_cursor_id_new_from_id(zval *object, int64_t cursorid TSRMLS_DC) /* {{{ */
{
	php_phongo_cursorid_t     *intern;

	object_init_ex(object, php_phongo_cursorid_ce);

	intern = Z_CURSORID_OBJ_P(object);
	intern->id = cursorid;
} /* }}} */

void php_phongo_objectid_new_from_oid(zval *object, const bson_oid_t *oid TSRMLS_DC) /* {{{ */
{
	php_phongo_objectid_t     *intern;

	object_init_ex(object, php_phongo_objectid_ce);

	intern = Z_OBJECTID_OBJ_P(object);
	bson_oid_to_string(oid, intern->oid);
	intern->initialized = true;
} /* }}} */

php_phongo_server_description_type_t php_phongo_server_description_type(mongoc_server_description_t *sd)
{
	const char* name = mongoc_server_description_type(sd);
	int i;

	for (i = 0; i < PHONGO_SERVER_DESCRIPTION_TYPES; i++) {
		if (!strcmp(name, php_phongo_server_description_type_map[i].name)) {
			return php_phongo_server_description_type_map[i].type;
		}
	}

	return PHONGO_SERVER_UNKNOWN;
}

void php_phongo_server_to_zval(zval *retval, mongoc_server_description_t *sd) /* {{{ */
{
	mongoc_host_list_t *host = mongoc_server_description_host(sd);
	const bson_t       *is_master = mongoc_server_description_ismaster(sd);
	bson_iter_t         iter;

	array_init(retval);

	ADD_ASSOC_STRING(retval, "host", host->host);
	ADD_ASSOC_LONG_EX(retval, "port", host->port);
	ADD_ASSOC_LONG_EX(retval, "type", php_phongo_server_description_type(sd));
	ADD_ASSOC_BOOL_EX(retval, "is_primary", !strcmp(mongoc_server_description_type(sd), php_phongo_server_description_type_map[PHONGO_SERVER_RS_PRIMARY].name));
	ADD_ASSOC_BOOL_EX(retval, "is_secondary", !strcmp(mongoc_server_description_type(sd), php_phongo_server_description_type_map[PHONGO_SERVER_RS_SECONDARY].name));
	ADD_ASSOC_BOOL_EX(retval, "is_arbiter", !strcmp(mongoc_server_description_type(sd), php_phongo_server_description_type_map[PHONGO_SERVER_RS_ARBITER].name));
	ADD_ASSOC_BOOL_EX(retval, "is_hidden", bson_iter_init_find_case(&iter, is_master, "hidden") && bson_iter_as_bool(&iter));
	ADD_ASSOC_BOOL_EX(retval, "is_passive", bson_iter_init_find_case(&iter, is_master, "passive") && bson_iter_as_bool(&iter));

	if (bson_iter_init_find(&iter, is_master, "tags") && BSON_ITER_HOLDS_DOCUMENT(&iter)) {
		const uint8_t         *bytes;
		uint32_t       	       len;
		php_phongo_bson_state  state = PHONGO_BSON_STATE_INITIALIZER;

		/* Use native arrays for debugging output */
		state.map.root_type = PHONGO_TYPEMAP_NATIVE_ARRAY;
		state.map.document_type = PHONGO_TYPEMAP_NATIVE_ARRAY;

		bson_iter_document(&iter, &len, &bytes);
		php_phongo_bson_to_zval_ex(bytes, len, &state);

#if PHP_VERSION_ID >= 70000
		ADD_ASSOC_ZVAL_EX(retval, "tags", &state.zchild);
#else
		ADD_ASSOC_ZVAL_EX(retval, "tags", state.zchild);
#endif
	}

	{
		php_phongo_bson_state  state = PHONGO_BSON_STATE_INITIALIZER;
		/* Use native arrays for debugging output */
		state.map.root_type = PHONGO_TYPEMAP_NATIVE_ARRAY;
		state.map.document_type = PHONGO_TYPEMAP_NATIVE_ARRAY;

		php_phongo_bson_to_zval_ex(bson_get_data(is_master), is_master->len, &state);

#if PHP_VERSION_ID >= 70000
		ADD_ASSOC_ZVAL_EX(retval, "last_is_master", &state.zchild);
#else
		ADD_ASSOC_ZVAL_EX(retval, "last_is_master", state.zchild);
#endif
	}
	ADD_ASSOC_LONG_EX(retval, "round_trip_time", (phongo_long) mongoc_server_description_round_trip_time(sd));

} /* }}} */

void php_phongo_read_concern_to_zval(zval *retval, const mongoc_read_concern_t *read_concern) /* {{{ */
{
	const char *level = mongoc_read_concern_get_level(read_concern);

	array_init_size(retval, 1);

	if (level) {
		ADD_ASSOC_STRING(retval, "level", level);
	}
} /* }}} */

/* Prepare tagSets for BSON encoding by converting each array in the set to an
 * object. This ensures that empty arrays will serialize as empty documents.
 *
 * php_phongo_read_preference_tags_are_valid() handles actual validation of the
 * tag set structure. */
void php_phongo_read_preference_prep_tagsets(zval *tagSets TSRMLS_DC) /* {{{ */
{
	HashTable     *ht_data;

	if (Z_TYPE_P(tagSets) != IS_ARRAY) {
		return;
	}

	ht_data = HASH_OF(tagSets);

#if PHP_VERSION_ID >= 70000
	{
		zval *tagSet;

		ZEND_HASH_FOREACH_VAL(ht_data, tagSet) {
			ZVAL_DEREF(tagSet);
			if (Z_TYPE_P(tagSet) == IS_ARRAY) {
				SEPARATE_ZVAL_NOREF(tagSet);
				convert_to_object(tagSet);
			}
		} ZEND_HASH_FOREACH_END();
	}
#else
	{
		HashPosition   pos;
		zval         **tagSet;

		for (zend_hash_internal_pointer_reset_ex(ht_data, &pos);
		     zend_hash_get_current_data_ex(ht_data, (void **) &tagSet, &pos) == SUCCESS;
		     zend_hash_move_forward_ex(ht_data, &pos)) {
			if (Z_TYPE_PP(tagSet) == IS_ARRAY) {
				SEPARATE_ZVAL_IF_NOT_REF(tagSet);
				convert_to_object(*tagSet);
			}
		}
	}
#endif

	return;
} /* }}} */

/* Checks if tags is valid to set on a mongoc_read_prefs_t. It may be null or an
 * array of one or more documents. */
bool php_phongo_read_preference_tags_are_valid(const bson_t *tags) /* {{{ */
{
	bson_iter_t iter;

	if (bson_empty0(tags)) {
		return true;
	}

	if (!bson_iter_init(&iter, tags)) {
		return false;
	}

	while (bson_iter_next(&iter)) {
		if (!BSON_ITER_HOLDS_DOCUMENT(&iter)) {
			return false;
		}
	}

	return true;
} /* }}} */

void php_phongo_read_preference_to_zval(zval *retval, const mongoc_read_prefs_t *read_prefs) /* {{{ */
{
	const bson_t *tags = mongoc_read_prefs_get_tags(read_prefs);
	mongoc_read_mode_t mode = mongoc_read_prefs_get_mode(read_prefs);

	array_init_size(retval, 3);

	switch (mode) {
		case MONGOC_READ_PRIMARY: ADD_ASSOC_STRING(retval, "mode", "primary"); break;
		case MONGOC_READ_PRIMARY_PREFERRED: ADD_ASSOC_STRING(retval, "mode", "primaryPreferred"); break;
		case MONGOC_READ_SECONDARY: ADD_ASSOC_STRING(retval, "mode", "secondary"); break;
		case MONGOC_READ_SECONDARY_PREFERRED: ADD_ASSOC_STRING(retval, "mode", "secondaryPreferred"); break;
		case MONGOC_READ_NEAREST: ADD_ASSOC_STRING(retval, "mode", "nearest"); break;
		default: /* Do nothing */
			break;
	}

	if (!bson_empty0(tags)) {
		/* Use PHONGO_TYPEMAP_NATIVE_ARRAY for the root type since tags is an
		 * array; however, inner documents and arrays can use the default. */
		php_phongo_bson_state  state = PHONGO_BSON_STATE_INITIALIZER;
		state.map.root_type = PHONGO_TYPEMAP_NATIVE_ARRAY;

		php_phongo_bson_to_zval_ex(bson_get_data(tags), tags->len, &state);
#if PHP_VERSION_ID >= 70000
		ADD_ASSOC_ZVAL_EX(retval, "tags", &state.zchild);
#else
		ADD_ASSOC_ZVAL_EX(retval, "tags", state.zchild);
#endif
	}

	if (mongoc_read_prefs_get_max_staleness_seconds(read_prefs) != MONGOC_NO_MAX_STALENESS) {
		ADD_ASSOC_LONG_EX(retval, "maxStalenessSeconds", mongoc_read_prefs_get_max_staleness_seconds(read_prefs));
	}
} /* }}} */

void php_phongo_write_concern_to_zval(zval *retval, const mongoc_write_concern_t *write_concern) /* {{{ */
{
	const char *wtag = mongoc_write_concern_get_wtag(write_concern);
	const int32_t w = mongoc_write_concern_get_w(write_concern);
	const int32_t wtimeout = mongoc_write_concern_get_wtimeout(write_concern);

	array_init_size(retval, 4);

	if (wtag) {
		ADD_ASSOC_STRING(retval, "w", wtag);
	} else if (mongoc_write_concern_get_wmajority(write_concern)) {
		ADD_ASSOC_STRING(retval, "w", PHONGO_WRITE_CONCERN_W_MAJORITY);
	} else if (w != MONGOC_WRITE_CONCERN_W_DEFAULT) {
		ADD_ASSOC_LONG_EX(retval, "w", w);
	}

	if (mongoc_write_concern_journal_is_set(write_concern)) {
		ADD_ASSOC_BOOL_EX(retval, "j", mongoc_write_concern_get_journal(write_concern));
	}

	if (wtimeout != 0) {
		ADD_ASSOC_LONG_EX(retval, "wtimeout", wtimeout);
	}
} /* }}} */
/* }}} */


static mongoc_uri_t *php_phongo_make_uri(const char *uri_string, bson_t *options TSRMLS_DC) /* {{{ */
{
	mongoc_uri_t *uri;
	bson_error_t  error;

	uri = mongoc_uri_new_with_error(uri_string, &error);
	MONGOC_DEBUG("Connection string: '%s'", uri_string);

	if (!uri) {
		phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Failed to parse MongoDB URI: '%s'. %s.", uri_string, error.message);
		return NULL;
	}

	return uri;
} /* }}} */

static const char *php_phongo_bson_type_to_string(bson_type_t type) /* {{{ */
{
	switch (type) {
		case BSON_TYPE_EOD: return "EOD";
		case BSON_TYPE_DOUBLE: return "double";
		case BSON_TYPE_UTF8: return "string";
		case BSON_TYPE_DOCUMENT: return "document";
		case BSON_TYPE_ARRAY: return "array";
		case BSON_TYPE_BINARY: return "Binary";
		case BSON_TYPE_UNDEFINED: return "undefined";
		case BSON_TYPE_OID: return "ObjectId";
		case BSON_TYPE_BOOL: return "boolean";
		case BSON_TYPE_DATE_TIME: return "UTCDateTime";
		case BSON_TYPE_NULL: return "null";
		case BSON_TYPE_REGEX: return "Regex";
		case BSON_TYPE_DBPOINTER: return "DBPointer";
		case BSON_TYPE_CODE: return "Javascript";
		case BSON_TYPE_SYMBOL: return "symbol";
		case BSON_TYPE_CODEWSCOPE: return "Javascript with scope";
		case BSON_TYPE_INT32: return "32-bit integer";
		case BSON_TYPE_TIMESTAMP: return "Timestamp";
		case BSON_TYPE_INT64: return "64-bit integer";
		case BSON_TYPE_DECIMAL128: return "Decimal128";
		case BSON_TYPE_MAXKEY: return "MaxKey";
		case BSON_TYPE_MINKEY: return "MinKey";
		default: return "unknown";
	}
} /* }}} */

#define PHONGO_URI_INVALID_TYPE(iter, expected) \
	phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, \
	                       "Expected %s for \"%s\" URI option, %s given", \
	                       (expected), \
	                       bson_iter_key(&(iter)), \
	                       php_phongo_bson_type_to_string(bson_iter_type(&(iter))))

static bool php_phongo_apply_options_to_uri(mongoc_uri_t *uri, bson_t *options TSRMLS_DC) /* {{{ */
{
	bson_iter_t iter;

	/* Return early if there are no options to apply */
	if (bson_empty0(options) || !bson_iter_init(&iter, options)) {
		return true;
	}

	while (bson_iter_next(&iter)) {
		const char *key = bson_iter_key(&iter);

		/* Skip read preference, read concern, and write concern options, as
		 * those will be processed by other functions. */
		if (!strcasecmp(key, MONGOC_URI_JOURNAL) ||
		    !strcasecmp(key, MONGOC_URI_MAXSTALENESSSECONDS) ||
		    !strcasecmp(key, MONGOC_URI_READCONCERNLEVEL) ||
		    !strcasecmp(key, MONGOC_URI_READPREFERENCE) ||
		    !strcasecmp(key, MONGOC_URI_READPREFERENCETAGS) ||
		    !strcasecmp(key, MONGOC_URI_SAFE) ||
		    !strcasecmp(key, MONGOC_URI_SLAVEOK) ||
		    !strcasecmp(key, MONGOC_URI_W) ||
		    !strcasecmp(key, MONGOC_URI_WTIMEOUTMS)) {
			continue;
		}

		if (mongoc_uri_option_is_bool(key)) {
			/* The option's type is not validated because bson_iter_as_bool() is
			 * used to cast the value to a boolean. Validation may be introduced
			 * in PHPC-990. */
			if (!mongoc_uri_set_option_as_bool(uri, key, bson_iter_as_bool(&iter))) {
				phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Failed to parse \"%s\" URI option", key);
				return false;
			}

			continue;
		}

		if (mongoc_uri_option_is_int32(key)) {
			if (!BSON_ITER_HOLDS_INT32(&iter)) {
				PHONGO_URI_INVALID_TYPE(iter, "32-bit integer");
				return false;
			}

			if (!mongoc_uri_set_option_as_int32(uri, key, bson_iter_int32(&iter))) {
				phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Failed to parse \"%s\" URI option", key);
				return false;
			}

			continue;
		}

		if (mongoc_uri_option_is_utf8(key)) {
			if (!BSON_ITER_HOLDS_UTF8(&iter)) {
				PHONGO_URI_INVALID_TYPE(iter, "string");
				return false;
			}

			if (!mongoc_uri_set_option_as_utf8(uri, key, bson_iter_utf8(&iter, NULL))) {
				/* Assignment uses mongoc_uri_set_appname() for the "appname"
				 * option, which validates length in addition to UTF-8 encoding.
				 * For BC, we report the invalid string to the user. */
				if (!strcasecmp(key, MONGOC_URI_APPNAME)) {
					phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Invalid appname value: '%s'", bson_iter_utf8(&iter, NULL));
				} else {
					phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Failed to parse \"%s\" URI option", key);
				}
				return false;
			}

			continue;
		}

		if (!strcasecmp(key, "username")) {
			if (!BSON_ITER_HOLDS_UTF8(&iter)) {
				PHONGO_URI_INVALID_TYPE(iter, "string");
				return false;
			}

			if (!mongoc_uri_set_username(uri, bson_iter_utf8(&iter, NULL))) {
				phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Failed to parse \"%s\" URI option", key);
				return false;
			}

			continue;
		}

		if (!strcasecmp(key, "password")) {
			if (!BSON_ITER_HOLDS_UTF8(&iter)) {
				PHONGO_URI_INVALID_TYPE(iter, "string");
				return false;
			}

			if (!mongoc_uri_set_password(uri, bson_iter_utf8(&iter, NULL))) {
				phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Failed to parse \"%s\" URI option", key);
				return false;
			}

			continue;
		}

		if (!strcasecmp(key, MONGOC_URI_AUTHMECHANISM)) {
			if (!BSON_ITER_HOLDS_UTF8(&iter)) {
				PHONGO_URI_INVALID_TYPE(iter, "string");
				return false;
			}

			if (!mongoc_uri_set_auth_mechanism(uri, bson_iter_utf8(&iter, NULL))) {
				phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Failed to parse \"%s\" URI option", key);
				return false;
			}

			continue;
		}

		if (!strcasecmp(key, MONGOC_URI_AUTHSOURCE)) {
			if (!BSON_ITER_HOLDS_UTF8(&iter)) {
				PHONGO_URI_INVALID_TYPE(iter, "string");
				return false;
			}

			if (!mongoc_uri_set_auth_source(uri, bson_iter_utf8(&iter, NULL))) {
				phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Failed to parse \"%s\" URI option", key);
				return false;
			}

			continue;
		}

		if (!strcasecmp(key, MONGOC_URI_AUTHMECHANISMPROPERTIES)) {
			bson_t properties;
			uint32_t len;
			const uint8_t *data;

			if (!BSON_ITER_HOLDS_DOCUMENT(&iter)) {
				PHONGO_URI_INVALID_TYPE(iter, "array or object");
				return false;
			}

			bson_iter_document(&iter, &len, &data);

			if (!bson_init_static(&properties, data, len)) {
				phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Could not initialize BSON structure for auth mechanism properties");
				return false;
			}

			if (!mongoc_uri_set_mechanism_properties(uri, &properties)) {
				phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Failed to parse \"%s\" URI option", key);
				return false;
			}

			continue;
		}
	}

	return true;
} /* }}} */

static bool php_phongo_apply_rc_options_to_uri(mongoc_uri_t *uri, bson_t *options TSRMLS_DC) /* {{{ */
{
	bson_iter_t iter;
	mongoc_read_concern_t *new_rc;
	const mongoc_read_concern_t *old_rc;

	if (!(old_rc = mongoc_uri_get_read_concern(uri))) {
		phongo_throw_exception(PHONGO_ERROR_MONGOC_FAILED TSRMLS_CC, "mongoc_uri_t does not have a read concern");

		return false;
	}

	/* Return early if there are no options to apply */
	if (bson_empty0(options)) {
		return true;
	}

	if (!bson_iter_init_find_case(&iter, options, MONGOC_URI_READCONCERNLEVEL)) {
		return true;
	}

	new_rc = mongoc_read_concern_copy(old_rc);

	if (bson_iter_init_find_case(&iter, options, MONGOC_URI_READCONCERNLEVEL)) {
		if (!BSON_ITER_HOLDS_UTF8(&iter)) {
			PHONGO_URI_INVALID_TYPE(iter, "string");
			mongoc_read_concern_destroy(new_rc);

			return false;
		}

		mongoc_read_concern_set_level(new_rc, bson_iter_utf8(&iter, NULL));
	}

	mongoc_uri_set_read_concern(uri, new_rc);
	mongoc_read_concern_destroy(new_rc);

	return true;
} /* }}} */

static bool php_phongo_apply_rp_options_to_uri(mongoc_uri_t *uri, bson_t *options TSRMLS_DC) /* {{{ */
{
	bson_iter_t iter;
	mongoc_read_prefs_t *new_rp;
	const mongoc_read_prefs_t *old_rp;

	if (!(old_rp = mongoc_uri_get_read_prefs_t(uri))) {
		phongo_throw_exception(PHONGO_ERROR_MONGOC_FAILED TSRMLS_CC, "mongoc_uri_t does not have a read preference");

		return false;
	}

	/* Return early if there are no options to apply */
	if (bson_empty0(options)) {
		return true;
	}

	if (!bson_iter_init_find_case(&iter, options, MONGOC_URI_SLAVEOK) &&
	    !bson_iter_init_find_case(&iter, options, MONGOC_URI_READPREFERENCE) &&
	    !bson_iter_init_find_case(&iter, options, MONGOC_URI_READPREFERENCETAGS) &&
	    !bson_iter_init_find_case(&iter, options, MONGOC_URI_MAXSTALENESSSECONDS)
	) {
		return true;
	}

	new_rp = mongoc_read_prefs_copy(old_rp);

	if (bson_iter_init_find_case(&iter, options, MONGOC_URI_SLAVEOK)) {
		if (!BSON_ITER_HOLDS_BOOL(&iter)) {
			PHONGO_URI_INVALID_TYPE(iter, "boolean");
			mongoc_read_prefs_destroy(new_rp);

			return false;
		}

		if (bson_iter_bool(&iter)) {
			mongoc_read_prefs_set_mode(new_rp, MONGOC_READ_SECONDARY_PREFERRED);
		}
	}

	if (bson_iter_init_find_case(&iter, options, MONGOC_URI_READPREFERENCE)) {
		const char *str;

		if (!BSON_ITER_HOLDS_UTF8(&iter)) {
			PHONGO_URI_INVALID_TYPE(iter, "string");
			mongoc_read_prefs_destroy(new_rp);

			return false;
		}

		str = bson_iter_utf8(&iter, NULL);

		if (0 == strcasecmp("primary", str)) {
			mongoc_read_prefs_set_mode(new_rp, MONGOC_READ_PRIMARY);
		} else if (0 == strcasecmp("primarypreferred", str)) {
			mongoc_read_prefs_set_mode(new_rp, MONGOC_READ_PRIMARY_PREFERRED);
		} else if (0 == strcasecmp("secondary", str)) {
			mongoc_read_prefs_set_mode(new_rp, MONGOC_READ_SECONDARY);
		} else if (0 == strcasecmp("secondarypreferred", str)) {
			mongoc_read_prefs_set_mode(new_rp, MONGOC_READ_SECONDARY_PREFERRED);
		} else if (0 == strcasecmp("nearest", str)) {
			mongoc_read_prefs_set_mode(new_rp, MONGOC_READ_NEAREST);
		} else {
			phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Unsupported %s value: '%s'", bson_iter_key(&iter), str);
			mongoc_read_prefs_destroy(new_rp);

			return false;
		}
	}

	if (bson_iter_init_find_case(&iter, options, MONGOC_URI_READPREFERENCETAGS)) {
		bson_t tags;
		uint32_t len;
		const uint8_t *data;

		if (!BSON_ITER_HOLDS_ARRAY(&iter)) {
			PHONGO_URI_INVALID_TYPE(iter, "array");
			mongoc_read_prefs_destroy(new_rp);

			return false;
		}

		bson_iter_array(&iter, &len, &data);

		if (!bson_init_static(&tags, data, len)) {
			phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Could not initialize BSON structure for read preference tags");
			mongoc_read_prefs_destroy(new_rp);

			return false;
		}

		if (!php_phongo_read_preference_tags_are_valid(&tags)) {
			phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Read preference tags must be an array of zero or more documents");
			mongoc_read_prefs_destroy(new_rp);

			return false;
		}

		mongoc_read_prefs_set_tags(new_rp, &tags);
	}

	if (mongoc_read_prefs_get_mode(new_rp) == MONGOC_READ_PRIMARY &&
	    !bson_empty(mongoc_read_prefs_get_tags(new_rp))) {
		phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Primary read preference mode conflicts with tags");
		mongoc_read_prefs_destroy(new_rp);

		return false;
	}

	/* Handle maxStalenessSeconds, and make sure it is not combined with primary
	 * readPreference */
	if (bson_iter_init_find_case(&iter, options, MONGOC_URI_MAXSTALENESSSECONDS)) {
		int64_t max_staleness_seconds;

		if (!BSON_ITER_HOLDS_INT(&iter)) {
			PHONGO_URI_INVALID_TYPE(iter, "integer");
			mongoc_read_prefs_destroy(new_rp);

			return false;
		}

		max_staleness_seconds = bson_iter_as_int64(&iter);

		if (max_staleness_seconds != MONGOC_NO_MAX_STALENESS) {

			if (max_staleness_seconds < MONGOC_SMALLEST_MAX_STALENESS_SECONDS) {
				phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected maxStalenessSeconds to be >= %d, %" PRId64 " given", MONGOC_SMALLEST_MAX_STALENESS_SECONDS, max_staleness_seconds);
				mongoc_read_prefs_destroy(new_rp);

				return false;
			}

			if (max_staleness_seconds > INT32_MAX) {
				phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected maxStalenessSeconds to be <= %d, %" PRId64 " given", INT32_MAX, max_staleness_seconds);
				mongoc_read_prefs_destroy(new_rp);

				return false;
			}

			if (mongoc_read_prefs_get_mode(new_rp) == MONGOC_READ_PRIMARY) {
				phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Primary read preference mode conflicts with maxStalenessSeconds");
				mongoc_read_prefs_destroy(new_rp);

				return false;
			}
		}

		mongoc_read_prefs_set_max_staleness_seconds(new_rp, max_staleness_seconds);
	}

	/* This may be redundant in light of the last check (primary with tags), but
	 * we'll check anyway in case additional validation is implemented. */
	if (!mongoc_read_prefs_is_valid(new_rp)) {
		phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Read preference is not valid");
		mongoc_read_prefs_destroy(new_rp);

		return false;
	}

	mongoc_uri_set_read_prefs_t(uri, new_rp);
	mongoc_read_prefs_destroy(new_rp);

	return true;
} /* }}} */

static bool php_phongo_apply_wc_options_to_uri(mongoc_uri_t *uri, bson_t *options TSRMLS_DC) /* {{{ */
{
	bson_iter_t iter;
	int32_t wtimeoutms;
	mongoc_write_concern_t *new_wc;
	const mongoc_write_concern_t *old_wc;

	if (!(old_wc = mongoc_uri_get_write_concern(uri))) {
		phongo_throw_exception(PHONGO_ERROR_MONGOC_FAILED TSRMLS_CC, "mongoc_uri_t does not have a write concern");

		return false;
	}

	/* Return early if there are no options to apply */
	if (bson_empty0(options)) {
		return true;
	}

	if (!bson_iter_init_find_case(&iter, options, MONGOC_URI_JOURNAL) &&
	    !bson_iter_init_find_case(&iter, options, MONGOC_URI_SAFE) &&
	    !bson_iter_init_find_case(&iter, options, MONGOC_URI_W) &&
	    !bson_iter_init_find_case(&iter, options, MONGOC_URI_WTIMEOUTMS)) {
		return true;
	}

	wtimeoutms = mongoc_write_concern_get_wtimeout(old_wc);

	new_wc = mongoc_write_concern_copy(old_wc);

	if (bson_iter_init_find_case(&iter, options, MONGOC_URI_SAFE)) {
		if (!BSON_ITER_HOLDS_BOOL(&iter)) {
			PHONGO_URI_INVALID_TYPE(iter, "boolean");
			mongoc_write_concern_destroy(new_wc);

			return false;
		}

		mongoc_write_concern_set_w(new_wc, bson_iter_bool(&iter) ? 1 : MONGOC_WRITE_CONCERN_W_UNACKNOWLEDGED);
	}

	if (bson_iter_init_find_case(&iter, options, MONGOC_URI_WTIMEOUTMS)) {
		if (!BSON_ITER_HOLDS_INT32(&iter)) {
			PHONGO_URI_INVALID_TYPE(iter, "32-bit integer");
			mongoc_write_concern_destroy(new_wc);

			return false;
		}

		wtimeoutms = bson_iter_int32(&iter);
	}

	if (bson_iter_init_find_case(&iter, options, MONGOC_URI_JOURNAL)) {
		if (!BSON_ITER_HOLDS_BOOL(&iter)) {
			PHONGO_URI_INVALID_TYPE(iter, "boolean");
			mongoc_write_concern_destroy(new_wc);

			return false;
		}

		mongoc_write_concern_set_journal(new_wc, bson_iter_bool(&iter));
	}

	if (bson_iter_init_find_case(&iter, options, MONGOC_URI_W)) {
		if (BSON_ITER_HOLDS_INT32(&iter)) {
			int32_t value = bson_iter_int32(&iter);

			switch (value) {
				case MONGOC_WRITE_CONCERN_W_ERRORS_IGNORED:
				case MONGOC_WRITE_CONCERN_W_UNACKNOWLEDGED:
					mongoc_write_concern_set_w(new_wc, value);
					break;

				default:
					if (value > 0) {
						mongoc_write_concern_set_w(new_wc, value);
						break;
					}
					phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Unsupported w value: %d", value);
					mongoc_write_concern_destroy(new_wc);

					return false;
			}
		} else if (BSON_ITER_HOLDS_UTF8(&iter)) {
			const char *str = bson_iter_utf8(&iter, NULL);

			if (0 == strcasecmp(PHONGO_WRITE_CONCERN_W_MAJORITY, str)) {
				mongoc_write_concern_set_wmajority(new_wc, wtimeoutms);
			} else {
				mongoc_write_concern_set_wtag(new_wc, str);
			}
		} else {
			PHONGO_URI_INVALID_TYPE(iter, "32-bit integer or string");
			mongoc_write_concern_destroy(new_wc);

			return false;
		}
	}

	/* Only set wtimeout if it's still applicable; otherwise, clear it. */
	if (mongoc_write_concern_get_w(new_wc) > 1 ||
		mongoc_write_concern_get_wmajority(new_wc) ||
		mongoc_write_concern_get_wtag(new_wc)) {
		mongoc_write_concern_set_wtimeout(new_wc, wtimeoutms);
	} else {
		mongoc_write_concern_set_wtimeout(new_wc, 0);
	}

	if (mongoc_write_concern_get_journal(new_wc)) {
		int32_t w = mongoc_write_concern_get_w(new_wc);

		if (w == MONGOC_WRITE_CONCERN_W_UNACKNOWLEDGED || w == MONGOC_WRITE_CONCERN_W_ERRORS_IGNORED) {
			phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Journal conflicts with w value: %d", w);
			mongoc_write_concern_destroy(new_wc);

			return false;
		}
	}

	/* This may be redundant in light of the last check (unacknowledged w with
	   journal), but we'll check anyway in case additional validation is
	   implemented. */
	if (!mongoc_write_concern_is_valid(new_wc)) {
		phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Write concern is not valid");
		mongoc_write_concern_destroy(new_wc);

		return false;
	}

	mongoc_uri_set_write_concern(uri, new_wc);
	mongoc_write_concern_destroy(new_wc);

	return true;
} /* }}} */

#ifdef MONGOC_ENABLE_SSL
static inline char *php_phongo_fetch_ssl_opt_string(zval *zoptions, const char *key, int key_len)
{
	int        plen;
	zend_bool  pfree;
	char      *pval, *value;

	pval = php_array_fetchl_string(zoptions, key, key_len, &plen, &pfree);
	value = pfree ? pval : estrndup(pval, plen);

	return value;
}

static mongoc_ssl_opt_t *php_phongo_make_ssl_opt(zval *zoptions TSRMLS_DC)
{
	mongoc_ssl_opt_t *ssl_opt;

	if (!zoptions) {
		return NULL;
	}

#if defined(MONGOC_ENABLE_SSL_SECURE_CHANNEL) || defined(MONGOC_ENABLE_SSL_SECURE_TRANSPORT)
	if (php_array_existsc(zoptions, "ca_dir")) {
		phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "\"ca_dir\" option is not supported by Secure Channel and Secure Transport");
		return NULL;
	}

	if (php_array_existsc(zoptions, "capath")) {
		phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "\"capath\" option is not supported by Secure Channel and Secure Transport");
		return NULL;
	}
#endif

#if defined(MONGOC_ENABLE_SSL_LIBRESSL) || defined(MONGOC_ENABLE_SSL_SECURE_TRANSPORT)
	if (php_array_existsc(zoptions, "crl_file")) {
		phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "\"crl_file\" option is not supported by LibreSSL and Secure Transport");
		return NULL;
	}
#endif

	ssl_opt = ecalloc(1, sizeof(mongoc_ssl_opt_t));

	/* Check canonical option names first and fall back to SSL context options
	 * for backwards compatibility. */
	if (php_array_existsc(zoptions, "allow_invalid_hostname")) {
		ssl_opt->allow_invalid_hostname = php_array_fetchc_bool(zoptions, "allow_invalid_hostname");
	}

	if (php_array_existsc(zoptions, "weak_cert_validation")) {
		ssl_opt->weak_cert_validation = php_array_fetchc_bool(zoptions, "weak_cert_validation");
	} else if (php_array_existsc(zoptions, "allow_self_signed")) {
		ssl_opt->weak_cert_validation = php_array_fetchc_bool(zoptions, "allow_self_signed");
	}

	if (php_array_existsc(zoptions, "pem_file")) {
		ssl_opt->pem_file = php_phongo_fetch_ssl_opt_string(zoptions, ZEND_STRL("pem_file"));
	} else if (php_array_existsc(zoptions, "local_cert")) {
		ssl_opt->pem_file = php_phongo_fetch_ssl_opt_string(zoptions, ZEND_STRL("local_cert"));
	}

	if (php_array_existsc(zoptions, "pem_pwd")) {
		ssl_opt->pem_pwd = php_phongo_fetch_ssl_opt_string(zoptions, ZEND_STRL("pem_pwd"));
	} else if (php_array_existsc(zoptions, "passphrase")) {
		ssl_opt->pem_pwd = php_phongo_fetch_ssl_opt_string(zoptions, ZEND_STRL("passphrase"));
	}

	if (php_array_existsc(zoptions, "ca_file")) {
		ssl_opt->ca_file = php_phongo_fetch_ssl_opt_string(zoptions, ZEND_STRL("ca_file"));
	} else if (php_array_existsc(zoptions, "cafile")) {
		ssl_opt->ca_file = php_phongo_fetch_ssl_opt_string(zoptions, ZEND_STRL("cafile"));
	}

	if (php_array_existsc(zoptions, "ca_dir")) {
		ssl_opt->ca_dir = php_phongo_fetch_ssl_opt_string(zoptions, ZEND_STRL("ca_dir"));
	} else if (php_array_existsc(zoptions, "capath")) {
		ssl_opt->ca_dir = php_phongo_fetch_ssl_opt_string(zoptions, ZEND_STRL("capath"));
	}

	if (php_array_existsc(zoptions, "crl_file")) {
		ssl_opt->crl_file = php_phongo_fetch_ssl_opt_string(zoptions, ZEND_STRL("crl_file"));
	}

	return ssl_opt;
}

static void php_phongo_free_ssl_opt(mongoc_ssl_opt_t *ssl_opt)
{
	if (ssl_opt->pem_file) {
		str_efree(ssl_opt->pem_file);
	}

	if (ssl_opt->pem_pwd) {
		str_efree(ssl_opt->pem_pwd);
	}

	if (ssl_opt->ca_file) {
		str_efree(ssl_opt->ca_file);
	}

	if (ssl_opt->ca_dir) {
		str_efree(ssl_opt->ca_dir);
	}

	if (ssl_opt->crl_file) {
		str_efree(ssl_opt->crl_file);
	}

	efree(ssl_opt);
}
#endif

/* APM callbacks */
static void php_phongo_dispatch_handlers(const char *name, zval *z_event)
{
#if PHP_VERSION_ID >= 70000
	zval        *value;

	ZEND_HASH_FOREACH_VAL(MONGODB_G(subscribers), value) {
		/* We can't use the zend_call_method_with_1_params macro here, as it
		 * does a sizeof() on the name argument, which does only work with
		 * constant names, but not with parameterized ones as it does
		 * "sizeof(char*)" in that case. */
		zend_call_method(value, NULL, NULL, name, strlen(name), NULL, 1, z_event, NULL TSRMLS_CC);
	} ZEND_HASH_FOREACH_END();
#else
	HashPosition pos;
	TSRMLS_FETCH();

	zend_hash_internal_pointer_reset_ex(MONGODB_G(subscribers), &pos);
	for (;; zend_hash_move_forward_ex(MONGODB_G(subscribers), &pos)) {
		zval  **value;

		if (zend_hash_get_current_data_ex(MONGODB_G(subscribers), (void **) &value, &pos) == FAILURE) {
			break;
		}

		/* We can't use the zend_call_method_with_1_params macro here, as it
		 * does a sizeof() on the name argument, which does only work with
		 * constant names, but not with parameterized ones as it does
		 * "sizeof(char*)" in that case. */
		zend_call_method(value, NULL, NULL, name, strlen(name), NULL, 1, z_event, NULL TSRMLS_CC);
	}
#endif
}

static void php_phongo_command_started(const mongoc_apm_command_started_t *event)
{
	php_phongo_commandstartedevent_t *p_event;
#if PHP_VERSION_ID >= 70000
	zval  z_event;
#else
	zval *z_event = NULL;
#endif
	TSRMLS_FETCH();

	/* Return early if there are no APM subscribers to notify */
	if (!MONGODB_G(subscribers) || zend_hash_num_elements(MONGODB_G(subscribers)) == 0) {
		return;
	}

#if PHP_VERSION_ID >= 70000
	object_init_ex(&z_event, php_phongo_commandstartedevent_ce);
	p_event = Z_COMMANDSTARTEDEVENT_OBJ_P(&z_event);
#else
	MAKE_STD_ZVAL(z_event);
	object_init_ex(z_event, php_phongo_commandstartedevent_ce);
	p_event = Z_COMMANDSTARTEDEVENT_OBJ_P(z_event);
#endif

	p_event->client = mongoc_apm_command_started_get_context(event);
	p_event->command_name = estrdup(mongoc_apm_command_started_get_command_name(event));
	p_event->server_id = mongoc_apm_command_started_get_server_id(event);
	p_event->operation_id = mongoc_apm_command_started_get_operation_id(event);
	p_event->request_id = mongoc_apm_command_started_get_request_id(event);
	p_event->command = bson_copy(mongoc_apm_command_started_get_command(event));
	p_event->database_name = estrdup(mongoc_apm_command_started_get_database_name(event));

#if PHP_VERSION_ID >= 70000
	php_phongo_dispatch_handlers("commandStarted", &z_event);
#else
	php_phongo_dispatch_handlers("commandStarted", z_event);
#endif
	zval_ptr_dtor(&z_event);
}

static void php_phongo_command_succeeded(const mongoc_apm_command_succeeded_t *event)
{
	php_phongo_commandsucceededevent_t *p_event;
#if PHP_VERSION_ID >= 70000
	zval  z_event;
#else
	zval *z_event = NULL;
#endif
	TSRMLS_FETCH();

	/* Return early if there are no APM subscribers to notify */
	if (!MONGODB_G(subscribers) || zend_hash_num_elements(MONGODB_G(subscribers)) == 0) {
		return;
	}

#if PHP_VERSION_ID >= 70000
	object_init_ex(&z_event, php_phongo_commandsucceededevent_ce);
	p_event = Z_COMMANDSUCCEEDEDEVENT_OBJ_P(&z_event);
#else
	MAKE_STD_ZVAL(z_event);
	object_init_ex(z_event, php_phongo_commandsucceededevent_ce);
	p_event = Z_COMMANDSUCCEEDEDEVENT_OBJ_P(z_event);
#endif

	p_event->client = mongoc_apm_command_succeeded_get_context(event);
	p_event->command_name = estrdup(mongoc_apm_command_succeeded_get_command_name(event));
	p_event->server_id = mongoc_apm_command_succeeded_get_server_id(event);
	p_event->operation_id = mongoc_apm_command_succeeded_get_operation_id(event);
	p_event->request_id = mongoc_apm_command_succeeded_get_request_id(event);
	p_event->duration_micros = mongoc_apm_command_succeeded_get_duration(event);
	p_event->reply = bson_copy(mongoc_apm_command_succeeded_get_reply(event));

#if PHP_VERSION_ID >= 70000
	php_phongo_dispatch_handlers("commandSucceeded", &z_event);
#else
	php_phongo_dispatch_handlers("commandSucceeded", z_event);
#endif
	zval_ptr_dtor(&z_event);
}

static void php_phongo_command_failed(const mongoc_apm_command_failed_t *event)
{
	php_phongo_commandfailedevent_t *p_event;
#if PHP_VERSION_ID >= 70000
	zval  z_event;
#else
	zval *z_event = NULL;
#endif
	bson_error_t tmp_error;
	zend_class_entry *default_exception_ce;
	TSRMLS_FETCH();

	default_exception_ce = zend_exception_get_default(TSRMLS_C);

	/* Return early if there are no APM subscribers to notify */
	if (!MONGODB_G(subscribers) || zend_hash_num_elements(MONGODB_G(subscribers)) == 0) {
		return;
	}

#if PHP_VERSION_ID >= 70000
	object_init_ex(&z_event, php_phongo_commandfailedevent_ce);
	p_event = Z_COMMANDFAILEDEVENT_OBJ_P(&z_event);
#else
	MAKE_STD_ZVAL(z_event);
	object_init_ex(z_event, php_phongo_commandfailedevent_ce);
	p_event = Z_COMMANDFAILEDEVENT_OBJ_P(z_event);
#endif

	p_event->client = mongoc_apm_command_failed_get_context(event);
	p_event->command_name = estrdup(mongoc_apm_command_failed_get_command_name(event));
	p_event->server_id = mongoc_apm_command_failed_get_server_id(event);
	p_event->operation_id = mongoc_apm_command_failed_get_operation_id(event);
	p_event->request_id = mongoc_apm_command_failed_get_request_id(event);
	p_event->duration_micros = mongoc_apm_command_failed_get_duration(event);

	/* We need to process and convert the error right here, otherwise
	 * debug_info will turn into a recursive loop, and with the wrong trace
	 * locations */
	mongoc_apm_command_failed_get_error(event, &tmp_error);
	{
#if PHP_VERSION_ID < 70000
		MAKE_STD_ZVAL(p_event->z_error);
		object_init_ex(p_event->z_error, phongo_exception_from_mongoc_domain(tmp_error.domain, tmp_error.code));
		zend_update_property_string(default_exception_ce, p_event->z_error, ZEND_STRL("message"), tmp_error.message TSRMLS_CC);
		zend_update_property_long(default_exception_ce, p_event->z_error, ZEND_STRL("code"), tmp_error.code TSRMLS_CC);
#else
		object_init_ex(&p_event->z_error, phongo_exception_from_mongoc_domain(tmp_error.domain, tmp_error.code));
		zend_update_property_string(default_exception_ce, &p_event->z_error, ZEND_STRL("message"), tmp_error.message TSRMLS_CC);
		zend_update_property_long(default_exception_ce, &p_event->z_error, ZEND_STRL("code"), tmp_error.code TSRMLS_CC);
#endif
	}

#if PHP_VERSION_ID >= 70000
	php_phongo_dispatch_handlers("commandFailed", &z_event);
#else
	php_phongo_dispatch_handlers("commandFailed", z_event);
#endif
	zval_ptr_dtor(&z_event);
}


/* Sets the callbacks for APM */
int php_phongo_set_monitoring_callbacks(mongoc_client_t *client)
{
	int retval;

	mongoc_apm_callbacks_t *callbacks = mongoc_apm_callbacks_new();

	mongoc_apm_set_command_started_cb(callbacks, php_phongo_command_started);
	mongoc_apm_set_command_succeeded_cb(callbacks, php_phongo_command_succeeded);
	mongoc_apm_set_command_failed_cb(callbacks, php_phongo_command_failed);
	retval = mongoc_client_set_apm_callbacks(client, callbacks, client);

	mongoc_apm_callbacks_destroy(callbacks);

	return retval;
}

/* Creates a hash for a client by concatenating the URI string with serialized
 * options arrays. On success, a persistent string is returned (i.e. pefree()
 * should be used to free it) and hash_len will be set to the string's length.
 * On error, an exception will have been thrown and NULL will be returned. */
static char *php_phongo_manager_make_client_hash(const char *uri_string, zval *options, zval *driverOptions, size_t *hash_len TSRMLS_DC)
{
	char                 *hash = NULL;
	smart_str             var_buf = {0};
	php_serialize_data_t  var_hash;

#if PHP_VERSION_ID >= 70000
	zval                  args;

	array_init_size(&args, 4);
	ADD_ASSOC_LONG_EX(&args, "pid", getpid());
	ADD_ASSOC_STRING(&args, "uri", uri_string);

	if (options) {
		ADD_ASSOC_ZVAL_EX(&args, "options", options);
		Z_ADDREF_P(options);
	} else {
		ADD_ASSOC_NULL_EX(&args, "options");
	}

	if (driverOptions) {
		ADD_ASSOC_ZVAL_EX(&args, "driverOptions", driverOptions);
		Z_ADDREF_P(driverOptions);
	} else {
		ADD_ASSOC_NULL_EX(&args, "driverOptions");
	}

	PHP_VAR_SERIALIZE_INIT(var_hash);
	php_var_serialize(&var_buf, &args, &var_hash);
	PHP_VAR_SERIALIZE_DESTROY(var_hash);

	if (!EG(exception)) {
		*hash_len = ZSTR_LEN(var_buf.s);
		hash = pestrndup(ZSTR_VAL(var_buf.s), *hash_len, 1);
	}

	zval_ptr_dtor(&args);
#else
	zval                 *args;

	MAKE_STD_ZVAL(args);
	array_init_size(args, 4);
	ADD_ASSOC_LONG_EX(args, "pid", getpid());
	ADD_ASSOC_STRING(args, "uri", uri_string);

	if (options) {
		ADD_ASSOC_ZVAL_EX(args, "options", options);
		Z_ADDREF_P(options);
	} else {
		ADD_ASSOC_NULL_EX(args, "options");
	}

	if (driverOptions) {
		ADD_ASSOC_ZVAL_EX(args, "driverOptions", driverOptions);
		Z_ADDREF_P(driverOptions);
	} else {
		ADD_ASSOC_NULL_EX(args, "driverOptions");
	}

	PHP_VAR_SERIALIZE_INIT(var_hash);
	php_var_serialize(&var_buf, &args, &var_hash TSRMLS_CC);
	PHP_VAR_SERIALIZE_DESTROY(var_hash);

	if (!EG(exception)) {
		*hash_len = var_buf.len;
		hash = pestrndup(var_buf.c, *hash_len, 1);
	}

	zval_ptr_dtor(&args);
#endif

	smart_str_free(&var_buf);

	return hash;
}

static mongoc_client_t *php_phongo_make_mongo_client(const mongoc_uri_t *uri TSRMLS_DC) /* {{{ */
{
	const char *mongoc_version, *bson_version;

#ifdef HAVE_SYSTEM_LIBMONGOC
	mongoc_version = mongoc_get_version();
#else
	mongoc_version = "bundled";
#endif

#ifdef HAVE_SYSTEM_LIBBSON
	bson_version = bson_get_version();
#else
	bson_version = "bundled";
#endif

	MONGOC_DEBUG("Creating Manager, phongo-%s[%s] - mongoc-%s(%s), libbson-%s(%s), php-%s",
		PHP_MONGODB_VERSION,
		PHP_MONGODB_STABILITY,
		MONGOC_VERSION_S,
		mongoc_version,
		BSON_VERSION_S,
		bson_version,
		PHP_VERSION
	);

	return mongoc_client_new_from_uri(uri);
} /* }}} */

static void php_phongo_persist_client(const char *hash, size_t hash_len, mongoc_client_t *client TSRMLS_DC)
{
	php_phongo_pclient_t *pclient = (php_phongo_pclient_t *) pecalloc(1, sizeof(php_phongo_pclient_t), 1);

	pclient->pid = (int) getpid();
	pclient->client = client;

#if PHP_VERSION_ID >= 70000
	zend_hash_str_update_ptr(&MONGODB_G(pclients), hash, hash_len, pclient);
#else
	zend_hash_update(&MONGODB_G(pclients), hash, hash_len + 1, &pclient, sizeof(php_phongo_pclient_t *), NULL);
#endif
}

static mongoc_client_t *php_phongo_find_client(const char *hash, size_t hash_len TSRMLS_DC)
{
#if PHP_VERSION_ID >= 70000
	php_phongo_pclient_t *pclient;

	if ((pclient = zend_hash_str_find_ptr(&MONGODB_G(pclients), hash, hash_len)) != NULL) {
		return pclient->client;
	}
#else
	php_phongo_pclient_t **pclient;

	if (zend_hash_find(&MONGODB_G(pclients), hash, hash_len + 1, (void**) &pclient) == SUCCESS) {
		return (*pclient)->client;
	}
#endif

	return NULL;
}

void phongo_manager_init(php_phongo_manager_t *manager, const char *uri_string, zval *options, zval *driverOptions TSRMLS_DC) /* {{{ */
{
	char             *hash = NULL;
	size_t            hash_len = 0;
	bson_t            bson_options = BSON_INITIALIZER;
	mongoc_uri_t     *uri = NULL;
#ifdef MONGOC_ENABLE_SSL
	mongoc_ssl_opt_t *ssl_opt = NULL;
#endif

	if (!(hash = php_phongo_manager_make_client_hash(uri_string, options, driverOptions, &hash_len TSRMLS_CC))) {
		/* Exception should already have been thrown and there is nothing to free */
		return;
	}

	if ((manager->client = php_phongo_find_client(hash, hash_len TSRMLS_CC))) {
		MONGOC_DEBUG("Found client for hash: %s\n", hash);
		goto cleanup;
	}

	if (options) {
		php_phongo_zval_to_bson(options, PHONGO_BSON_NONE, &bson_options, NULL TSRMLS_CC);
	}

	/* An exception may be thrown during BSON conversion */
	if (EG(exception)) {
		goto cleanup;
	}

	if (!(uri = php_phongo_make_uri(uri_string, &bson_options TSRMLS_CC))) {
		/* Exception should already have been thrown */
		goto cleanup;
	}

	if (!php_phongo_apply_options_to_uri(uri, &bson_options TSRMLS_CC) ||
	    !php_phongo_apply_rc_options_to_uri(uri, &bson_options TSRMLS_CC) ||
	    !php_phongo_apply_rp_options_to_uri(uri, &bson_options TSRMLS_CC) ||
	    !php_phongo_apply_wc_options_to_uri(uri, &bson_options TSRMLS_CC)) {
		/* Exception should already have been thrown */
		goto cleanup;
	}

#ifdef MONGOC_ENABLE_SSL
	/* Construct SSL options even if SSL is not enabled so that exceptions can
	 * be thrown for unsupported driver options. */
	ssl_opt = php_phongo_make_ssl_opt(driverOptions TSRMLS_CC);

	/* An exception may be thrown during SSL option creation */
	if (EG(exception)) {
		goto cleanup;
	}
#else
	if (mongoc_uri_get_ssl(uri)) {
		phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Cannot create SSL client. SSL is not enabled in this build.");
		goto cleanup;
	}
#endif

	manager->client = php_phongo_make_mongo_client(uri TSRMLS_CC);

	if (!manager->client) {
		phongo_throw_exception(PHONGO_ERROR_RUNTIME TSRMLS_CC, "Failed to create Manager from URI: '%s'", uri_string);
		goto cleanup;
	}

#ifdef MONGOC_ENABLE_SSL
	if (ssl_opt && mongoc_uri_get_ssl(uri)) {
		mongoc_client_set_ssl_opts(manager->client, ssl_opt);
	}
#endif

	MONGOC_DEBUG("Created client hash: %s\n", hash);
	php_phongo_persist_client(hash, hash_len, manager->client TSRMLS_CC);

cleanup:
	if (hash) {
		pefree(hash, 1);
	}

	bson_destroy(&bson_options);

	if (uri) {
		mongoc_uri_destroy(uri);
	}

#ifdef MONGOC_ENABLE_SSL
	if (ssl_opt) {
		php_phongo_free_ssl_opt(ssl_opt);
	}
#endif
} /* }}} */

void php_phongo_new_utcdatetime_from_epoch(zval *object, int64_t msec_since_epoch TSRMLS_DC) /* {{{ */
{
	php_phongo_utcdatetime_t     *intern;

	object_init_ex(object, php_phongo_utcdatetime_ce);

	intern = Z_UTCDATETIME_OBJ_P(object);
	intern->milliseconds = msec_since_epoch;
	intern->initialized = true;
} /* }}} */

void php_phongo_new_timestamp_from_increment_and_timestamp(zval *object, uint32_t increment, uint32_t timestamp TSRMLS_DC) /* {{{ */
{
	php_phongo_timestamp_t     *intern;

	object_init_ex(object, php_phongo_timestamp_ce);

	intern = Z_TIMESTAMP_OBJ_P(object);
	intern->increment = increment;
	intern->timestamp = timestamp;
	intern->initialized = true;
} /* }}} */
void php_phongo_new_javascript_from_javascript(int init, zval *object, const char *code, size_t code_len TSRMLS_DC) /* {{{ */
{
	php_phongo_new_javascript_from_javascript_and_scope(init, object, code, code_len, NULL TSRMLS_CC);
} /* }}} */
void php_phongo_new_javascript_from_javascript_and_scope(int init, zval *object, const char *code, size_t code_len, const bson_t *scope TSRMLS_DC) /* {{{ */
{
	php_phongo_javascript_t     *intern;

	if (init) {
		object_init_ex(object, php_phongo_javascript_ce);
	}

	intern = Z_JAVASCRIPT_OBJ_P(object);
	intern->code = estrndup(code, code_len);
	intern->code_len = code_len;
	intern->scope = scope ? bson_copy(scope) : NULL;
} /* }}} */
void php_phongo_new_binary_from_binary_and_type(zval *object, const char *data, size_t data_len, bson_subtype_t type TSRMLS_DC) /* {{{ */
{
	php_phongo_binary_t     *intern;

	object_init_ex(object, php_phongo_binary_ce);

	intern = Z_BINARY_OBJ_P(object);
	intern->data = estrndup(data, data_len);
	intern->data_len = data_len;
	intern->type = (uint8_t) type;
} /* }}} */

void php_phongo_new_decimal128(zval *object, const bson_decimal128_t *decimal TSRMLS_DC) /* {{{ */
{
	php_phongo_decimal128_t *intern;

	object_init_ex(object, php_phongo_decimal128_ce);

	intern = Z_DECIMAL128_OBJ_P(object);
	memcpy(&intern->decimal, decimal, sizeof(bson_decimal128_t));
	intern->initialized = true;
} /* }}} */

/* qsort() compare callback for alphabetizing regex flags upon initialization */
static int php_phongo_regex_compare_flags(const void *f1, const void *f2) {
	if (* (const char *) f1 == * (const char *) f2) {
		return 0;
	}

	return (* (const char *) f1 > * (const char *) f2) ? 1 : -1;
}

void php_phongo_new_regex_from_regex_and_options(zval *object, const char *pattern, const char *flags TSRMLS_DC) /* {{{ */
{
	php_phongo_regex_t     *intern;

	object_init_ex(object, php_phongo_regex_ce);

	intern = Z_REGEX_OBJ_P(object);
	intern->pattern_len = strlen(pattern);
	intern->pattern = estrndup(pattern, intern->pattern_len);
	intern->flags_len = strlen(flags);
	intern->flags = estrndup(flags, intern->flags_len);

	/* Ensure flags are alphabetized upon initialization. This may be removed
	 * once CDRIVER-1883 is implemented. */
	qsort((void *) intern->flags, intern->flags_len, 1, php_phongo_regex_compare_flags);
} /* }}} */

/* {{{ Memory allocation wrappers */
static void* php_phongo_malloc(size_t num_bytes) /* {{{ */
{
	return pemalloc(num_bytes, 1);
} /* }}} */

static void* php_phongo_calloc(size_t num_members, size_t num_bytes) /* {{{ */
{
	return pecalloc(num_members, num_bytes, 1);
} /* }}} */

static void* php_phongo_realloc(void *mem, size_t num_bytes) { /* {{{ */
	return perealloc(mem, num_bytes, 1);
} /* }}} */

static void php_phongo_free(void *mem) /* {{{ */
{
	if (mem) {
		pefree(mem, 1);
	}
} /* }}} */

/* }}} */

/* {{{ M[INIT|SHUTDOWN] R[INIT|SHUTDOWN] G[INIT|SHUTDOWN] MINFO INI */

ZEND_INI_MH(OnUpdateDebug)
{
	void ***ctx = NULL;
	char *tmp_dir = NULL;

	TSRMLS_SET_CTX(ctx);

	/* Close any previously open log files */
	if (MONGODB_G(debug_fd)) {
		if (MONGODB_G(debug_fd) != stderr && MONGODB_G(debug_fd) != stdout) {
			fclose(MONGODB_G(debug_fd));
		}
		MONGODB_G(debug_fd) = NULL;
	}

	if (!new_value || (new_value && !ZSTR_VAL(new_value)[0])
		|| strcasecmp("0", ZSTR_VAL(new_value)) == 0
		|| strcasecmp("off", ZSTR_VAL(new_value)) == 0
		|| strcasecmp("no", ZSTR_VAL(new_value)) == 0
		|| strcasecmp("false", ZSTR_VAL(new_value)) == 0
	   ) {
		mongoc_log_trace_disable();
		mongoc_log_set_handler(NULL, NULL);

#if PHP_VERSION_ID >= 70000
		return OnUpdateString(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage TSRMLS_CC);
#else
		return OnUpdateString(entry, new_value, new_value_length, mh_arg1, mh_arg2, mh_arg3, stage TSRMLS_CC);
#endif
	}


	if (strcasecmp(ZSTR_VAL(new_value), "stderr") == 0) {
		MONGODB_G(debug_fd) = stderr;
	} else if (strcasecmp(ZSTR_VAL(new_value), "stdout") == 0) {
		MONGODB_G(debug_fd) = stdout;
	} else if (
		strcasecmp("1", ZSTR_VAL(new_value)) == 0
		|| strcasecmp("on", ZSTR_VAL(new_value)) == 0
		|| strcasecmp("yes", ZSTR_VAL(new_value)) == 0
		|| strcasecmp("true", ZSTR_VAL(new_value)) == 0
	) {
		tmp_dir = NULL;
	} else {
		tmp_dir = ZSTR_VAL(new_value);
	}

	if (!MONGODB_G(debug_fd)) {
		time_t t;
		int fd = -1;
		char *prefix;
		int len;
		phongo_char *filename;

		time(&t);
		len = spprintf(&prefix, 0, "PHONGO-%ld", t);

		fd = php_open_temporary_fd(tmp_dir, prefix, &filename TSRMLS_CC);
		if (fd != -1) {
			const char *path = ZSTR_VAL(filename);
			MONGODB_G(debug_fd) = VCWD_FOPEN(path, "a");
		}
		efree(filename);
		efree(prefix);
		close(fd);
	}

	mongoc_log_trace_enable();
	mongoc_log_set_handler(php_phongo_log, ctx);

#if PHP_VERSION_ID >= 70000
	return OnUpdateString(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage TSRMLS_CC);
#else
	return OnUpdateString(entry, new_value, new_value_length, mh_arg1, mh_arg2, mh_arg3, stage TSRMLS_CC);
#endif
}


/* {{{ INI entries */
PHP_INI_BEGIN()
#if PHP_VERSION_ID >= 70000
	STD_PHP_INI_ENTRY(PHONGO_DEBUG_INI,      PHONGO_DEBUG_INI_DEFAULT,   PHP_INI_ALL, OnUpdateDebug, debug,      zend_mongodb_globals,    mongodb_globals)
#else
	{ 0, PHP_INI_ALL, (char *)PHONGO_DEBUG_INI, sizeof(PHONGO_DEBUG_INI), OnUpdateDebug, (void *) XtOffsetOf(zend_mongodb_globals, debug), (void *) &mglo, NULL, (char *)PHONGO_DEBUG_INI_DEFAULT, sizeof(PHONGO_DEBUG_INI_DEFAULT)-1, NULL, 0, 0, 0, NULL },
#endif
PHP_INI_END()
/* }}} */

static inline void php_phongo_pclient_destroy(php_phongo_pclient_t *pclient)
{
	/* Do not destroy mongoc_client_t objects created by other processes. This
	 * ensures that we do not shutdown sockets that may still be in use by our
	 * parent process (see: CDRIVER-2049). While this is a leak, we are already
	 * in MSHUTDOWN at this point. */
	if (pclient->pid == getpid()) {
		mongoc_client_destroy(pclient->client);
	}

	pefree(pclient, 1);
}

#if PHP_VERSION_ID >= 70000
static void php_phongo_pclient_dtor(zval *zv)
{
	php_phongo_pclient_destroy((php_phongo_pclient_t *) Z_PTR_P(zv));
}
#else
static void php_phongo_pclient_dtor(void *pp)
{
	php_phongo_pclient_destroy(*((php_phongo_pclient_t **) pp));
}
#endif

/* {{{ PHP_RINIT_FUNCTION */
PHP_RINIT_FUNCTION(mongodb)
{
	/* Initialize HashTable for APM subscribers, which is initialized to NULL in
	 * GINIT and destroyed and reset to NULL in RSHUTDOWN. */
	if (MONGODB_G(subscribers) == NULL) {
		ALLOC_HASHTABLE(MONGODB_G(subscribers));
		zend_hash_init(MONGODB_G(subscribers), 0, NULL, ZVAL_PTR_DTOR, 0);
	}

	return SUCCESS;
}
/* }}} */

/* {{{ PHP_GINIT_FUNCTION */
PHP_GINIT_FUNCTION(mongodb)
{
	bson_mem_vtable_t bsonMemVTable = {
		php_phongo_malloc,
		php_phongo_calloc,
		php_phongo_realloc,
		php_phongo_free,
	};
#if PHP_VERSION_ID >= 70000
#if defined(COMPILE_DL_MONGODB) && defined(ZTS)
	ZEND_TSRMLS_CACHE_UPDATE();
#endif
#endif
	memset(mongodb_globals, 0, sizeof(zend_mongodb_globals));
	mongodb_globals->bsonMemVTable = bsonMemVTable;

	/* Initialize HashTable for persistent clients */
	zend_hash_init_ex(&mongodb_globals->pclients, 0, NULL, php_phongo_pclient_dtor, 1, 0);
}
/* }}} */

static zend_class_entry *php_phongo_fetch_internal_class(const char *class_name, size_t class_name_len TSRMLS_DC)
{
#if PHP_VERSION_ID >= 70000
	zend_class_entry *pce;

	if ((pce = zend_hash_str_find_ptr(CG(class_table), class_name, class_name_len))) {
		return pce;
	}
#else
	zend_class_entry **pce;

	if (zend_hash_find(CG(class_table), class_name, class_name_len + 1, (void **) &pce) == SUCCESS) {
		return *pce;
	}
#endif

	return NULL;
}

/* {{{ PHP_MINIT_FUNCTION */
PHP_MINIT_FUNCTION(mongodb)
{
	char *php_version_string;

	(void)type; /* We don't care if we are loaded via dl() or extension= */

	REGISTER_INI_ENTRIES();

	/* Initialize libmongoc */
	mongoc_init();

	/* Set handshake options */
	php_version_string = malloc(4 + sizeof(PHP_VERSION) + 1);
	snprintf(php_version_string, 4 + sizeof(PHP_VERSION) + 1, "PHP %s", PHP_VERSION);
	mongoc_handshake_data_append("ext-mongodb:PHP", PHP_MONGODB_VERSION, php_version_string);
	free(php_version_string);

	/* Initialize libbson */
	bson_mem_set_vtable(&MONGODB_G(bsonMemVTable));

	/* Prep default object handlers to be used when we register the classes */
	memcpy(&phongo_std_object_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
	phongo_std_object_handlers.clone_obj            = NULL;
	/*
	phongo_std_object_handlers.get_debug_info       = NULL;
	phongo_std_object_handlers.compare_objects      = NULL;
	phongo_std_object_handlers.cast_object          = NULL;
	phongo_std_object_handlers.count_elements       = NULL;
	phongo_std_object_handlers.get_closure          = NULL;
	*/

	/* Initialize zend_class_entry dependencies.
	 *
	 * Although DateTimeImmutable was introduced in PHP 5.5.0,
	 * php_date_get_immutable_ce() is not available in PHP versions before
	 * 5.5.24 and 5.6.8.
	 *
	 * Although JsonSerializable was introduced in PHP 5.4.0,
	 * php_json_serializable_ce is not exported in PHP versions before 5.4.26
	 * and 5.5.10. For later PHP versions, looking up the class manually also
	 * helps with distros that disable LTDL_LAZY for dlopen() (e.g. Fedora).
	 */
	php_phongo_date_immutable_ce = php_phongo_fetch_internal_class(ZEND_STRL("datetimeimmutable") TSRMLS_CC);
	php_phongo_json_serializable_ce = php_phongo_fetch_internal_class(ZEND_STRL("jsonserializable") TSRMLS_CC);

	if (php_phongo_json_serializable_ce == NULL) {
		zend_error(E_ERROR, "JsonSerializable class is not defined. Please ensure that the 'json' module is loaded before the 'mongodb' module.");
		return FAILURE;
	}

	/* Register base BSON classes first */
	php_phongo_type_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_serializable_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_unserializable_init_ce(INIT_FUNC_ARGS_PASSTHRU);

	php_phongo_binary_interface_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_decimal128_interface_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_javascript_interface_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_maxkey_interface_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_minkey_interface_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_objectid_interface_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_regex_interface_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_timestamp_interface_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_utcdatetime_interface_init_ce(INIT_FUNC_ARGS_PASSTHRU);

	php_phongo_binary_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_decimal128_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_javascript_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_maxkey_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_minkey_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_objectid_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_persistable_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_regex_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_timestamp_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_utcdatetime_init_ce(INIT_FUNC_ARGS_PASSTHRU);

	php_phongo_bulkwrite_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_command_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_cursor_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_cursorid_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_manager_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_query_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_readconcern_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_readpreference_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_server_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_writeconcern_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_writeconcernerror_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_writeerror_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_writeresult_init_ce(INIT_FUNC_ARGS_PASSTHRU);

	/* Register base exception classes first */
	php_phongo_exception_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_runtimeexception_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_connectionexception_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_writeexception_init_ce(INIT_FUNC_ARGS_PASSTHRU);

	php_phongo_authenticationexception_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_bulkwriteexception_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_connectiontimeoutexception_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_executiontimeoutexception_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_invalidargumentexception_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_logicexception_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_sslconnectionexception_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_unexpectedvalueexception_init_ce(INIT_FUNC_ARGS_PASSTHRU);

	/* Register base APM classes first */
	php_phongo_subscriber_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_commandsubscriber_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_commandfailedevent_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_commandstartedevent_init_ce(INIT_FUNC_ARGS_PASSTHRU);
	php_phongo_commandsucceededevent_init_ce(INIT_FUNC_ARGS_PASSTHRU);

	REGISTER_STRING_CONSTANT("MONGODB_VERSION", (char *)PHP_MONGODB_VERSION, CONST_CS | CONST_PERSISTENT);
	REGISTER_STRING_CONSTANT("MONGODB_STABILITY", (char *)PHP_MONGODB_STABILITY, CONST_CS | CONST_PERSISTENT);

	return SUCCESS;
}
/* }}} */

/* {{{ PHP_MSHUTDOWN_FUNCTION */
PHP_MSHUTDOWN_FUNCTION(mongodb)
{
	(void)type; /* We don't care if we are loaded via dl() or extension= */

	/* Destroy HashTable for persistent clients. The HashTable destructor will
	 * destroy any mongoc_client_t objects that were created by this process. */
	zend_hash_destroy(&MONGODB_G(pclients));

	bson_mem_restore_vtable();
	/* Cleanup after libmongoc */
	mongoc_cleanup();

	UNREGISTER_INI_ENTRIES();

	return SUCCESS;
}
/* }}} */

/* {{{ PHP_RSHUTDOWN_FUNCTION */
PHP_RSHUTDOWN_FUNCTION(mongodb)
{
	/* Destroy HashTable for APM subscribers, which was initialized in RINIT */
	if (MONGODB_G(subscribers)) {
		zend_hash_destroy(MONGODB_G(subscribers));
		FREE_HASHTABLE(MONGODB_G(subscribers));
		MONGODB_G(subscribers) = NULL;
	}

	return SUCCESS;
}
/* }}} */

/* {{{ PHP_GSHUTDOWN_FUNCTION */
PHP_GSHUTDOWN_FUNCTION(mongodb)
{
	mongodb_globals->debug = NULL;
	if (mongodb_globals->debug_fd) {
		fclose(mongodb_globals->debug_fd);
		mongodb_globals->debug_fd = NULL;
	}
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION */
PHP_MINFO_FUNCTION(mongodb)
{
	php_info_print_table_start();
	php_info_print_table_header(2, "MongoDB support", "enabled");
	php_info_print_table_row(2, "MongoDB extension version", PHP_MONGODB_VERSION);
	php_info_print_table_row(2, "MongoDB extension stability", PHP_MONGODB_STABILITY);

#ifdef HAVE_SYSTEM_LIBBSON
	php_info_print_table_row(2, "libbson headers version", BSON_VERSION_S);
	php_info_print_table_row(2, "libbson library version", bson_get_version());
#else
	php_info_print_table_row(2, "libbson bundled version", BSON_VERSION_S);
#endif

#ifdef HAVE_SYSTEM_LIBMONGOC
	php_info_print_table_row(2, "libmongoc headers version", MONGOC_VERSION_S);
	php_info_print_table_row(2, "libmongoc library version", mongoc_get_version());
#else
	/* Bundled libraries, buildtime = runtime */
	php_info_print_table_row(2, "libmongoc bundled version", MONGOC_VERSION_S);
#endif

#ifdef MONGOC_ENABLE_SSL
	php_info_print_table_row(2, "libmongoc SSL", "enabled");
# if defined(MONGOC_ENABLE_SSL_OPENSSL)
	php_info_print_table_row(2, "libmongoc SSL library", "OpenSSL");
# elif defined(MONGOC_ENABLE_SSL_LIBRESSL)
	php_info_print_table_row(2, "libmongoc SSL library", "LibreSSL");
# elif defined(MONGOC_ENABLE_SSL_SECURE_TRANSPORT)
	php_info_print_table_row(2, "libmongoc SSL library", "Secure Transport");
# elif defined(MONGOC_ENABLE_SSL_SECURE_CHANNEL)
	php_info_print_table_row(2, "libmongoc SSL library", "Secure Channel");
# else
	php_info_print_table_row(2, "libmongoc SSL library", "unknown");
# endif
#else
	php_info_print_table_row(2, "libmongoc SSL", "disabled");
#endif

#ifdef MONGOC_ENABLE_CRYPTO
	php_info_print_table_row(2, "libmongoc crypto", "enabled");
# if defined(MONGOC_ENABLE_CRYPTO_LIBCRYPTO)
	php_info_print_table_row(2, "libmongoc crypto library", "libcrypto");
# elif defined(MONGOC_ENABLE_CRYPTO_COMMON_CRYPTO)
	php_info_print_table_row(2, "libmongoc crypto library", "Common Crypto");
# elif defined(MONGOC_ENABLE_CRYPTO_CNG)
	php_info_print_table_row(2, "libmongoc crypto library", "CNG");
# else
	php_info_print_table_row(2, "libmongoc crypto library", "unknown");
# endif
# ifdef MONGOC_ENABLE_CRYPTO_SYSTEM_PROFILE
	php_info_print_table_row(2, "libmongoc crypto system profile", "enabled");
# else
	php_info_print_table_row(2, "libmongoc crypto system profile", "disabled");
# endif
#else
	php_info_print_table_row(2, "libmongoc crypto", "disabled");
#endif

#ifdef MONGOC_ENABLE_SASL
	php_info_print_table_row(2, "libmongoc SASL", "enabled");
#else
	php_info_print_table_row(2, "libmongoc SASL", "disabled");
#endif

	php_info_print_table_end();

	DISPLAY_INI_ENTRIES();
}
/* }}} */
/* }}} */

/* {{{ Shared function entries for disabling constructors and unserialize() */
PHP_FUNCTION(MongoDB_disabled___construct) /* {{{ */
{
	phongo_throw_exception(PHONGO_ERROR_RUNTIME TSRMLS_CC, "Accessing private constructor");
} /* }}} */

PHP_FUNCTION(MongoDB_disabled___wakeup) /* {{{ */
{
	if (zend_parse_parameters_none() == FAILURE) {
		return;
	}

	phongo_throw_exception(PHONGO_ERROR_RUNTIME TSRMLS_CC, "%s", "MongoDB\\Driver objects cannot be serialized");
} /* }}} */
 /* }}} */

/* {{{ mongodb_functions[]
*/
ZEND_BEGIN_ARG_INFO_EX(ai_bson_fromPHP, 0, 0, 1)
	ZEND_ARG_INFO(0, value)
ZEND_END_ARG_INFO();

ZEND_BEGIN_ARG_INFO_EX(ai_bson_toPHP, 0, 0, 1)
	ZEND_ARG_INFO(0, bson)
	ZEND_ARG_ARRAY_INFO(0, typemap, 0)
ZEND_END_ARG_INFO();

ZEND_BEGIN_ARG_INFO_EX(ai_bson_toJSON, 0, 0, 1)
	ZEND_ARG_INFO(0, bson)
ZEND_END_ARG_INFO();

ZEND_BEGIN_ARG_INFO_EX(ai_bson_fromJSON, 0, 0, 1)
	ZEND_ARG_INFO(0, json)
ZEND_END_ARG_INFO();

ZEND_BEGIN_ARG_INFO_EX(ai_mongodb_driver_monitoring_subscriber, 0, 0, 1)
	ZEND_ARG_OBJ_INFO(0, subscriber, MongoDB\\Driver\\Monitoring\\Subscriber, 0)
ZEND_END_ARG_INFO();

static const zend_function_entry mongodb_functions[] = {
	ZEND_NS_NAMED_FE("MongoDB\\BSON", fromPHP, PHP_FN(MongoDB_BSON_fromPHP), ai_bson_fromPHP)
	ZEND_NS_NAMED_FE("MongoDB\\BSON", toPHP, PHP_FN(MongoDB_BSON_toPHP), ai_bson_toPHP)
	ZEND_NS_NAMED_FE("MongoDB\\BSON", toJSON, PHP_FN(MongoDB_BSON_toJSON), ai_bson_toJSON)
	ZEND_NS_NAMED_FE("MongoDB\\BSON", toCanonicalExtendedJSON, PHP_FN(MongoDB_BSON_toCanonicalExtendedJSON), ai_bson_toJSON)
	ZEND_NS_NAMED_FE("MongoDB\\BSON", toRelaxedExtendedJSON, PHP_FN(MongoDB_BSON_toRelaxedExtendedJSON), ai_bson_toJSON)
	ZEND_NS_NAMED_FE("MongoDB\\BSON", fromJSON, PHP_FN(MongoDB_BSON_fromJSON), ai_bson_fromJSON)
	ZEND_NS_NAMED_FE("MongoDB\\Driver\\Monitoring", addSubscriber, PHP_FN(MongoDB_Driver_Monitoring_addSubscriber), ai_mongodb_driver_monitoring_subscriber)
	ZEND_NS_NAMED_FE("MongoDB\\Driver\\Monitoring", removeSubscriber, PHP_FN(MongoDB_Driver_Monitoring_removeSubscriber), ai_mongodb_driver_monitoring_subscriber)
	PHP_FE_END
};
/* }}} */

static const zend_module_dep mongodb_deps[] = {
	ZEND_MOD_REQUIRED("date")
	ZEND_MOD_REQUIRED("json")
	ZEND_MOD_REQUIRED("spl")
	ZEND_MOD_REQUIRED("standard")
	ZEND_MOD_END
};

/* {{{ mongodb_module_entry
 */
zend_module_entry mongodb_module_entry = {
	STANDARD_MODULE_HEADER_EX,
	NULL,
	mongodb_deps,
	"mongodb",
	mongodb_functions,
	PHP_MINIT(mongodb),
	PHP_MSHUTDOWN(mongodb),
	PHP_RINIT(mongodb),
	PHP_RSHUTDOWN(mongodb),
	PHP_MINFO(mongodb),
	PHP_MONGODB_VERSION,
	PHP_MODULE_GLOBALS(mongodb),
	PHP_GINIT(mongodb),
	PHP_GSHUTDOWN(mongodb),
	NULL,
	STANDARD_MODULE_PROPERTIES_EX
};
/* }}} */

#ifdef COMPILE_DL_MONGODB
ZEND_GET_MODULE(mongodb)
#endif

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: noet sw=4 ts=4 fdm=marker
 * vim<600: noet sw=4 ts=4
 */
