#include "WimpLib:Choices.h"

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

#include "WimpLib:PtrArray.h"
#include "WimpLib:Exception.h"
#include "WimpLib:File.h"
#include "WimpLib:Mem.h"
#include "WimpLib:Task.h"
#include "WimpLib:Utils.h"

static const char ReadPath[] = "Choices:";
static const char WritePath[] = "<Choices$Write>.";
static const char Err_UnableToReadFile[] = "ChErr1";
static const char Err_UnableToWriteFile[] = "ChErr2";
static const char Err_UnableToWriteFile_Corrupted[] = "ChErr3";

static char Ch_ReadFilename[MAXPATH];
static char Ch_WriteFilename[MAXPATH];
static char Ch_String[16][256];

static PtrArray* Ch_Lines = NULL;
static bool Ch_bWrite = false;
static int Ch_Index = 0;

static void throw_Choices_ReadFile(void)
{
	FILE* file;
	char* volatile line = NULL;
	char* const string = &Ch_String[Ch_Index][0];
	int i;

	// Flush existing content
	for (i = 0; i < PtrArray_Count(Ch_Lines); i++)
		mem_free(PtrArray_Get(Ch_Lines, i));

	PtrArray_Clear(Ch_Lines);

	// Read every line in the file
	if ((file = fopen(Ch_ReadFilename, "rb")) == NULL)
	{
		// File may not exists but if it does, report an error
		if (File_GetFileType(Ch_ReadFilename) != -2)
			throw_string(Err_UnableToReadFile);
		else return;
	}

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

			String_StripBlanks(string);

			line = throw_mem_allocstring(string);
			PtrArray_Insert(Ch_Lines, -1, line);
			line = NULL;
		}

		if (!feof(file))
			throw_last_oserror();
	}
	catch
	{
		if (line) mem_free(line);
		fclose(file);
		throw_current();
	}
	catch_end

	fclose(file);
}

void Choices_FlushPendingWrites(void)
{
	if (!Ch_bWrite) return;

	try
	{
		FILE* file;

		// File will exists
		if ((file = fopen(Ch_WriteFilename, "rb")) == NULL)
		{
			throw_os(File_EnsurePath(Ch_WriteFilename));
			// Write an empty file and reopen it
			if ((file = fopen(Ch_WriteFilename, "wb+")) == NULL)
				throw_user(0); // protected file
			fclose(file);
			File_SetFileType(Ch_WriteFilename, 0xFFF);
		}
		else
		{
			fclose(file);
		}

		// Write the file
		if ((file = fopen(Ch_WriteFilename, "wb+")) != NULL)
		{
			try
			{
				int i;

				for (i = 0; i < PtrArray_Count(Ch_Lines); i++)
				{
					if (fprintf(file, "%s\n", (char*) PtrArray_Get(Ch_Lines, i)) == EOF)
						throw_string(Err_UnableToWriteFile);
				}

				fclose(file);
			}
			catch
			{
				fclose(file);

				// Choices file has been corrupted
				throw_current();
			}
			catch_end
		}
		else
		{
			throw_last_oserror();
		}
	}
	catch
	{
		const exception* e = exception_current();

		// Report only errors not due to write protected file
		if (e->type != exception_user)
			Task_ReportException();
	}
	catch_end

	// Even if something went wrong because called on Wimp_Poll Idle event
	Ch_bWrite = false;
}

void throw_Choices_Choices(const char* pfilename)
{
	Ch_Lines = New_PtrArray(10);
	snprintf(Ch_ReadFilename, MAXPATH, "%s%s", ReadPath, pfilename);
	snprintf(Ch_WriteFilename, MAXPATH, "%s%s", WritePath, pfilename);

	throw_Choices_ReadFile();
}

void Choices_NotChoices(void)
{
	int i;
	Choices_FlushPendingWrites();

	// Flush existing content
	for (i = 0; i < PtrArray_Count(Ch_Lines); i++)
		mem_free(PtrArray_Get(Ch_Lines, i));

	Delete_PtrArray(Ch_Lines);
	Ch_Lines = NULL;
}

const char* Choices_GetFilename(void)
{
	return &Ch_ReadFilename[strlen(ReadPath)];
}

static int Choices_FindSection(const char* psection)
{
	int   len = strlen(psection);
	int   i;
	const char* pline;

	for (i = 0; i < PtrArray_Count(Ch_Lines); i++)
	{
		pline = PtrArray_Get(Ch_Lines, i);

		// Test for start of section marker
		if (pline[0] != '[')
			continue;

		// Test if text following is section
		if (strncmp(pline + 1, psection, len))
			continue;

		// Test for end of section marker
		if (pline[len + 1] == ']')
			return i;

	}

	return i;
}

