/* 
 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
 *
 * This program 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; version 2 of the
 * License.
 * 
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

/**
 * Configuration file reader/writer.
 */

#include <ctype.h>
#include <stdio.h>
#include <stdarg.h>
#include <float.h>

#include "base/config_file.h"
#include "base/util_functions.h"
#include "base/string_utilities.h"

#ifdef WIN32
#define vsnprintf _vsnprintf
#endif

using namespace std;
using namespace base;

// Maximum size for internal buffers for loading data.
#define MAX_BUFFER_SIZE 512

// What character starts a comment line.
const std::string CommentIndicators = ";#";

// What character separates key and value.
const std::string EqualIndicators = "=:"; 

// White spaces to skip.
const std::string WhiteSpace = " \t\n\r";

typedef struct
{
  std::string key;
  std::string value;
  std::string comment; // Note: comments must be before the value entry in the file.
} ConfigEntry;

typedef std::vector<ConfigEntry> EntryList;
typedef EntryList::iterator EntryListIterator;

typedef struct
{
  std::string name;
  std::string comment;
  EntryList keys;
} ConfigSection;

typedef std::vector<ConfigSection> SectionList;
typedef SectionList::iterator SectionListIterator;

/**
 * Extracts the next key from the next key/value pair in the line and returns it.
 */
std::string extract_next_word(std::string& line)
{
  int position = line.find_first_of(EqualIndicators);
  std::string word = std::string("");
  
  if (position > -1)
  {
    word = line.substr(0, position);
    line.erase(0, position + 1);
  }
  else
  {
    word = line;
    line = "";
  }
  
  return base::trim(word);
}

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

/**
 * Writes the formatted output to the file stream, returning the number of bytes written.
 */
int write_to_stream(fstream& stream, const char* fmt, ...)
{
  char buf[MAX_BUFFER_SIZE];
  int length;
  
  memset(buf, 0, MAX_BUFFER_SIZE);
  va_list args;
  
  va_start(args, fmt);
  length = vsnprintf(buf, MAX_BUFFER_SIZE, fmt, args);
  va_end(args);
  
  if (buf[length] != '\n' && buf[length] != '\r')
    buf[length++] = '\n';
  
  stream.write(buf, length);
  
  return length;
}

//----------------- ConfigurationFile::Private ------------------------------------------------------

class ConfigurationFile::Private
{
  int _flags;
  SectionList _sections;
  bool _dirty;
  
public:
  Private(std::string file_name, ConfigFileFlags flags);
  
  ConfigEntry* get_entry_in_section(std::string key, std::string section);
  ConfigSection*	get_section(std::string section);
  
  bool set_value(std::string key_name, std::string value, std::string comment, std::string section_name);
  void set_dirty();
  void clear();
  
  bool create_key(std::string key, std::string value, std::string comment, std::string section);
  bool delete_key(std::string key, std::string section_name);
  bool create_section(std::string section_name, std::string comment);
  bool delete_section(std::string name);
  
  int section_count();
  int key_count();
  bool is_dirty();
  
  std::string comment_string(std::string comment);
  
  bool load(const std::string& file_name);
  bool save(const std::string& file_name);
  
};

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

ConfigurationFile::Private::Private(std::string file_name, ConfigFileFlags flags)
{
  _dirty = false;
  _flags = flags;
  
  // If a filename was given load the file, otherwise create a default section.
  if (file_name.size() > 0)
    load(file_name);
  else
    _sections.push_back(ConfigSection());
}

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

/**
 * Looks up the given key in the given section and returns its entry if found, otherwise NULL.
 */
ConfigEntry* ConfigurationFile::Private::get_entry_in_section(std::string key, std::string section_name)
{
  ConfigSection* section = get_section(section_name);
  
  if (section == NULL)
    return NULL;
  
  for (EntryListIterator iterator = section->keys.begin(); iterator != section->keys.end(); iterator++)
  {
    if (strcasecmp(iterator->key.c_str(), key.c_str()) == 0)
      return &(*iterator);
  }
  
  return NULL;
}

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

/**
 * Looks up the given section in our list and returns a pointer to it if found. Otherwise NULL
 * is returned.
 */
ConfigSection* ConfigurationFile::Private::get_section(std::string section_name)
{
  for (SectionListIterator iterator = _sections.begin(); iterator != _sections.end(); iterator++)
  {
    if (strcasecmp(iterator->name.c_str(), section_name.c_str()) == 0)
      return &(*iterator);
  }
  
  return NULL;
}

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

