// This file is part of The New Aspell
// Copyright (C) 2002 by Kevin Atkinson under the GNU LGPL license
// version 2.0 or 2.1.  You should have received a copy of the LGPL
// license along with this library if you did not you can find
// it at http://www.gnu.org/.

//
// NOTE: This program currently uses a very ugly mix of the internal
//       API and the external C interface.  The eventual goal is to
//       use only the external C++ interface, however, the external
//       C++ interface is currently incomplete.  The C interface is
//       used in some places because without the strings will not get
//       converted properly when the encoding is not the same as the
//       internal encoding used by aspell.
// 

#include <deque>

#include <ctype.h>

#include "settings.h"

#include "aspell.h"

#include "asc_ctype.hpp"
#include "check_funs.hpp"
#include "config.hpp"
#include "document_checker.hpp"
#include "enumeration.hpp"
#include "errors.hpp"
#include "file_util.hpp"
#include "fstream.hpp"
#include "info.hpp"
#include "iostream.hpp"
#include "posib_err.hpp"
#include "speller.hpp"
#include "stack_ptr.hpp"
#include "string_enumeration.hpp"
#include "string_map.hpp"
#include "word_list.hpp"

#include "speller_impl.hpp"
#include "data.hpp"

using namespace acommon;

// action functions declarations

void print_ver();
void print_help();
void config();

void check(bool interactive);
void pipe();
void filter();
void list();
void dicts();

void master();
void personal();
void repl();
void soundslike();

#define EXIT_ON_ERR(command) \
  do{PosibErrBase pe(command);\
  if(pe.has_err()){CERR<<"Error: "<< pe.get_err()->mesg << "\n"; exit(1);}\
  } while(false)
#ifdef WIN32PORT
#define EXIT_ON_ERR_SET(command, type, var)\
  typedef type temp_t;\
  temp_t var;\
  do{PosibErr<temp_t> pe(command);\
  if(pe.has_err()){CERR<<"Error: "<< pe.get_err()->mesg << "\n"; exit(1);}\
  else {var=pe.data;}\
  } while(false)
#else
#define EXIT_ON_ERR_SET(command, type, var)\
  type var;\
  do{PosibErr<type> pe(command);\
  if(pe.has_err()){CERR<<"Error: "<< pe.get_err()->mesg << "\n"; exit(1);}\
  else {var=pe.data;}\
  } while(false)
#endif
#define EXIT_ON_ERR_SET(command, type, var)\
  typedef type temp_t;\
  temp_t var;\
  do{PosibErr<temp_t> pe(command);\
  if(pe.has_err()){CERR<<"Error: "<< pe.get_err()->mesg << "\n"; exit(1);}\
  else {var=pe.data;}\
  } while(false)
#define BREAK_ON_ERR(command) \
  do{PosibErrBase pe(command);\
  if(pe.has_err()){CERR<<"Error: "<< pe.get_err()->mesg << "\n"; break;}\
  } while(false)
#ifdef WIN32PORT
#define BREAK_ON_ERR_SET(command, type, var)\
  typedef type temp_t;\
  temp_t var;\
  do{PosibErr<temp_t> pe(command);\
  if(pe.has_err()){CERR<<"Error: "<< pe.get_err()->mesg << "\n"; break;}\
  else {var=pe.data;}\
  } while(false)
#else
#define BREAK_ON_ERR_SET(command, type, var)\
  type var;\
  do{PosibErr<type> pe(command);\
  if(pe.has_err()){CERR<<"Error: "<< pe.get_err()->mesg << "\n"; break;}\
  else {var=pe.data;}\
  } while(false)
#endif


/////////////////////////////////////////////////////////
//
// Command line options functions and classes
// (including main)
//

typedef std::deque<String> Args;
typedef Config        Options;
enum Action {do_create, do_merge, do_dump, do_test, do_other};

Args              args;
StackPtr<Options> options(new_config());
Action            action  = do_other;

struct PossibleOption {
  const char * name;
  char         abrv;
  int          num_arg;
  bool         is_command;
};

#define OPTION(name,abrv,num)         {name,abrv,num,false}
#define COMMAND(name,abrv,num)        {name,abrv,num,true}
#define ISPELL_COMP(abrv,num)         {"",abrv,num,false}

