#include "CDDesc.h"

#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#include "WimpLib:DCS.h"
#include "WimpLib:DocView.h"
#include "WimpLib:Exception.h"
#include "WimpLib:File.h"
#include "WimpLib:Hourglass.h"
#include "WimpLib:mem.h"
#include "WimpLib:Message.h"
#include "WimpLib:PtrArray.h"
#include "WimpLib:SaveAs.h"
#include "WimpLib:Task.h"
#include "WimpLib:Utils.h"
#include "WimpLib:Window.h"

#include "CDDB.h"
#include "CDTreeView.h"
#include "DigitalCD.h"
#include "DocEvents.h"

#define MAX_MSGS 10

struct CDDesc
{
	int             nr_tracks;
	PtrArray*       pTrackList;
	Array*          pPrefList;
	unsigned long   id;
	const char*     collective;
	const char*     author;
	const char*     date;
	const char*     box;
	const char*     album;
	bool            bModified;
	bool            bDeleted;
};

struct CDDescList
{
	Document  m_Document;
	PtrArray* CDs; // Main list
	PtrArray* m_pTrackList; // Search lists
	bool      m_bUpdateStarted;
	int       m_ModifedCount;
	CDDescList_Type m_Type;
	struct
	{
		CDDesc*   pDesc;
		int       msgref;
	} CDDBMap[MAX_MSGS];
	int       m_NextMsg;
};

static CDDescList* pCDDescsList = NULL;

static const char DatabaseFile[] = "<DigitalCDRes$Dir>.CDs";

/*-------------------------------------------------------------------------*
 *--- CDDesc Creation/Destruction -----------------------------------------*
 *-------------------------------------------------------------------------*/

static void throw_CDDesc_CDDesc(CDDesc* This, unsigned long id)
{
	This->nr_tracks = 0;
	This->pTrackList = New_PtrArray(10);
	This->pPrefList = New_Array(4, 10);
	This->id = id;
	This->collective = throw_StrCol_Add(DigitalCD.pCollectivesCol, NULL);
	This->author = throw_StrCol_Add(DigitalCD.pArtistsCol, NULL);
	This->date = throw_StrCol_Add(DigitalCD.pDatesCol, NULL);
	This->box = throw_StrCol_Add(DigitalCD.pBoxesCol, NULL);
	This->album = throw_StrCol_Add(DigitalCD.pAlbumsCol, NULL);
	This->bModified = false;
	This->bDeleted = false;
}

static void CDDesc_DeleteTrack(CDDesc* This, int i)
{
	CDTrack* pTrack;

	if ((i < 0) || (i >= This->nr_tracks))
		return;

	pTrack = PtrArray_Get(This->pTrackList, i);
	PtrArray_Remove(This->pTrackList, i);
	Delete_CDTrack(pTrack);

	This->nr_tracks--;
}

static void CDDesc_NotCDDesc(CDDesc* This)
{
	while(This->nr_tracks > 0)
		CDDesc_DeleteTrack(This, 0);
	Delete_PtrArray(This->pTrackList);
	Delete_Array(This->pPrefList);
	StrCol_Remove(DigitalCD.pCollectivesCol, This->collective);
	StrCol_Remove(DigitalCD.pArtistsCol, This->author);
	StrCol_Remove(DigitalCD.pDatesCol, This->date);
	StrCol_Remove(DigitalCD.pBoxesCol, This->box);
	StrCol_Remove(DigitalCD.pAlbumsCol, This->album);
}

/**
 * Allocate new descriptor + copy.
 */
static CDDesc* throw_New_CDDesc(unsigned long id)
{
	CDDesc* This = throw_mem_calloc(2, sizeof(*This));
	CDDesc* pCopy = This + 1;

	try
	{
		throw_CDDesc_CDDesc(This, id);
		throw_CDDesc_CDDesc(pCopy, id);
	}
	catch
	{
		CDDesc_NotCDDesc(This);
		CDDesc_NotCDDesc(pCopy);
		mem_free(This);
		throw_current();
	}
	catch_end

	return This;
}

static void Delete_CDDesc(CDDesc* This)
{
	if (This)
	{
		CDDesc* pCopy = This + 1;
		CDDesc_NotCDDesc(This);
		CDDesc_NotCDDesc(pCopy);
		mem_free(This);
	}
}

/*-------------------------------------------------------------------------*
 *--- CDDesc Edition ------------------------------------------------------*
 *-------------------------------------------------------------------------*/

const char* CDDesc_GetLabel(const CDDesc* This)
{
	const char* text = This->album;

	if ((text == NULL) || !*text)
		text = SPrintf(Msg_Lookup("UnkCD3:CD %lu"), This->id);

	return text;
}

const char* CDDesc_GetMetaString(const CDDesc* This, EMetaId id)
{
	switch(id)
	{
		case EMetaId_StreamCollective: return This->collective ? This->collective : &nil; break;
		case EMetaId_StreamArtist: return This->author ? This->author : &nil; break;
		case EMetaId_StreamDate: return This->date ? This->date : &nil; break;
		case EMetaId_StreamBox: return This->box ? This->box : &nil; break;
		case EMetaId_StreamAlbum: return This->album ? This->album : &nil; break;
	}

	return &nil;
}

