#include "WimpLib:DocView.h"

#include <assert.h>
#include <string.h>
#include "swis.h"

#include "WimpLib:Exception.h"
#include "WimpLib:File.h"
#include "WimpLib:mem.h"
#include "WimpLib:Task.h"
#include "WimpLib:Utils.h"

static DocList* TheDocList = NULL;

#define EDoc_New        0x00000001 // reset when document is saved
#define EDoc_ValidPath  0x00000002 // has valid file name
#define EDoc_Modified   0x00000004 // changed since last saved
#define EDoc_AutoDelete 0x00000008 // delete document when no more views
#define EDoc_NonFile    0x00000010 // document is not a file (path is not a file path, etc)

/*////////////////////////////////////////////////////////////////////////////
 * Document list
 */

DocList* throw_DocList_Get(void)
{
	if (TheDocList == NULL)
		TheDocList = New_List();

	return TheDocList;
}

void Delete_DocList(DocList* This)
{
	Delete_List(This);
	TheDocList = NULL;
}

int DocList_Count(const DocList* This)
{
	return List_Count(This);
}

Document* DocList_GetDocument(const DocList* This, int Index) throws(index)
{
	return List_Get(This, Index);
}

Document* DocList_Find(const DocList* This, const char* pname) throws(os mem)
{
	Document* pDoc = NULL;
	char* volatile pcanonical = NULL;

	try
	{
		int i;

		pcanonical = throw_File_CanonicalPath(pname);

		for (i = 0; i < DocList_Count(This); i++)
		{
			if (!strcmp(Document_GetPathName(DocList_GetDocument(This, i)), pcanonical))
			{
				pDoc = DocList_GetDocument(This, i);
				break;
			}
		}
	}
	catch
	{
		pDoc = NULL;
	}
	catch_end

	mem_free(pcanonical);

	return pDoc;
}

bool DocList_IsSafe(const DocList* This)
{
	int i;

	for (i = 0; i < DocList_Count(This); i++)
	{
		if (Document_IsModified(DocList_GetDocument(This, i)))
			return false;
	}

	return true;
}

void DocList_RemoveAllDocuments(DocList* This)
{
	while(DocList_Count(This))
	{
		Document* pDocument = DocList_GetDocument(This, 0);
		Document_SetModifiedFlag(pDocument, false);
		Document_SetAutoDeleteFlag(pDocument, true);
		Document_OnCloseDocument(pDocument);
	}
}

/*////////////////////////////////////////////////////////////////////////////
 * Document construction/destruction
 */

void throw_Document_Document(Document* This, const file_type* ptype, const DocumentVPtr* VPtr)
{
	memset(This, 0, sizeof(*This));

	This->m_VPtr = VPtr;
	This->m_strPathName = throw_mem_allocstring(NULL);
	This->m_FileType = (ptype != 0) ? ptype[0] : 0xffd;
	This->m_ExportTypes = ptype;
	This->m_pViews = New_List();
	This->m_Flags = EDoc_New | EDoc_AutoDelete;

	List_InsertBefore(throw_DocList_Get(), NULL, This);
}

bool Document_IsKindOf(const Document* This, const DocumentVPtr* VPtr)
{
	return (This->m_VPtr == VPtr);
}

void Document_VNotDocument(Document* This)
{
	This->m_VPtr->FDestructor(This);
}

void Document_NotDocument(Document* This)
{
	DocList* pDocList = throw_DocList_Get();

	// do not call CLearContents here !
#ifdef _DEBUG
	if (Document_IsModified(This))
		trace("Warning: destroying an unsaved document\n");
#endif

	if (This->m_pViews)
	{
		// there should be no views left!
		while (List_Count(This->m_pViews))
		{
			View* pView = List_Get(This->m_pViews, 0);
			pView->m_pDocument = NULL;
			List_Remove(This->m_pViews, 0);
		}
		Delete_List(This->m_pViews);
		This->m_pViews = NULL;
	}

	mem_free(This->m_strPathName);
	This->m_strPathName = NULL;

	List_RemoveNode(pDocList, List_FindNode(pDocList, NULL, This));
}