const PossibleOption possible_options[] = {
  OPTION("master",           'd',  1),
  OPTION("personal",         'p',  1),
  OPTION("ignore",            'W', 1),
  OPTION("backup",           'b' , 0),
  OPTION("dont-backup",      'x' , 0),
  OPTION("run-together",     'C',  0),
  OPTION("dont-run-together",'B',  0),

  COMMAND("version",   'v', 0),
  COMMAND("help",      '?', 0),
  COMMAND("config",    '\0', 0),
  COMMAND("check",     'c', 0),
  COMMAND("pipe",      'a', 0),
  COMMAND("filter",    '\0', 0),
  COMMAND("soundslike",'\0', 0),
  COMMAND("list",      'l', 0),
  COMMAND("dicts",     '\0', 0),

  COMMAND("dump",   '\0', 1),
  COMMAND("create", '\0', 1),
  COMMAND("merge",  '\0', 1),

  ISPELL_COMP('n',0), ISPELL_COMP('P',0), ISPELL_COMP('m',0),
  ISPELL_COMP('S',0), ISPELL_COMP('w',1), ISPELL_COMP('T',1),

  {"",'\0'}, {"",'\0'}
};

const PossibleOption * possible_options_end = possible_options + sizeof(possible_options)/sizeof(PossibleOption) - 2;

struct ModeAbrv {
  char abrv;
  const char * mode;
  const char * desc;
};
static const ModeAbrv mode_abrvs[] = {
  {'e', "mode=email","enter Email mode."},
  {'H', "mode=sgml", "enter Html/Sgml mode."},
  {'t', "mode=tex",  "enter TeX mode."},
};

static const ModeAbrv *  mode_abrvs_end = mode_abrvs + 3;

const PossibleOption * find_option(char c) {
  const PossibleOption * i = possible_options;
  while (i != possible_options_end && i->abrv != c) 
    ++i;
  return i;
}

static inline bool str_equal(const char * begin, const char * end, 
			     const char * other) 
{
  while(begin != end && *begin == *other)
    ++begin, ++other;
  return (begin == end && *other == '\0');
}

static const PossibleOption * find_option(const char * begin, const char * end) {
  const PossibleOption * i = possible_options;
  while (i != possible_options_end 
	 && !str_equal(begin, end, i->name))
    ++i;
  return i;
}

static const PossibleOption * find_option(const char * str) {
  const PossibleOption * i = possible_options;
  while (i != possible_options_end 
	 && !strcmp(str, i->name) == 0)
    ++i;
  return i;
}

#ifdef WIN32PORT
int cmdline_main (int argc, char *argv[]) 
#else
int main (int argc, const char *argv[]) 
#endif
{
  EXIT_ON_ERR(options->read_in_settings());

  if (argc == 1) {print_help(); return 0;}

  int i = 1;
  const PossibleOption * o;
  const char           * parm;

  //
  // process command line options by setting the oprepreate options
  // in "options" and/or pushing non-options onto "argv"
  //
  PossibleOption other_opt = OPTION("",'\0',0);
  String option_name;
  while (i != argc) {
    if (argv[i][0] == '-') {
      if (argv[i][1] == '-') {
	// a long arg
	const char * c = argv[i] + 2;
	while(*c != '=' && *c != '\0') ++c;
	o = find_option(argv[i] + 2, c);
	if (o == possible_options_end) {
	  option_name.assign(argv[i] + 2, 0, c - argv[i] - 2);
	  const char * base_name = Config::base_name(option_name.c_str());
	  PosibErr<const KeyInfo *> ki = options->keyinfo(base_name);
          if (!ki.has_err(unknown_key)) {
            other_opt.name    = option_name.c_str();
            other_opt.num_arg = ki.data->type == KeyInfoBool ? 0 : 1;
            o = &other_opt;
          }
	} 
	if (*c == '=') ++c;
	parm = c;
      } else {
	// a short arg
	const ModeAbrv * j = mode_abrvs;
	while (j != mode_abrvs_end && j->abrv != argv[i][1]) ++j;
	if (j == mode_abrvs_end) {
	  o = find_option(argv[i][1]);
	  if (argv[i][1] == 'v' && argv[i][2] == 'v') 
	    // Hack for -vv
	    parm = argv[i] + 3;
	  else
	    parm = argv[i] + 2;
	} else {
	  other_opt.name = "mode";
	  other_opt.num_arg = 1;
	  o = &other_opt;
	  parm = j->mode + 5;
	}
      }
      if (o == possible_options_end) {
	CERR << "Error: Invalid Option: " << argv[i] << "\n";
	return 1;
      }
      if (o->num_arg == 0) {
	if (parm[0] != '\0') {
	  CERR << "Error: " << String(argv[i], parm - argv[i])
	       << " does not take any parameters." << "\n";
	  return 1;
	}
	i += 1;
      } else { // o->num_arg == 1
	if (parm[0] == '\0') {
	  if (i + 1 == argc) {
	    CERR << "Error: You must specify a parameter for " 
		 << argv[i] << "\n";
	    return 1;
	  }
	  parm = argv[i + 1];
	  i += 2;
	} else {
	  i += 1;
	}
      }
      if (o->is_command) {
	args.push_back(o->name);
	if (o->num_arg == 1)
	  args.push_back(parm);
      } else {
	if (o->name[0] != '\0') {
	  EXIT_ON_ERR(options->replace(o->name, parm));
	}
      }
    } else {
      args.push_back(argv[i]);
      i += 1;
    }
  }

  if (args.empty()) {
    CERR << "Error: You must specify an action" << "\n";
    return 1;
  }

  //
  // perform the requisted action
  //
#ifdef WIN32PORT
	// Evidently, the assignment somehow references memory that isn't available
	// when the destructor for the String is called.  This change makes sure that
	// the string gets its own copy of the data. -- TL
  String action_str;
  action_str.append(args.front());
#else
  String action_str = args.front();
#endif
  args.pop_front();
  if (action_str == "help")
    print_help();
  else if (action_str == "version")
    print_ver();
  else if (action_str == "config")
    config();
  else if (action_str == "dicts")
    dicts();
  else if (action_str == "check")
    check(true);
  else if (action_str == "pipe")
    pipe();
  else if (action_str == "list")
    check(false);
  else if (action_str == "filter")
    filter();
  else if (action_str == "soundslike")
    soundslike();
  else if (action_str == "dump")
    action = do_dump;
  else if (action_str == "create")
    action = do_create;
  else if (action_str == "merge")
    action = do_merge;
  else {
    CERR << "Error: Unknown Action: " << action_str << "\n";
    return 1;
  }

  if (action != do_other) {
    if (args.empty()) {
      CERR << "Error: Unknown Action: " << action_str << "\n";
      return 1;
    }
#ifdef WIN32PORT
	// Evidently, the assignment somehow references memory that isn't available
	// when the destructor for the String is called.  This change makes sure that
	// the string gets its own copy of the data. -- TL
    String what_str;
	what_str.append(args.front());
#else
    String what_str = args.front();
#endif
    args.pop_front();
    if (what_str == "config")
      config();
    else if (what_str == "dicts")
      dicts();
    else if (what_str == "master")
      master();
    else if (what_str == "personal")
      personal();
    else if (what_str == "repl")
      repl();
    else {
      CERR << "Error: Unknown Action: " << action_str 
	   << " " << what_str << "\n";
      return 1;
    }
  }

  return 0;
}