unsigned long CDDesc_GetId(const CDDesc* This)
{
	return This->id;
}

Array* CDDesc_GetPrefList(const CDDesc* This)
{
	return ((CDDesc*) This)->pPrefList;
}

void CDDesc_SetModifiedFlag(CDDesc* This, bool bModified)
{
	CDDescList* pDoc = CDDescList_Get();

	if (This->bModified != bModified)
	{
		This->bModified = bModified;
		pDoc->m_ModifedCount += bModified ? 1 : -1;

		Document_SetModifiedFlag(&pDoc->m_Document, pDoc->m_ModifedCount ? true : false);
	}
}

void CDDesc_SetDeletedFlag(CDDesc* This, bool bDeleted)
{
	CDDescList* pDoc = CDDescList_Get();

	if (This->bDeleted != bDeleted)
	{
		This->bDeleted = bDeleted;

		try
		{
			DocEvent e = {bDeleted ? EDocEvent_RemoveElement : EDocEvent_InsertElement, NULL, pDoc};

			for (int i = 0; i < This->nr_tracks; i++)
			{
				e.pElement = CDDesc_GetTrack(This, i);
				DocEvents_NotifyListeners(pDoc, &e);
			}

			// Can get away with non-track since ptr is matched against list in listeners
			if (bDeleted)
			{
				e.pElement = This;
				DocEvents_NotifyListeners(pDoc, &e);
			}
		}
		catch
		{
			App_ReportException();
		}
		catch_end
	}
}

bool throw_CDDesc_SetMetaString(CDDesc* This, EMetaId id, const char* pString)
{
	StrCol* pCol = NULL;
	const char** ppText = NULL;

	switch(id)
	{
		case EMetaId_StreamCollective:
		{
			pCol = DigitalCD.pCollectivesCol;
			ppText = &This->collective;
		}
		break;
		case EMetaId_StreamArtist:
		{
			pCol = DigitalCD.pArtistsCol;
			ppText = &This->author;
		}
		break;
		case EMetaId_StreamDate:
		{
			pCol = DigitalCD.pDatesCol;
			ppText = &This->date;
		}
		break;
		case EMetaId_StreamBox:
		{
			pCol = DigitalCD.pBoxesCol;
			ppText = &This->box;
		}
		break;
		case EMetaId_StreamAlbum:
		{
			pCol = DigitalCD.pAlbumsCol;
			ppText = &This->album;
		}
		break;
	}

	if (ppText == NULL) return false;

	if (!strcmp(nvl(*ppText), nvl(pString)))
		return false;

	throw_StrCol_Set(pCol, ppText, pString);

	return true;
}

int CDDesc_CountTracks(const CDDesc* This)
{
	return This->nr_tracks;
}

CDTrack* CDDesc_GetTrack(const CDDesc* This, int i)
{
	if ((i < 0) || (i >= This->nr_tracks)) return NULL;

	return PtrArray_Get(This->pTrackList, i);
}

static CDTrack* throw_CDDesc_AddTrack(CDDesc* This)
{
	CDTrack* pTrack = throw_New_CDTrack(This);
	PtrArray_Insert(This->pTrackList, -1, pTrack);

	This->nr_tracks++;
	CDTrack_SetOrder(pTrack, This->nr_tracks);

	return pTrack;
}

/*-------------------------------------------------------------------------*
 *--- CDDesc definition Load/Save -----------------------------------------*
 *-------------------------------------------------------------------------*/

static void throw_CDDesc_Copy(CDDesc* This, const CDDesc* pDesc, bool bKeepTracksCount) throws(mem)
{
	int tracks = pDesc->nr_tracks;
	int i;

	throw_StrCol_Set(DigitalCD.pCollectivesCol, &This->collective, pDesc->collective);
	throw_StrCol_Set(DigitalCD.pArtistsCol, &This->author, pDesc->author);
	throw_StrCol_Set(DigitalCD.pDatesCol, &This->date, pDesc->date);
	throw_StrCol_Set(DigitalCD.pBoxesCol, &This->box, pDesc->box);
	throw_StrCol_Set(DigitalCD.pAlbumsCol, &This->album, pDesc->album);

	if (!bKeepTracksCount)
	{
		while (tracks > This->nr_tracks)
			throw_CDDesc_AddTrack(This);

		while (tracks < This->nr_tracks)
			CDDesc_DeleteTrack(This, tracks);
	}

	if (tracks > This->nr_tracks)
		tracks = This->nr_tracks;

	for (i = 0; i < tracks; i++)
	{
		CDTrack* pTrack = CDDesc_GetTrack(This, i);
		CDTrack* pRefTrack = CDDesc_GetTrack(pDesc, i);
		throw_CDTrack_Copy(pTrack, pRefTrack);
	}

	for (; i < This->nr_tracks; i++)
	{
		CDTrack* pTrack = CDDesc_GetTrack(This, i);
		throw_CDTrack_Clear(pTrack);
	}

	// Copy pref list
	Array_Clear(This->pPrefList);
	for (i = 0; i < Array_Count(pDesc->pPrefList); i++)
	{
		int value = *(int*) Array_Get(pDesc->pPrefList, i);
		if ((value >= 0)
		||  (value < tracks))
			Array_Insert(This->pPrefList, i, &value);
	}
}

