/**
 ** Supermodel
 ** A Sega Model 3 Arcade Emulator.
 ** Copyright 2011 Bart Trzynadlowski 
 **
 ** This file is part of Supermodel.
 **
 ** Supermodel is free software: you can redistribute it and/or modify it under
 ** the terms of the GNU General Public License as published by the Free 
 ** Software Foundation, either version 3 of the License, or (at your option)
 ** any later version.
 **
 ** Supermodel is distributed in the hope that it will be useful, but WITHOUT
 ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 ** FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 ** more details.
 **
 ** You should have received a copy of the GNU General Public License along
 ** with Supermodel.  If not, see <http://www.gnu.org/licenses/>.
 **/
 
/*
 * INIFile.cpp
 *
 * INI file management. Implementation of the CINIFile class.
 *
 * To-Do List
 * ----------
 * - Add boolean on/off, true/false keywords.
 * - Allow a "default" section to be specified (other than "").
 * - Note that linePtr does not necessarily correspond to actual lines in the
 *   file (newlines should be counted by the tokenizer for that).
 *
 * Grammar
 * -------
 *
 * Section:		'[' Identifier ']'
 * Line:		Identifier '=' Argument
 * Argument:	Number
 *				String
 *
 * Overview
 * --------
 * INI files are linear in nature, divided into sections. Each section has
 * settings associated with it which may be either numerical values or strings.
 * Presently, only unsigned 32-bit integers are supported. 
 *
 * INI files are first opened, then parsed, and finally closed. The parse tree
 * is cleared only during object construction and when the INI file is closed.
 * Default settings may be inserted before a file is opened or parsed. When an
 * INI file is written out, the current INI file on the disk is cleared. If an
 * error occurs during this process, the data will be lost.
 *
 * Blank sections (sections for which no settings are defined) are not stored
 * in the parse tree. Sections are added only when a setting is found in that 
 * section. A default section name can be specified (for settings that are not
 * associated with any section), otherwise an empty string ("") is used.
 */
 
#include "Supermodel.h"
using namespace std;


/******************************************************************************
 Basic Functions
******************************************************************************/

BOOL CINIFile::Write(const char *comment)
{
	BOOL	writeSuccess;
	
	// In order to truncate, we must close and reopen as truncated
	File.close();
	File.clear();	// required to clear EOF flag (open() does not clear)
	File.open(FileName.c_str(),fstream::out|fstream::trunc);
	if (File.fail())
	{
		return FAIL;
	}
	
	// Output comment
	if (comment != NULL)
		File << comment << endl;
		
	// Iterate through all sections sequentially
	for (unsigned i = 0; i < Sections.size(); i++)
	{
		// Output section name
		if (Sections[i].Name != "")	// if null name, don't output section at all
			File << "[ " << Sections[i].Name << " ]" << endl << endl;
		
		// Iterate through all settings within this section
		for (unsigned j = 0; j < Sections[i].Settings.size(); j++)
		{
			// Output setting
			File << Sections[i].Settings[j].Name << " = ";
			if (Sections[i].Settings[j].isNumber)
				File << Sections[i].Settings[j].value << endl;
			else
				File << '\"' << Sections[i].Settings[j].String << '\"' << endl;
		}
		
		// New line
		File << endl;
	}
	
	writeSuccess = File.good()?OKAY:FAIL;
		
	// Close and reopen as read/write
	File.close();
	File.open(FileName.c_str(),fstream::in|fstream::out);
	if (File.fail())
	{
		//printf("unable to re-open %s for reading/writing\n", FileName.c_str());
		return FAIL;
	}
	
	// Report any errors that occurred during writing
	return writeSuccess;
}

BOOL CINIFile::Open(const char *fileNameStr)
{
	FileName = fileNameStr;
	File.open(fileNameStr, fstream::in|fstream::out);
	if (File.fail())
	{
		//printf("unable to open %s\n", fileNameStr);
		return FAIL;
	}
	return OKAY;
}

void CINIFile::Close(void)
{
	File.close();
	Sections.clear();	// clear the parse tree!
}


/******************************************************************************
 Management of Settings
******************************************************************************/

