//
// Copyright (c) 2009-2010 Ronald Kluth
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

/**
 * @file DatabaseAccessor.h
 * @author Ronald Kluth
 * @date 2009/06/17
 * @brief Declaration and implementation of class DatabaseAccessor
 * @since 0.1
 */

#ifndef LOG_OUTPUT_DATABASEACCESSOR_INCLUDED
#define LOG_OUTPUT_DATABASEACCESSOR_INCLUDED

#ifdef CPPLOG_USE_DATABASE

#include "../../setup.h"
#include "../Consumer.h"
#include "../StringLiteral.h"
#include "../detail/SqlTableCreator.h"
#include "../detail/SqlInserter.h"

#include <Poco/Data/Session.h>
#include <Poco/Runnable.h>
#include <Poco/Thread.h>
#include <Poco/ThreadLocal.h>
#include <Poco/Types.h>

#if defined CPPLOG_USE_ODBC && defined CPPLOG_USE_SQLITE
#error "CPPLOG_USE_ODBC and CPPLOG_USE_SQLITE are defined: ambiguous setup"
#endif

#if defined CPPLOG_USE_ODBC
	#include <Poco/Data/ODBC/Connector.h>
	#include <Poco/Data/ODBC/SessionImpl.h>
#elif defined CPPLOG_USE_SQLITE
	#include <Poco/Data/SQLite/Connector.h>
#else
#error "Neither CPPLOG_USE_ODBC nor CPPLOG_USE_SQLITE are defined: cannot register connector"
#endif

#include <algorithm> // for_each
#include <cstddef> // size_t
#include <memory> // auto_ptr
#include <vector>

namespace Log {

/**
 * @brief Base class that gives log consumers database access
 *
 * Even basic database interaction requires different SQL syntax on different
 * database management systems. This class provides an abstraction layer for
 * common database operations so that log consumers can use a single interface
 * regardless of the underlying database, as long as it is supported by this
 * class.
 *
 * @note This class requires the Data module of the Poco C++ libraries.
 */
class DatabaseAccessor
{
public:
	/// Pull SqlTableCreator into this scope
	typedef Detail::SqlTableCreator SqlTableCreator;
	/// Pulls SqlInserter into this scope, improves usability
	typedef Detail::SqlInserter SqlInserter;

	/// Construction
	DatabaseAccessor( const std::string& connectionString )
	:	session_( 0 )
	{
#if defined CPPLOG_USE_ODBC
		Poco::Data::ODBC::Connector::registerConnector();
		session_.reset( new Poco::Data::Session( "ODBC", connectionString ) );
		Poco::Data::ODBC::SessionImpl* impl = static_cast< Poco::Data::ODBC::SessionImpl* >( session_->impl() );
		if( impl->isAutoCommit() )
		{
			impl->autoCommit( "", false );
		}
#elif defined CPPLOG_USE_SQLITE
		Poco::Data::SQLite::Connector::registerConnector();
		session_.reset( new Poco::Data::Session( "SQLite", connectionString ) );
#endif
	}

	/// Destruction
	~DatabaseAccessor()
	{
	}

	/// Create a table by appending columns to a table builder object
	SqlTableCreator createTable( const std::string& tableName )
	{
		return SqlTableCreator( tableName, *session_ );
	}

	/// Create an SQL inserter for reuse of a prepared statement
	std::auto_ptr< SqlInserter > createInserter()
	{
		return std::auto_ptr< SqlInserter >( new SqlInserter( *session_, false ) );
	}

	/// Insert data into a table by appending values to an insert builder object
	SqlInserter insertIntoTable( const std::string& tableName )
	{
		return SqlInserter( tableName, *session_, true );
	}

	/// Check if a table with the given name already exists
	bool tableExists( const std::string& tableName )
	{
		using namespace Poco::Data;
		using namespace Poco::Data::Keywords;
		int count = 0;
		Statement stmt( *session_ );
		try
		{
#if defined CPPLOG_USE_ODBC
			stmt <<	"SELECT count(table_name) FROM information_schema.tables "
					"WHERE table_name = ?;", bind( tableName ), into( count );
#elif defined CPPLOG_USE_SQLITE
			stmt <<	"Select count(*) from sqlite_master where name=?;"
					,useRef( tableName ), into( count );
#else
#error "DatabaseAccessor::tableExists() requires ODBC or SQLITE to be enabled"
#endif
			stmt.execute();
		}
		catch( const DataException& )
		{
			throw;
		}
		return count > 0;
	}