void Delete_Document(Document* This)
{
	if (This)
	{
		Document_SetModifiedFlag(This, false);
		Document_SetAutoDeleteFlag(This, false);
		Document_CloseDocument(This);
		Document_VNotDocument(This);
		mem_free(This);
	}
}

/*///////////////////////////////////////////////////////////////////////////
 * Document attributes
 */

const char* Document_GetPathName(const Document* This)
{
	return This->m_strPathName;
}

void throw_Document_SetPathName(Document* This, const char* pstrPathName)
{
	char* pstr = NULL;

	if (This->m_Flags & (EDoc_New|EDoc_NonFile))
		pstr = throw_mem_allocstring(pstrPathName);
	else
		pstr = throw_File_CanonicalPath(pstrPathName);

	mem_free(This->m_strPathName);
	This->m_strPathName = pstr;

	// Update the title of the associated views
	Document_UpdateViewsCount(This);
}

file_type Document_GetFileType(const Document* This)
{
	return This->m_FileType;
}

const file_type* Document_GetExportTypes(const Document* This)
{
	return This->m_ExportTypes;
}

void Document_SetFileType(Document* This, file_type type)
{
	This->m_FileType = type;
}

bool Document_IsNew(const Document* This)
{
	return ((This->m_Flags & EDoc_New) != 0);
}

bool Document_HasValidPath(const Document* This)
{
	return ((This->m_Flags & (EDoc_ValidPath|EDoc_NonFile)) == EDoc_ValidPath);
}

bool Document_IsModified(const Document* This)
{
	return ((This->m_Flags & EDoc_Modified) != 0);
}

void Document_SetModifiedFlag(Document* This, bool bModified)
{
	if (bModified)
		This->m_Flags |= EDoc_Modified;
	else
	{
		// If document is safe (e.g. after a load) document is not new
		This->m_Flags &= ~(EDoc_New | EDoc_Modified);
	}

	// Update the title of the associated views
	Document_UpdateViewsCount(This);
}

bool Document_GetAutoDeleteFlag(const Document* This)
{
	return ((This->m_Flags & EDoc_AutoDelete) != 0);
}

void Document_SetAutoDeleteFlag(Document* This, bool bAutoDelete)
{
	if (bAutoDelete)
		This->m_Flags |= EDoc_AutoDelete;
	else
		This->m_Flags &= ~EDoc_AutoDelete;
}

void Document_SetNonFileFlag(Document* This, bool bNonFile)
{
	if (bNonFile)
		This->m_Flags |= EDoc_NonFile;
	else
		This->m_Flags &= ~EDoc_NonFile;
}

/*///////////////////////////////////////////////////////////////////////////
 * View public operations
 */

void throw_Document_AddView(Document* This, View* pView) throws(nomem)
{
	throw_assert(pView->m_pDocument == NULL); // must not be already attached
	throw_assert(List_Find(This->m_pViews, 0, pView) == -1);   // must not be in list

	List_InsertBefore(This->m_pViews, NULL, pView);
	pView->m_pDocument = This;
	View_VOnInitialUpdate(pView);

	Document_OnChangedViewList(This);    // must be the last thing done to the document
}

void Document_RemoveView(Document* This, View* pView)
{
	throw_assert(pView->m_pDocument == This); // must be attached to us

	List_RemoveNode(This->m_pViews, List_FindNode(This->m_pViews, NULL, pView));
	pView->m_pDocument = NULL;

	Document_OnChangedViewList(This);    // must be the last thing done to the document
}

View* Document_GetView(const Document* This, int Index) throws(index)
{
	return List_Get(This->m_pViews, Index);
}

int Document_GetViewsCount(const Document* This)
{
	return List_Count(This->m_pViews);
}

void Document_UpdateAllViews(const Document* This, const View* pSender, int HintType , const void* pHintData)
{
	int i;
	bool bDone = false;

	for (i = 0; !bDone && (i < List_Count(This->m_pViews)); i++)
	{
		View* pView = Document_GetView(This, i);

		bDone = View_VOnUpdate(pView, pSender, HintType, pHintData);
	}
}

