/***********************************************************************
 dbdriver.cpp - Implements the DBDriver class.

 Copyright (c) 2005-2009 by Educational Technology Resources, Inc.
 Others may also hold copyrights on code in this file.  See the
 CREDITS.txt file in the top directory of the distribution for details.

 This file is part of MySQL++.

 MySQL++ is free software; you can redistribute it and/or modify it
 under the terms of the GNU Lesser General Public License as published
 by the Free Software Foundation; either version 2.1 of the License, or
 (at your option) any later version.

 MySQL++ is distributed in the hope that it will be useful, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
 License for more details.

 You should have received a copy of the GNU Lesser General Public
 License along with MySQL++; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
 USA
***********************************************************************/

#define MYSQLPP_NOT_HEADER
#include "dbdriver.h"

#include "exceptions.h"

#include <cstring>
#include <memory>
#include <sstream>

// An argument was added to mysql_shutdown() in MySQL 4.1.3 and 5.0.1.
#if ((MYSQL_VERSION_ID >= 40103) && (MYSQL_VERSION_ID <= 49999)) || (MYSQL_VERSION_ID >= 50001)
#	define SHUTDOWN_ARG ,SHUTDOWN_DEFAULT
#else
#	define SHUTDOWN_ARG
#endif

using namespace std;