static int Choices_FindNextSection(int i)
{
	const char* pline;

	for(; i < PtrArray_Count(Ch_Lines); i++)
	{
		pline = PtrArray_Get(Ch_Lines, i);

		// Test for start of section marker
		if (pline[0] == '[')
			return i;
	}

	return i;
}

static int Choices_RewindEmptyLines(int i) throws(index)
{
	const char* pline;

	for(i = i - 1; i >= 0; i--)
	{
		for(pline = PtrArray_Get(Ch_Lines, i); *pline > 0; pline++)
		{
			if (*pline > 32)
				return i + 1;
		}
	}

	return i + 1;
}

static int Choices_ConvertInt(const char* pValue)
{
	char* pdummy;

	return (int) strtol(pValue, &pdummy, 0);
}

bool Choices_Write(const char* psection, const char* pvariable, const char* pformat, ...)
{
	int line = Choices_FindSection(psection);
	char* const string = &Ch_String[Ch_Index][0];
	int len;
	int endline;
	char* volatile pnewline = NULL;
	const char* pline;
	const char* pValue;
	const char* pc;
	const char* ps;
	va_list arg;

	try
	{
		if (line == PtrArray_Count(Ch_Lines))
		{
			// insert section
			pnewline = throw_mem_allocstring(NULL);
			PtrArray_Insert(Ch_Lines, line, pnewline);
			pnewline = NULL;
			line++;

			pnewline = throw_mem_allocprint("[%s]", psection);
			PtrArray_Insert(Ch_Lines, line, pnewline);
			pnewline = NULL;
		}

		line++;

		// id found
		endline = Choices_FindNextSection(line);
		endline = Choices_RewindEmptyLines(endline);

		while(line < endline)
		{
			pline = PtrArray_Get(Ch_Lines, line);

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

			// loop if not found
			if (pValue != NULL)
			{
				for (pc = pline, ps = pvariable; *ps; pc++, ps++)
				{
					if (toupper(*pc) != toupper(*ps))
						break;
				}

				if (!*ps) break;
			}
			line++;
		}

		// "variable = value"
		snprintf(string, 256, "%s = ", pvariable);
		len = strlen(string);
		va_start(arg, pformat);
		vsnprintf(string + len, 256 - len, pformat, arg);
		va_end(arg);
		pnewline = throw_mem_allocstring(string);
		PtrArray_Insert(Ch_Lines, line, pnewline);
		pnewline = NULL;
	}
	catch
	{
		mem_free(pnewline);
		return false;
	}
	catch_end

	// remove previous value
	if (line < endline)
	{
		mem_free(PtrArray_Get(Ch_Lines, line + 1));
		PtrArray_Remove(Ch_Lines, line + 1);
	}

	Ch_bWrite = true;
	return true;
}

const char* Choices_Read(const char* psection, const char* pvariable, const char* defval)
{
	int line = Choices_FindSection(psection);
	char* string = &Ch_String[Ch_Index][0];
	int endline;
	const char* pline;
	const char* pValue;
	const char* pc;
	const char* ps;

	// Loop on buffers
	Ch_Index = (Ch_Index + 1) & 7;

	if (line == PtrArray_Count(Ch_Lines))
		return defval;

	line++;

	// id found
	endline = Choices_FindNextSection(line);

	while(line < endline)
	{
		pline = PtrArray_Get(Ch_Lines, line);

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

		// loop if not found
		if (pValue != NULL)
		{
			for (pc = pline, ps = pvariable; *ps; pc++, ps++)
			{
				if (toupper(*pc) != toupper(*ps))
					break;
			}

			if (!*ps)
			{
				snprintf(string, 256, "%s", pValue + 1);
				String_StripBlanks(string);
				return string;
			}
		}
		line++;
	}

	return defval;
}

int Choices_ReadInt(const char* psection, const char* pvariable, int defval)
{
	const char* pValue = Choices_Read(psection, pvariable, NULL);

	if (pValue) return Choices_ConvertInt(pValue);

	return defval;
}

bool Choices_ReadBool(const char* psection, const char* pvariable, bool defval)
{
	const char* pValue = Choices_Read(psection, pvariable, NULL);

	if (pValue) return (atoi(pValue) != 0);

	return (defval != false);
}

void Choices_Update(const char* psection, const char* pvariable, const char** pval)
{
	const char* pValue = Choices_Read(psection, pvariable, *pval);

	pValue = mem_allocstring(pValue);

	if (pValue)
	{
		mem_free(*pval);
		*pval = (char*) pValue;
	}
}