/////////////////////////////////////////////////////////
//
// Action Functions
//
//

///////////////////////////
//
// config
//

void config () 
{
  StackPtr<Config> config(new_config());
  EXIT_ON_ERR(config->read_in_settings(options));

  if (args.size() == 0)
    config->write_to_stream(COUT);
  else {
    EXIT_ON_ERR_SET(config->retrieve(args[0]), String, value);
    COUT << value << "\n";
  }
}

///////////////////////////
//
// dicts
//

void dicts() 
{
  DictInfoList * dlist = get_dict_info_list(options);

  StackPtr<DictInfoEnumeration> dels(dlist->elements());

  const DictInfo * entry;

  while ( (entry = dels->next()) != 0) 
  {
    COUT << entry->name << "\n";
  }

}


///////////////////////////
//
// pipe
//

// precond: strlen(str) > 0
char * trim_wspace (char * str)
{
  int last = strlen(str) - 1;
  while (asc_isspace(str[0])) {
    ++str;
    --last;
  }
  while (last > 0 && asc_isspace(str[last])) {
    --last;
  }
  str[last + 1] = '\0';
  return str;
}

bool get_word_pair(char * line, char * & w1, char * & w2)
{
  w2 = strchr(line, ',');
  if (!w2) {
    CERR << "ERROR: Invalid Input\n";
    return false;
  }
  *w2 = '\0';
  ++w2;
  w1 = trim_wspace(line);
  w2 = trim_wspace(w2);
  return true;
}

void print_elements(const AspellWordList * wl) {
  AspellStringEnumeration * els = aspell_word_list_elements(wl);
  int count = 0;
  const char * w;
  String line;
  while ( (w = aspell_string_enumeration_next(els)) != 0 ) {
    ++count;
    line += w;
    line += ", ";
  }
  line.resize(line.size() - 2);
  COUT << count << ": " << line << "\n";
}

void status_fun(void * d, Token, int correct)
{
  if (*static_cast<bool *>(d) && correct)
    COUT << "*\n";
}

DocumentChecker * new_checker(AspellSpeller * speller, 
			      bool & print_star) 
{
  EXIT_ON_ERR_SET(new_document_checker(reinterpret_cast<Speller *>(speller)),
		  StackPtr<DocumentChecker>, checker);
  checker->set_status_fun(status_fun, &print_star);
  return checker.release();
}