// Finds index of section in the section list. Returns FAIL if not found.
BOOL CINIFile::LookUpSection(unsigned *idx, string SectionName)
{
	for (unsigned i = 0; i < Sections.size(); i++)
	{
		if (Sections[i].Name == SectionName)
		{
			*idx = i;
			return OKAY;
		}
	}
	return FAIL;
}

// Finds index of the setting in the given section. Returns FAIL if not found.
BOOL CINIFile::LookUpSetting(unsigned *idx, unsigned sectionIdx, string SettingName)
{
	for (unsigned i = 0; i < Sections[sectionIdx].Settings.size(); i++)
	{
		if (Sections[sectionIdx].Settings[i].Name == SettingName)
		{
			*idx = i;
			return OKAY;
		}
	}
	return FAIL;
}


// Assigns a value to the given setting, creating the setting if it does not exist. Nulls out the string (sets it to "").
void CINIFile::Set(string SectionName, string SettingName, int value)
{
	unsigned	sectionIdx, settingIdx;
	
	if (OKAY != LookUpSection(&sectionIdx, SectionName))
	{
		//printf("unable to find %s:%s, creating section\n", SectionName.c_str(), SettingName.c_str());
		struct Section	NewSection;
		
		NewSection.Name = SectionName;
		Sections.push_back(NewSection);
		sectionIdx = Sections.size()-1;	// the new section will be at the last index
	}
	
	if (OKAY != LookUpSetting(&settingIdx, sectionIdx, SettingName))
	{
		//printf("unable to find %s:%s, creating setting\n", SectionName.c_str(), SettingName.c_str());
		struct Setting	NewSetting;
		
		NewSetting.Name = SettingName;
		Sections[sectionIdx].Settings.push_back(NewSetting);
		settingIdx = Sections[sectionIdx].Settings.size()-1;
	}
	
	// Update value of this setting
	Sections[sectionIdx].Settings[settingIdx].isNumber = TRUE;
	Sections[sectionIdx].Settings[settingIdx].value = value;	
	Sections[sectionIdx].Settings[settingIdx].String = "";
}

// Assigns the string to the given setting, creating the setting if it does not exist. Zeros out the value.
void CINIFile::Set(string SectionName, string SettingName, string String)
{
	unsigned	sectionIdx, settingIdx;
	
	if (OKAY != LookUpSection(&sectionIdx, SectionName))
	{
		//printf("unable to find %s:%s, creating section\n", SectionName.c_str(), SettingName.c_str());
		struct Section	NewSection;
		
		NewSection.Name = SectionName;
		Sections.push_back(NewSection);
		sectionIdx = Sections.size()-1;	// the new section will be at the last index
	}
	
	if (OKAY != LookUpSetting(&settingIdx, sectionIdx, SettingName))
	{
		//printf("unable to find %s:%s, creating setting\n", SectionName.c_str(), SettingName.c_str());
		struct Setting	NewSetting;
		
		NewSetting.Name = SettingName;
		Sections[sectionIdx].Settings.push_back(NewSetting);
		settingIdx = Sections[sectionIdx].Settings.size()-1;
	}
	
	// Update string
	Sections[sectionIdx].Settings[settingIdx].isNumber = FALSE;
	Sections[sectionIdx].Settings[settingIdx].String = String;
	Sections[sectionIdx].Settings[settingIdx].value = 0;
}

// Obtains a numerical setting, if it exists, otherwise does nothing.
BOOL CINIFile::Get(string SectionName, string SettingName, int& value)
{
	unsigned	sectionIdx, settingIdx;
	
	if (OKAY != LookUpSection(&sectionIdx, SectionName))
		return FAIL;
	if (OKAY != LookUpSetting(&settingIdx, sectionIdx, SettingName))
		return FAIL;
	
	value = Sections[sectionIdx].Settings[settingIdx].value;
	
	return OKAY;
}

BOOL CINIFile::Get(string SectionName, string SettingName, unsigned& value)
{
	int intVal;
	if (Get(SectionName, SettingName, intVal) == FAIL || intVal < 0)
		return FAIL;

	value = (unsigned)intVal;

	return OKAY;
}