void Choices_UpdateInt(const char* psection, const char* pvariable, int* pval, int min, int max)
{
	const char* pValue = Choices_Read(psection, pvariable, NULL);

	if (pValue) *pval = Choices_ConvertInt(pValue);

	if (*pval < min) *pval = min;
	if (*pval > max) *pval = max;
}

void Choices_UpdateBool(const char* psection, const char* pvariable, bool* pval)
{
	const char* pValue = Choices_Read(psection, pvariable, NULL);

	if (pValue) *pval = (atoi(pValue) != 0);
}

//-----------------------------------------------------------------------------------------------------------------

const char* Choices_ReadFrom(const char* pfilename, const char* psection, const char* pvariable, const char* defval)
{
	bool volatile  bFound = false;
	char* volatile pValue = NULL;
	static char    string[256];

	try
	{
		FILE* file;

		// scan the file for the corresponding id
		if ((file = fopen(pfilename, "rb")) == NULL)
			throw_string(Err_UnableToReadFile);

		try
		{
			long int restorepos;
			long int endpos;

			if (File_FindSection(file, psection, true))
			{
				// id found
				restorepos = ftell(file);
				File_FindEndOfSection(file);
				endpos = ftell(file);
				fseek(file, restorepos, SEEK_SET);

				while(ftell(file) < endpos)
				{
					const char* pc;
					const char* ps;

					// extract a line
					if (fgets(string, 255, file) == NULL)
						throw_string(Err_UnableToReadFile);

					// 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;

					for (pc = string, ps = pvariable; *pc; pc++, ps++)
					{
						if (toupper(*pc) != toupper(*ps))
							break;
					}

					if (*pc == *ps) // == 0
					{
						// id found
						bFound = true;
						break;
					}
				}
			}
		}
		catch
		{
			fclose(file);
			throw_current();
		}
		catch_end

		fclose(file);
	}
	catch
	{
		// No error reporting
		pValue = (char*) defval;
	}
	catch_end

	if (bFound)
		return pValue;

	return defval;
}

int Choices_ReadIntFrom(const char* pfilename, const char* psection, const char* pvariable, int defval)
{
	const char* pValue = Choices_ReadFrom(pfilename, psection, pvariable, NULL);

	if (pValue) return Choices_ConvertInt(pValue);

	return defval;
}

bool Choices_ReadBoolFrom(const char* pfilename, const char* psection, const char* pvariable, bool defval)
{
	const char* pValue = Choices_ReadFrom(pfilename, psection, pvariable, NULL);

	if (pValue) return (atoi(pValue) != 0);

	return (defval != false);
}

