/**
 ** Supermodel
 ** A Sega Model 3 Arcade Emulator.
 ** Copyright 2011 Bart Trzynadlowski, Nik Henson 
 **
 ** 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 an iterator to retrieve all settings associated with a given section.
 *   This will allow detection of invalid setting names, if the caller desires.
 * - Add boolean on/off, true/false keywords.
 * - 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.
 *
 * Sections are added only when a setting is found in that section. Empty
 * sections are not added to the tree with the exception of the default ("")
 * section. A default section name can be specified, which creates an alias
 * for the default section. The alias will be output when the file is
 * written (there will be no settings without an explicit section).
 */
 
#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++)
  {
    if (Sections[i].Settings.size() != 0)
    {
      // Output section name
      if (Sections[i].Name != "")
        File << "[ " << Sections[i].Name << " ]" << endl << endl;
      else  // if null name, use default section name (if exists) or nothing at all
      {
        if (DefaultSectionName != "")
          File << "[ " << DefaultSectionName << " ]" << 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;

  // Try to open for reading AND writing
  File.open(fileNameStr, fstream::in|fstream::out);
  if (File.fail())
    return FAIL;
    
  InitParseTree();
  return OKAY;
}

bool CINIFile::OpenAndCreate(const char *fileNameStr)
{
  FileName = fileNameStr;

  // Try to open for reading and writing
  File.open(fileNameStr, fstream::in|fstream::out);
  if (File.fail())
  {
    // File does not exist, try opening as write only (create it)
    File.clear();
    File.open(fileNameStr, fstream::out);
    if (File.fail())
      return FAIL;
  }
  
  InitParseTree();
  return OKAY;
}

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


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

// Finds index of first matching 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) ||
      ((Sections[i].Name == "") && (SectionName == DefaultSectionName)) ||  // if default section, also accept its alias
      ((Sections[i].Name == DefaultSectionName) && (SectionName == "")))    // ...
    {
      *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)
{
  struct Setting  NewSetting;
  unsigned    sectionIdx, settingIdx;
  
  // Check if the section exists anywhere in parse tree. If not, create it
  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
  }
  
  // Search through all sections with the requested name for the first occurance of the desired setting
  for (unsigned i = 0; i < Sections.size(); i++)
  {
    if ((Sections[i].Name == SectionName) ||
      ((Sections[i].Name == "") && (SectionName == DefaultSectionName)) ||  // accept alias for default section
      ((Sections[i].Name == DefaultSectionName) && (SectionName == "")))    // ...
    {
      for (unsigned j = 0; j < Sections[i].Settings.size(); j++)
      {
        if (Sections[i].Settings[j].Name == SettingName)
        {
          // Found it! Update value of this setting
          sectionIdx = i;
          settingIdx = j;
          goto UpdateIntValue;
        }
      }
    }
  }
  
  // Couldn't find setting, create it in the first matching section found earlier 
  NewSetting.Name = SettingName;
  Sections[sectionIdx].Settings.push_back(NewSetting);
  settingIdx = Sections[sectionIdx].Settings.size()-1;

  // Update the setting!  
UpdateIntValue:
  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)
{
  struct Setting  NewSetting;
  unsigned    sectionIdx, settingIdx;
  
  // Check if the section exists anywhere in parse tree. If not, create it
  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
  }
  
  // Search through all sections with the requested name for the first occurance of the desired setting
  for (unsigned i = 0; i < Sections.size(); i++)
  {
    if ((Sections[i].Name == SectionName) ||
      ((Sections[i].Name == "") && (SectionName == DefaultSectionName)) ||  // accept alias for default section
      ((Sections[i].Name == DefaultSectionName) && (SectionName == "")))    // ...
    {
      for (unsigned j = 0; j < Sections[i].Settings.size(); j++)
      {
        if (Sections[i].Settings[j].Name == SettingName)
        {
          // Found it! Update value of this setting
          sectionIdx = i;
          settingIdx = j;
          goto UpdateString;
        }
      }
    }
  }
  
  // Couldn't find setting, create it in the first matching section found earlier
  NewSetting.Name = SettingName;
  Sections[sectionIdx].Settings.push_back(NewSetting);
  settingIdx = Sections[sectionIdx].Settings.size()-1;

  // Update the setting!  
UpdateString:
  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)
{
  for (unsigned i = 0; i < Sections.size(); i++)
  {
    if ((Sections[i].Name == SectionName) ||
      ((Sections[i].Name == "") && (SectionName == DefaultSectionName)) ||  // accept alias for default section
      ((Sections[i].Name == DefaultSectionName) && (SectionName == "")))    // ...
    {
      for (unsigned j = 0; j < Sections[i].Settings.size(); j++)
      {
        if (Sections[i].Settings[j].Name == SettingName)
        {
          value = Sections[i].Settings[j].value;
          return OKAY;
        }
      }
    }
  }

  return FAIL;
}

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

bool CINIFile::Get(string SectionName, string SettingName, bool& value)
{
  int intVal;
  if (Get(SectionName, SettingName, intVal) == FAIL)
    return FAIL;

  value = intVal > 0;

  return OKAY;
}

// Obtains a string setting, if it exists, otherwise does nothing.
bool CINIFile::Get(string SectionName, string SettingName, string& String)
{
  for (unsigned i = 0; i < Sections.size(); i++)
  {
    if ((Sections[i].Name == SectionName) ||
      ((Sections[i].Name == "") && (SectionName == DefaultSectionName)) ||  // accept alias for default section
      ((Sections[i].Name == DefaultSectionName) && (SectionName == "")))    // ...
    {
      for (unsigned j = 0; j < Sections[i].Settings.size(); j++)
      {
        if (Sections[i].Settings[j].Name == SettingName)
        {
          String = Sections[i].Settings[j].String;
          return OKAY;
        }
      }
    }
  }

  return FAIL;
}

void CINIFile::SetDefaultSectionName(string SectionName)
{
  DefaultSectionName = SectionName;
}


/******************************************************************************
 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, except \n.
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') || (linePtr[0] == '\n'))
    {
      //printf("tokenizer: warning: string is missing end quote\n");
      break;
    }
    else
      T.String += linePtr[0];
    
    ++linePtr;
  }
  
  return T;
}

//static bool IsOverflown(unsigned long long number, bool isNeg)
//{
//  return (!isNeg && number > 0x000000007FFFFFFFULL) || (isNeg && number > 0x0000000080000000ULL);
//}

// 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 (IsOverflown(number, isNeg))
        //  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 (IsOverflown(number, isNeg))
        //  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; // do not advance linePtr (so we do not de-sync the parser)
    }
      
    // 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 (linePtr[0]=='-' || 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
******************************************************************************/

/*
 * Parse tree is initialized with a blank section in case the user adds 
 * settings with defined sections before adding settings without an explicit
 * section. If this is not done, settings without a section will be wrongly
 * output as part of the previous section in the parse tree.
 */
void CINIFile::InitParseTree(void)
{
  struct Section  FirstSection;
    
  FirstSection.Name = "";
  Sections.clear();
  Sections.push_back(FirstSection);
}

bool CINIFile::Parse(void)
{
  CToken  T, U, V, W;
  string  currentSection; // current section we're processing
  bool  parseStatus = OKAY;
  
  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;
}