/******************************************************************************
 JPrefsManager.cc

	Buffers the data in a JPrefsFile and provides a base class for
	application-specific preferences management.  The functions to access
	the data are protected because they should be hidden behind a clean
	interface in the derived class.

	UpgradeData() is called after the file has been read.  Since UpgradeData()
	must work on an empty file, this insures that the program has a valid set
	of preferences even if the file could not be read.

	Some programs enforce that only a single copy is running for each user,
	so an open prefs file means that the program crashed while editing the
	preferences.  If this is the case, pass kTrue for eraseFileIfOpen to
	the constructor.  If this is not the case, DeleteFile() can
	be implemented to assert() because it should never be called.

	Derived classes must implement the following functions:

		CreateFile
			Return a derived class of JPrefsFile.

		DeleteFile
			Delete the specified file.  This will never be called if
			eraseFileIfOpen is kFalse in the constructor.

		UpgradeData
			Upgrade the preferences data from the specified version.

	BASE CLASS = JContainer

	Copyright © 1997 John Lindal. All rights reserved.

 ******************************************************************************/

#include <JPrefsManager.h>
#include <JPrefsFile.h>
#include <JString.h>
#include <JStaticBuffer.h>
#include <jStrStreamUtil.h>
#include <jGlobals.h>
#include <jAssert.h>

// JBroadcaster messages

const JCharacter* JPrefsManager::kDataChanged = "DataChanged::JPrefsManager";
const JCharacter* JPrefsManager::kDataRemoved = "DataRemoved::JPrefsManager";

// JError data

const JCharacter* JPrefsManager::kWrongVersion = "WrongVersion::JPrefsManager";
const JCharacter* JPrefsManager::kWrongVersionMsg =
	"The preferences file has the wrong version.";

/******************************************************************************
 Constructor (protected)

	Derived class must call UpgradeData().

 ******************************************************************************/

JPrefsManager::JPrefsManager
	(
	const JCharacter*	fileName,
	const JFileVersion	currentVersion,
	const JBoolean		eraseFileIfOpen
	)
	:
	JContainer(),
	itsCurrentFileVersion(currentVersion),
	itsEraseFileIfOpenFlag(eraseFileIfOpen)
{
	itsFileName = new JString(fileName);
	assert( itsFileName != NULL );

	itsData = new JArray<PrefItem>;
	assert( itsData != NULL );
	itsData->SetCompareFunction(ComparePrefIDs);

	InstallOrderedSet(itsData);
}

/******************************************************************************
 Destructor

	Derived classes must call SaveToDisk().

 ******************************************************************************/

JPrefsManager::~JPrefsManager()
{
	delete itsFileName;

	const JSize count = itsData->GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		PrefItem item = itsData->GetElement(i);
		delete item.data;
		}
	delete itsData;
}

/******************************************************************************
 GetData (protected)

 ******************************************************************************/