#define BREAK_ON_SPELLER_ERR\
  do {if (aspell_speller_error(speller)) {\
    CERR<<"Error: "<< aspell_speller_error_message(speller) << "\n"; break;\
  } } while (false)

void pipe() 
{
#ifndef WIN32
  // set up stdin and stdout to be line buffered
  assert(setvbuf(stdin, 0, _IOLBF, 0) == 0); 
  assert(setvbuf(stdout, 0, _IOLBF, 0) == 0);
#endif

  bool terse_mode = true;
  bool do_time = options->retrieve_bool("time");
  clock_t start,finish;
  start = clock();

  AspellCanHaveError * ret 
    = new_aspell_speller(reinterpret_cast<AspellConfig *>(options.get()));
  if (aspell_error(ret)) {
    CERR << "Error: " << aspell_error_message(ret) << "\n";
    exit(1);
  }
  AspellSpeller * speller = to_aspell_speller(ret);
  Config * config = reinterpret_cast<Speller *>(speller)->config();
  if (do_time)
    COUT << "Time to load word list: " 
         << (clock() - start)/(double)CLOCKS_PER_SEC << "\n";
  bool print_star = true;
  StackPtr<DocumentChecker> checker(new_checker(speller, print_star));
  int c;
  const char * w;
  CharVector buf;
  char * line;
  char * word;
  char * word2;
  int    ignore;
  PosibErrBase err;

  print_ver();

  for (;;) {
    buf.clear();
    fflush(stdout);
    while (c = getchar(), c != '\n' && c != EOF)
      buf.push_back(static_cast<char>(c));
    buf.push_back('\n'); // always add new line so strlen > 0
    buf.push_back('\0');
    line = buf.data();
    ignore = 0;
    switch (line[0]) {
    case '\n':
      if (c != EOF) continue;
      else          break;
    case '*':
      word = trim_wspace(line + 1);
      aspell_speller_add_to_personal(speller, word, -1);
      BREAK_ON_SPELLER_ERR;
      break;
    case '&':
      word = trim_wspace(line + 1);
      aspell_speller_add_to_personal
	(speller, 
	 reinterpret_cast<Speller *>(speller)->to_lower(word), -1);
      BREAK_ON_SPELLER_ERR;
      break;
    case '@':
      word = trim_wspace(line + 1);
      aspell_speller_add_to_session(speller, word, -1);
      BREAK_ON_SPELLER_ERR;
      break;
    case '#':
      aspell_speller_save_all_word_lists(speller);
      BREAK_ON_SPELLER_ERR;
      break;
    case '+':
      word = trim_wspace(line + 1);
      err = config->replace("mode", word);
      if (err.get_err())
	config->replace("mode", "tex");
      reload_filters(reinterpret_cast<Speller *>(speller));
      checker.del();
      checker = new_checker(speller, print_star);
      break;
    case '-':
      config->remove("filter");
      reload_filters(reinterpret_cast<Speller *>(speller));
      checker.del();
      checker = new_checker(speller, print_star);
      break;
    case '~':
      break;
    case '!':
      terse_mode = true;
      print_star = false;
      break;
    case '%':
      terse_mode = false;
      print_star = true;
      break;
    case '$':
      if (line[1] == '$') {
	switch(line[2]) {
	case 'r':
	  switch(line[3]) {
	  case 'a':
	    if (get_word_pair(line + 4, word, word2))
	      aspell_speller_store_replacement(speller, word, -1, word2, -1);
	    break;
	  }
	  break;
	case 'c':
	  switch (line[3]) {
	  case 's':
	    if (get_word_pair(line + 4, word, word2))
	      BREAK_ON_ERR(err = config->replace(word, word2));
	    break;
	  case 'r':
	    word = trim_wspace(line + 4);
	    BREAK_ON_ERR_SET(config->retrieve(word), 
			     PosibErr<String>, ret);
	    break;
	  }
	  break;
	case 'p':
	  switch (line[3]) {
	  case 'p':
	    print_elements(aspell_speller_personal_word_list(speller));
	    break;
	  case 's':
	    print_elements(aspell_speller_session_word_list(speller));
	    break;
	  }
	  break;
	case 'l':
	  COUT << config->retrieve("lang") << "\n";
	  break;
	}
	break;
      } else {
	// continue on (no break)
      }
    case '^':
      ignore = 1;
    default:
      line += ignore;
      checker->process(line, strlen(line));
      while (Token token = checker->next_misspelling()) {
	word = line + token.offset;
	word[token.len] = '\0';
	start = clock();
        const AspellWordList * suggestions 
	  = aspell_speller_suggest(speller, word, -1);
	finish = clock();
	if (!aspell_word_list_empty(suggestions)) {
	  COUT << "& " << word 
	       << " " << aspell_word_list_size(suggestions) 
	       << " " << token.offset + ignore
	       << ":";
	  AspellStringEnumeration * els 
	    = aspell_word_list_elements(suggestions);
	  if (options->retrieve_bool("reverse")) {
	    Vector<String> sugs;
	    sugs.reserve(aspell_word_list_size(suggestions));
	    while ( ( w = aspell_string_enumeration_next(els)) != 0)
	      sugs.push_back(w);
	    Vector<String>::reverse_iterator i = sugs.rbegin();
	    while (true) {
	      COUT << " " << *i;
	      ++i;
	      if (i == sugs.rend()) break;
	      COUT << ",";
	    }
	  } else {
	    while ( ( w = aspell_string_enumeration_next(els)) != 0) {
	      COUT << " " << w;
	      if (!aspell_string_enumeration_at_end(els))
		COUT << ",";
	    }
	  }
	  delete_aspell_string_enumeration(els);
	  COUT << "\n";
	} else {
	  COUT << "# " << word << " " 
	       << token.offset + ignore
	       << "\n";
	}
	if (do_time)
	  COUT << "Suggestion Time: " 
	       << (finish-start)/(double)CLOCKS_PER_SEC << "\n";
      }
      COUT << "\n";
    }
    if (c == EOF) break;
  }

  delete_aspell_speller(speller);
}