// Obtains a string setting, if it exists, otherwise does nothing.
BOOL CINIFile::Get(string SectionName, string SettingName, string& String)
{
	unsigned	sectionIdx, settingIdx;
	
	if (OKAY != LookUpSection(&sectionIdx, SectionName))
		return FAIL;
	if (OKAY != LookUpSetting(&settingIdx, sectionIdx, SettingName))
		return FAIL;
	
	String = Sections[sectionIdx].Settings[settingIdx].String;
	
	return OKAY;
}


/******************************************************************************
 Tokenizer
 
 Never need to check for newlines because it is assumed these have been
 stripped by getline().
******************************************************************************/

// Token types
#define TOKEN_INVALID		-1
#define TOKEN_NULL			0
#define TOKEN_IDENTIFIER	1
#define TOKEN_NUMBER		2
#define TOKEN_STRING		3


// Token constructor (initializes token to null)
CINIFile::CToken::CToken(void)
{
	type = TOKEN_NULL;
}

// Returns true for white space, comment symbol, or null terminator.
static BOOL IsBlank(char c)
{
	if (isspace(c) || (c==';') || (c=='\0'))
		return TRUE;
	return FALSE;
}

// Fetches a string. Tolerates all characters between quotes.
CINIFile::CToken CINIFile::GetString(void)
{
	CToken	T;
	
	T.type = TOKEN_STRING;
	
	// Search for next quote
	++linePtr;
	while (1)
	{
		if (linePtr[0] == '\"')
		{
			++linePtr;	// so we can find next token
			break;
		}
		else if (linePtr[0] == '\0')
		{
			//printf("tokenizer: warning: string is missing end quote\n");
			break;
		}
		else
			T.String += linePtr[0];
		
		++linePtr;
	}
	
	return T;
}			

// Fetch number (decimal or hexadecimal positive/negative integer).
// linePtr must point to a character and therefore linePtr[1] is guaranteed to be within bounds.
CINIFile::CToken CINIFile::GetNumber(void)
{
	CToken	T;
	unsigned long long	number = 0;
	BOOL                isNeg = FALSE;
	int					overflow = 0;
	
	T.type = TOKEN_NUMBER;
	
	// See if begins with minus sign 
	if (linePtr[0]=='-')
	{
		isNeg = TRUE;
		linePtr++;
	}

	// Hexadecimal?
	if ((linePtr[0]=='0') && ((linePtr[1]=='X') || (linePtr[1]=='x')))
	{
		linePtr += 2;	// advance to digits
		
		// Ensure that at we have at least one digit
		if (!isxdigit(linePtr[0]))
		{
			//printf("tokenizer: invalid hexadecimal number\n");
			T.type = TOKEN_INVALID;
			return T;
		}
			
		// Read number digit by digit
		while (1)
		{
			if (isxdigit(linePtr[0]))
			{
				number <<= 4;
				if (isdigit(linePtr[0]))
					number |= (linePtr[0]-'0');
				else if (isupper(linePtr[0]))
					number |= (linePtr[0]-'A');
				else	// must be lowercase...
					number |= (linePtr[0]-'a');
				++linePtr;
				
				// Check for overflows
				if (!isNeg && number > 0x000000007FFFFFFFULL || isNeg && number > 0x0000000080000000ULL)
					overflow = 1;
			}
			else if (IsBlank(linePtr[0]))
				break;
			else
			{
				//printf("tokenizer: invalid hexadecimal number\n");
				T.type = TOKEN_INVALID;
				return T;
			}
		}
	}
	
	// Decimal?
	else
	{
		// Read number digit by digit
		while (1)
		{
			if (isdigit(linePtr[0]))
			{
				number *= 10;
				number += (linePtr[0]-'0');
				++linePtr;
				
				// Check for overflows
				if (!isNeg && number > 0x000000007FFFFFFFULL || isNeg && number > 0x0000000080000000ULL)
					overflow = 1;
			}
			else if (IsBlank(linePtr[0]))
				break;
			else
			{
				//printf("tokenizer: invalid number\n");
				T.type = TOKEN_INVALID;
				return T;
			}
		}
	}
	
	//if (overflow)
	//	printf("tokenizer: number exceeds 32 bits and has been truncated\n");
	
	T.number = (isNeg ? -(int)number : (int)number);
	return T;
}

