/** @file

    $Id$
    
    $Date$
    $Author$

    @copy &copy; Marc W&auml;ckerlin
    @license LGPL, see file <a href="license.html">COPYING</a>

    $Log$
    Revision 1.3  2005/11/29 12:39:42  marc
    make it compilable with gcc 4.0.2 and newer doxygen

    Revision 1.2  2005/01/28 07:49:32  marc
    Save configuration using file.hpp

    Revision 1.1  2005/01/07 00:31:38  marc
    initial version


         1         2         3         4         5         6         7         8
    5678901234567890123456789012345678901234567890123456789012345678901234567890
*/

#include <mrw/configfile.hpp>
#include <mrw/exception.hpp>
#include <mrw/string.hpp>
#include <mrw/file.hpp>
#include <fstream>

mrw::ConfigFileReader::Value&
  mrw::ConfigFileReader::operator()(const std::string& section,
                                    const std::string& name,
                                    const std::string& default_)
  throw(std::bad_exception) {
  _values[section].insert(std::make_pair(name, Value(default_)));
  return _values[section].find(name)->second;
}

mrw::ConfigFileReader& mrw::ConfigFileReader::load(const std::string& filename)
  throw(std::exception) {
  _filename = filename;
  return reload();
}

mrw::ConfigFileReader& mrw::ConfigFileReader::reload() throw(std::exception) {
  return parse(mrw::File::read(_filename));
}

mrw::ConfigFileReader& mrw::ConfigFileReader::parse(const std::string& file)
  throw(std::exception) {
  _values.erase(_values.begin(), _values.end());
  std::string section;
  for (std::string::size_type pos(0);
       (pos=file.find_first_of("[=#", pos))!=std::string::npos; ++pos) {
    switch (file[pos]) {
    case '[': _sections[section]=pos; section=parseSection(file, pos); break;
    case '=': _values[section].insert(parseValue(file, pos)); break;
    case '#': parseComment(file, pos); break;
    };
  }
  _sections[section]=file.size();
  return *this;
}

std::string mrw::ConfigFileReader::parseSection(const std::string& file,
                                                std::string::size_type& pos)
  const throw(std::exception) {
  std::string::size_type start(pos);
  if ((pos=file.find(']', pos))==std::string::npos)
    throw mrw::invalid_argument
      ("section not terminated in file '"+_filename+"'\n"
       "environment is: "+file.substr(start-10>0?start-10:0));
  return file.substr(start+1, pos-start-1);
}

mrw::ConfigFileReader::Values::mapped_type::value_type
mrw::ConfigFileReader::parseValue(const std::string& file,
                                  std::string::size_type& pos)
  const throw(std::exception) {
  // find the begin of the variable name
  std::string::size_type startName(file.rfind('\n', pos));
  if (startName==std::string::npos) startName=0;
  startName=file.find_first_not_of("\n\t ", startName);
  if (startName==pos)
    throw mrw::invalid_argument
      ("empty variable name in file '"+_filename+"'\n"
       "environment is: "+file.substr(startName-10>0?startName-10:0, pos+10));
  // find the end of the variable name
  std::string::size_type endName(file.find_last_not_of("\n\t ", pos-1)+1);
  // find the begin of the variable contents
  std::string::size_type startVal(file.find_first_not_of("\n\t ", ++pos));
  if (startVal==std::string::npos)
    return std::make_pair // end of file, empty value
      (file.substr(startName, endName-startName), Value("", pos, pos));
  // find the end of the variable contents
  std::string::size_type realStart(startVal); // doesn't cut the first quote
  std::string::size_type endVal(file[startVal]=='"'         ? // quoted with "
                                file.find('"', ++startVal)  :
                                file[startVal]=='\''        ? // quoted with '
                                file.find('\'', ++startVal) :
                                file.find_first_of("[=#", startVal)); // normal
  if (endVal!=std::string::npos && file[endVal]=='=')
    endVal=file.rfind('\n', endVal); // jump before following variable name
  endVal = file.find_last_not_of("\n\t ", // remove trailing white spaces
                                  endVal!=std::string::npos?endVal-1:endVal)+1;
  std::string::size_type realEnd(endVal!=std::string::npos && // quotes?
                                 (file[realStart]=='"' && file[endVal]=='"' ||
                                  file[realStart]=='\'' && file[endVal]=='\'')
                                 ? endVal+1 : endVal);
  if (endVal<=startVal)
    return std::make_pair // empty value
      (file.substr(startName, endName-startName), Value("", pos, pos));
  return std::make_pair // normal case
    (file.substr(startName, endName-startName),
     Value(file.substr(startVal, endVal-startVal), realStart, pos=realEnd));
}

std::string mrw::ConfigFileReader::parseComment(const std::string& file,
                                                std::string::size_type& pos)
  const throw(std::bad_exception) {
  std::string::size_type start(pos);
  pos = file.find('\n', pos);
  return file.substr(start, pos-start);
}

// GNU g++ prior to 3.4 does not implement covariant returns
#if __GNUC__ == 3 && __GNUC_MINOR__ < 4
mrw::ConfigFileReader&
#else
mrw::ConfigFileWriter&
#endif
mrw::ConfigFileWriter::reload() throw(std::exception) {
#if __GNUC__ == 3 && __GNUC_MINOR__ < 4
  return parse(_file=mrw::File::read(_filename));
#else
  return dynamic_cast<mrw::ConfigFileWriter&>
    (parse(_file=mrw::File::read(_filename)));
#endif
}

mrw::ConfigFileWriter& mrw::ConfigFileWriter::save() throw(std::exception) {
  if (_filename=="") return *this;
  // work in changes
  bool changed(false);
  for (Values::iterator sec(_values.begin()); sec!=_values.end(); ++sec)
    for (Values::mapped_type::iterator var(sec->second.begin());
         var!=sec->second.end(); ++var)
      if (var->second.changed) {
        var->second.changed = false;
        changed = true;
        // check if we need quoting
        std::string value(var->second.value);
        if (value.find_first_of("[=#")!=std::string::npos)
          if (value.find('"')==std::string::npos)
            value = '"'+value+'"';
          else if (value.find('\'')==std::string::npos)
            value = '\''+value+'\'';
          else
            throw mrw::invalid_argument("Configuration value is not quotable: "
                                        +value);
        // recalculate all positions
        if (var->second.start &&
            value.size() != var->second.end-var->second.start) {
          int diff(value.size()-var->second.end+var->second.start);
          for (Values::iterator sec2(_values.begin());
               sec2!=_values.end(); ++sec2)
            for (Values::mapped_type::iterator var2(sec2->second.begin());
                 var2!=sec2->second.end(); ++var2)
              if (var2->second.start>var->second.end) {
                var2->second.start += diff;
                var2->second.end += diff;
              }
          for (Sections::iterator sec2(_sections.begin());
               sec2!=_sections.end(); ++sec2)
            if (sec2->second>=var->second.end) sec2->second += diff;
        }
        // set the new value
        if (var->second.start) // simple change
          _file.replace(var->second.start, var->second.end-var->second.start,
                        value);
        else if (_sections.find(sec->first)!=_sections.end()) // new value
          _file.insert(_sections[sec->first], "\n"+var->first+" = "+value
                       +(_file[_sections[sec->first]]=='\n'?"":"\n"));
        else { // even a new section
          _file+="\n["+sec->first+"]\n"+var->first+" = "+value;
          _sections[sec->first] = _file.size();
        }
      }
  if (changed) mrw::File::save(_filename, _file);
  return *this;
}