bool ConfigurationFile::Private::set_value(std::string key_name, std::string value,
                                           std::string comment, std::string section_name)
{
  ConfigEntry* entry = get_entry_in_section(key_name, section_name);
  ConfigSection* section = get_section(section_name);
  
  if (section == NULL)
  {
    if (!(_flags & AutoCreateSections) || !create_section(section_name, ""))
      return false;
    
    section = get_section(section_name);
  }
  
  // Sanity check...
  if (section == NULL)
    return false;
  
  // Create key if it does not yet exist.
  // Note: we allow to insert empty values for a key too. This allows to write key-only entries to the file.
  if (entry == NULL && (_flags & AutoCreateKeys))
  {
    ConfigEntry new_entry;
    new_entry.key = key_name;
    new_entry.value = value;
    new_entry.comment = comment;
    section->keys.push_back(new_entry);
  }
  
  if (entry != NULL)
  {
    entry->value = value;
    entry->comment = comment;
    
    _dirty = true;
    
    return true;
  }
  
  return false;
}

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

void ConfigurationFile::Private::set_dirty()
{
  _dirty = true;
}

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

void ConfigurationFile::Private::clear()
{
  _dirty = false;
  _sections.clear();
}

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

bool ConfigurationFile::Private::delete_section(std::string name)
{
  for (SectionListIterator iterator = _sections.begin(); iterator != _sections.end(); iterator++)
  {
    if (strcasecmp((*iterator).name.c_str(), name.c_str()) == 0)
    {
      _sections.erase(iterator);
      return true;
    }
  }
  
  return false;
}

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

/**
 * Removes the given key from the given selection. Returns true if successful, otherwise false.
 */
bool ConfigurationFile::Private::delete_key(std::string key, std::string section_name)
{
  ConfigSection* section = get_section(section_name);
  
  if (section == NULL)
    return false;
  
  for (EntryListIterator iterator = section->keys.begin(); iterator != section->keys.end(); iterator++)
  {
    if (strcasecmp(iterator->key.c_str(), key.c_str()) == 0)
    {
      section->keys.erase(iterator);
      return true;
    }
  }
  
  return false;
}

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

/**
 * Given a key, a value and a section, this function will attempt to locate the
 * Key within the given section, and if it finds it, change the keys value to
 * the new value. If it does not locate the key, it will create a new key with
 * the proper value and place it in the section requested.
 */
bool ConfigurationFile::Private::create_key(std::string key, std::string value, std::string comment, std::string section)
{
  int old_flags = _flags;
  bool result  = false;
  
  _flags |= AutoCreateKeys;
  
  result = set_value(key, value, comment, section);
  
  _flags = old_flags;
  
  return result;
}

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

/**
 * Creates a new section if no section with that name exists already. Returns true if a new section
 * has been created, otherwise false.
 */
bool ConfigurationFile::Private::create_section(std::string section_name, std::string comment)
{
  
  if (get_section(section_name) != NULL)
    return false;
  
  ConfigSection section;
  section.name = section_name;
  section.comment = comment;
  _sections.push_back(section);
  _dirty = true;
  
  return true;
}

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

/**
 * Returns the number of sections.
 */
int ConfigurationFile::Private::section_count()
{ 
  return _sections.size(); 
}

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

/**
 * Returns the total number of keys in all sections.
 */
int ConfigurationFile::Private::key_count()
{
  int result = 0;
  
  for (SectionListIterator iterator = _sections.begin(); iterator != _sections.end(); iterator++)
    result += iterator->keys.size();
  
  return result;
}

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

bool ConfigurationFile::Private::is_dirty()
{
  return _dirty;
}

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

/**
 * Converts the given text to a comment string (prefixing it with the comment character) if it is 
 * not empty and does not yet have a comment character at the first position.
 */
std::string ConfigurationFile::Private::comment_string(std::string comment)
{
  std::string result;
  
  comment = base::trim(comment);
  
  if (comment.size() == 0)
    return comment;
  
  if (comment.find_first_of(CommentIndicators) != 0)
  {
    // Add a comment indicator at the first position in the result.
    result = CommentIndicators[0];
    result += " ";
  }
  
  result += comment;
  
  return result;
}

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

