#ifdef DBA_ENABLE_MYSQL
//////////////////////////////////////////////////////////////////////
// Copyright (c) 2010, Oliver 'kfs1' Smith <oliver@kfs.org>
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// - Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// - Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// - Neither the name of KingFisher Software nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
//////////////////////////////////////////////////////////////////////
//

#include "dbaConn.h"						// Main include file
#include <errmsg.h>						// Include the MySQL error codes.
#include <sstream>						// For stringstream.

namespace DBA
{

//////////////////////////////////////////////////////////////////////
// Constructor for a MySQLConnection - not a lot here :)

MySQLConnection::MySQLConnection()
	: Connection()
	, m_conn(NULL)
	, m_result(NULL)
	, m_columns(NULL)
{
}

//////////////////////////////////////////////////////////////////////
// Destructor - ensure cleanup.

MySQLConnection::~MySQLConnection()
{
	m_columns = NULL ;

	if ( m_currentResult != NULL )
		ReleaseResult() ;

	if ( m_conn != NULL )
		Disconnect() ;
}

//////////////////////////////////////////////////////////////////////
// Connect to a MySQL database.

void
MySQLConnection::Connect(const Credentials& credentials, unsigned int flags, bool autoReconnect /* = false */)
{
	// Initialize MySQL
	MYSQL* mysql = mysql_init(NULL) ;
	if ( mysql == NULL )
		throw std::runtime_error("mysql_init failed") ;

	// Attempt to connect.
	m_conn = mysql_real_connect(mysql
								, credentials.host, credentials.username, credentials.password, credentials.database, credentials.port
								, NULL
								, flags
								) ;

	if ( m_conn == NULL )
	{
		std::stringstream error ;
		error << "Unable to connect to database "
					<< credentials.username<<"@"<<credentials.host<<":"<<credentials.port<<"/"<<credentials.database
					<< ". Error #" << mysql_errno(mysql) << ": " << mysql_error(mysql) ;

		mysql_close(mysql) ;

		throw std::runtime_error(error.rdbuf()->str()) ;
	}

	// Set the auto-reconnect flag on versions of MySQL that have it.
# if MYSQL_VERSION_ID >= 50003
	my_bool reconnect = (autoReconnect ? 1 : 0) ;
	mysql_options(m_conn, MYSQL_OPT_RECONNECT, &reconnect) ;
# endif

	// Force utf8 as the connection characterset.
	if ( mysql_set_character_set(m_conn, "utf8") != 0 )
	{
		std::stringstream error ;
		error << "Unable to set MySQL connection character set to UTF8. Error #" << mysql_errno(m_conn) << ": " << mysql_error(m_conn) ;

		mysql_close(m_conn) ;
		m_conn = NULL ;

		throw std::runtime_error(error.rdbuf()->str()) ;
	}
}

//////////////////////////////////////////////////////////////////////
// Disconnect from database

void
MySQLConnection::Disconnect()
{
	if ( m_currentResult != NULL )
		m_currentResult->Release() ;

	if ( m_conn == NULL )
		return ;

	mysql_close(m_conn) ;
	m_conn = NULL ;
}

//////////////////////////////////////////////////////////////////////
// Clean up result sets

void
MySQLConnection::_releaseResultSet()
{
	// Clean up the MySQL result
	if ( m_result )
	{
		mysql_free_result(m_result) ;
		m_result = NULL ;
	}

	// Drain any remaining result sets (e.g. from a CALL query)
	if ( m_conn != NULL )
	{
		while ( mysql_next_result(m_conn) == 0 )
		{
			m_result = mysql_store_result(m_conn) ;
			if ( m_result != NULL )
			{
				mysql_free_result(m_result) ;
				m_result = NULL ;
			}
		}
	}
}

//////////////////////////////////////////////////////////////////////
// Escape a string for any illegal SQL characters.

void
MySQLConnection::EscapeString(const char* src, char* dest, size_t destSize)
{
	size_t srcLen = strlen(src) ;
	const size_t destLen = destSize - 1 ;
	if ( srcLen > destLen / 2 )
		srcLen = destLen / 2 ;

	if ( m_conn )
		mysql_real_escape_string(m_conn, dest, src, srcLen) ;
	else
		mysql_escape_string(dest, src, srcLen) ;
}

//////////////////////////////////////////////////////////////////////
// Execute SQL statement

void
MySQLConnection::_execute(size_t stlen, const char* statement)
{
	unsigned int queryResult = mysql_real_query(m_conn, statement, stlen) ;

	// Unless we lost the connection, we did actual work.
	if ( queryResult != CR_SERVER_GONE_ERROR && queryResult != CR_SERVER_LOST )
		m_executed = true ;

	if ( queryResult != 0 )
	{
		// Failed
		m_error = mysql_errno(m_conn) ;

		ThrowError("Statement Error: ") ;
	}
}

//////////////////////////////////////////////////////////////////////
// Retrieve results from execution

void
MySQLConnection::_processResultSet()
{
	m_result = mysql_store_result(m_conn) ;

	if ( m_result == NULL )
	{
		// Should there have been a result?
		if (mysql_field_count(m_conn) != 0 )
		{
			m_error = mysql_errno(m_conn) ;
			if ( m_error != 0 )
			{
				ThrowError("MySQL error retrieving results from query: ") ;
			}
			else
				throw std::runtime_error("Failed to retrieve result set") ;
		}
		m_rows = m_cols = 0 ;
	}
	else
	{
		m_rows = (size_t)mysql_num_rows(m_result) ;
		m_cols = (size_t)mysql_num_fields(m_result) ;
	}

	m_affectedRows = mysql_affected_rows(m_conn) ;
	m_lastInsertID = mysql_insert_id(m_conn) ;
}

//////////////////////////////////////////////////////////////////////
// Fetch next row, or return false.

bool
MySQLConnection::FetchRow()
{
	m_currentColumn = 0 ;

	// Conditions under which we shouldn't be getting called
	if ( !m_executed )
		throw std::logic_error("No query executed to fetch rows from") ;

	if ( m_error )
		throw std::logic_error("Can't fetch rows, query failed") ;

	if ( m_result == NULL )
		throw std::logic_error("No result set to query") ;

	// Have we reached the end of the data, or is the result
	// empty columns?
	if ( HasRows() == false || m_cols == 0 )
	{
		return false ;
	}

	m_columns = mysql_fetch_row(m_result) ;
	if ( m_columns == NULL )
	{
		m_error = mysql_errno(m_conn) ;
		ThrowError("Error retrieving row: ") ;
	}

	++m_currentRow ;

	return true ;
}

//////////////////////////////////////////////////////////////////////
// Some queries (e.g. CALL) can return multiple result sets, it's
// also possible to generate multiple result sets by issuing multiple
// statements in one command (e.g. "SELECT 1; SELECT 2").

bool
MySQLConnection::NextResultSet()
{
	ReleaseResult() ;

	m_error = mysql_next_result(m_conn) ;
	if ( m_error > 0 )
		ThrowError("Error retrieving next result set") ;
	if ( m_error < 0 )
		return false ;

	m_executed = true ;

	_processResultSet() ;

	return true ;
}



} ; // namespace DBA

#endif // DBA_ENABLE_MYSQL