static void throw_CDDesc_ClearMetadata(CDDesc* This)
{
	int i;

	// clear all texts
	throw_StrCol_Set(DigitalCD.pCollectivesCol, &This->collective, NULL);
	throw_StrCol_Set(DigitalCD.pArtistsCol, &This->author, NULL);
	throw_StrCol_Set(DigitalCD.pDatesCol, &This->date, NULL);
	throw_StrCol_Set(DigitalCD.pBoxesCol, &This->box, NULL);
	throw_StrCol_Set(DigitalCD.pAlbumsCol, &This->album, NULL);

	for (i = 0; i < This->nr_tracks; i++)
	{
		CDTrack* pTrack = CDDesc_GetTrack(This, i);
		throw_CDTrack_Clear(pTrack);
	}

	// Remove pref list
	Array_Clear(This->pPrefList);
}

static int throw_CDDesc_ReadMetadata(CDDesc* This, FILE* file) throws(mem)
{
	int      tracks = This->nr_tracks;
	long int restorepos;
	char*    pValue;
	bool     bAppend = (tracks == 0);
	int      i;
	char     string[256];

	throw_CDDesc_ClearMetadata(This);

	// id found

	while(true)
	{
		char* pc;
		int track;
		CDTrack* pTrack;

		// extract a line
		restorepos = ftell(file);
		if (fgets(string, 255, file) == NULL)
		{
			if (feof(file))
				break;
			throw_last_oserror();
		}
		if (string[0] == '[')
			break;

		// find '=' in string
		pValue = strchr(string, '=');

		// loop if not found
		if (pValue == NULL) continue;

		*pValue = 0;
		pValue++;
		String_StripBlanks(pValue);
		if (!*pValue) continue;

		String_StripBlanks(string);
		if (!*string) continue;
		pc = string;
		while(*pc)
		{
			*pc = toupper(*pc);
			pc++;
		}

		if (!strcmp(string, "AUTHOR"))
		{
			throw_CDDesc_SetMetaString(This, EMetaId_StreamArtist, pValue);
			continue;
		}

		if (!strcmp(string, "COLLECTIVE"))
		{
			throw_CDDesc_SetMetaString(This, EMetaId_StreamCollective, pValue);
			continue;
		}

		if (!strcmp(string, "TITLE"))
		{
			throw_CDDesc_SetMetaString(This, EMetaId_StreamAlbum, pValue);
			continue;
		}

		if (!strcmp(string, "BOX"))
		{
			throw_CDDesc_SetMetaString(This, EMetaId_StreamBox, pValue);
			continue;
		}

		if (!strcmp(string, "DATE"))
		{
			throw_CDDesc_SetMetaString(This, EMetaId_StreamDate, pValue);
			continue;
		}

		if (!strcmp(string, "TRACKS"))
		{
			if (!bAppend) continue;

			tracks = atoi(pValue);
			while(This->nr_tracks < tracks)
			{
				pTrack = throw_CDDesc_AddTrack(This);
			}
			continue;
		}

		if (!strcmp(string, "PREFLIST"))
		{
			char* pstr = pValue;
			int value;

			if (Array_Count(This->pPrefList)) continue;

			while((pstr = strchr(pValue, ',')) != NULL)
			{
				*pstr = '\0';
				String_StripBlanks(pValue);
				value = atoi(pValue) - 1;
				Array_Insert(This->pPrefList, -1, &value);
				pValue = pstr + 1;
			}

			value = atoi(pValue) - 1;
			Array_Insert(This->pPrefList, -1, &value);

			continue;
		}

		track = atoi(string + 1);
		if ((track < 1) || track > 99) continue;

		if (track > This->nr_tracks)
		{
			if (!bAppend) continue;

			while(This->nr_tracks < track)
			{
				pTrack = throw_CDDesc_AddTrack(This);
			}
		}

		pTrack = CDDesc_GetTrack(This, track - 1);

		switch(string[0])
		{
			case 'A':
			{
				throw_CDTrack_SetMetaString(pTrack, EMetaId_StreamArtist, pValue);
				continue;
			}
			break;
			case 'C':
			{
				throw_CDTrack_SetMetaString(pTrack, EMetaId_StreamCollective, pValue);
				continue;
			}
			break;
			case 'T':
			{
				throw_CDTrack_SetMetaString(pTrack, EMetaId_StreamTitle, pValue);
				continue;
			}
			break;
		}
	}

	// restore last read line (EOF or next section)
	fseek(file, restorepos, SEEK_SET);

	// Ensure pref list has only valid track numbers
	for (i = Array_Count(This->pPrefList) - 1; i >= 0; i--)
	{
		int value = *(int*) Array_Get(This->pPrefList, i);
		if ((value < 0)
		||  (value >= tracks))
			Array_Remove(This->pPrefList, i);
	}

	// Fix work copy
	throw_CDDesc_Copy(This + 1, This, false);

	return tracks;
}