namespace mysqlpp {

DBDriver::DBDriver() :
is_connected_(false)
{
	// We won't allow calls to mysql_*() functions that take a MYSQL
	// object until we get a connection up.  Such calls are nonsense.
	// MySQL++ coped with them before, but this masks bugs.
	memset(&mysql_, 0, sizeof(mysql_));
}


DBDriver::DBDriver(const DBDriver& other) :
is_connected_(false)
{
	copy(other);
}


DBDriver::~DBDriver()
{
	if (connected()) {
		disconnect();
	}

	OptionList::const_iterator it;
	for (it = applied_options_.begin(); it != applied_options_.end(); ++it) {
		delete *it;
	}
}


bool
DBDriver::connect(const char* host, const char* socket_name,
		unsigned int port, const char* db, const char* user,
		const char* password)
{
	return is_connected_ =
			connect_prepare() &&
			mysql_real_connect(&mysql_, host, user, password, db,
				port, socket_name, mysql_.client_flag);
}


bool
DBDriver::connect(const MYSQL& other)
{
	return is_connected_ =
			connect_prepare() &&
			mysql_real_connect(&mysql_, other.host, other.user,
				other.passwd, other.db, other.port, other.unix_socket,
				other.client_flag);
}


bool
DBDriver::connect_prepare()
{
	// Drop previous connection, if any, then prepare underlying C API
	// library to establish a new connection.
	if (connected()) {
		disconnect();
	}

	// Set up to call MySQL C API
	mysql_init(&mysql_);

    // Apply any pending options
	error_message_.clear();
	OptionListIt it = pending_options_.begin();
	while (it != pending_options_.end() && set_option_impl(*it)) {
		++it;
	}
	if (it == pending_options_.end()) {
		pending_options_.clear();
		return true;
	}
	else {
		return false;
	}
}


void
DBDriver::copy(const DBDriver& other)
{
	if (other.connected()) {
		connect(other.mysql_);
	}
	else {
		is_connected_ = false;
	}
}


void
DBDriver::disconnect()
{
	if (is_connected_) {
		mysql_close(&mysql_);
		memset(&mysql_, 0, sizeof(mysql_));
		is_connected_ = false;
		error_message_.clear();
	}
}


bool
DBDriver::enable_ssl(const char* key, const char* cert,
		const char* ca, const char* capath, const char* cipher)
{
	error_message_.clear();
#if defined(HAVE_MYSQL_SSL_SET)
	return mysql_ssl_set(&mysql_, key, cert, ca, capath, cipher) == 0;
#else
	(void)key;
	(void)cert;
	(void)ca;
	(void)capath;
	(void)cipher;
	return false;
#endif
}


size_t
DBDriver::escape_string(std::string* ps, const char* original,
		size_t length)
{
	error_message_.clear();

	if (ps == 0) {
		// Can't do any real work!
		return 0;
	}
	else if (original == 0) {
		// ps must point to the original data as well as to the
		// receiving string, so get the pointer and the length from it.
		original = ps->data();
		length = ps->length();
	}
	else if (length == 0) {
		// We got a pointer to a C++ string just for holding the result
		// and also a C string pointing to the original, so find the
		// length of the original.
		length = strlen(original);
	}

	char* escaped = new char[length * 2 + 1];
	length = escape_string(escaped, original, length);
	ps->assign(escaped, length);
	delete[] escaped;

	return length;
}


size_t
DBDriver::escape_string_no_conn(std::string* ps, const char* original,
		size_t length)
{
	if (ps == 0) {
		// Can't do any real work!
		return 0;
	}
	else if (original == 0) {
		// ps must point to the original data as well as to the
		// receiving string, so get the pointer and the length from it.
		original = ps->data();
		length = ps->length();
	}
	else if (length == 0) {
		// We got a pointer to a C++ string just for holding the result
		// and also a C string pointing to the original, so find the
		// length of the original.
		length = strlen(original);
	}

	char* escaped = new char[length * 2 + 1];
	length = DBDriver::escape_string_no_conn(escaped, original, length);
	ps->assign(escaped, length);
	delete[] escaped;

	return length;
}


DBDriver&
DBDriver::operator=(const DBDriver& rhs)
{
	copy(rhs);
	return *this;
}


string
DBDriver::query_info()
{
	error_message_.clear();
	const char* i = mysql_info(&mysql_);
	return i ? string(i) : string();
}


bool
DBDriver::set_option(unsigned int o, bool arg)
{
	// If we get through this loop and n is 1, only one bit is set in
	// the option value, which is as it should be.
	int n = o;
	while (n && ((n & 1) == 0)) {
		n >>= 1;
	}
	
	if ((n == 1) &&
			(o >= CLIENT_LONG_PASSWORD) &&
#if MYSQL_VERSION_ID > 40000	// highest flag value varies by version
			(o <= CLIENT_MULTI_RESULTS)
#else
			(o <= CLIENT_TRANSACTIONS)
#endif
			) {
		// Option value seems sane, so go ahead and set/clear the flag
		if (arg) {
			mysql_.client_flag |= o;
		}
		else {
			mysql_.client_flag &= ~o;
		}

		return true;
	}
	else {
		// Option value is outside the range we understand, or caller
		// erroneously passed a value with multiple bits set.
		return false;
	}
}


bool
DBDriver::set_option(Option* o)
{
	if (connected()) {
		return set_option_impl(o);
	}
	else {
		error_message_.clear();
        pending_options_.push_back(o);
		return true;  // we won't know if it fails until ::connect()
	}
}


bool
DBDriver::set_option_impl(Option* o)
{
	std::ostringstream os;
	std::auto_ptr<Option> cleanup(o);

	switch (o->set(this)) {
		case Option::err_NONE:
			applied_options_.push_back(o);
			cleanup.release();
			break;

		case Option::err_api_limit:
			os << "Option not supported by database driver v" <<
					client_version();
			throw BadOption(os.str(), typeid(*o)); // mandatory throw!

		case Option::err_api_reject:
			os << "Database driver failed to set option";
			break;

		case Option::err_connected:
			os << "Option can only be set before connection is established";
			break;

		case Option::err_disconnected:
			os << "Option can only be set while the connection is established";
			break;
	}

	error_message_ = os.str();
	return error_message_.empty();
}


bool
DBDriver::shutdown()
{
	error_message_.clear();
	return mysql_shutdown(&mysql_ SHUTDOWN_ARG);
}


bool
DBDriver::thread_aware()
{
#if defined(MYSQLPP_PLATFORM_WINDOWS) || defined(HAVE_PTHREAD) || defined(HAVE_SYNCH_H)
	// Okay, good, MySQL++ itself is thread-aware, but only return true
	// if the underlying C API library is also thread-aware.
	return mysql_thread_safe();
#else
	// MySQL++ itself isn't thread-aware, so we don't need to do any
	// further tests.  All pieces must be thread-aware to return true.
	return false;	
#endif
}

} // end namespace mysqlpp

