/******************************************************************************
 JXDocumentManager.cc

	Singleton class that manages all JXDocuments and provide the following
	services:

	1)  GetNewFileName()

		Convenient source of names for newly created documents.

	2)  JXDocumentMenu

		Instant menu of all open documents

	3)  Safety save

		Periodically save all unsaved documents in temporary files.
		Also protects against X Server crashes and calls to assert().

		While it would be nice to also catch Ctrl-C, this must remain
		uncaught because it provides a way to quickly stop a runaway
		program.  Also, in practice, X programs are usually backgrounded,
		in which case Ctrl-C is irrelevant.

	4)  Dependencies between documents

		Classes derived from JXDocument can override NeedDocument()
		if they require that other documents stay open.  When closed,
		such required documents merely deactivate.

	5)  Searching for files that are supposed to exist

		Dependencies between documents usually imply that one document
		stores the file names of other documents that it has to open.
		If the user moves or renames these files, call FindFile() to
		search for them.

		Derived classes can override this function to search application
		specific directories.

	A derived class is the best place to put all the code associated
	with determining a file's type and creating the appropriate derived
	class of JXDocument.

	BASE CLASS = virtual JBroadcaster

	Copyright © 1997-98 by John Lindal. All rights reserved.

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

#include <JXDocumentManager.h>
#include <JXFileDocument.h>
#include <JXSafetySaveTask.h>
#include <JXDocumentMenu.h>
#include <JXUpdateDocMenuTask.h>
#include <JXImage.h>
#include <JXColormap.h>
#include <jXGlobals.h>
#include <JString.h>
#include <jFileUtil.h>
#include <jDirUtil.h>
#include <jAssert.h>

static const JCharacter* kNewDocName = "Untitled ";
const JInteger kLastShortcut         = 9;

const JSize kSecondsToMilliseconds = 1000;

// JBroadcaster message types

const JCharacter* JXDocumentManager::kDocMenuNeedsUpdate =
	"DocMenuNeedsUpdate::JXDocumentManager";

/******************************************************************************
 Constructor

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

JXDocumentManager::JXDocumentManager
	(
	const ShortcutStyle	style,
	const JBoolean		useShortcutZero
	)
	:
	JBroadcaster(),
	itsShortcutStyle( style ),
	itsFirstShortcut( useShortcutZero ? 0 : 1 )
{
	itsDocList = new JArray<DocInfo>;
	assert( itsDocList != NULL );

	itsLastNewFileName = new JString;
	assert( itsLastNewFileName != NULL );

	itsNewDocCount = 0;

	itsFileMap = new JArray<FileMap>;
	assert( itsFileMap != NULL );

	itsPerformSafetySaveFlag = kTrue;

	itsSafetySaveTask = new JXSafetySaveTask(this);
	assert( itsSafetySaveTask != NULL );

	itsUpdateDocMenuTask = NULL;

	JXSetDocumentManager(this);
}

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

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

JXDocumentManager::~JXDocumentManager()
{
	assert( itsDocList->IsEmpty() );
	delete itsDocList;

	const JSize count = itsFileMap->GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		FileMap file = itsFileMap->GetElement(i);
		delete file.oldName;
		delete file.newName;
		}
	delete itsFileMap;

	delete itsLastNewFileName;
	delete itsSafetySaveTask;
	delete itsUpdateDocMenuTask;
}

/******************************************************************************
 DocumentCreated (virtual)

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

void
JXDocumentManager::DocumentCreated
	(
	JXDocument* doc
	)
{
	DocInfo info(doc);

	// Find the first place to insert the document:
	//   1) At a jump in the shortcut sequence
	//   2) At the end of the shortcut sequence
	//   3) At the end of the list

	JInteger newShortcut  = itsFirstShortcut;
	JIndex insertionIndex = 0;

	const JSize docCount = itsDocList->GetElementCount();
	for (JIndex i=1; i<=docCount && newShortcut <= kLastShortcut; i++)
		{
		const DocInfo info1 = itsDocList->GetElement(i);
		if (info1.shortcut > newShortcut)
			{
			insertionIndex = i;
			break;
			}
		newShortcut++;
		}

	if (insertionIndex == 0 &&
		docCount < (JSize) (kLastShortcut - itsFirstShortcut + 1))
		{
		insertionIndex = docCount + 1;
		newShortcut    = docCount + itsFirstShortcut;
		}

	// insert the new document

	if (insertionIndex > 0)
		{
		info.shortcut = newShortcut;
		itsDocList->InsertElementAtIndex(insertionIndex, info);
		}
	else
		{
		itsDocList->AppendElement(info);
		}

	if (itsPerformSafetySaveFlag)
		{
		(JXGetApplication())->InstallIdleTask(itsSafetySaveTask);
		}

	// update the menu shortcuts

	DocumentMenusNeedUpdate();
}

/******************************************************************************
 DocumentDeleted (virtual)

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

void
JXDocumentManager::DocumentDeleted
	(
	JXDocument* doc
	)
{
	const JSize count = itsDocList->GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		DocInfo info = itsDocList->GetElement(i);
		if (info.doc == doc)
			{
			delete info.menuIcon;
			itsDocList->RemoveElement(i);

			// move the first document without a shortcut into the empty slot

			if (info.shortcut != kNoShortcutForDoc)
				{
				for (JIndex j=i; j<=count-1; j++)
					{
					DocInfo info1 = itsDocList->GetElement(j);
					if (info1.shortcut == kNoShortcutForDoc)
						{
						info1.shortcut = info.shortcut;
						itsDocList->SetElement(j, info1);
						itsDocList->MoveElementToIndex(j, i);
						break;
						}
					}
				}

			// check if other documents want to close -- recursive

			CloseDocuments();
			break;
			}
		}

	// remove SafetySave() idle task

	if (itsDocList->IsEmpty())
		{
		(JXGetApplication())->RemoveIdleTask(itsSafetySaveTask);
		}

	// update the menu shortcuts

	DocumentMenusNeedUpdate();
}

/******************************************************************************
 DocumentMenusNeedUpdate (private)

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

void
JXDocumentManager::DocumentMenusNeedUpdate()
{
	if (itsUpdateDocMenuTask == NULL)
		{
		itsUpdateDocMenuTask = new JXUpdateDocMenuTask(this);
		assert( itsUpdateDocMenuTask != NULL );
		(JXGetApplication())->InstallUrgentTask(itsUpdateDocMenuTask);
		}
}

/******************************************************************************
 UpdateAllDocumentMenus (private)

	This must be called via an UrgentTask because we can't call GetName()
	for the new document until long after DocumentCreated() is called.

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

void
JXDocumentManager::UpdateAllDocumentMenus()
{
	Broadcast(DocMenuNeedsUpdate());
}

/******************************************************************************
 DocumentMustStayOpen

	Call this with kTrue if a document must remain open even if nobody else
	needs it.

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

void
JXDocumentManager::DocumentMustStayOpen
	(
	JXDocument*		doc,
	const JBoolean	stayOpen
	)
{
	const JSize count = itsDocList->GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		DocInfo info = itsDocList->GetElement(i);
		if (info.doc == doc)
			{
			info.keepOpen = stayOpen;
			itsDocList->SetElement(i, info);
			break;
			}
		}

	if (stayOpen == kFalse)
		{
		CloseDocuments();
		}
}

/******************************************************************************
 OKToCloseDocument

	Returns kTrue if the given document can be closed.

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

JBoolean
JXDocumentManager::OKToCloseDocument
	(
	JXDocument* doc
	)
	const
{
	const JSize count = itsDocList->GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		const DocInfo info = itsDocList->GetElement(i);
		if (info.doc != doc && (info.doc)->NeedDocument(doc))
			{
			return kFalse;
			}
		}

	return kTrue;
}

/******************************************************************************
 CloseDocuments

	Close invisible documents that are no longer needed by any other documents.

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

void
JXDocumentManager::CloseDocuments()
{
	JSize count = itsDocList->GetElementCount();

	JSize closeCount;
	do
		{
		closeCount = 0;
		for (JIndex i=1; i<=count; i++)
			{
			const DocInfo info = itsDocList->GetElement(i);
			if (!(info.doc)->IsActive() && !(info.keepOpen) &&
				OKToCloseDocument(info.doc) && (info.doc)->Close())
				{
				closeCount++;
				count = itsDocList->GetElementCount();	// several documents may have closed
				}
			}
		}
		while (closeCount > 0);
}

/******************************************************************************
 FileDocumentIsOpen

	If there is a JXFileDocument that uses the specified file, we return it.

	With full RTTI support to allow safe downcasting, this could be generalized
	to DocumentIsOpen() by defining a pure virtual IsEqual() function in
	JXDocument and requiring derived classes to implement it appropriately.

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

JBoolean
JXDocumentManager::FileDocumentIsOpen
	(
	const JCharacter*	fileName,
	JXFileDocument**	doc
	)
	const
{
	*doc = NULL;

	// check that the file exists

	if (!JFileExists(fileName))
		{
		return kFalse;
		}

	// search for an open JXFileDocument that uses this file

	const JSize count = itsDocList->GetElementCount();
	JString trueDocFile;
	for (JIndex i=1; i<=count; i++)
		{
		const DocInfo info = itsDocList->GetElement(i);
		const JXFileDocument* fileDoc = (info.doc)->CastToJXFileDocument();
		if (fileDoc != NULL)
			{
			JBoolean onDisk;
			const JString docName = fileDoc->GetFullName(&onDisk);

			if (onDisk && JSameDirEntry(fileName, docName))
				{
				*doc = const_cast<JXFileDocument*>(fileDoc);
				return kTrue;
				}
			}
		}

	return kFalse;
}

/******************************************************************************
 FindFile (virtual)

	Searches for the given file, assuming that it has been moved elsewhere.
	currPath should be the path the user selected.  This can help in the cases
	where an entire directory subtree was moved because then the requested file
	is probably still somewhere in the subtree.

	This function is virtual so derived classes can provide additional
	search techniques.

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

JBoolean
JXDocumentManager::FindFile
	(
	const JCharacter*	fileName,
	const JCharacter*	currPath,
	JString*			newFileName,
	const JBoolean		askUser
	)
	const
{
	// if the file exists, we are done

	if (JFileExists(fileName))
		{
		*newFileName = fileName;
		return kTrue;
		}

	// search the directory tree below currPath

	JString path, name, newPath;
	JSplitPathAndName(fileName, &path, &name);

	if (JSearchSubdirs(currPath, name, kTrue, kTrue, &newPath))
		{
		*newFileName = newPath + name;
		return kTrue;
		}

	// check for known case of move/rename

	if (SearchFileMap(fileName, newFileName))
		{
		return kTrue;
		}

	// ask the user to find it

	if (askUser)
		{
		JString instrMsg = "Unable to locate ";
		instrMsg += fileName;
		instrMsg += "\n\nPlease find it.";

		while ((JGetChooseSaveFile())->ChooseFile("New name of file:", instrMsg, newFileName))
			{
			JString newPath, newName;
			JSplitPathAndName(*newFileName, &newPath, &newName);
			if (newName != name)
				{
				JString warnMsg = name;
				warnMsg += " was requested.\n\nYou selected ";
				warnMsg += newName;
				warnMsg += ".\n\nAre you sure that this is correct?";
				if (!(JGetUserNotification())->AskUserNo(warnMsg))
					{
					continue;
					}
				}

			JString trueName;
			const JBoolean ok = JGetTrueName(*newFileName, &trueName);
			assert( ok );

			FileMap map;
			map.oldName = new JString(fileName);
			assert( map.oldName != NULL );
			map.newName = new JString(trueName);
			assert( map.newName != NULL );
			itsFileMap->AppendElement(map);

			*newFileName = trueName;
			return kTrue;
			}
		}

	newFileName->Clear();
	return kFalse;
}

/******************************************************************************
 SearchFileMap (private)

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

JBoolean
JXDocumentManager::SearchFileMap
	(
	const JCharacter*	fileName,
	JString*			newFileName
	)
	const
{
	const JSize mapCount = itsFileMap->GetElementCount();
	for (JIndex i=mapCount; i>=1; i--)
		{
		FileMap map          = itsFileMap->GetElement(i);
		const JBoolean match = JConvertToBoolean( *(map.oldName) == fileName );
		if (match && JFileExists(*(map.newName)))
			{
			*newFileName = *(map.newName);
			return kTrue;
			}
		else if (match)		// newName no longer exists (lazy checking)
			{
			delete map.oldName;
			delete map.newName;
			itsFileMap->RemoveElement(i);
			}
		}

	return kFalse;
}

/******************************************************************************
 GetNewFileName

	Return a suitable name for a new document.  Since this is often
	called in the constructor for JXFileDocument, we return a JString&

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

const JString&
JXDocumentManager::GetNewFileName()
{
	itsNewDocCount++;
	if (itsNewDocCount > 99)	// really big numbers look silly
		{
		itsNewDocCount = 1;
		}

	*itsLastNewFileName = kNewDocName + JString(itsNewDocCount, 0);
	return *itsLastNewFileName;
}

/******************************************************************************
 UpdateDocumentMenu

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

void
JXDocumentManager::UpdateDocumentMenu
	(
	JXDocumentMenu* menu
	)
{
	if (menu->IsOpen())
		{
		DocumentMenusNeedUpdate();
		return;
		}

	menu->RemoveAllItems();

	const JSize count = itsDocList->GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		DocInfo info = itsDocList->GetElement(i);
		const JString name = (info.doc)->GetName();

		menu->AppendItem(name);
		if ((info.doc)->NeedsSave())
			{
			menu->SetItemFontStyle(i, (menu->GetColormap())->GetRedColor());
			}

		if (info.menuIcon == NULL)
			{
			// This can't be done in DocumentCreated() because that is called
			// when GetMenuIcon() is still pure virtual.

			info.menuIcon = new JXImage((info.doc)->GetDisplay(),
										(info.doc)->GetColormap(),
										(info.doc)->GetMenuIcon());
			assert( info.menuIcon != NULL );
			itsDocList->SetElement(i, info);
			}
		if ((info.menuIcon)->GetDisplay()  == menu->GetDisplay() &&
			(info.menuIcon)->GetColormap() == menu->GetColormap())
			{
			menu->SetItemImage(i, info.menuIcon, kFalse);
			}

		if (itsShortcutStyle != kNoShortcuts &&
			info.shortcut != kNoShortcutForDoc)
			{
			JString nmShortcut;
			if (itsShortcutStyle == kCtrlShortcuts)
				{
				nmShortcut = "Ctrl-0";
				}
			else
				{
				assert( itsShortcutStyle == kMetaShortcuts );
				nmShortcut = "Meta-0";
				}
			nmShortcut.SetCharacter(nmShortcut.GetLength(), '0' + info.shortcut);
			menu->SetItemNMShortcut(i, nmShortcut);
			}
		}
}

/******************************************************************************
 ActivateDocument

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

void
JXDocumentManager::ActivateDocument
	(
	const JIndex index
	)
{
	if (itsDocList->IndexValid(index))		// doc might close while menu is open
		{
		const DocInfo info = itsDocList->GetElement(index);
		(info.doc)->Activate();
		}
}

/******************************************************************************
 ShouldSafetySave

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

void
JXDocumentManager::ShouldSafetySave
	(
	const JBoolean doIt
	)
{
	itsPerformSafetySaveFlag = doIt;

	if (itsPerformSafetySaveFlag && !itsDocList->IsEmpty())
		{
		(JXGetApplication())->InstallIdleTask(itsSafetySaveTask);
		}
	else if (!itsPerformSafetySaveFlag)
		{
		(JXGetApplication())->RemoveIdleTask(itsSafetySaveTask);
		}
}

/******************************************************************************
 GetSafetySaveInterval

	Returns the safety save interval in seconds.

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

JSize
JXDocumentManager::GetSafetySaveInterval()
	const
{
	return itsSafetySaveTask->GetPeriod() / kSecondsToMilliseconds;
}

/******************************************************************************
 SetSafetySaveInterval

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

void
JXDocumentManager::SetSafetySaveInterval
	(
	const JSize deltaSeconds
	)
{
	itsSafetySaveTask->SetPeriod(deltaSeconds * kSecondsToMilliseconds);
}

/******************************************************************************
 SafetySave

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

void
JXDocumentManager::SafetySave
	(
	const SafetySaveReason reason
	)
{
	const JSize docCount = itsDocList->GetElementCount();
	for (JIndex i=1; i<=docCount; i++)
		{
		DocInfo info = itsDocList->GetElement(i);
		(info.doc)->SafetySave(reason);
		}
}

#define JTemplateType JXDocumentManager::DocInfo
#include <JArray.tmpls>
#undef JTemplateType

#define JTemplateType JXDocumentManager::FileMap
#include <JArray.tmpls>
#undef JTemplateType