/*///////////////////////////////////////////////////////////////////////////
 * View protected operations
 */

void Document_OnChangedViewList(Document* This)
{
	// update the frame counts as needed
	Document_UpdateViewsCount(This);
}

void Document_UpdateViewsCount(const Document* This)
{
	// walk all views (mark and sweep approach)
	int i = 0;

	for (i = 0; i < List_Count(This->m_pViews); i++)
	{
		View* pView = Document_GetView(This, i);
		View_VSetViewNumber(pView, i + 1);
	}
}

void Document_SendInitialUpdate(const Document* This)
{
	// walk through all views and call OnInitialUpdate
	int i = 0;

	for (i = 0; i < List_Count(This->m_pViews); i++)
	{
		View* pView = Document_GetView(This, i);
		View_VOnInitialUpdate(pView);
	}
}

/*///////////////////////////////////////////////////////////////////////////
 * Document commands helper
 */

void Document_OnNewDocument(Document* This)
{
	Document_ClearDocument(This);

	Document_SendInitialUpdate(This);
}

bool Document_OnClearDocument(Document* This)
{
	if(!Document_OnCloseDocument(This))
		return false;

	Document_ClearDocument(This);

	return true;
}

bool Document_OnOpenDocument(Document* This, const char* strPathName)
{
	bool bret = true;
	const _kernel_oserror* err = NULL;

	throw_assert(!Document_IsModified(This));

	Document_ClearDocument(This);
	This->m_Flags &= ~EDoc_New;
	try
	{
		if (!(This->m_Flags & EDoc_NonFile))
		{
			throw_Document_SetPathName(This, strPathName);
			This->m_Flags |= EDoc_ValidPath;
		}

		err = XFer_ReceiveFromFile
					( strPathName
					, (XFer_ReceiveProc) This->m_VPtr->FOpenDocument
					, This
					, NULL
					);
		if (err)
		{
			const _kernel_oserror e = *err;

			Document_ClearDocument(This);
			if (!(This->m_Flags & EDoc_NonFile))
				throw_Document_SetPathName(This, strPathName);
			if (err != XFer_NoTransfer)
				Task_DescribeError(&e, "ErrDocFRead:%s", strPathName);
			bret = false;
		}
		Document_SetModifiedFlag(This, false);
		Document_SendInitialUpdate(This);
	}
	catch
	{
		Task_ReportException();
		bret = false;
	}
	catch_end

	return bret;
}

bool Document_OnReceiveDocument(Document* This, const Msg_FileData* rcv)
{
	bool bret = true;
	bool bsafe = ((rcv->hdr.action == EMsg_DataLoad) || (rcv->hdr.action == EMsg_DataOpen));
	const _kernel_oserror* err = NULL;

	throw_assert(!Document_IsModified(This));

	Document_ClearDocument(This);
	if (bsafe)
		This->m_Flags &= ~EDoc_New;

	try
	{
		if (!(This->m_Flags & EDoc_NonFile))
			throw_Document_SetPathName(This, rcv->name);

		err = XFer_Receive((Msg*) rcv
				, (XFer_ReceiveProc) This->m_VPtr->FOpenDocument
				, This);
		if (err)
		{
			Document_ClearDocument(This);
			if (err != XFer_NoTransfer)
				Task_ReportOSError(err);
			bret = false;
			bsafe = true;
		}
		Document_SetModifiedFlag(This, !bsafe);
		if (bsafe)
			This->m_Flags |= EDoc_ValidPath;

		Document_SendInitialUpdate(This);
	}
	catch
	{
		Task_ReportException();
		bret = false;
	}
	catch_end

	return bret;
}

bool Document_OnCloseDocument(Document* This)
{
	if (!Document_SaveIfModified(This))
		return false;

	Document_SetModifiedFlag(This, false);
	Document_CloseDocument(This);

	return true;
}