///////////////////////////
//
// check
//

enum UserChoice {None, Ignore, IgnoreAll, Replace, ReplaceAll, 
		 Add, AddLower, Exit, Abort};

struct Mapping {
  char primary[9];
  UserChoice reverse[256];
  void to_aspell();
  void to_ispell();
  char & operator[] (UserChoice c) {return primary[c];}
  UserChoice & operator[] (char c) 
    {return reverse[static_cast<unsigned char>(c)];}
};

void abort_check();

void check(bool interactive)
{
  String file_name;
  String new_name;
  FILE * in = 0;
  FILE * out = 0;
  Mapping mapping;

  if (interactive) {
    if (args.size() == 0) {
      CERR << "Error: You must specify a file name.\n";
      exit(-1);
    }
    
    file_name = args[0];
    new_name = file_name;
    new_name += ".new";

    in = fopen(file_name.c_str(), "r");
    if (!in) {
      CERR << "Error: Could not open the file \"" << file_name
	   << "\" for reading.\n";
      exit(-1);
    }
    
    out = fopen(new_name.c_str(), "w");
    if (!out) {
      CERR << "Error: Could not open the file \"" << file_name
           << "\" for writing.  File not saved.\n";
      exit(-1);
    }

    if (!options->have("mode"))
      set_mode_from_extension(options, file_name);
    
    String m = options->retrieve("keymapping");
    if (m == "aspell")
      mapping.to_aspell();
    else if (m == "ispell")
      mapping.to_ispell();
    else {
      CERR << "Error: Invalid keymapping: " << m << "\n";
      exit(-1);
    }

  } else {
    in = stdin;
  }

  AspellCanHaveError * ret 
    = new_aspell_speller(reinterpret_cast<AspellConfig *>(options.get()));
  if (aspell_error(ret)) {
    CERR << "Error: " << aspell_error_message(ret) << "\n";
    exit(1);
  }
  AspellSpeller * speller = to_aspell_speller(ret);

  state = new CheckerString(speller,in,out,64);
 
  word_choices = new Choices;

  menu_choices = new Choices;
  menu_choices->push_back(Choice(mapping[Ignore],     "Ignore"));
  menu_choices->push_back(Choice(mapping[IgnoreAll],  "Ignore all"));
  menu_choices->push_back(Choice(mapping[Replace],    "Replace"));
  menu_choices->push_back(Choice(mapping[ReplaceAll], "Replace all"));
  menu_choices->push_back(Choice(mapping[Add],        "Add"));
  menu_choices->push_back(Choice(mapping[AddLower],   "Add Lower"));
  menu_choices->push_back(Choice(mapping[Abort],      "Abort"));
  menu_choices->push_back(Choice(mapping[Exit],       "Exit"));

  String new_word;
  Vector<String> sug_con;
  StackPtr<StringMap> replace_list(new_string_map());
  const char * w;

  if (interactive)
    begin_check();

  while (state->next_misspelling()) {

    CharVector word0;
    char * word = state->get_word(word0);

    if (interactive) {

      //
      // check if it is in the replace list
      //

      if ((w = replace_list->lookup(word)) != 0) {
	state->replace(w);
	continue;
      }

      //
      // print the line with the misspelled word highlighted;
      //

      display_misspelled_word();

      //
      // print the suggestions and menu choices
      //

      const AspellWordList * suggestions = aspell_speller_suggest(speller, 
								  word, -1);
      AspellStringEnumeration * els = aspell_word_list_elements(suggestions);
      sug_con.resize(0);
      while (sug_con.size() != 10 
	     && (w = aspell_string_enumeration_next(els)) != 0)
	sug_con.push_back(w);
      delete_aspell_string_enumeration(els);

      // disable suspend
      unsigned int suggestions_size = sug_con.size();
      unsigned int suggestions_mid = suggestions_size / 2;
      if (suggestions_size % 2) suggestions_mid++; // if odd
      word_choices->resize(0);
      for (unsigned int j = 0; j != suggestions_mid; ++j) {
	word_choices->push_back(Choice('0' + j+1, sug_con[j]));
	if (j + suggestions_mid != suggestions_size) 
	  word_choices
	    ->push_back(Choice(j+suggestions_mid+1 == 10 
			       ? '0' 
			       : '0' + j+suggestions_mid+1,
			       sug_con[j+suggestions_mid]));
      }
      //enable suspend
      display_menu();

    choice_prompt:

      prompt("? ");

    choice_loop:

      //
      // Handle the users choice
      //

      int choice;
      get_choice(choice);
      
      if (choice == '0') choice = '9' + 1;
    
      switch (mapping[choice]) {
      case Exit:
	goto exit_loop;
      case Abort:
	prompt("Are you sure you want to abort? ");
	get_choice(choice);
	if (choice == 'y' || choice == 'Y')
	  goto abort_loop;
	goto choice_prompt;
      case Ignore:
	break;
      case IgnoreAll:
	aspell_speller_add_to_session(speller, word, -1);
	break;
      case Add:
	aspell_speller_add_to_personal(speller, word, -1);
	break;
      case AddLower:
	aspell_speller_add_to_personal
	  (speller, 
	   reinterpret_cast<Speller *>(speller)->to_lower(word), -1);
	break;
      case Replace:
      case ReplaceAll:
	prompt("With: ");
	get_line(new_word);
	if (new_word.size() == 0)
	  goto choice_prompt;
	if (new_word[0] >= '1' && new_word[0] < (char)suggestions_size + '1')
	  new_word = sug_con[new_word[0]-'1'];
	state->replace(new_word);
	if (mapping[choice] == ReplaceAll)
	  replace_list->replace(word, new_word);
	break;
      default:
	if (choice >= '1' && choice < (char)suggestions_size + '1') { 
	  state->replace(sug_con[choice-'1']);
	} else {
	  error("Sorry that is an invalid choice!");
	  goto choice_loop;
	}
      }

    } else { // !interactive
      
      COUT << word << "\n";
      
    }
  }
exit_loop:
  {
    aspell_speller_save_all_word_lists(speller);
    state.del(); // to close the file handles
    delete_aspell_speller(speller);
    
    bool keep_backup = options->retrieve_bool("backup");
    String backup_name = file_name;
    backup_name += ".bak";
    if (keep_backup)
      rename_file(file_name, backup_name);
    rename_file(new_name, file_name);
    
    //end_check();
    
    return;
  }
abort_loop:
  {
    state.del(); // to close the file handles
    delete_aspell_speller(speller);

    remove_file(new_name);

    return;
  }
}