bool Choices_WriteTo(const char* pfilename, const char* psection, const char* pvariable, const char* pformat, ...)
{
	bool volatile  bSaved = false;
	char* volatile pOldContent = NULL;

	try
	{
		FILE*       file;
		static char string[256];

		int volatile  TotalSize = 0;
		int volatile  HeaderSize = 0;
		int volatile  TrailerPos = 0;
		bool volatile bSection = false;

		// scan the file for the corresponding id
		if ((file = fopen(pfilename, "rb")) == NULL)
		{
			throw_os(File_EnsurePath(pfilename));
			// Write an empty file and reopen it
			if ((file = fopen(pfilename, "wb+")) == NULL)
				throw_user(0);
			fclose(file);
			File_SetFileType(pfilename, 0xFFF);
			if ((file = fopen(pfilename, "rb")) == NULL)
				throw_string(Err_UnableToReadFile);
		}

		try
		{
			long int restorepos;
			long int endpos;
			char*    pValue;

			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, psection, true))
			{
				bSection = true;

				// id found
				restorepos = ftell(file);
				File_FindEndOfSection(file);
				endpos = ftell(file);
				fseek(file, restorepos, SEEK_SET);
				// Init for insert at end of section
				HeaderSize = (int) endpos;
				TrailerPos = (int) endpos;

				while(ftell(file) < endpos)
				{
					const char* pc;
					const char* ps;

					restorepos = ftell(file);

					// extract a line
					if (fgets(string, 255, file) == NULL)
						throw_string(Err_UnableToReadFile);

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

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

					*pValue = 0;

					String_StripBlanks(string);
					if (!*string) continue;

					for (pc = string, ps = pvariable; *pc; pc++, ps++)
					{
						if (toupper(*pc) != toupper(*ps))
							break;
					}

					if (*pc == *ps) // == 0
					{
						// id found
						// Init for replace line of variable
						HeaderSize = (int) restorepos;
						TrailerPos = (int) ftell(file);
						break;
					}
				}
			}

			// 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(Err_UnableToReadFile);
			}
		}
		catch
		{
			fclose(file);
			throw_current();
		}
		catch_end

		fclose(file);

		// Write the file
		if ((file = fopen(pfilename, "wb+")) != NULL)
		{
			try
			{
				va_list arg;

				if (HeaderSize > 0)
				{
					if (fwrite(pOldContent, 1, HeaderSize, file) != HeaderSize)
						throw_string(Err_UnableToWriteFile);
					// Insert a return if required
					if (pOldContent[HeaderSize - 1] != '\n')
					{
						if (fprintf(file, "\n") == EOF)
							throw_string(Err_UnableToWriteFile);
					}
				}

				if (!bSection)
				{
					// insert section
					if (fprintf(file, "[%s]\n", psection) == EOF)
						throw_string(Err_UnableToWriteFile);
				}

				// insert variable name
				if (fprintf(file, "%s = ", pvariable) == EOF)
					throw_string(Err_UnableToWriteFile);

				// insert variable value
				va_start(arg, pformat);
				if (vfprintf(file, pformat, arg) == EOF)
				{
					va_end(arg);
					throw_string(Err_UnableToWriteFile);
				}
				va_end(arg);

				// insert return
				if (fprintf(file, "\n") == EOF)
					throw_string(Err_UnableToWriteFile);

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

				if (fflush(file))
					throw_string(Err_UnableToWriteFile);

				fclose(file);

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

				if ((file = fopen(pfilename, "wb+")) != NULL)
				{
					size_t size = fwrite(pOldContent, 1, TotalSize, file);
					fclose(file);
					if (size == TotalSize) throw_current();
				}
				// Choices file has been corrupted
				throw_string(Err_UnableToWriteFile_Corrupted);
			}
			catch_end
		}
		else
		{
			throw_last_oserror();
		}
	}
	catch
	{
		const exception* e = exception_current();

		// Report only errors not due to write protected file
		if (e->type != exception_user)
			Task_ReportException();
	}
	catch_end

	mem_free(pOldContent);

	return bSaved;
}


#define File_BufLen 1024
static char File_Buf[File_BufLen];

bool Choices_RemoveSectionFrom(const char* pfilename, const char* psection)
{
	bool volatile  bSaved = false;
	char* volatile pOldContent = NULL;

	try
	{
		FILE* file;

		int volatile TotalSize = 0;
		int volatile HeaderSize = 0;
		int volatile TrailerPos = 0;

		// scan the file for the corresponding id
		if ((file = fopen(pfilename, "rb")) == NULL)
			throw_string(Err_UnableToReadFile);

		try
		{
			int      len = strlen(psection);
			long int restorepos;

			fseek(file, 0, SEEK_END);
			TotalSize = (int) ftell(file);
			// Init for insert at end of file
			HeaderSize = TotalSize;
			TrailerPos = TotalSize;
			// Set position to start of file
			fseek(file, 0, SEEK_SET);

			// Scan file until end of file
			while(true)
			{
				// Try to read a line
				if (fgets(File_Buf, File_BufLen, file) == NULL)
					throw_user(0);

				// Test for start of section marker
				if (File_Buf[0] != '[')
					continue;

				// Test if text following is section
				if (strncmp(File_Buf + 1, psection, len))
					continue;

				// Test for end of section marker
				if (File_Buf[len + 1] == ']')
					break;
			}

			// id found
			restorepos = ftell(file);
			File_FindEndOfSection(file);
			// Init for insert at end of section
			HeaderSize = (int) restorepos - strlen(File_Buf);
			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(Err_UnableToReadFile);
			}
		}
		catch
		{
			fclose(file);
			throw_current();
		}
		catch_end

		fclose(file);

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

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

				if (fflush(file))
					throw_string(Err_UnableToWriteFile);

				fclose(file);

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

				if ((file = fopen(pfilename, "wb+")) != NULL)
				{
					size_t size = fwrite(pOldContent, 1, TotalSize, file);
					fclose(file);
					if (size == TotalSize) throw_current();
				}
				// Choices file has been corrupted
				throw_string(Err_UnableToWriteFile_Corrupted);
			}
			catch_end
		}
		else
		{
			throw_user(0);
		}
	}
	catch
	{
		const exception* e = exception_current();

		// Report only errors not due to write protected file
		if (e->type != exception_user)
			Task_ReportException();
	}
	catch_end

	mem_free(pOldContent);

	return bSaved;
}