/**
 * Save copy in database.
 */
static bool CDDesc_SaveCopy(CDDesc* pCopy)
{
	bool volatile  bSaved = false;
	char* volatile pOldContent = NULL;
	static char    our_id[10];
	FILE* file;
	int   nr;
	int   TotalSize = 0;
	int   HeaderSize = 0;
	int   TrailerPos = 0;
	bool  bFound = false;

	snprintf(our_id, sizeof(our_id), "%ld", pCopy->id);

	bool off = !WLib_Hourglass_IsOn();
	if (off) WLib_Hourglass_On();

	try
	{
		// scan the CDs file for the corresponding id
		if ((file = fopen(DatabaseFile, "rb")) == NULL)
			throw_string("CDErr1");

		try
		{
			fseek(file, 0, SEEK_END);
			TotalSize = (int) ftell(file);
			fseek(file, 0, SEEK_SET);
			// Init for insert at end of file
			HeaderSize = TotalSize;
			TrailerPos = TotalSize;

			if (File_FindSection(file, our_id, true))
			{
				bFound = true;
				// id found
				HeaderSize = (int) ftell(file);
				File_FindEndOfSection(file);
				TrailerPos = (int) ftell(file);
			}

			// alloc memory for restore area
			if (TotalSize > 0)
			{
				pOldContent = throw_mem_alloc(TotalSize);
				fseek(file, 0, SEEK_SET);
				if (!fread(pOldContent, 1, TotalSize, file))
					throw_string("CDErr1");
			}
		}
		catch
		{
			fclose(file);
			throw_current();
		}
		catch_end

		fclose(file);

		// Write the file
		if ((file = fopen(DatabaseFile, "wb+")) != NULL)
		{
			try
			{
				if (HeaderSize > 0)
				{
					if (fwrite(pOldContent, 1, HeaderSize, file) != HeaderSize)
						throw_user(0);
					// Insert a return if required
					if (pOldContent[HeaderSize - 1] != '\n')
					{
						if (fprintf(file, "\n") == EOF)
							throw_user(0);
					}
				}

				if (!pCopy->bDeleted)
				{
					if (!bFound)
					{
						// insert id
						if (fprintf(file, "[%s]\n", our_id) == EOF)
							throw_user(0);
					}

					// write CD collective
					if (*(pCopy->collective ? pCopy->collective : &nil))
						if (fprintf(file, "Collective = %s\n", pCopy->collective) == EOF)
							throw_user(0);

					// write CD author
					if (*(pCopy->author ? pCopy->author : &nil))
						if (fprintf(file, "Author = %s\n", pCopy->author) == EOF)
							throw_user(0);

					// write CD date
					if (*(pCopy->date ? pCopy->date : &nil))
						if (fprintf(file, "Date = %s\n", pCopy->date) == EOF)
							throw_user(0);

					// write CD box
					if (*(pCopy->box ? pCopy->box : &nil))
						if (fprintf(file, "Box = %s\n", pCopy->box) == EOF)
							throw_user(0);

					// write CD title
					if (*(pCopy->album ? pCopy->album : &nil))
						if (fprintf(file, "Title = %s\n", pCopy->album) == EOF)
							throw_user(0);

					// write number of tracks
					if (fprintf(file, "Tracks = %d\n", pCopy->nr_tracks) == EOF)
						throw_user(0);

					// write pref list
					if (Array_Count(pCopy->pPrefList) > 0)
					{
						int i;

						if (fprintf(file, "PrefList =") == EOF)
							throw_user(0);

						for (i = 0; i < Array_Count(pCopy->pPrefList) - 1; i++)
						{
							if (fprintf(file, " %d,", *(int*) Array_Get(pCopy->pPrefList, i) + 1) == EOF)
								throw_user(0);
						}
						if (fprintf(file, " %d\n", *(int*) Array_Get(pCopy->pPrefList, i) + 1) == EOF)
							throw_user(0);
					}

					// write tracks info
					for (nr = 0; nr < pCopy->nr_tracks; nr++)
					{
						CDTrack* pTrack = CDDesc_GetTrack(pCopy, nr);
						const char* pstr;

						// write track author
						pstr = CDTrack_GetMetaString(pTrack, EMetaId_StreamArtist);
						if (strlen(pstr))
							if (fprintf(file, "A%d = %s\n", CDTrack_GetOrder(pTrack), pstr) == EOF)
								throw_user(0);

						// write track collective
						pstr = CDTrack_GetMetaString(pTrack, EMetaId_StreamCollective);
						if (strlen(pstr))
							if (fprintf(file, "C%d = %s\n", CDTrack_GetOrder(pTrack), pstr) == EOF)
								throw_user(0);

						// write track title
						pstr = CDTrack_GetMetaString(pTrack, EMetaId_StreamTitle);
						if (strlen(pstr))
							if (fprintf(file, "T%d = %s\n", CDTrack_GetOrder(pTrack), pstr) == EOF)
								throw_user(0);
					}
				}

				// write file trailer
				if (TrailerPos < TotalSize)
				{
					if (fwrite(pOldContent + TrailerPos, 1, TotalSize - TrailerPos, file) != (TotalSize - TrailerPos))
						throw_user(0);
				}

				fclose(file);

				bSaved = true;
			}
			catch
			{
				fclose(file);

				if ((file = fopen(DatabaseFile, "wb+")) != NULL)
				{
					size_t size = fwrite(pOldContent, 1, TotalSize, file);
					fclose(file);
					if (size == TotalSize) throw_current();
				}
				throw_string("CDErr3");
			}
			catch_end
		}
		else
		{
			throw_user(0);
		}

		// Ensure main version contains this copy
		throw_CDDesc_Copy(pCopy - 1, pCopy, false);
	}
	catch
	{
		const exception* e = exception_current();

		// Report only errors not due to write protected file
		if (e->type != exception_user)
			App_ReportException();
		else
			App_ReportError("CDErr2");
	}
	catch_end

	mem_free(pOldContent);

	if (off) WLib_Hourglass_Off();

	if (bSaved) CDDesc_SetModifiedFlag(pCopy, false);

	return bSaved;
}