void Mapping::to_aspell() 
{
  memset(this, 0, sizeof(Mapping));
  primary[Ignore    ] = 'i';
  reverse['i'] = Ignore;
  reverse[' '] = Ignore;
  reverse['\n'] = Ignore;

  primary[IgnoreAll ] = 'I';
  reverse['I'] = IgnoreAll;

  primary[Replace   ] = 'r';
  reverse['r'] = Replace;

  primary[ReplaceAll] = 'R';
  reverse['R'] = ReplaceAll;

  primary[Add       ] = 'a';
  reverse['A'] = Add;
  reverse['a'] = Add;

  primary[AddLower  ] = 'l';
  reverse['L'] = AddLower;
  reverse['l'] = AddLower;

  primary[Abort     ] = 'b';
  reverse['b'] = Abort;
  reverse['B'] = Abort;
  reverse[control('c')] = Abort;

  primary[Exit      ] = 'x';
  reverse['x'] = Exit;
  reverse['X'] = Exit;
}

void Mapping::to_ispell() 
{
  memset(this, 0, sizeof(Mapping));
  primary[Ignore    ] = ' ';
  reverse[' '] = Ignore;
  reverse['\n'] = Ignore;

  primary[IgnoreAll ] = 'A';
  reverse['A'] = IgnoreAll;
  reverse['a'] = IgnoreAll;

  primary[Replace   ] = 'R';
  reverse['R'] = ReplaceAll;
  reverse['r'] = Replace;

  primary[ReplaceAll] = 'E';
  reverse['E'] = ReplaceAll;
  reverse['e'] = Replace;

  primary[Add       ] = 'I';
  reverse['I'] = Add;
  reverse['i'] = Add;

  primary[AddLower  ] = 'U';
  reverse['U'] = AddLower;
  reverse['u'] = AddLower;

  primary[Abort     ] = 'Q';
  reverse['Q'] = Abort;
  reverse['q'] = Abort;
  reverse[control('c')] = Abort;

  primary[Exit      ] = 'X';
  reverse['X'] = Exit;
  reverse['x'] = Exit;
}

