/* This file is part of cardwords
   (c) 1999 Tobias Peters
   see file COPYING for the copyright terms.
   
   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; either version 2 of the License, or
   (at your option) any later version.

   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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

// cardwords_options.cc

// CardWords_Options is a class that parses an options file, provides options,
// lets options being set, and can save them in an options file.

#include "cardwords_options.hh"
#include <fstream.h>
#include <strstream.h>
#include <string.h>
#include "cardwords_gettext.hh"
#include <string> // Getting rid of "cardwords_machinestring.hh".
#include "cardwords_exceptions.hh"
#include "cardwords_max.hh"
#include "cardwords_stringconverters.hh"

// #define DEBUG_CARDWORDS_OPTIONS

CardWords_Options::CardWords_Options(const string & fname)
{
  saved = true;
  if (fname.length() != 0) {
    read_file (fname);
  }
}
const string &
CardWords_Options::get_filename(void) const
{
  return filename;
}
void
CardWords_Options::read_file(const string & fname)
{
  assert (fname.length() != 0);
  ifstream file(fname.c_str());
  string line;
  string type;
  string option;
  string value;
  
  bool was_empty = (options.size() == 0);
  
  while (file) {
    getline(file, line, '\n');

    if (line == "" // empty line
        || line[0] == '#' // comment
        ) {
      continue;
    }
    size_t equal_sign_pos = line.find('=');
    if (equal_sign_pos >= line.length()) {
      throw CardWords_XBadInput(gettext("Options file"),
                                gettext("`=' character"));
    }

    // assign to value the part of the line after the `='.
    value.assign(line,
                 equal_sign_pos + 1,
                 line.length() - (equal_sign_pos + 1) );

    size_t type_begin = line.find_first_not_of(" \t", 0);

    assert (type_begin <= equal_sign_pos);
    if (type_begin == equal_sign_pos) {
      throw CardWords_XBadInput(gettext("Options file"),
                                gettext("option type-name"));
    }
    
    size_t type_end   = line.find_first_of(" \t", type_begin);

    type.assign(line, type_begin, type_end - type_begin);
    if (type != "string" && type != "unsigned" && type != "bool") {
      // wrong type
      throw CardWords_XBadInput(gettext("Options file: wrong type-name"),
                                gettext("valid type-name"));
    }

    size_t option_begin = line.find_first_not_of(" \t", type_end);
    assert (option_begin <= equal_sign_pos);
    if (option_begin == equal_sign_pos) {
      throw CardWords_XBadInput(gettext("Options file"),
                                gettext("name of the option"));
    }
    
    size_t option_end   = cardwords_min(line.find_first_of(" \t",
                                                           option_begin),
                                        equal_sign_pos);

    option.assign(line, option_begin, option_end - option_begin);

    options_iterator iter;

    if (type == "string") {
      iter = options.find(option);
      if (iter != options.end()) {
        if (iter->second.set(value) // use all space and tab chars
            == -1) {
          cerr << gettext("WARNING: CardWords_Options::read_file(): "\
                          "Attempt to change the "\
                          "option type of option `")
               << option
               << gettext("' while reading the options file `")
               << fname
               << gettext("'. Ignoring this attempt.")
               << endl;
          continue;
        }
      }
      else {
        options.insert(Value_Type(option, Data_Type(value)));
      }
    }
    else if (type == "bool") {
      string mangled_value; // value without whitespace
      size_t value_begin = value.find_first_not_of(" \t", 0);

      size_t value_end;
      bool bool_value;
      
      if (value_begin >= value.length()) {
        goto INVALID_BOOL_VALUE;
      }

      value_end = cardwords_min(value.find_first_of(" \t", value_begin),
                                value.length());

      mangled_value.assign(value, value_begin, value_end - value_begin);

      assert (mangled_value.length() > 0);

      if (mangled_value[0] == 't'
          || mangled_value[0] == 'T'
          || mangled_value == "1") {
        bool_value = true;
      }
      else if (mangled_value[0] == 'f'
               || mangled_value[0] == 'F'
               || mangled_value == "0") {
        bool_value = false;
      }
      else {
      INVALID_BOOL_VALUE:
        throw CardWords_XBadInput(value.c_str(),
                                  gettext("bool value in options file"));
      }
      
      iter = options.find(option);
      if (iter != options.end()) {
        if(iter->second.set(bool_value) == -1) {
          // type mismatch
          cerr << gettext("WARNING: CardWords_Options::read_file(): "\
                          "Attempt to change the "\
                          "option type of option `")
               << option
               << gettext("' while reading the options file `")
               << filename
               << gettext("'. Ignoring this attempt.")
               << endl;
          continue;
        }
      }
      else {
        options.insert(Value_Type(option, Data_Type(bool_value)));
      }
    }
    else if (type == "unsigned") {
      unsigned unsigned_value = string2unsigned(value);

      iter = options.find(option);
      if (iter != options.end()) {
        if(iter->second.set(unsigned_value) == -1) {
          // type mismatch
          cerr << gettext("WARNING: CardWords_Options::read_file(): "\
                          "Attempt to change the "\
                          "option type of option `")
               << option
               << gettext("' while reading the options file `")
               << filename
               << gettext("'. Ignoring this attempt.")
               << endl;
          continue;
        }
      }
      else {
        options.insert(Value_Type(option,
                                  Data_Type(unsigned_value)));
      }
    }
  }

  if (filename == "") {
    filename = fname;
  }
  if (was_empty == true) {
    saved = true;
  }
}
CardWords_Options::~CardWords_Options()
{
}
    
// save returns 0 on success, -1 on error:
int
CardWords_Options::save(const string & fname)
{
  if (fname != "") {
    filename = fname;
  }
  if (is_saved() == false) {
    if (filename == "") {
      return -1;
    }
    ofstream file(filename.c_str(), ios::out | ios::trunc);

    if (file.good()) {
      if (print(file).good()) {
        saved = true;
        return 0;
      }
    }
    return -1;
  }
  return 0;
}
// print prints all current options to the given ostream. used for
// debugging and save():
ostream &
CardWords_Options::print(ostream &ostr = cout) const
{
  map<string, Data_Type>::const_iterator iter;

  for (iter = options.begin();
       iter != options.end();
       ++iter) {
    switch ((*iter).second.get_type()) {
    case String_Type:
      {
        string string_value;
        int return_value = (*iter).second.get(&string_value);
        assert(return_value == 0);

        ostr << "string " << (*iter).first << " =" << string_value << endl;
      }
      break;
    case Bool_Type:
      {
        bool bool_value;
        int return_value = (*iter).second.get(&bool_value);
        assert(return_value == 0);

        ostr << "bool " << (*iter).first << " = "
             << (bool_value ? "true" : "false") << endl;
      }
      break;
    case Unsigned_Type:
      {
        unsigned unsigned_value;
        int return_value = (*iter).second.get(&unsigned_value);
        assert(return_value == 0);

        ostr << "unsigned " << (*iter).first << " = "
             << unsigned_value << endl;
      }
      break;
    default:
      assert(0);
    }
  }
  return ostr;
}


// if the current options are saved:
bool
CardWords_Options::is_saved(void) const
{
  return saved;
}

// Set an option: returns 0 on success, returns -1 in case of a type
// mismatch, that is, an option with this name was declared before,
// but had a value of a different type.
int
CardWords_Options::set(const string & option_name,
                       const string & string_value)
{
  options_iterator iter = options.find(option_name);

  if (iter != options.end()) {
    if (iter->second.get_type() != Unknown_Type
        && iter->second.get_type() != String_Type) {
      return -1;
    }
    if (iter->second.set(string_value) == 0) {
      saved = false;
    }
  }
  else {
    options.insert(Value_Type(option_name, Data_Type(string_value)));
    saved = false;
  }
  return 0;
}

int
CardWords_Options::set(const string & option_name, bool bool_value)
{
  options_iterator iter = options.find(option_name);

  if (iter != options.end()) {
    if (iter->second.get_type() != Unknown_Type
        && iter->second.get_type() != Bool_Type) {
      return -1;
    }
    if (iter->second.set(bool_value) == 0) {
      saved = false;
    }
  }
  else {
    options.insert(Value_Type(option_name, Data_Type(bool_value)));
    saved = false;
  }
  return 0;
}

int
CardWords_Options::set(const string & option_name, unsigned unsigned_value)
{
  options_iterator iter = options.find(option_name);

  if (iter != options.end()) {
    if (iter->second.get_type() != Unknown_Type
        && iter->second.get_type() != Unsigned_Type) {
      return -1;
    }
    if (iter->second.set(unsigned_value) == 0) {
      saved = false;
    }
  }
  else {
    options.insert(Value_Type(option_name, Data_Type(unsigned_value)));
    saved = false;
  }
  return 0;
}

// Return the type of an option:
enum CardWords_Options::Type
CardWords_Options::get_type(const string & option_name) const
{
  options_const_iterator iter = options.find(option_name);
  if (iter == options.end()) {
    return Unknown_Type;
  }
  return iter->second.get_type();
}

// Accessing the options: returns 0 on success, -1 on failure
int
CardWords_Options::get(const string & option_name,
                       string * string_value) const
{
  assert(string_value != 0);
  return (get(option_name, string_value, 0, 0) == String_Type
          ? 0
          : -1);
}
int
CardWords_Options::get(const string & option_name, bool * bool_value) const
{
  assert(bool_value != 0);
  return (get(option_name, 0, bool_value, 0) == Bool_Type
          ? 0
          : -1);
}

int
CardWords_Options::get(const string & option_name,
                       unsigned * unsigned_value) const
{
  assert(unsigned_value != 0);
  return (get(option_name, 0, 0, unsigned_value) == Unsigned_Type
          ? 0
          : -1);
}

// One for all: returns the type of that option. Only one of these pointers
// will be set, according to the return value, or none, in case of
// Unknown_Type. Pointers may be 0, in which case they are not used.
enum CardWords_Options::Type
CardWords_Options::get(const string & option_name,
                       string * string_value,
                       bool * bool_value,
                       unsigned * unsigned_value) const
{
  options_const_iterator iter = options.find(option_name);
  enum Type option_type = (iter == options.end()
                           ? Unknown_Type
                           : iter->second.get_type());
  switch(option_type) {
  case String_Type:
    if (string_value != 0) {
      int check_value = iter->second.get(string_value);
      assert (check_value == 0);
    }
    break;
  case Bool_Type:
    if (bool_value != 0) {
      int check_value = iter->second.get(bool_value);
      assert (check_value == 0);
    }
    break;
  case Unsigned_Type:
    if (unsigned_value != 0) {
      int check_value = iter->second.get(unsigned_value);
      assert (check_value == 0);
    }
    break;
  default:
    ;
  }
  return option_type;
}

      
CardWords_Options::Data_Type::Data_Type(const string & string_init_value)
{
  option_type = String_Type;
  string_value = string_init_value;
}

CardWords_Options::Data_Type::Data_Type(bool bool_init_value)
  : bool_value(bool_init_value)
{
  option_type = Bool_Type;
}
CardWords_Options::Data_Type::Data_Type(unsigned unsigned_init_value)
  : unsigned_value(unsigned_init_value)
{
  option_type = Unsigned_Type;
}
CardWords_Options::Data_Type::Data_Type(const Data_Type &other)
{
  option_type = other.option_type;
  if (option_type == String_Type) {
    string_value = other.string_value;
  }
  else {
    bool_value  = other.bool_value;
    unsigned_value = other.unsigned_value;
  }
}
CardWords_Options::Data_Type::Data_Type()
{
  option_type = Unknown_Type;
}
  
CardWords_Options::Data_Type::~Data_Type()
{
}

CardWords_Options::Data_Type &
CardWords_Options::Data_Type::operator = (const Data_Type & other)
{
  if (option_type != Unknown_Type && option_type != other.option_type) {
    cerr << gettext("WARNING: CardWords_Options::Data_Type::operator =():"\
                    "Changing the option_type.")
         << endl;
  }

  option_type = other.option_type;
  if (option_type == String_Type) {
    string_value = other.string_value;
  }
  else {
    bool_value  = other.bool_value;
    unsigned_value = other.unsigned_value;
  }
  return *this;
}

// set returns -1 on type mismatch, 0 if the set changed something
// and 1 if the option had already had this value:
int
CardWords_Options::Data_Type::set(const string & string_init_value)
{
  if (option_type != Unknown_Type && option_type != String_Type) {
    return -1;
  }
  if (option_type == String_Type) {
    if (string_init_value == string_value) {
      // Value already there, no need to change
      return 1;
    }
  }
  else {
    // Was Unknown_Type until now.
    option_type = String_Type;
  }

  string_value = string_init_value;
  return 0;
}
  
int
CardWords_Options::Data_Type::set(bool bool_init_value)
{
  if (option_type != Unknown_Type && option_type != Bool_Type) {
    return -1;
  }
  if (option_type == Bool_Type && bool_init_value == bool_value) {
    // Value already there, no need to change
    return 1;
  }
  else {
    // Was Unknown_Type until now.
    option_type = Bool_Type;
  }

  bool_value = bool_init_value;
  return 0;
}
int
CardWords_Options::Data_Type::set(unsigned unsigned_init_value)
{
  if (option_type != Unknown_Type && option_type != Unsigned_Type) {
    return -1;
  }
  if (option_type == Unsigned_Type && unsigned_init_value == unsigned_value) {
    // Value already there, no need to change
    return 1;
  }
  else {
    // Was Unknown_Type until now.
    option_type = Unsigned_Type;
  }

  unsigned_value = unsigned_init_value;
  return 0;
}

// return the type
enum CardWords_Options::Type
CardWords_Options::Data_Type::get_type(void) const
{
  return option_type;
}
// Accessing the options: returns 0 on success, -1 on failure
int
CardWords_Options::Data_Type::get(string * string_dest_value) const
{
  assert(string_dest_value != 0);
  
  if (option_type != String_Type) {
    return -1;
  }
  *string_dest_value = string_value;
  return 0;
}
int
CardWords_Options::Data_Type::get(bool * bool_dest_value)const
{
  assert(bool_dest_value != 0);
  
  if (option_type != Bool_Type) {
    return -1;
  }
  *bool_dest_value = bool_value;
  return 0;
}
  
int
CardWords_Options::Data_Type::get(unsigned * unsigned_dest_value) const
{
  assert(unsigned_dest_value != 0);
  
  if (option_type != Unsigned_Type) {
    return -1;
  }
  *unsigned_dest_value = unsigned_value;
  return 0;
}

void
CardWords_Options::false_entry(const string & option_name,
                               enum CardWords_Options::Type option_type)
{
  switch (option_type) {
  case String_Type:
    false_entry(option_name, "string");
    break;
  case Bool_Type:
    false_entry(option_name, "bool");
    break;
  case Unsigned_Type:
    false_entry(option_name, "unsigned");
    break;
  case Unknown_Type:
    assert (0);
  }
  assert (00);
}

void
CardWords_Options::false_entry(const string & option_name,
                               const string & option_type)
{
  assert (get_filename() != "");
  cerr << gettext("ERROR: The option `")
       << option_name
       << gettext("' in the options file must\n"\
                  "be of type `")
       << option_type
       << gettext("', but it is not.\n"\
                  "Maybe the program has a bug or you edited the options\n"\
                  "file by hand. This error is unrecoverable.\n"\
                  "Before starting the program again, you will need to\n"\
                  "edit the options file `")
       << get_filename()
       << gettext("' or delete it.")
       << endl;
  exit(1);
}