/*-------------------------------------------------------------------------*
 *--- CDDesc update from drive info, CDDB, ... ----------------------------*
 *-------------------------------------------------------------------------*/

void throw_CDDesc_FixCDSize(CDDesc* pCopy, int tracks)
{
	CDDescList* pDoc = CDDescList_Get();
	CDDesc* This = pCopy - 1;

	while(This->nr_tracks < tracks)
		throw_CDDesc_AddTrack(This);

	while(This->nr_tracks > tracks)
		CDDesc_DeleteTrack(This, tracks);

	while(pCopy->nr_tracks < tracks)
	{
		CDTrack* pTrack = throw_CDDesc_AddTrack(pCopy);
		try
		{
			DocEvent e = {EDocEvent_InsertElement, pTrack, pDoc};
			DocEvents_NotifyListeners(pDoc, &e);
		}
		catch
		{
			App_ReportException();
		}
		catch_end
	}

	while(pCopy->nr_tracks > tracks)
	{
		CDTrack* pTrack = CDDesc_GetTrack(pCopy, tracks);
		try
		{
			DocEvent e = {EDocEvent_RemoveElement, pTrack, pDoc};
			DocEvents_NotifyListeners(pDoc, &e);
		}
		catch
		{
			App_ReportException();
		}
		catch_end
		CDDesc_DeleteTrack(pCopy, tracks);
	}
}

void CDDesc_RefreshViews(CDDesc* This, void* pSender, bool bModified)
{
	if (bModified) CDDesc_SetModifiedFlag(This, true);

	for (int i = CDDesc_CountTracks(This) - 1; i >= 0; i--)
	{
		CDTrack_RefreshViews(CDDesc_GetTrack(This, i), pSender, false);
	}
}

// Note: needs to work even if pDesc is NULL
static void CDDesc_DecodeCDDBMsg(CDDesc* This, CDDB_QueryResults_Message* pmsg)
{
	int i;
	CDDB_FreeArea_Message msg;
	bool bModified = false;

	if (pmsg->hdr.action != CDDB_QueryResults_MessageNum) return;

	if (pmsg->area != NULL)
	{
		if ((  (pmsg->return_code == CDDB_QueryResults_Code_LocalMatch)
		    || (pmsg->return_code == CDDB_QueryResults_Code_RemoteFinished))
		   && (This != NULL))
		{
			try
			{
				const char* author = strrchr(pmsg->area->disc_title, '/');
				const char* album = pmsg->area->disc_title;

				if (author)
				{
					album = SPrintf("%.*s", author - pmsg->area->disc_title, pmsg->area->disc_title);
					author++;
				}
				bModified |= throw_CDDesc_SetMetaString(This, EMetaId_StreamAlbum, album);
				bModified |= throw_CDDesc_SetMetaString(This, EMetaId_StreamArtist, author);

				for (i = 0; i < CDDesc_CountTracks(This); i++)
				{
					CDTrack* pTrack = CDDesc_GetTrack(This, i);
					bModified |= throw_CDTrack_SetMetaString(pTrack, EMetaId_StreamTitle, pmsg->area->tracks[i].title);
				}

				CDDesc_SetModifiedFlag(This, bModified);
				CDDesc_SaveCopy(This);
				CDDesc_RefreshViews(This, CDDescList_Get(), false);
			}
			catch
			{
				App_ReportException();
			}
			catch_end
		}

		msg.hdr.ref = pmsg->hdr.id;
		msg.hdr.action = CDDB_FreeArea_MessageNum;
		msg.hdr.size = sizeof(msg);
		msg.flags = 0;
		msg.area_id = pmsg->area_id;
		Task_SendMsg((Msg*) &msg, pmsg->hdr.task);
	}
}

