//------------------------------------------------------------------------------
//	Copyright (C) 2009, 2010 Humboldt-Universitaet zu Berlin
//
//	This library 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.
//
//	This library 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 this library; if not, write to the Free Software
//	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//------------------------------------------------------------------------------

/**
 * @file DatabaseWriter.cpp
 * @author Ronald Kluth
 * @date created at 2009/02/18
 * @brief Implementation of class odemx::data::output::DatabaseWriter
 * @sa DatabaseWriter.h
 * @since 3.0
 */

#include <odemx/data/output/DatabaseWriter.h>
#include <odemx/base/SimTime.h>
#include <odemx/base/Simulation.h>
#include <odemx/data/output/DefaultTimeFormat.h>

#include <CppLog/DeclareInfoTypeList.h>
#include <CppLog/DeclareSqlColumnTypes.h>
#include <Poco/Data/DataException.h>

namespace odemx {
namespace data {
namespace output {

CPPLOG_DECLARE_INFO_TYPE_LIST( SimRecordInfos,
		ChannelInfo, ClassScopeInfo, SenderLabelInfo,
		SenderTypeInfo, StringTimeInfo, SimIdInfo );

CPPLOG_DECLARE_SQL_COLUMN_TYPES( SimRecordInfos, simRecordSqlTypes,
		"VARCHAR NOT NULL", "VARCHAR", "VARCHAR NOT NULL",
		"VARCHAR NOT NULL", "VARCHAR NOT NULL", "INTEGER NOT NULL" );

std::tr1::shared_ptr< DatabaseWriter >
DatabaseWriter::create( const std::string& connectionString,
		buffer::SimRecordBuffer::SizeType bufferLimit )
{
	return std::tr1::shared_ptr< DatabaseWriter >(
			new DatabaseWriter( connectionString, bufferLimit ) );
}

DatabaseWriter::DatabaseWriter( const std::string& connectionString,
		buffer::SimRecordBuffer::SizeType bufferLimit )
:	db_( connectionString )
,	recordBuffer_( bufferLimit )
,	lastRecordSim_( 0 )
,	simIds_()
,	currentSimKey_( 0 )
,	currentRecordKey_( 0 )
,	timeFormat_( new DefaultTimeFormat() )
{
	// create tables if necessary,
	// create SQL inserters for all tables
	initialize();
}

DatabaseWriter::~DatabaseWriter()
{
	try
	{
		if( ! recordBuffer_.isEmpty() )
		{
			flush();
		}
	}
	catch( ... )
	{
		// destructors should not throw
	}
}

void DatabaseWriter::consume( const Log::ChannelId channelId,
		const SimRecord& simRecord )
{
	// check if a new simulation entry needs to be inserted
	// and set currentSimKey_ accordingly
	checkRecordSim( &simRecord.getSimulation() );

	// add the current record to the buffer
	// the value of currentSimKey_ has been updated if needed
	const std::string& timeString = timeFormat_->timeToString( simRecord.getTime() );
	recordBuffer_.put( channelId, simRecord, currentSimKey_, timeString );

	// check if buffer limit is reached, store data if necessary
	if( recordBuffer_.isFull() )
	{
		try
		{
			flush();
		}
		catch( const Poco::Data::DataException& )
		{
			throw;
		}
	}
}

void DatabaseWriter::setTimeFormat( TimeFormat* timeFormat )
{
	timeFormat_.reset( timeFormat );
}

void DatabaseWriter::flush()
{
	using namespace std::tr1::placeholders;

	db_.storeData( recordBuffer_.getStorage(),
			std::tr1::bind( &DatabaseWriter::insertRecord, this, _1 ) );
	recordBuffer_.clear();
}

void DatabaseWriter::checkTables()
{
#ifdef CPPLOG_USE_POSTGRESQL
	static const char* primaryKeyType = "SERIAL";
#else // SQLite
	static const char* primaryKeyType = "INTEGER NOT NULL";
#endif

	try
	{
		if( ! db_.tableExists( "odemx_simulation_run" ) )
		{
			db_.createTable( "odemx_simulation_run" )
				.column( "id", primaryKeyType )
				.column( "date", "TIMESTAMP NOT NULL" )
				.column( "label", "VARCHAR NOT NULL" )
				.column( "description", "VARCHAR NOT NULL" )
				.column( "uuid", "CHAR(40) NOT NULL" )
				.primaryKey( "id" );
		}

		if( ! db_.tableExists( "odemx_sim_record" ) )
		{
			db_.createTable( "odemx_sim_record" )
				.column( "id", primaryKeyType )
				.column( "text", "VARCHAR" )
				.columns< SimRecordInfos >( simRecordSqlTypes )
				.primaryKey( "id" );
		}

		if( ! db_.tableExists( "odemx_sim_record_detail" ) )
		{
			db_.createTable( "odemx_sim_record_detail" )
				.column( "id", primaryKeyType )
				.column( "name", "VARCHAR NOT NULL" )
				.column( "value", "VARCHAR NOT NULL" )
				.column( "record_id", "INTEGER NOT NULL" )
				.primaryKey( "id" );
		}
	}
	catch( const Poco::Data::DataException& )
	{
		throw;
	}
}

void DatabaseWriter::checkRecordSim( const base::Simulation* currentSim )
{
	// do nothing for currently active sim, pointer and key are correct
	if( currentSim == lastRecordSim_ )
	{
		return;
	}

	// simulation changed
	lastRecordSim_ = currentSim;

	// check if there is an id stored for this simulation
	SimIdMap::const_iterator found = simIds_.find( currentSim );
	if( found != simIds_.end() )
	{
		currentSimKey_ = found->second;
	}
	else // new simulation
	{
		// insert simulation into database, sets currentSimKey_ to last insert ID
		insertSimIntoTable( currentSim );
		// store sim with key
		simIds_.insert( SimIdMap::value_type( currentSim, currentSimKey_ ) );
	}
}

void DatabaseWriter::insertSimIntoTable( const base::Simulation* sim )
{
	// get current time of insertion
	std::string currentDbTime = db_.getCurrentDatabaseTime();

	try
	{
		// ID column is automatically generated by DBMS
		simInserter_->value( currentDbTime )
					.value( sim->getLabel()  )
					.value( sim->getDescription() )
					.value( sim->getId().toString() )
					.execute();

		// retrieve ID of the last inserted row for record foreign key
#ifdef CPPLOG_USE_POSTGRESQL
		currentSimKey_ = db_.getLastInsertId( "odemx_simulation_run_id_seq" );
#else
		currentSimKey_ = db_.getLastInsertId();
#endif
	}
	catch( const Poco::Data::DataException& )
	{
		throw;
	}
}

void DatabaseWriter::initialize()
{
	try
	{
		// make sure that all tables exist in the connected database
		checkTables();

		// initialize SQL inserters, we do not insert values
		// into ID columns because the DBMS generates the keys
		simInserter_ = db_.createInserter();
		simInserter_->table( "odemx_simulation_run" )
						.column( "date" )
						.column( "label" )
						.column( "description" )
						.column( "uuid" );

		recordInserter_ = db_.createInserter();
		recordInserter_->table( "odemx_sim_record" )
						.column( "text" )
						.columns< SimRecordInfos >();

		detailInserter_ = db_.createInserter();
		detailInserter_->table( "odemx_sim_record_detail" )
						.column( "name" )
						.column( "value" )
						.column( "record_id" );
	}
	catch( const Poco::Data::DataException& )
	{
		throw;
	}
}

void DatabaseWriter::insertRecord( const buffer::SimRecordBuffer::StoredRecord& record )
{
	try
	{
		// insert the stored record
		recordInserter_->value( std::string( record.getText().c_str() ) )
						.values< SimRecordInfos >( record )
						.execute();

		// retrieve the ID generated for the last inserted row for detail foreign key
#ifdef CPPLOG_USE_POSTGRESQL
		currentRecordKey_ = db_.getLastInsertId( "odemx_sim_record_id_seq" );
#else
		currentRecordKey_ = db_.getLastInsertId();
#endif

		// check record details
		if( record.hasDetails() )
		{
			for( SimRecord::DetailVec::const_iterator detailIter = record.getDetails().begin();
				 detailIter != record.getDetails().end(); ++detailIter )
			{
				// insert record details into table
				detailInserter_->value( std::string( detailIter->first.c_str() ) )
								.value( detailIter->second.convert< std::string >() )
								.value( currentRecordKey_ )
								.execute();
			}
		}
	}
	catch( const Poco::Data::DataException& )
	{
		throw;
	}
}

const buffer::SimRecordBuffer& DatabaseWriter::getBuffer() const
{
	return recordBuffer_;
}

} } } // namespace odemx::data::output
