/* $Id$ */

/**
 * \file 
 * $Revision$
 * $Date$
 * 
 * Copyright (C) 2014 The University of Sydney, Australia
 *
 * This file is part of GPlates.
 *
 * GPlates is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, version 2, as published by
 * the Free Software Foundation.
 *
 * GPlates 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 General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#ifndef GPLATES_SCRIBE_TRANSCRIBEENUMPROTOCOL_H
#define GPLATES_SCRIBE_TRANSCRIBEENUMPROTOCOL_H

#include <string>

#include "ScribeExceptions.h"
#include "Transcribe.h"
#include "TranscribeResult.h"

#include "global/GPlatesAssert.h"

#include "utils/CallStackTracker.h"


/**
 * The file and line number of a transcribe call site.
 *
 * Defined here to avoid having to include heavyweight "Scribe.h".
 */
#ifndef TRANSCRIBE_SOURCE
#define TRANSCRIBE_SOURCE \
		GPlatesUtils::CallStack::Trace(__FILE__, __LINE__)
#endif


namespace GPlatesScribe
{
	// Forward declarations.
	class Scribe;


	/**
	 * Associate an enumeration value (integer) with a name (string).
	 *
	 * Used in function @a transcribe_enum_protocol.
	 */
	struct EnumValue
	{
		EnumValue(
				const char *name_,
				int value_) :
			name(name_),
			value(value_)
		{  }

		const char *name;
		int value;
	};


	/**
	 * A helper function for transcribing an enumeration.
	 *
	 * The client provides a sequence of enumeration values and associated names.
	 *
	 * Note that 'TRANSCRIBE_UNKNOWN_TYPE' is returned if an unknown enumeration value is
	 * encountered while loading - this can happen when loading an archive generated by a
	 * future version of GPlates that added a new enumeration value.
	 *
	 * With this protocol the associated names are transcribed instead of the (integer) values.
	 * This has the benefit of providing better backwards and forwards compatibility when
	 * changes are made to an enumerated type over time (such as adding, removing, inserting or
	 * even changing enumeration values).
	 * For example if, in the example below, 'ENUM_VALUE_1' and 'ENUM_VALUE_2' have their order
	 * swapped (and hence their integer values get swapped) then old applications can still read
	 * transcriptions generated by new applications (forwards compatibility) and vice versa
	 * (backwards compatibility).
	 *
	 *
	 * A 'transcribe()' overload for an enumeration can be implemented as:
	 *
	 *
	 *	#include "scribe/TranscribeEnumProtocol.h"
	 *
	 *	enum Enum
	 *	{
	 *		ENUM_VALUE_1,
	 *		ENUM_VALUE_2,
	 *		ENUM_VALUE_3
	 *		
	 *		// NOTE: Any new values should also be added to @a transcribe.
	 *	};
	 *  
	 *  //
	 *  // Transcribe for sessions/projects.
	 *  //
	 *	GPlatesScribe::TranscribeResult
	 *	transcribe(
	 *			GPlatesScribe::Scribe &scribe,
	 *			Enum &e,
	 *			bool transcribed_construct_data)
	 *	{
	 *		// WARNING: Changing the string ids will break backward/forward compatibility.
	 *		//          So don't change the string ids even if the enum name changes.
	 *		static const GPlatesScribe::EnumValue enum_values[] =
	 *		{
	 *			GPlatesScribe::EnumValue("ENUM_VALUE_1", ENUM_VALUE_1),
	 *			GPlatesScribe::EnumValue("ENUM_VALUE_2", ENUM_VALUE_2),
	 *			GPlatesScribe::EnumValue("ENUM_VALUE_3", ENUM_VALUE_3)
	 *		};
	 *	
	 *		return GPlatesScribe::transcribe_enum_protocol(
	 *				TRANSCRIBE_SOURCE,
	 *				scribe,
	 *				e,
	 *				enum_values,
	 *				enum_values + sizeof(enum_values) / sizeof(enum_values[0]));
	 *	}
	 *
	 *
	 * ...and if the enumeration is inside a class and is *private* then you'll need to
	 * implement it as a friend function declared and defined in the class body as in...
	 *
	 *
	 *	class MyClass
	 *	{
	 *	private: // private enum...
	 *
	 *		enum Enum
	 *		{
	 *			ENUM_VALUE_1,
	 *			ENUM_VALUE_2,
	 *			ENUM_VALUE_3
	 *			
	 *			// NOTE: Any new values should also be added to @a transcribe.
	 *		};
	 *
	 *		//
	 *		// Transcribe for sessions/projects.
	 *		//
	 *		// Use friend function (injection) so can access private enum.
	 *		// And implement in class body otherwise some compilers will complain
	 *		// that the enum argument is not accessible (since enum is private).
	 *		//
	 *		friend
	 *		GPlatesScribe::TranscribeResult
	 *		transcribe(
	 *				GPlatesScribe::Scribe &scribe,
	 *				Enum &e,
	 *				bool transcribed_construct_data)
	 *		{
	 *			// WARNING: Changing the string ids will break backward/forward compatibility.
	 *			//          So don't change the string ids even if the enum name changes.
	 *			static const GPlatesScribe::EnumValue enum_values[] =
	 *			{
	 *				GPlatesScribe::EnumValue("ENUM_VALUE_1", ENUM_VALUE_1),
	 *				GPlatesScribe::EnumValue("ENUM_VALUE_2", ENUM_VALUE_2),
	 *				GPlatesScribe::EnumValue("ENUM_VALUE_3", ENUM_VALUE_3)
	 *			};
	 *		
	 *			return GPlatesScribe::transcribe_enum_protocol(
	 *					TRANSCRIBE_SOURCE,
	 *					scribe,
	 *					e,
	 *					enum_values,
	 *					enum_values + sizeof(enum_values) / sizeof(enum_values[0]));
	 *		}
	 *	
	 *	public:
	 *		...
	 *	};
	 */
	template <typename EnumType, typename EnumValueIter>
	TranscribeResult
	transcribe_enum_protocol(
			const GPlatesUtils::CallStack::Trace &transcribe_source, // Use 'TRANSCRIBE_SOURCE' here
			Scribe &scribe,
			EnumType &e,
			EnumValueIter enum_values_begin,
			EnumValueIter enum_values_end);
}