// Fetch identifier
CINIFile::CToken CINIFile::GetIdentifier(void)
{
	CToken	T;
	
	T.type = TOKEN_IDENTIFIER;
	while (1)
	{
		if (isalpha(linePtr[0]) || isdigit(linePtr[0]) || (linePtr[0]=='_'))
		{
			T.String += linePtr[0];
			++linePtr;
		}
		else
			break;
	}
	
	return T;
}

// Fetch token
CINIFile::CToken CINIFile::GetToken(void)
{
	CToken	T;
	
	while (1)
	{
		// Gobble up whitespace
		if (isspace(linePtr[0]))
			++linePtr;
		
		// Comment or end of line
		else if ((linePtr[0]==';') || (linePtr[0]=='\0'))
		{
			T.type = TOKEN_NULL;
			return T;
		}
			
		// Delimiters
		else if ((linePtr[0]=='[') || (linePtr[0]==']') || (linePtr[0]=='='))
		{
			T.type = *linePtr++;
			return T;
		}
		
		// Identifier?
		else if (isalpha(linePtr[0]) || (linePtr[0] == '_'))
		{
			T = GetIdentifier();
			return T;
		}
		
		// Number? (+/-?)
		else if (isdigit(linePtr[0]))
		{
			T = GetNumber();
			return T;
		}
		
		// String?
		else if (linePtr[0]=='\"')
		{
			T = GetString();
			return T;
		}
		
		// Illegal symbol
		else
			break;	// return null token
	}
	
	// If we got here, invalid token
	T.type = TOKEN_INVALID;
	return T;
}


/******************************************************************************
 Parser
******************************************************************************/

BOOL CINIFile::Parse(void)
{
	CToken	T, U, V, W;
	string	currentSection;	// current section we're processing
	BOOL	parseStatus = OKAY;
	
	//currentSection = defaultSection;	// default "global" section
	lineNum = 0;
	
	if (!File.is_open())
		return FAIL;
	File.clear();
	if (!File.good())
		return FAIL;
	
	while (!File.eof())
	{
		++lineNum;
		File.getline(lineBuf,2048);
		if (File.fail())
			return FAIL;
		linePtr = lineBuf;	// beginning of line
		
		// Top level
		T = GetToken();
		U = GetToken();
		V = GetToken();
		W = GetToken();	// should always be null
		switch (T.type)
		{
		// [ Identifier ]
		case '[':
			if ((U.type==TOKEN_IDENTIFIER) && (V.type==']') && (W.type==TOKEN_NULL))
			{
				//printf("Section: %s\n", U.String.c_str());
				currentSection = U.String;
			}
			else
			{
				parseStatus = FAIL;
				//printf("%d: parse error\n", lineNum);
			}
			break;
		
		// Identifier '=' Argument
		case TOKEN_IDENTIFIER:
			if (U.type != '=')
			{
				parseStatus = FAIL;
				//printf("%d: expected '=' after identifier\n", lineNum);
			}
			else
			{
				if (((V.type==TOKEN_NUMBER) || (V.type==TOKEN_STRING)) && (W.type==TOKEN_NULL))
				{
					if (V.type == TOKEN_NUMBER)
					{
						//printf("\t%s = %X\n", T.String.c_str(), V.number);
						Set(currentSection, T.String, V.number);
					}
					else if (V.type == TOKEN_STRING)
					{
						//printf("\t%s = %s\n", T.String.c_str(), V.String.c_str());
						Set(currentSection, T.String, V.String);
					}
				}
				else
				{
					parseStatus = FAIL;
					//printf("%d: expected a number or string after '='\n", lineNum);
				}
			}
			break;
		
		// Blank line
		case TOKEN_NULL:
			break;
		
		// Illegal
		default:
			parseStatus = FAIL;
			//printf("%d: parse error\n", lineNum);
			break;
		}
	}	
	
	//printf("end of file reached\n");
	return parseStatus;
}