void CDDesc_Identify(CDDesc* This, int drive)
{
	CDDescList* pDoc = CDDescList_Get();

	if ((Options()->Drivers.CDDB.bSendQuery)
	&&  !*CDDesc_GetMetaString(This, EMetaId_StreamAlbum))
	{
		CDDB_Query_Message msg;
		CDDB_QueryResults_Message* reply;
		HTask dummy;

		msg.hdr.size = sizeof(msg);
		msg.hdr.ref = 0;
		msg.hdr.action = CDDB_Query_MessageNum;
		msg.flags = 0;
		if (Options()->Drivers.CDDB.bAllowRemote)
			msg.flags |= CDDB_Query_Flags_AllowRemote;
		if (Options()->Drivers.CDDB.bShowQuery)
			msg.flags |= CDDB_Query_Flags_OpenStatus;
		msg.drive_number = drive;

		reply = (CDDB_QueryResults_Message*) Task_PostAndWaitForReply((Msg*) &msg, 0);

		if ((reply == NULL)
		&&  (strlen(Options()->Drivers.CDDB.pPath)))
		{
			StartTask(&dummy, Options()->Drivers.CDDB.pPath);
			reply = (CDDB_QueryResults_Message*) Task_PostAndWaitForReply((Msg*) &msg, 0);
		}

		if ((reply != NULL)
		&&  (reply->hdr.action == CDDB_QueryResults_MessageNum))
		{
			// Retain the last messages as reply may just signal remote access
			// and AcornCD uses ref of first attempt when several attempts are made
			pDoc->CDDBMap[pDoc->m_NextMsg].pDesc = This;
			pDoc->CDDBMap[pDoc->m_NextMsg].msgref = reply->hdr.ref;
			pDoc->m_NextMsg++;
			if (pDoc->m_NextMsg >= MAX_MSGS)
				pDoc->m_NextMsg = 0;
			CDDesc_DecodeCDDBMsg(This, reply);
		}
	}
}

/*-------------------------------------------------------------------------*
 *--- CDDescList Document callbacks ---------------------------------------*
 *-------------------------------------------------------------------------*/

// Virtual destructor

static void CDDescList_NotCDDescList(Document* pDoc)
{
	CDDescList* This = (CDDescList*) pDoc;
	CDDesc* pDesc;
	int i;

	if (pCDDescsList == This)
		pCDDescsList = NULL;

	for (i = 0; i < PtrArray_Count(This->CDs); i++)
	{
		pDesc = PtrArray_Get(This->CDs, i);
		Delete_CDDesc(pDesc);
	}

	Delete_PtrArray(This->CDs);
	Delete_PtrArray(This->m_pTrackList);

	Document_NotDocument(pDoc);
}

// Ask DCS

static DCS_Enum CDDescList_AskDiscardCancelSave(const Document* pDoc)
{
	IGNORE(pDoc);

	return DCS_Ask(Msg_Lookup("DCS"));
}

// Clear the content

static void CDDescList_ClearContents(Document* pDoc)
{
	IGNORE(pDoc);
}

// Load the content of an input source

static const _kernel_oserror* CDDescList_LoadDocument(Document* pDoc, XFer* xfer, file_type type, const ScreenPos* pInfo)
{
	IGNORE(pDoc);
	IGNORE(xfer);
	IGNORE(type);
	IGNORE(pInfo);

	return NULL;
}

// Send content to an output destination, one chunk at a time

static bool CDDescList_SendDocument(Document* pDoc, file_type type, sXFer_Chunk* pchunk)
{
	IGNORE(pDoc);
	IGNORE(type);
	IGNORE(pchunk);

	return false;
}

static const DocumentVPtr CDDescList_VPtr =
{ CDDescList_NotCDDescList
, CDDescList_AskDiscardCancelSave
, SaveAs_AskSaveAs
, CDDescList_ClearContents
, CDDescList_LoadDocument
, CDDescList_SendDocument
};

/*-------------------------------------------------------------------------*
 *--- CDDescList Construction/Destruction ---------------------------------*
 *-------------------------------------------------------------------------*/

static EListenerAction CDDescList_CDDBQueryHandler(void* handle, const Event* e)
{
	CDDescList* This = handle;
	CDDB_QueryResults_Message* pmsg = (CDDB_QueryResults_Message*) e->pData;
	CDDesc* pDesc = NULL;

	// CCDB response from remote
	for (int i = 0; i < MAX_MSGS; i++)
	{
		if (pmsg->hdr.ref == This->CDDBMap[i].msgref)
		{
			pDesc = This->CDDBMap[i].pDesc;
			break;
		}
	}

	// Deal with message even if pDesc is NULL
	CDDesc_DecodeCDDBMsg(pDesc, pmsg);

	return EListenerAction_ContinueEvent;
}