	/// Get maximum value of @c column in @c table and store it in @c valueHolder
	template < typename T >
	void getMaxColumnValue( std::string const& table, std::string const& column,
			T& valueHolder )
	{
		using namespace Poco::Data;
		using namespace Poco::Data::Keywords;
		try
		{
			*session_ << "SELECT MAX(" << column << ") FROM " << table
						,into( valueHolder ), now;
		}
		catch( const DataException& )
		{
			throw;
		}
	}

	/// Count the number of rows in the given table
	Poco::UInt64 getRowCount( std::string const& table )
	{
		using namespace Poco::Data;
		using namespace Poco::Data::Keywords;
		try
		{
			Poco::UInt64 rowCount = 0;
			*session_ << "SELECT COUNT(*) FROM " << table
						,into( rowCount ), now;
			return rowCount;
		}
		catch( const DataException& )
		{
			throw;
		}
	}

	/// Get the current time from the database
	std::string getCurrentDatabaseTime()
	{
		using namespace Poco::Data;
		using namespace Poco::Data::Keywords;
		std::string currentTime;
		Statement stmt( *session_ );
		try
		{
#if defined CPPLOG_USE_ODBC
			stmt << "SELECT CURRENT_TIMESTAMP;", into( currentTime );
#elif defined CPPLOG_USE_SQLITE
			stmt << "SELECT DATETIME('now');", into( currentTime );
#else
#error "DatabaseAccessor::getCurrentDatabaseTime() requires ODBC or SQLITE to be enabled"
#endif
			stmt.execute();
		}
		catch( const DataException& )
		{
			throw;
		}
		return currentTime;
	}

	/**
	 * @brief Writes buffered records to the database
	 * @tparam RecordBufferT Buffer type, must support forward iteration
	 * @tparam InsertFuncT Function type to call with RecordBufferT::value_type
	 *
	 * @param buffer Record buffer to write the database, is cleared afterwards
	 * @param insertFunc Function called for each record to write to the database
	 *
	 * This method is implmented in terms of std::for_each, meaning the buffer
	 * must provide the methods @c begin and @c end, which return iterators to
	 * the start and jsut past the end of the given buffer. The insert function
	 * or functor must also provide the correct signature, which only takes
	 * one buffered record as parameter.
	 */
	template < typename RecordBufferT, typename InsertFuncT >
	void storeData( RecordBufferT& buffer, const InsertFuncT& insertFunc )
	{
		using namespace Poco::Data;
		using namespace Poco::Data::Keywords;

		// BEGIN TRANSACTION
		session_->begin();

		std::for_each( buffer.begin(), buffer.end(), insertFunc );

		// COMMIT
		session_->commit();
	}

#if defined CPPLOG_USE_SQLITE || defined CPPLOG_USE_POSTGRESQL

	/// Get the last automatically generated serial/identity value
	Poco::UInt64 getLastInsertId( const std::string& seqName = "" )
	{
		using namespace Poco::Data;
		using namespace Poco::Data::Keywords;
		try
		{
			Poco::UInt64 lastId;

#if defined CPPLOG_USE_POSTGRESQL
			*session_ << "SELECT currval('" << seqName << "')", into( lastId ), now;
#elif defined CPPLOG_USE_SQLITE
			*session_ << "SELECT last_insert_rowid();", into( lastId ), now;
#else
#error "DatabaseAccessor::getLastInsertId() requires SQLITE or POSTGRESQL to be enabled"
#endif

			return lastId;
		}
		catch( const DataException& )
		{
			throw;
		}
	}
#endif

private:
	/// Pointer to the database session the log information is written to
	std::auto_ptr< Poco::Data::Session > session_;
};

} // namespace Log

#endif /* CPPLOG_USE_DATABASE */

#endif /* LOG_OUTPUT_DATABASEACCESSOR_INCLUDED */