bool ConfigurationFile::Private::load(const std::string& file_name)
{
  ifstream file(file_name.c_str());
  
  if (file.is_open())
  {
    bool done = false;
    int old_flags = _flags;
    
    std::string line;
    std::string comment;
    char buffer[MAX_BUFFER_SIZE]; 
    ConfigSection* section = get_section("");
    
    // These need to be set for loading. We'll restore the original values later.
    _flags |= AutoCreateKeys;
    _flags |= AutoCreateSections;
    
    while (!done)
    {
      memset(buffer, 0, MAX_BUFFER_SIZE);
      file.getline(buffer, MAX_BUFFER_SIZE);
      
      line = base::trim(buffer);
      done = file.eof() || file.bad() || file.fail();
      
      // Skip empty lines.
      if (line.size() == 0)
        continue;
      
      if (line.find_first_of(CommentIndicators) == 0)
      {
        comment += "\n";
        comment += line;
      }
      else
        if (line.find_first_of('[') == 0)
        {
          // Start a new section.
          line.erase(0, 1);
          line.erase(line.find_last_of(']'), 1);
          
          create_section(line, comment);
          section = get_section(line);
          comment = "";
        }
        else
          if (line.size() > 0)
          {
            // Normal key/value pair or a single key.
            std::string key = extract_next_word(line);
            std::string value = line;
            
            if (key.size() > 0)
            {
              set_value(key, value, comment, section->name);
              comment = "";
            }
          }
    }
    
    // Restore the original flag values.
    _flags = old_flags;
  }
  else
    return false;
  
  file.close();
  
  return true;
}

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

bool ConfigurationFile::Private::save(const std::string& file_name)
{
  if (file_name.size() == 0)
    return false;
  
  fstream file(file_name.c_str(), ios::out | ios::trunc);
  
  if (file.is_open())
  {
    SectionListIterator section_iterator;
    EntryListIterator entry_iterator;
    ConfigEntry entry;
    
    for (section_iterator = _sections.begin(); section_iterator != _sections.end(); section_iterator++)
    {
      bool wrote_comment = false;
      
      if (section_iterator->comment.size() > 0)
      {
        wrote_comment = true;
        write_to_stream(file, "\n%s", comment_string(section_iterator->comment).c_str());
      }
      
      if (section_iterator->name.size() > 0)
        write_to_stream(file, "%s[%s]", wrote_comment ? "" : "\n", section_iterator->name.c_str());
      
      for (entry_iterator = section_iterator->keys.begin(); entry_iterator != section_iterator->keys.end();
           entry_iterator++)
      {
        if (entry_iterator->key.size() > 0)
        {
          if (entry_iterator->value.size() > 0)
          {
            write_to_stream(file, "%s%s%s%s%c%s",
                            entry_iterator->comment.size() > 0 ? "\n" : "",
                            comment_string(entry_iterator->comment).c_str(),
                            entry_iterator->comment.size() > 0 ? "\n" : "",
                            entry_iterator->key.c_str(),
                            EqualIndicators[0],
                            entry_iterator->value.c_str());
          }
          else
          {
            write_to_stream(file, "%s%s%s%s",
                            entry_iterator->comment.size() > 0 ? "\n" : "",
                            comment_string(entry_iterator->comment).c_str(),
                            entry_iterator->comment.size() > 0 ? "\n" : "",
                            entry_iterator->key.c_str());
          }
        }
      }
    }
    
  }
  else
    return false;
  
  _dirty = false;
  
  file.flush();
  file.close();
  
  return true;
}

//----------------- ConfigurationFile --------------------------------------------------------------

ConfigurationFile::ConfigurationFile(std::string file_name, ConfigFileFlags flags)
{
  data = new Private(file_name, flags);
}

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

ConfigurationFile::ConfigurationFile(ConfigFileFlags flags)
{
  data = new Private("", flags);
}

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

ConfigurationFile::~ConfigurationFile()
{
  delete data;
}

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

void ConfigurationFile::clear()
{
  data->clear();
}

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

bool ConfigurationFile::is_dirty()
{
  return data->is_dirty();
}

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

/**
 * Checks if the a key in a given section exists.
 */
bool ConfigurationFile::has_key(const std::string& key, const std::string& section)
{
  return data->get_entry_in_section(key, section) != NULL;
}

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

bool ConfigurationFile::has_section(const std::string& section_name)
{
  return data->get_section(section_name) != NULL;
}

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

/**
 * Loads the given file and adds its content to the current content. Comments will be retained.
 */
bool ConfigurationFile::load(const std::string& file_name)
{
  return data->load(file_name);
}

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

/**
 * Saves the content to the given file.
 */
bool ConfigurationFile::save(const std::string& file_name)
{
  return data->save(file_name);
}

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

/**
 * Sets the comment for a given entry. Returns true if the entry was found, otherwise false.
 */
bool ConfigurationFile::set_key_comment(std::string key, std::string comment, std::string section_name)
{
  ConfigEntry* entry = data->get_entry_in_section(key, section_name);
  if (entry == NULL)
    return false;
  
  data->set_dirty();
  entry->comment = comment;
  return true;
}

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

/**
 * Sets the comment for a given section. Returns true if the section was found, otherwise false.
 */
bool ConfigurationFile::set_section_comment(std::string section_name, std::string comment)
{
  ConfigSection* section = data->get_section(section_name);
  if (section == NULL)
    return false;
  
  data->set_dirty();
  section->comment = comment;
  return true;
}

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