static CDDescList* throw_New_CDDescList(CDDescList_Type type) throws(mem)
{
	CDDescList* This = throw_mem_calloc(sizeof(*This), 1);
	CDDesc* pDesc = NULL;
	FILE* file = NULL;
	char* pId;
	int id;
	static char string[256];

	This->m_Type = type;
	throw_Document_Document(&This->m_Document, NULL, &CDDescList_VPtr);
	Document_SetNonFileFlag(&This->m_Document, true);
	Document_SetAutoDeleteFlag(&This->m_Document, false);

	try
	{
		if (This->m_Type == CDDescList_TypeMain)
		{
			throw_Document_SetPathName(&This->m_Document, Msg_Lookup("CDDatabase"));
 			This->CDs = New_PtrArray(10);

			// scan the CDs file for the corresponding ids
			if ((file = fopen(DatabaseFile, "rb")) == NULL)
				throw_string("CDErr1");

			bool off = !WLib_Hourglass_IsOn();
			WLib_Hourglass_On();

			try
			{
				while(true)
				{
					// extract a line
					if (fgets(string, 255, file) == NULL) break;

					if (string[0] != '[') continue;

					// find ']' in string
					pId = strchr(string + 1, ']');

					// loop if not found
					if ((pId == NULL) || (pId == string + 1)) continue;

					*pId = 0;
					pId--;
					// look for digits only
					while(pId > string)
					{
						if (!isdigit(*pId)) break;
						pId--;
					}

					if (pId != string) continue;

					id = atoi(string + 1);

					if (id > 0)
					{
						pDesc = throw_New_CDDesc(id);

						throw_CDDesc_ReadMetadata(pDesc, file);
						PtrArray_Insert(This->CDs, -1, pDesc);

						pDesc = NULL;
					}
				}

				if (!feof(file))
					throw_last_oserror();
			}
			catch
			{
				// We will report an error but accept what has been read so far
				const exception* e = exception_current();

				// Report only errors not due to write protected file
				if (e->type != exception_user)
					App_ReportException();
				else
					App_ReportError("CDErr1");

				Delete_CDDesc(pDesc);
			}
			catch_end

			fclose(file);

			if (off) WLib_Hourglass_Off();

			throw_Task_AddMsgListener(CDDB_QueryResults_MessageNum, CDDescList_CDDBQueryHandler, This, false);
		}
		else
		{
			throw_Document_SetPathName(&This->m_Document, Msg_Lookup("CDSearch"));
			This->m_pTrackList = New_PtrArray(10);
		}

		throw_New_CDTreeView(This);
		CDDescList_Show(This, NULL);
	}
	catch
	{
		Task_RemoveMsgListener(CDDB_QueryResults_MessageNum, CDDescList_CDDBQueryHandler, This);
		Delete_Document(&This->m_Document);
		throw_current();
	}
	catch_end

	return This;
}

static void Delete_CDDescList(CDDescList* This)
{
	if (This)
	{
		Task_RemoveMsgListener(CDDB_QueryResults_MessageNum, CDDescList_CDDBQueryHandler, This);
		Delete_Document(&This->m_Document);
	}
}

CDDescList* CDDescList_Get(void)
{
	if (!pCDDescsList)
		pCDDescsList = throw_New_CDDescList(CDDescList_TypeMain);

	return pCDDescsList;
}

void CDDescList_Release(void)
{
	if (!pCDDescsList) return;

	Delete_CDDescList(pCDDescsList);
	pCDDescsList = NULL;
}

CDDescList* throw_New_CDSearchList(void)
{
	return throw_New_CDDescList(CDDescList_TypeSearch);
}

/*-------------------------------------------------------------------------*
 *--- CDDescList Edition --------------------------------------------------*
 *-------------------------------------------------------------------------*/

void CDDescList_SaveChanges(CDDescList* This)
{
	CDDesc* pCopy;
	CDDesc* pDesc;
	int i;

	try
	{
		for (i = PtrArray_Count(This->CDs) - 1; i >= 0; i--)
		{
			pDesc = PtrArray_Get(This->CDs, i);
			pCopy = pDesc + 1;

			if (pCopy->bModified)
				CDDesc_SaveCopy(pCopy);

			if (pCopy->bDeleted)
			{
				Delete_CDDesc(pDesc);
				PtrArray_Remove(This->CDs, i);
			}
		}
	}
	catch
	{
		App_ReportException();
	}
	catch_end
}

void CDDescList_RevertChanges(CDDescList* This)
{
	CDDesc* pCopy = NULL;

	CDDescList_StartUpdate(This);
	try
	{
		for (int i = 0; i < PtrArray_Count(This->CDs); i++)
		{
			pCopy = PtrArray_Get(This->CDs, i);
			pCopy++;

			if (pCopy->bModified)
			{
				throw_CDDesc_Copy(pCopy, pCopy - 1, false);
				CDDesc_SetDeletedFlag(pCopy, false);
				CDDesc_RefreshViews(pCopy, This, false);
				CDDesc_SetModifiedFlag(pCopy, false);
			}
		}
	}
	catch
	{
		App_ReportException();
	}
	catch_end
	CDDescList_EndUpdate(This);
}