///////////////////////////
//
// filter
//

void filter()
{
  //assert(setvbuf(stdin, 0, _IOLBF, 0) == 0);
  //assert(setvbuf(stdout, 0, _IOLBF, 0) == 0);
  CERR << "Sorry \"filter\" is currently unimplemented.\n";
  exit(3);
}


///////////////////////////
//
// print_ver
//

void print_ver () {
  COUT << "@(#) International Ispell Version 3.1.20 " 
       << "(but really Aspell " << VERSION << ")" << "\n";
}

///////////////////////////////////////////////////////////////////////
//
// These functions use implementation details of the default speller
// module
//

///////////////////////////
//
// master
//

class IstreamVirEnumeration : public StringEnumeration {
  FStream * in;
  String data;
public:
  IstreamVirEnumeration(FStream & i) : in(&i) {}
#ifdef WIN32PORT
  StringEnumeration * clone() const {
#else
  IstreamVirEnumeration * clone() const {
#endif
    return new IstreamVirEnumeration(*this);
  }
  void assign (const StringEnumeration * other) {
    *this = *static_cast<const IstreamVirEnumeration *>(other);
  }
  Value next() {
    *in >> data;
    if (!*in) return 0;
    else return data.c_str();
  }
  bool at_end() const {return *in;}
};

void dump (aspeller::LocalWordSet lws) 
{
  using namespace aspeller;

  switch (lws.word_set->basic_type) {
  case DataSet::basic_word_set:
    {
      BasicWordSet  * ws = static_cast<BasicWordSet *>(lws.word_set);
      BasicWordSet::Emul els = ws->detailed_elements();
      BasicWordInfo wi;
      while (wi = els.next(), wi)
	wi.write(COUT,*(ws->lang()), lws.local_info.convert) << "\n";
    }
    break;
  case DataSet::basic_multi_set:
    {
      BasicMultiSet::Emul els 
	= static_cast<BasicMultiSet *>(lws.word_set)->detailed_elements();
      LocalWordSet ws;
      while (ws = els.next(), ws) 
	dump (ws);
    }
    break;
  default:
    abort();
  }
}

void master () {
  using namespace aspeller;

  if (args.size() != 0) {
    options->replace("master", args[0].c_str());
  }

  StackPtr<Config> config(new_basic_config());
  EXIT_ON_ERR(config->read_in_settings(options));

  if (action == do_create) {
    
    EXIT_ON_ERR(create_default_readonly_word_set
                (new IstreamVirEnumeration(CIN),
                 *config));

  } else if (action == do_merge) {
    
    CERR << "Can't merge a master word list yet.  Sorry\n";
    exit (1);
  
  } else if (action == do_dump) {

    EXIT_ON_ERR_SET(add_data_set(config->retrieve("master-path"), 
                                 *config),
                    LoadableDataSet *, mas);
    LocalWordSetInfo wsi;
    wsi.set(mas->lang(), config);
    dump(LocalWordSet(mas,wsi));
    delete mas;
    
  }
}

///////////////////////////
//
// personal
//

void personal () {
  using namespace aspeller;

  if (args.size() != 0) {
    EXIT_ON_ERR(options->replace("personal", args[0].c_str()));
  }
  options->replace("module", "aspeller");
  if (action == do_create || action == do_merge) {
    CERR << "Sorry \"create/merge personal\" is currently unimplemented.\n";
    exit(3);

    // FIXME
#if 0
    StackPtr<Speller> speller(new_speller(options));

    if (action == do_create) {
      if (file_exists(speller->config()->retrieve("personal-path"))) {
        CERR << "Sorry I won't overwrite \"" 
             << speller->config()->retrieve("personal-path") << "\"" << "\n";
        exit (1);
      }
      speller->personal_word_list().data->clear();
    }

    String word;
    while (CIN >> word) 
      speller->add_to_personal(word);

    speller->save_all_word_lists();
#endif

  } else { // action == do_dump

    StackPtr<Config> config(new_basic_config());
    EXIT_ON_ERR(config->read_in_settings(options));

    WritableWordSet * per = new_default_writable_word_set();
    per->load(config->retrieve("personal-path"), config);
    WritableWordSet::Emul els = per->detailed_elements();
    LocalWordSetInfo wsi;
    wsi.set(per->lang(), config);
    BasicWordInfo wi;
    while (wi = els.next(), wi) {
      wi.write(COUT,*(per->lang()), wsi.convert);
      COUT << "\n";
    }
    delete per;
  }
}

///////////////////////////
//
// repl
//

void repl() {
  using namespace aspeller;

  if (args.size() != 0) {
    options->replace("repl", args[0].c_str());
  }

  if (action == do_create || action == do_merge) {

    CERR << "Sorry \"create/merge repl\" is currently unimplemented.\n";
    exit(3);

    // FIXME
#if 0
    SpellerImpl speller(options);

    if (action == do_create) {
      if (file_exists(speller->config()->retrieve("repl-path"))) {
        CERR << "Sorry I won't overwrite \"" 
             << speller->config()->retrieve("repl-path") << "\"" << "\n";
        exit (1);
      }
      speller->personal_repl().clear();
    }
    
    try {
      String word,repl;

      while (true) {
	get_word_pair(word,repl,':');
	EXIT_ON_ERR(speller->store_repl(word,repl,false));
      }

    } catch (bad_cin) {}

    EXIT_ON_ERR(speller->personal_repl().synchronize());
#endif
  } else if (action == do_dump) {

    StackPtr<Config> config(new_basic_config());
    EXIT_ON_ERR(config->read_in_settings());

    WritableReplacementSet * repl = new_default_writable_replacement_set();
    repl->load(config->retrieve("repl-path"), config);
    WritableReplacementSet::Emul els = repl->elements();
 
    ReplacementList rl;
    while ( !(rl = els.next()).empty() ) {
      while (!rl.elements->at_end()) {
	COUT << rl.misspelled_word << ": " << rl.elements->next() << "\n";
      }
      delete rl.elements;
    }
    delete repl;
  }

}

//////////////////////////
//
// soundslike
//

void soundslike() {
  using namespace aspeller;

  Language lang;
  EXIT_ON_ERR(lang.setup("",options));
  String word;
  while (CIN >> word) {
    COUT << word << '\t' << lang.to_soundslike(word) << "\n";
  } 
}

///////////////////////////////////////////////////////////////////////


///////////////////////////
//
// print_help
//

void print_help_line(char abrv, char dont_abrv, const char * name, 
		     KeyInfoType type, const char * desc, bool no_dont = false) 
{
  String command;
  if (abrv != '\0') {
    command += '-';
    command += abrv;
    if (dont_abrv != '\0') {
      command += '|';
      command += '-';
      command += dont_abrv;
    }
    command += ',';
  }
  command += "--";
  if (type == KeyInfoBool && !no_dont) command += "[dont-]";
  if (type == KeyInfoList) command += "add|rem-";
  command += name;
  if (type == KeyInfoString || type == KeyInfoList) 
    command += "=<str>";
  if (type == KeyInfoInt)
    command += "=<int>";
  printf("  %-27s %s\n", command.c_str(), desc);
}

void print_help () {
  printf(
    "\n"
    "Aspell " VERSION " alpha.  Copyright 2000 by Kevin Atkinson.\n"
    "\n"
    "Usage: aspell [options] <command>\n"
    "\n"
    "<command> is one of:\n"
    "  -?|help          display this help message\n"
    "  -c|check <file>  to check a file\n"
    "  -a|pipe          \"ispell -a\" compatibility mode\n"
    "  -l|list          produce a list of misspelled words from standard input\n"
    "  [dump] config    dumps the current configuration to stdout\n"
    "  config <key>     prints the current value of an option\n"
    "  soundslike       returns the soundslike equivalent for each word entered\n"
    "  filter           passes standard input through filters\n"
    "  -v|version       prints a version line\n"
    "  dump|create|merge master|personal|repl [word list]\n"
    "    dumps, creates or merges a master, personal, or replacement word list.\n"
    "\n"
    "[options] is any of the following:\n"
    "\n");
  Enumeration<KeyInfoEnumeration> els = options->possible_elements();
  const KeyInfo * k;
  while (k = els.next(), k) {
    if (k->desc == 0) continue;
    const PossibleOption * o = find_option(k->name);
    print_help_line(o->abrv, 
		    strncmp((o+1)->name, "dont-", 5) == 0 ? (o+1)->abrv : '\0',
		    k->name, k->type, k->desc);
    if (strcmp(k->name, "mode") == 0) {
      for (const ModeAbrv * j = mode_abrvs;
           j != mode_abrvs_end;
           ++j)
      {
        print_help_line(j->abrv, '\0', j->mode, KeyInfoBool, j->desc, true);
      }
    }
  }

}