//
// Implementation
//

namespace GPlatesScribe
{
	//
	// These implementation functions are purely to avoid having to include the heavyweight "Scribe.h"
	// in this header file (and hence indirectly include it in many other headers).
	//
	// This is necessary because sometimes the client must include this file in their header file
	// instead of their ".cc" file if they need to use friend function injection for access to
	// private class enums (as described above).
	//
	namespace EnumImplementation
	{
		bool
		is_scribe_saving(
				Scribe &scribe);

		bool
		is_scribe_loading(
				Scribe &scribe);

		TranscribeResult
		transcribe_enum_name(
				Scribe &scribe,
				const std::string &enum_name);
	}


	template <typename EnumType, typename EnumValueIter>
	TranscribeResult
	transcribe_enum_protocol(
			const GPlatesUtils::CallStack::Trace &transcribe_source,
			Scribe &scribe,
			EnumType &e,
			EnumValueIter enum_values_begin,
			EnumValueIter enum_values_end)
	{
		// Track the file/line of the call site for exception messages.
		GPlatesUtils::CallStackTracker call_stack_tracker(transcribe_source);

		std::string enum_name;

		if (EnumImplementation::is_scribe_saving(scribe))
		{
			// Search through the available enumeration values to find a match.
			EnumValueIter enum_values_iter = enum_values_begin;
			for ( ; enum_values_iter != enum_values_end; ++enum_values_iter)
			{
				const EnumValue &enum_value = *enum_values_iter;

				if (enum_value.value == static_cast<int>(e))
				{
					// Found a match so get the enumeration value's string name.
					enum_name = enum_value.name;
					break;
				}
			}

			// Throw exception in save path (should only happen due to programmer error).
			//
			// If this assertion is triggered then it means:
			//   * the transcribed enum value 'e' was not found in the list of enum values
			//     registered in (enum_values_begin, enum_values_end).
			//
			GPlatesGlobal::Assert<Exceptions::UnregisteredEnumValue>(
					enum_values_iter != enum_values_end,
					GPLATES_ASSERTION_SOURCE,
					typeid(EnumType),
					e);
		}

		// Transcribe the enumeration value's string name.
		TranscribeResult transcribe_result =
				EnumImplementation::transcribe_enum_name(scribe, enum_name);
		if (transcribe_result != TRANSCRIBE_SUCCESS)
		{
			return transcribe_result;
		}

		if (EnumImplementation::is_scribe_loading(scribe))
		{
			// Search through the available enumeration values to find a match.
			EnumValueIter enum_values_iter = enum_values_begin;
			for ( ; enum_values_iter != enum_values_end; ++enum_values_iter)
			{
				const EnumValue &enum_value = *enum_values_iter;

				if (enum_name == enum_value.name)
				{
					// Found a match so get the enumeration value associated with the string name.
					e = static_cast<EnumType>(enum_value.value);
					break;
				}
			}

			// If enum value not found in load path then it means the version of GPlates that
			// generated the transcription (archive) saved an enum value that we don't know about
			// (eg, a future version of GPlates added an enum value to EnumType).
			if (enum_values_iter == enum_values_end)
			{
				return TRANSCRIBE_UNKNOWN_TYPE;
			}
		}

		return TRANSCRIBE_SUCCESS;
	}
}

#endif // GPLATES_SCRIBE_TRANSCRIBEENUMPROTOCOL_H