int throw_CDDescList_Find(CDDescList* This, unsigned long id) throws(mem)
{
	CDDesc* pDesc = NULL;
	int i;

	if (id == 0) return -1;

	for (i = 0; i < PtrArray_Count(This->CDs); i++)
	{
		pDesc = PtrArray_Get(This->CDs, i);

		if (CDDesc_GetId(pDesc) == id)
			return i;
	}

	pDesc = throw_New_CDDesc(id);
	PtrArray_Insert(This->CDs, -1, pDesc);
	CDDesc_SetModifiedFlag(pDesc + 1, true);

	return PtrArray_Count(This->CDs) - 1;
}

CDDesc* throw_CDDescList_GetWorkDesc(CDDescList* This, int index, unsigned long id)
{
	CDDesc* pDesc = NULL;

	if (index < 0)
		index = throw_CDDescList_Find(This, id);

	if (index >= 0)
	{
		pDesc = PtrArray_Get(This->CDs, index);
		return pDesc + 1;
	}

	return NULL;
}

void CDDescList_ListIds(CDDescList* This, Array* pList) throws(mem)
{
	int i;

	for (i = 0; i < PtrArray_Count(This->CDs); i++)
	{
		unsigned long id;
		CDDesc* pDesc = PtrArray_Get(This->CDs, i);

		id = CDDesc_GetId(pDesc);
		Array_Insert(pList, -1, &id);
	}
}

int CDDescList_Count(CDDescList* This)
{
	return PtrArray_Count(This->CDs);
}

CDDescList_Type CDDescList_GetType(const CDDescList* This)
{
	return This->m_Type;
}

// Procedure to tell all views that updates will follow

void CDDescList_StartUpdate(CDDescList* This)
{
	DocEvent e = {EDocEvent_StartUpdate, NULL, This};

	try
	{
		DocEvents_NotifyListeners(NULL, &e);
		This->m_bUpdateStarted = true;
	}
	catch
	{
		App_ReportException();
	}
	catch_end
}

// Procedure to tell all views that updates end
// also sets if the document is modified

void CDDescList_EndUpdate(CDDescList* This)
{
	DocEvent e = {EDocEvent_EndUpdate, NULL, This};

	try
	{
		DocEvents_NotifyListeners(NULL, &e);
	}
	catch
	{
		App_ReportException();
	}
	catch_end

	This->m_bUpdateStarted = false;
}

bool CDDescList_IsBusy(const CDDescList* This)
{
	if (This->m_bUpdateStarted
	||  JobList_HasJobs(Task_GetJobList(), This))
		return true;

	return false;
}

void CDDescList_Show(CDDescList* This, CDDesc* pDesc)
{
	DocEvent e = {EDocEvent_ShowView, pDesc, This};

	try
	{
		DocEvents_NotifyListeners(This, &e);
	}
	catch
	{
		App_ReportException();
	}
	catch_end
}

void throw_CDSearchList_Add(CDDescList* This, CDTrack* pTrack)
{
	if (This->m_pTrackList != NULL)
	{
		PtrArray_Insert(This->m_pTrackList, -1, pTrack);
		try
		{
			DocEvent e = {EDocEvent_InsertElement, pTrack, This};
			DocEvents_NotifyListeners(This, &e);
		}
		catch
		{
			PtrArray_Remove(This->m_pTrackList, PtrArray_Count(This->m_pTrackList) - 1);
			throw_current();
		}
		catch_end
	}
}

int CDSearchList_TrackCount(CDDescList* This)
{
	// Search list?
	if (This->m_pTrackList != NULL)
		return PtrArray_Count(This->m_pTrackList);

	// Database
	int i, count = 0;

	for (i = 0; i < PtrArray_Count(This->CDs); i++)
	{
		CDDesc* pCopy = PtrArray_Get(This->CDs, i);
		pCopy++;
		count += CDDesc_CountTracks(pCopy);
	}

	return count;
}

static const char Err_InvalidIndex[] = "Array index out of bounds";

CDTrack* CDSearchList_GetTrack(CDDescList* This, int index) throws(index)
{
	// Search list?
	if (This->m_pTrackList != NULL)
		return PtrArray_Get(This->m_pTrackList, index);

	// Database
	int i, count = 0;

	for (i = 0; i < PtrArray_Count(This->CDs); i++)
	{
		CDDesc* pCopy = PtrArray_Get(This->CDs, i);
		pCopy++;
		count += CDDesc_CountTracks(pCopy);
		if (count > index)
		{
			index -= (count - CDDesc_CountTracks(pCopy));
			return PtrArray_Get(pCopy->pTrackList, index);
		}
	}

	throw_runtime(Err_InvalidIndex);

	return NULL;
}