/**
 * Sets the value for the given key in the given section. If the section does not exist yet and
 * the AutoCreateSections flag is set then it is created first. Similarly for the key (and the
 * AutoCreateKeys flag).
 * Returns true if the value could be set, false otherwise.
 */
bool ConfigurationFile::set_value(std::string key_name, std::string value, std::string comment,
                                  std::string section_name)
{
  return data->set_value(key_name, value, comment, section_name);
  
}

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

/**
 * Sets a float value and comment for a given key in a given section.
 */
bool ConfigurationFile::set_float(std::string key, float value, std::string comment, std::string section)
{
  char buffer[64];
  snprintf(buffer, 64, "%f", value);
  
  return data->set_value(key, buffer, comment, section);
}

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

/**
 * Sets a integer value and comment for a given key in a given section.
 */
bool ConfigurationFile::set_int(std::string key, int value, std::string comment, std::string section)
{
  char buffer[64];
  snprintf(buffer, 64, "%d", value);
  
  return data->set_value(key, buffer, comment, section);
}

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

/**
 * Sets a bool value and comment for a given key in a given section.
 */
bool ConfigurationFile::set_bool(std::string key, bool value, std::string comment, std::string section)
{
  return data->set_value(key, value ?  "True" : "False", comment, section);
}

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

/**
 * Returns the value of the given key as string. An empty string is returned if the key could not be
 * found or is empty.
 */
std::string ConfigurationFile::get_value(std::string key, std::string section) 
{
  ConfigEntry* entry = data->get_entry_in_section(key, section);
  
  return (entry == NULL) ? "" : entry->value;
}

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

/**
 * Same as get_value. Just for convenience.
 */
std::string ConfigurationFile::get_string(std::string key, std::string section)
{
  return get_value(key, section);
}

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

/**
 * Read a value as float.
 */
float ConfigurationFile::get_float(std::string key, std::string section)
{
  std::string value = get_value(key, section);
  
  if (value.size() == 0)
    return FLT_MIN;
  
  return (float) atof(value.c_str());
}

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

/**
 * Reade a value as integer.
 */
int	ConfigurationFile::get_int(std::string key, std::string section)
{
  std::string value = get_value(key, section);
  
  if (value.size() == 0)
    return INT_MIN;
  
  return atoi(value.c_str());
}

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

/**
 * Read a value as boolean.
 */
bool ConfigurationFile::get_bool(std::string key, std::string section)
{
  bool result = false;
  std::string value = get_value(key, section);
  
  if (value.find("1") == 0 || strcasecmp(value.c_str(), "true") == 0 || strcasecmp(value.c_str(), "yes") == 0)
    result = true;
  
  return result;
}

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

/**
 * Removes the given section. Returns true if successful, otherwise false.
 */
bool ConfigurationFile::delete_section(std::string name)
{
  return data->delete_section(name);
}

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

/**
 * Removes the given key from the given selection. Returns true if successful, otherwise false.
 */
bool ConfigurationFile::delete_key(std::string key, std::string section_name)
{
  return data->delete_key(key, section_name);
}

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

/**
 * Given a key, a value and a section, this function will attempt to locate the
 * Key within the given section, and if it finds it, change the keys value to
 * the new value. If it does not locate the key, it will create a new key with
 * the proper value and place it in the section requested.
 */
bool ConfigurationFile::create_key(std::string key, std::string value, std::string comment, std::string section)
{
  return data->create_key(key, value, comment, section);
}

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

/**
 * Creates a new section if no section with that name exists already. Returns true if a new section
 * has been created, otherwise false.
 */
bool ConfigurationFile::create_section(std::string section_name, std::string comment)
{
  return data->create_section(section_name, comment);
}

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

/**
 * Same as the previous function but accepts an additional entry list which is copied to the new
 * section (but only if a new section really has been created).
 
 bool ConfigurationFile::create_section(std::string section_name, std::string comment, EntryList entries)
 {
 if (!create_section(section_name, comment))
 return false;
 
 ConfigSection* section = get_section(section_name);
 
 if (!section)
 return false;
 
 section->name = section_name;
 for (EntryListIterator iterator = entries.begin(); iterator != entries.end(); iterator++)
 {
 ConfigEntry entry;
 entry.comment = iterator->comment;
 entry.key = iterator->key;
 entry.value = iterator->value;
 
 section->keys.push_back(entry);
 }
 
 _dirty = true;
 
 return true;
 }
 */
//--------------------------------------------------------------------------------------------------

/**
 * Returns the number of sections.
 */
int ConfigurationFile::section_count() 
{ 
  return data->section_count();
}

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

/**
 * Returns the total number of keys in all sections.
 */
int ConfigurationFile::key_count()
{
  return data->key_count();
}

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