JBoolean
JPrefsManager::GetData
	(
	const JPrefID&	id,
	JStaticBuffer*	data
	)
	const
{
	PrefItem item(id.GetID(), NULL);
	JIndex index;
	if (itsData->SearchSorted(item, JOrderedSetT::kAnyMatch, &index))
		{
		item = itsData->GetElement(index);
		data->SetData((item.data)->AllocateCString());
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 SetData (protected)

	This creates the item if it doesn't already exist.

 ******************************************************************************/

void
JPrefsManager::SetData
	(
	const JPrefID&	id,
	ostrstream&		data
	)
{
	JUnfreeze(data);
	data << ends;

	SetData(id, data.str());

	JUnfreeze(data);
}

void
JPrefsManager::SetData
	(
	const JPrefID&		id,
	const JCharacter*	data
	)
{
	PrefItem item(id.GetID(), NULL);
	JBoolean found;
	const JIndex index =
		itsData->SearchSorted1(item, JOrderedSetT::kAnyMatch, &found);
	if (found)
		{
		item = itsData->GetElement(index);
		*(item.data) = data;
		}
	else
		{
		item.data = new JString(data);
		assert( item.data != NULL );
		itsData->InsertElementAtIndex(index, item);
		}

	Broadcast(DataChanged(id));
}

/******************************************************************************
 RemoveData (protected)

 ******************************************************************************/

void
JPrefsManager::RemoveData
	(
	const JPrefID& id
	)
{
	PrefItem item(id.GetID(), NULL);

	JIndex index;
	if (itsData->SearchSorted(item, JOrderedSetT::kAnyMatch, &index))
		{
		item = itsData->GetElement(index);
		delete item.data;

		itsData->RemoveElement(index);

		Broadcast(DataRemoved(id));
		}
}

/******************************************************************************
 SaveToDisk

 ******************************************************************************/

JError
JPrefsManager::SaveToDisk()
	const
{
	// check that current file is readable

	JPrefsFile* file = NULL;
	JError err = Open(&file, kTrue);
	if (err.OK())
		{
		delete file;
		file = NULL;
		}
	else
		{
		return err;
		}

	// toss everything

	err = DeleteFile(*itsFileName);
	if (!err.OK())
		{
		return err;
		}

	// write current data

	err = Open(&file, kTrue);		// version is zero since file was deleted
	if (!err.OK())
		{
		return err;
		}

	file->SetVersion(itsCurrentFileVersion);

	const JSize count = itsData->GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		const PrefItem item = itsData->GetElement(i);
		file->SetData(item.id, *(item.data));
		}

	delete file;
	return JNoError();
}

/******************************************************************************
 UpgradeData (protected)

	Returns kTrue if the file had to be created.

 ******************************************************************************/

JBoolean
JPrefsManager::UpgradeData
	(
	const JBoolean reportError
	)
{
	JBoolean isNew = kFalse;

	JPrefsFile* file = NULL;
	const JError err = Open(&file, kTrue);
	if (err.OK())
		{
		isNew = file->IsEmpty();
		LoadData(file);
		UpgradeData(isNew, file->GetVersion());
		delete file;
		}
	else if (err == kWrongVersion)
		{
		UpgradeData(kTrue, 0);

		if (reportError)
			{
			(JGetUserNotification())->ReportError(
				"The preferences file is unreadable because it has been modified "
				"by a newer version of this program.");
			}
		}

	return isNew;
}

/******************************************************************************
 LoadData (private)

 ******************************************************************************/

void
JPrefsManager::LoadData
	(
	JPrefsFile* file
	)
{
	const JSize count = file->GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		JFAID id;
		const JBoolean ok = file->IndexToID(i, &id);
		assert( ok );

		JStaticBuffer data;
		file->GetElement(JFAIndex(i), &data);
		JString* s = new JString(data);
		assert( s != NULL );

		PrefItem item(id.GetID(), s);
		JBoolean isDuplicate;
		const JIndex j = itsData->GetInsertionSortIndex(item, &isDuplicate);
		assert( !isDuplicate );
		itsData->InsertElementAtIndex(j, item);
		}
}

/******************************************************************************
 Open (private)

 ******************************************************************************/

JError
JPrefsManager::Open
	(
	JPrefsFile**	file,
	const JBoolean	allowPrevVers
	)
	const
{
	*file = NULL;
	const JError err = CreateFile(*itsFileName, file);
	if (err.OK())
		{
		const JFileVersion vers = (**file).GetVersion();
		if (vers == itsCurrentFileVersion ||
			(allowPrevVers && vers < itsCurrentFileVersion))
			{
			return JNoError();
			}
		else
			{
			delete *file;
			*file = NULL;
			return WrongVersion();
			}
		}

	else if (err == JPrefsFile::kFileAlreadyOpen && itsEraseFileIfOpenFlag &&
			 DeleteFile(*itsFileName) == kJNoError)
		{
		return Open(file, allowPrevVers);		// now it will work
		}

	delete *file;
	*file = NULL;
	return err;
}

/******************************************************************************
 ComparePrefIDs (static private)

 ******************************************************************************/

JOrderedSetT::CompareResult
JPrefsManager::ComparePrefIDs
	(
	const PrefItem& p1,
	const PrefItem& p2
	)
{
	if (p1.id < p2.id)
		{
		return JOrderedSetT::kFirstLessSecond;
		}
	else if (p1.id == p2.id)
		{
		return JOrderedSetT::kFirstEqualSecond;
		}
	else
		{
		return JOrderedSetT::kFirstGreaterSecond;
		}
}

#define JTemplateType JPrefsManager::PrefItem
#include <JArray.tmpls>
#undef JTemplateType