bool Document_OnSaveDocument(Document* This)
{
	// Ask a filename for a new document
	if (Document_IsNew(This))
		return Document_OnSaveAs(This, NULL, false, false);

	return Document_DoFileSave(This, This->m_strPathName);
}

bool Document_OnSaveAs(Document* This, const View* pSender, bool bSetSelFlag, bool bAllowSelFlag)
{
	if (!pSender)
	{
		bSetSelFlag = false;
		bAllowSelFlag = false;
	}

	This->m_VPtr->FSaveAs(This, pSender, bSetSelFlag, bAllowSelFlag);

	return !Document_IsModified(This);
}

bool Document_Send(Document* This, const DragDrop_Info* pInfo, const char* leafname)
{
	return DragDrop_Send(    pInfo
			, leafname
			, 0
			, (XFer_SendProc) This->m_VPtr->FSendDocument
			, NULL
			, (XFer_OnSafeProc) Document_OnSaveIsSafe
			, NULL
			, Task_ReportOSError
			, false
			, This);
}

void Document_ClearDocument(Document* This)
{
	This->m_VPtr->FClearContents(This);
	mem_free(This->m_strPathName);      // no path name yet
	This->m_strPathName = mem_allocstring(NULL);
	Document_SetModifiedFlag(This, false);     // make clean
	This->m_Flags |= EDoc_New;
	This->m_Flags &= ~EDoc_ValidPath;
}

void Document_CloseDocument(Document* This)
{
	/* destroy all views viewing This document
	 * the last destroy may destroy us
	 */
	bool bAutoDelete = Document_GetAutoDeleteFlag(This);
	Document_SetAutoDeleteFlag(This, false);  // do not destroy document while closing views

	while (List_Count(This->m_pViews))
	{
		// get the view
		View* pView = List_Get(This->m_pViews, 0);
		throw_assert(pView->m_pDocument == This);

		// and close it
		Delete_View(pView);
	}

	Document_SetAutoDeleteFlag(This, bAutoDelete);

	// clean up contents of document before destroying the document itself
	This->m_VPtr->FClearContents(This);
	This->m_Flags &= ~EDoc_ValidPath;

	// delete the document if necessary
	if (This->m_Flags & EDoc_AutoDelete)
		Delete_Document(This);
}

bool Document_SaveIfModified(Document* This)
{
	// If document is unmodified, ok to continue
	if (!Document_IsModified(This))
		return true;

	switch(This->m_VPtr->FAskDCS(This))
	{
		case DCS_Save: // try to save document
		{
			return Document_OnSaveAs(This, NULL, false, false);
		}
		break;
		case DCS_Discard: // discard the document
		{
			return true;
		}
		break;
		default:
		{
			return false;
		}
		break;
	}

	// ok to continue
	return true;
}

/*
 * Save the document data to a file
 */

bool Document_DoFileSave(Document* This, const char* pstrPathName)
{
	const _kernel_oserror* err;

	err= XFer_SendToFile(pstrPathName
				, This->m_FileType
				, (XFer_SendProc) This->m_VPtr->FSendDocument
				, (XFer_OnSafeProc) Document_OnSaveIsSafe
				, This);

	if (err)
	{
		Task_ReportOSError(err);
		return false;
	}

	return true;
}

void Document_OnSaveIsSafe(Document* This, const char* filename)
{
	Document_SetModifiedFlag(This, false);
	try
	{
		if (!(This->m_Flags & EDoc_NonFile))
		{
			throw_Document_SetPathName(This, filename);
			This->m_Flags |= EDoc_ValidPath;
		}
	}
	catch
	{
		Task_ReportException();
	}
	catch_end
}

void Document_OpenDir(Document* This)
{
	if (!Document_IsNew(This))
	{
		char* pstr;
		char* pch;

		pstr = SPrintf("Filer_OpenDir %s", Document_GetPathName(This));
		pch = strrchr(pstr, FILE_FOLDER_CHAR);
		*pch = 0;
		// OS_CLI
		_swix(OS_CLI, _IN(0), pstr);
	}
}
