/*

Copyright (c) 2002-2005, Yauheni Akhotnikau
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.

- The name of the author may not be used to endorse or promote products derived
from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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 <iostream>
#include <fstream>
#include <cerrno>
#include <cstring>
#include <algorithm>
#include <functional>
#include <stdexcept>

#include <args_4/h/args_iostream.hpp>

#include <auto_ptr_3/h/obj_ptr.hpp>

#include <oess_1/defs/log/h/err.hpp>

#include <oess_1/defs/h/ex.hpp>

#include <oess_1/defs/util/h/logo.hpp>

#include <oess_1/file/h/file.hpp>

#include <oess_1/db/storage/h/errno.hpp>

#include <oess_1/db/storage/trx/h/types.hpp>
#include <oess_1/db/storage/trx/h/std_file_committer.hpp>

#define OPT_COUT(stream_ptr) !(stream_ptr) ? std::cout : *(stream_ptr)

const std::string	backup_file_suffix = "~";

void
show_usage()
{
	std::cout << OESS_1__LOGO << std::endl
		<< "Restore database or create snapshot by replaying trace files\n"
			"\n"
			"oess_trace_replay [options] [@reponse-file]\n\n"
			"options:\n"
			"\t--db <name-of-db>  database name (mandatory)\n"
			"\t--snapshot	making snapshot file (do not restore database)\n"
			"\t--remove-unused	remove unused (outdated) trace and "
					"snapshot files\n"
			"\t--verbose	verbose output when restoring DB\n"
			"\t--trace-load-details	verbose output when loading trace "
				"files"
			<< std::endl;
}

void
parse_resp_file(
	const char * resp_file_name,
	args_4::cmd_t & arg_db,
	args_4::cmd_t & arg_snapshot,
	args_4::cmd_t & arg_verbose,
	args_4::cmd_t & arg_remove_unused,
	args_4::cmd_t & arg_trace_load_details ) {

	args_4::cmd_parser_t args_parser;
	args_parser.add_cmds( &arg_db, &arg_snapshot, &arg_verbose,
		&arg_remove_unused, &arg_trace_load_details, 0 );

	args_4::words_from_file_t words_container(
		resp_file_name );

	args_4::iostream_error_stream_t args_error_stream;
	int error = args_parser.parse( words_container,
		&args_error_stream );
	if( args_4::c_ok != error ) {
		if( args_4::c_io_error == error ) {
			OESS_THROW_LOGIC( error,
				"I/O error with response file: "
				<< resp_file_name << ", error: "
				<< strerror(
					words_container.query_last_io_result() ) )
		}
		else
			OESS_THROW_LOGIC( error,
				"unable to parse response file: "
				<< resp_file_name )
	}
}

void
check_args(
	args_4::cmd_str_value_t & arg_db,
	args_4::cmd_on_off_switch_t & arg_snapshot,
	args_4::cmd_on_off_switch_t & arg_verbose,
	args_4::cmd_on_off_switch_t & arg_remove_unused,
	args_4::cmd_on_off_switch_t & arg_trace_load_details )
{
	if( !arg_db.is_defined() )
		OESS_THROW_LOGIC( args_4::c_mandatory_cmd_missed,
			"cmd-line argument missed: "
			<< arg_db.query_name() )
}

void
parse_args(
	int argc, char ** argv,
	args_4::cmd_str_value_t & arg_db,
	args_4::cmd_on_off_switch_t & arg_snapshot,
	args_4::cmd_on_off_switch_t & arg_verbose,
	args_4::cmd_on_off_switch_t & arg_remove_unused,
	args_4::cmd_on_off_switch_t & arg_trace_load_details ) {

	args_4::cmd_response_file_t arg_resp_file;
	args_4::cmd_parser_t args_parser;
	args_parser.add_cmds( &arg_db,
		&arg_snapshot,
		&arg_verbose,
		&arg_remove_unused,
		&arg_trace_load_details,
		&arg_resp_file, 0 );

	args_4::words_from_argv_t words_container(
		argc, argv );
	args_4::iostream_error_stream_t args_error_stream;

	int error = args_parser.parse( words_container,
		&args_error_stream );
	if( args_4::c_ok == error ) {
		// Если нужно, разбираем response-файлы.
		if( arg_resp_file.is_defined() ) {
			for( size_t i = 0, i_max =
				arg_resp_file.query_value_count();
				i != i_max; ++i )
				parse_resp_file( arg_resp_file.query_value( i ),
					arg_db, arg_snapshot, arg_verbose, arg_remove_unused,
					arg_trace_load_details );
		}
	}
	else
		OESS_THROW_LOGIC( error,
			"unable to parse cmd-line arguments" )

	check_args( arg_db, arg_snapshot, arg_verbose, arg_remove_unused,
		arg_trace_load_details );
}

std::string
make_main_file_name(
	const std::string & physic_name )
	{
		return physic_name + ".oess.dat";
	}

std::string
make_unclear_file_name(
	const std::string & physic_name )
	{
		return physic_name + ".unclear";
	}

void
backup_file(
	const std::string & what,
	// Может быть 0.
	std::ostream * verbose_stream )
	{
		if( oess_1::file::file_t::is_file_exists( what ) )
			{
				std::string backup_file_name = what + backup_file_suffix;

				OPT_COUT( verbose_stream ) << "renaming '"
					<< what << "' to '" << backup_file_name
					<< "..." << std::flush;
				oess_1::file::file_t::rename_file(
					what, backup_file_name, true );
				OPT_COUT( verbose_stream ) << "ok" << std::endl;
			}
	}

void
backup_main_file(
	// Физическое имя БД, чей основной файл нужно переименовать.
	const std::string & physic_name,
	// Поток для отображения дополнительной информации о ходе
	// выполнения действий. Может быть 0.
	std::ostream * verbose_stream )
	{
		backup_file( make_main_file_name( physic_name ),
			verbose_stream );
	}

void
backup_unclear_file(
	// Физическое имя БД, чей рабочий файл нужно переименовать.
	const std::string & physic_name,
	// Поток для отображения дополнительной информации о ходе
	// выполнения действий. Может быть 0.
	std::ostream * verbose_stream )
	{
		backup_file( make_unclear_file_name( physic_name ),
			verbose_stream );
	}

//
// Предикат для std::for_each.
// Поочередно удаляет указанные trace-файлы.
//
class	traces_eraser_t
	:	public std::unary_function<
			oess_1::db::storage::trx::trace_file_number_t, void >
	{
		// Физическое имя БД, которой принадлежать trace-файлы.
		const std::string &	m_physic_name;
		// Необязательный поток для отладочных сообщений.
		std::ostream *	m_verbose_stream;

	public :
		traces_eraser_t(
			const std::string & physic_name,
			std::ostream * verbose_stream )
			:	m_physic_name( physic_name )
			,	m_verbose_stream( verbose_stream )
			{}

		result_type
		operator()( argument_type a )
			{
				std::string name = oess_1::db::storage::trx::
						create_trace_file_name( m_physic_name, a );
				OPT_COUT( m_verbose_stream ) << "removing "
						<< name << std::endl;

				try
					{
						oess_1::file::file_t::remove( name );
					}
				catch( const std::exception & x )
					{
						std::cerr << x.what() << std::endl;
					}
			}
	};

//
// Предикат для std::for_each.
// Поочередно удаляет указанные snapshot-файлы.
//
class	snapshots_eraser_t
	:	public std::unary_function<
			oess_1::db::storage::trx::trace_file_number_t, void >
	{
		// Физическое имя БД, которой принадлежать trace-файлы.
		const std::string &	m_physic_name;
		// Необязательный поток для отладочных сообщений.
		std::ostream *	m_verbose_stream;

	public :
		snapshots_eraser_t(
			const std::string & physic_name,
			std::ostream * verbose_stream )
			:	m_physic_name( physic_name )
			,	m_verbose_stream( verbose_stream )
			{}

		result_type
		operator()( argument_type a )
			{
				std::string name = oess_1::db::storage::trx::
						create_snapshot_file_name( m_physic_name, a );
				OPT_COUT( m_verbose_stream ) << "removing "
						<< name << std::endl;

				try
					{
						oess_1::file::file_t::remove( name );
					}
				catch( const std::exception & x )
					{
						std::cerr << x.what() << std::endl;
					}
			}
	};


void
remove_unused_traces(
	// Физическое имя БД, с которой нужно работать.
	const std::string & physic_name,
	// Множество подлежащих удалению trace-файлов.
	const oess_1::db::storage::trx::trace_file_number_set_t &
			unused_traces,
	// Если равен 0, то информация о выполняемых действиях не отображается.
	std::ostream * verbose_stream )
	{
		if( !unused_traces.empty() )
			{
				OPT_COUT( verbose_stream ) << "removing unused (outdated) "
						"trace files..." << std::endl;
				std::for_each( unused_traces.begin(),
						unused_traces.end(),
						traces_eraser_t( physic_name, verbose_stream ) );
			}
	}

void
remove_unused_snapshots(
	// Физическое имя БД, с которой нужно работать.
	const std::string & physic_name,
	// Если равен 0, то информация о выполняемых действиях не отображается.
	std::ostream * verbose_stream )
	{
		using namespace oess_1::db::storage::trx;

		std::auto_ptr< snapshot_collection_t > snapshots(
				create_std_snapshot_collection( physic_name ) );

		trace_file_number_set_t non_actual( snapshots->non_actual() );
		if( !non_actual.empty() )
			{
				OPT_COUT( verbose_stream ) << "removing unused (outdated) "
						"snapshot files..." << std::endl;
				std::for_each( non_actual.begin(), non_actual.end(),
						snapshots_eraser_t( physic_name, verbose_stream ) );
			}
	}

void
make_snapshot(
	// Физическое имя БД, с которой нужно работать.
	const std::string & physic_name,
	// Конфигурация БД.
	const oess_1::db::storage::config_t & config,
	// Поток для отображения дополнительной информации о ходе
	// восстановления БД. Если равен 0, то дополнительная
	// информация не отображается.
	std::ostream * verbose_stream,
	// Должны ли быть удалены устаревшие файлы.
	bool is_remove_unused,
	// Поток для отображения дополнительной информации о ходе
	// анализа trace-файлов. Если равен 0, то дополнительная
	// информация не отображается.
	std::ostream * trace_load_details_stream )
	{
		using namespace oess_1::db::storage::trx;

		std::auto_ptr< trace_stream_t > trace_stream( 
				create_std_trace_stream( oess_1::db::storage::trace_all ) );	

		trace_stream->open( physic_name, config, true );

		snapshot_file_committer_t snapshot_committer( physic_name );

		OPT_COUT( verbose_stream ) << "replaying trace-stream..."
				<< std::endl;
		try
			{
				trace_stream->reverse_replay( snapshot_committer,
						0, 0, trace_load_details_stream );
			}
		catch( const oess_1::logic_ex_t & ex )
			{
				// Исключение с кодом c_no_traces_for_snapshot не является
				// ошибкой. В этом случае нужно считать, что воспроизведение
				// прошло успешно.
				if( oess_1::db::storage::err::c_no_traces_for_snapshot ==
						ex.query_err_code().m_code )
					OPT_COUT( verbose_stream ) << "no traces to include in "
							"new snapshot" << std::endl;
				else
					throw;
			}

		if( is_remove_unused )
			{
				trace_file_number_set_t outdated_traces(
						snapshot_committer.outdated_traces() );
				remove_unused_traces(
						physic_name, outdated_traces, verbose_stream );
				remove_unused_snapshots( physic_name, verbose_stream );
			}
	}

void
restore_db(
	// Физическое имя БД, с которой нужно работать.
	const std::string & physic_name,
	// Конфигурация БД.
	const oess_1::db::storage::config_t & config,
	// Поток для отображения дополнительной информации о ходе
	// восстановления БД. Если равен 0, то дополнительная
	// информация не отображается.
	std::ostream * verbose_stream,
	// Должны ли быть удалены устаревшие файлы.
	bool is_remove_unused,
	// Поток для отображения дополнительной информации о ходе
	// анализа trace-файлов. Если равен 0, то дополнительная
	// информация не отображается.
	std::ostream * trace_load_details_stream )
	{
		using namespace oess_1::db::storage::trx;

		std::auto_ptr< trace_stream_t > trace_stream( 
				create_std_trace_stream( config.trace_mode() ) );	

		trace_stream->open( physic_name, config, true );

		std::auto_ptr< committer_t > file_committer(
				create_std_file_committer( physic_name,
						config.trace_mode() ) );

		trace_file_number_set_t unused_traces;

		OPT_COUT( verbose_stream ) << "replaying trace-stream..."
				<< std::endl;
		trace_stream->reverse_replay( *file_committer, 0,
				&unused_traces, trace_load_details_stream );

		if( is_remove_unused )
			{
				remove_unused_traces( physic_name, unused_traces, verbose_stream );
				remove_unused_snapshots( physic_name, verbose_stream );
			}
	}

void
do_main_action(
	// Физическое имя БД, с которой нужно работать.
	const std::string & physic_name,
	// Если равен true, то нужно не восстанавливать БД, а формировать
	// очередной snapshot-файл.
	bool is_snapshot_mode,
	// Поток для отображения дополнительной информации о ходе
	// восстановления БД. Если равен 0, то дополнительная
	// информация не отображается.
	std::ostream * verbose_stream,
	// Должны ли быть удалены устаревшие файлы.
	bool is_remove_unused,
	// Поток для отображения дополнительной информации о ходе
	// анализа trace-файлов. Если равен 0, то дополнительная
	// информация не отображается.
	std::ostream * trace_load_details_stream )
	{
		// Нужно поднять конфигурацию, чтобы определить, что мы должны делать.
		oess_1::db::storage::config_t config;
		config = oess_1::db::storage::load_cfg_file(
				config.make_cfg_file_name( physic_name ) );

		if( oess_1::db::storage::trace_no == config.trace_mode() )
			throw std::logic_error( physic_name + ": cannot be used. "
					"trace_mode == trace_no." );

		if( is_snapshot_mode )
			{
				// Формировать snapshot можно только, если в trace-файлах
				// накапливается вся информация.
				if( oess_1::db::storage::trace_all != config.trace_mode() )
					throw std::logic_error( physic_name + ": snapshot cannot be "
							"created. trace_mode != trace_all" );

				make_snapshot( physic_name, config,
						verbose_stream, is_remove_unused,
						trace_load_details_stream );
			}
		else
			{
				// Нужно восстанавливать БД.
				if( oess_1::db::storage::trace_all == config.trace_mode() )
					{
						// В этом режиме нужно сделать backup-файлы.
						backup_main_file( physic_name, verbose_stream );
						backup_unclear_file( physic_name, verbose_stream );
					}
      
				restore_db( physic_name, config, verbose_stream,
					is_remove_unused, trace_load_details_stream );
			}
	}

void
do_work( int argc, char ** argv )
{
	if( 1 == argc )
		show_usage();
	else {
		args_4::cmd_str_value_t arg_db( "--db", false );
		args_4::cmd_on_off_switch_t arg_snapshot( "--snapshot", false );
		args_4::cmd_on_off_switch_t arg_verbose( "--verbose", false );
		args_4::cmd_on_off_switch_t arg_remove_unused(
			"--remove-unused", false );
		args_4::cmd_on_off_switch_t arg_trace_load_details(
			"--trace-load-details", false );
		parse_args( argc, argv, arg_db,
			arg_snapshot,
			arg_verbose,
			arg_remove_unused,
			arg_trace_load_details );

		do_main_action( arg_db.query_value(),
			arg_snapshot.is_defined(),
			arg_verbose.is_defined() ? &std::cout : 0,
			arg_remove_unused.is_defined(),
			arg_trace_load_details.is_defined() ? &std::cout : 0 );
	}
}

int
main( int argc, char ** argv ) {
	try {
		do_work( argc, argv );

		return 0;
	}
	catch( oess_1::ex_t & x ) {
		std::cerr << "*** " << x.query_err_code() << " ***"
			<< std::endl;
	}
	catch( const std::exception & x ) {
		std::cerr << "### " << x.what() << " ###"
			<< std::endl;
	}
	catch( ... ) {
		OESS_1_LOG_ERR( "Oops! Unknown exception!" << std::endl )
	}
	
	return 2;
}
