/*
 *This file is part of
 * ======================================================
 * 
 *           LyX, the High Level Word Processor
 * 	 
 *	    Copyright (C) 1995 Matthias Ettrich
 *
 *======================================================
 */

#include "config.h"

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <ctype.h>

#include "xdefinitions.h"
#include "sp_form.h"
#include "spellchecker.h"
#include "buffer.h"

// Spellchecker status
enum {
	ISP_OK = 1,
	ISP_ROOT,
	ISP_COMPOUNDWORD,
	ISP_UNKNOWN,
	ISP_MISSED,
	ISP_IGNORE
};

// Spellchecker options
enum {
	ISP_ALTLANG = 1,
	ISP_COMPOUND = 2,
	ISP_PDICT = 4,
	ISP_ROOTAFFIX = 8,
	ISP_ESC = 16
};

static bool RunSpellChecker(const char*);

static FILE *in, *out;  /* streams to communicate with ispell */
static pid_t isp_pid = -1;

static FD_form_spell_options *fd_form_spell_options = NULL;
static FD_form_spell_check *fd_form_spell_check = NULL;

static int isp_fd;

// Spellchecker options variables
static unsigned int isp_status = ISP_COMPOUND;
static char *PerDict=NULL, *AltLang=NULL, *EscChars=NULL;

void sigchldhandler(int sig);
extern void sigchldchecker(int sig);

struct isp_result {
  int flag;
  int count;
  char *string;
  char **misses;
	isp_result() {
		flag = ISP_UNKNOWN;
		count = 0;
		string = (char*)NULL;
		misses = (char**)NULL;
	}
	~isp_result() {
		if (string) delete[] string; // delete or delete[] ?
		if (misses) delete[] misses; // delete or delete[] ?
	}
};


/***** Spellchecker options *****/

// Rewritten to use ordinary LyX xforms loop and OK, Apply and Cancel set. (Asger Alstrup)

// Set values in form to current spellchecker options
void SpellOptionsUpdate(void) 
{
  if (isp_status&ISP_ALTLANG) {
    fl_set_button(fd_form_spell_options->buflang,0);
    fl_set_button(fd_form_spell_options->altlang,1);
  } else {
    fl_set_button(fd_form_spell_options->buflang,1);
    fl_set_button(fd_form_spell_options->altlang,0);
  }  
  fl_set_button(fd_form_spell_options->compounds, (isp_status&ISP_COMPOUND)? 1:0);
  fl_set_button(fd_form_spell_options->perdict, (isp_status&ISP_PDICT) ? 1:0);
  fl_set_button(fd_form_spell_options->esc_chars, (isp_status&ISP_ESC) ? 1:0);
  if (AltLang) fl_set_input(fd_form_spell_options->altlang_input,AltLang);
  if (PerDict) fl_set_input(fd_form_spell_options->perdict_input,PerDict);
  if (EscChars) fl_set_input(fd_form_spell_options->esc_chars_input, EscChars);
}

// Update spellchecker options
void SpellOptionsApplyCB(FL_OBJECT *, long)
{
  // Build new status from form data
  isp_status = fl_get_button(fd_form_spell_options->altlang) ? ISP_ALTLANG:0;
  isp_status |= fl_get_button(fd_form_spell_options->perdict) ? ISP_PDICT:0;
  isp_status |= fl_get_button(fd_form_spell_options->compounds) ? ISP_COMPOUND:0;
  isp_status |= fl_get_button(fd_form_spell_options->esc_chars) ? ISP_ESC:0;
  isp_status |= fl_get_button(fd_form_spell_options->rootaffix) ? ISP_ROOTAFFIX:0;

  // Update strings with data from input fields
  AltLang = StringReplace(AltLang,fl_get_input(fd_form_spell_options->altlang_input));
  PerDict = StringReplace(PerDict,fl_get_input(fd_form_spell_options->perdict_input));
  EscChars= StringReplace(EscChars,fl_get_input(fd_form_spell_options->esc_chars_input));

  // If the fields are empty, delete them and disable corresponding option
  if (StringEqual(AltLang,"")) { 
    delete [] AltLang; 
    AltLang = NULL; 
    isp_status &= ~ISP_ALTLANG;
  }
  if (StringEqual(PerDict,"")) { 
    delete [] PerDict; 
    PerDict = NULL;
    isp_status &= ~ISP_PDICT;
    }
  if (StringEqual(EscChars,"")) { 
    delete [] EscChars; 
    EscChars = NULL; 
    isp_status &= ~ISP_ESC;
  }

  // Update form
  SpellOptionsUpdate();
}

void SpellOptionsCancelCB(FL_OBJECT *, long)
{
  fl_hide_form(fd_form_spell_options->form_spell_options);
}

void SpellOptionsOKCB(FL_OBJECT * ob, long data)
{
  SpellOptionsApplyCB(ob, data);
  SpellOptionsCancelCB(ob, data);
}

// Show spellchecker options form

void SpellCheckerOptions(void)
{
  FL_OBJECT *obj;

  // Create form if nescessary
  if (fd_form_spell_options ==  NULL) {
    fd_form_spell_options = create_form_form_spell_options();
  }

  // Update form to current options
  SpellOptionsUpdate();

  fl_set_focus_object(fd_form_spell_options->form_spell_options,
                      fd_form_spell_options->altlang_input);

  // Show form
  if (fd_form_spell_options->form_spell_options->visible) {
    fl_raise_form(fd_form_spell_options->form_spell_options);
  } else
    fl_show_form(fd_form_spell_options->form_spell_options,
		 FL_PLACE_MOUSE,FL_FULLBORDER,
		 "Spellchecker Options");
}


/***** Spellchecker *****/

// Could also use a clean up. (Asger Alstrup)

static void create_ispell_pipe(char *lang)
{
  char buf[255];
  int pipein[2], pipeout[2];
  char *argv[12];
  int argc;

  isp_pid = -1;

  if(pipe(pipein)==-1 ||pipe(pipeout)==-1) {
/*    fl_show_message("Can't create pipe!", "",""); */
    return;
  }

  if ((out = fdopen(pipein[1], "w"))==NULL) {
/*    fl_show_message("Can't create stream for pipe!", "", "");*/
    return;
  }

  if ((in = fdopen(pipeout[0], "r"))==NULL) {
/*    fl_show_message("Can't create stream for pipe!", "", "");*/
    return;
  }

  setvbuf(out, NULL, _IOLBF, 0); 
  isp_fd = pipeout[0];

  isp_pid = fork();

  if(isp_pid==-1) {
/*    fl_show_message("Can't create child process!", "", "");*/
    return;
  }

  if(isp_pid==0) {        /* child process */
    dup2(pipein[0], STDIN_FILENO);
    dup2(pipeout[1], STDOUT_FILENO);
    close(pipein[0]);
    close(pipein[1]);
    close(pipeout[0]);
    close(pipeout[1]);

    argc = 0;
    argv[argc++] = "ispell";
    argv[argc++] = "-a"; // "Pipe" mode
    //argv[argc++] = "-t";
    
    if (!StringEqual(lang,"default")) {
      argv[argc++] = "-d"; // Dictionary file
      argv[argc++] = lang;
    }

    if (isp_status&ISP_COMPOUND)
      argv[argc++] = "-C"; // Consider run-together words as legal compounds
    else
      argv[argc++] = "-B"; // Report run-together words with missing blanks as errors

    if (isp_status&ISP_ESC) {
      argv[argc++] = "-w"; // Specify additional characters that can be part of a word
      // Put the escape chars in ""s
      char * escchars = new char [strlen(EscChars)+3];
      sprintf(escchars, "\"%s\"", EscChars);
      argv[argc++] = escchars;
    }
    if (isp_status&ISP_PDICT) {
      argv[argc++] = "-p"; // Specify an alternate personal dictionary
      argv[argc++] = PerDict;
    }
    argv[argc++] = NULL;

    execvp("ispell", argv);
 
    fprintf(stderr, "Failed to start ispell!\n");
    _exit(0);
  }

  /* parent process 
     read ispells identification message */
  fgets(buf, 256, in);

}


static isp_result *ispell_check_word(char *word)
{
  isp_result *result;
  char buf[1024], *p, *nb;
  int count, i;

  result = new isp_result;

  fputs(word, out); 
  fputc('\n', out);
  
  fgets(buf, 1024, in); 
  
  /* I think we have to check if ispell is still alive here because
     the signal-handler could have disabled blocking on the fd */
  if (isp_pid == -1) return (isp_result *) NULL;
  
  switch (*buf) {
  case '*':
    result->flag = ISP_OK;
    break;
  case '+':
    result->flag = ISP_ROOT;
    break;
  case '-':
    result->flag = ISP_COMPOUNDWORD;
    break;
  case '\n':
    result->flag = ISP_IGNORE;  /* This happens in case of numbers */
    break;
  case '#':
    result->flag = ISP_UNKNOWN;
    break;
  case '?':
  case '&':
    result->flag = ISP_MISSED;
    nb = StringCopy(buf);
    result->string = nb;
    p = strpbrk(nb+2, " ");
    sscanf(p, "%d", &count);
    result->count = count;
    if (count) result->misses = new char*[count];
    p = strpbrk(nb, ":");
    p +=2;
    for (i = 0; i<count; i++) {
      result->misses[i] = p;
      p = strpbrk(p, ",\n");
      *p = 0;
      p+=2;
    }
    break;
  default:
    result->flag = ISP_UNKNOWN;
  }

  *buf = 0;
  if (result->flag!=ISP_IGNORE) {
    while (*buf!='\n') fgets(buf, 255, in); /* wait for ispell to finish */
  }
  return result;
}


static inline void ispell_terminate()
{
  /* tell ispell to write the dictionary here */
  fputs("#\n", out);

  fflush(out);
  fclose(out);
}


static inline void ispell_insert_word(const char *word)
{
  fputc('*', out);
  fputs(word, out);
  fputc('\n', out);
}


static inline void ispell_accept_word(const char *word) 
{
   fputc('@', out);
   fputs(word, out);
   fputc('\n', out);
}


void ShowSpellChecker()
{
  FL_OBJECT *obj;
  int ret;
  if (fd_form_spell_check == NULL) {
    fd_form_spell_check = create_form_form_spell_check();
  }

  //Clear form
  fl_set_slider_value(fd_form_spell_check->slider, 0);
  fl_set_slider_bounds(fd_form_spell_check->slider, 0, 100);
  fl_set_object_label(fd_form_spell_check->text, "");
  fl_set_input(fd_form_spell_check->input, "");
  fl_clear_browser(fd_form_spell_check->browser);

  if (fd_form_spell_check->form_spell_check->visible) {
    fl_raise_form(fd_form_spell_check->form_spell_check);
  } else
    fl_show_form(fd_form_spell_check->form_spell_check,
		 FL_PLACE_MOUSE,FL_FULLBORDER,
		 "Spellchecker");
  fl_deactivate_object(fd_form_spell_check->slider); 

  // deactivate insert, accept, replace, and stop
  fl_deactivate_object(fd_form_spell_check->insert);
  fl_deactivate_object(fd_form_spell_check->accept);
  fl_deactivate_object(fd_form_spell_check->replace);
  fl_deactivate_object(fd_form_spell_check->stop);
  fl_deactivate_object(fd_form_spell_check->input);
  fl_deactivate_object(fd_form_spell_check->browser);

  while (true){   
     obj = fl_do_forms();
     if (obj == fd_form_spell_check->options){
	SpellCheckerOptions();
     }
     if (obj == fd_form_spell_check->start){
	// activate insert, accept, replace, and stop
	fl_activate_object(fd_form_spell_check->insert);
	fl_activate_object(fd_form_spell_check->accept);
	fl_activate_object(fd_form_spell_check->replace);
	fl_activate_object(fd_form_spell_check->stop);
	fl_activate_object(fd_form_spell_check->input);
	fl_activate_object(fd_form_spell_check->browser);
	// deactivate options and start
	fl_deactivate_object(fd_form_spell_check->options);
	fl_deactivate_object(fd_form_spell_check->start);

	ret = RunSpellChecker(bufferlist.current()->GetLanguage());
	
	// deactivate insert, accept, replace, and stop
	fl_deactivate_object(fd_form_spell_check->insert);
	fl_deactivate_object(fd_form_spell_check->accept);
	fl_deactivate_object(fd_form_spell_check->replace);
	fl_deactivate_object(fd_form_spell_check->stop);
	fl_deactivate_object(fd_form_spell_check->input);
	fl_deactivate_object(fd_form_spell_check->browser);
	// activate options and start
	fl_activate_object(fd_form_spell_check->options);
	fl_activate_object(fd_form_spell_check->start);

	// if RunSpellChecker returns False quit spellchecker
	if (!ret) break;
     }
     if (obj == fd_form_spell_check->done) break;
  }
  fl_hide_form(fd_form_spell_check->form_spell_check);
  EndOfSpellCheck();
  return;
}


static bool RunSpellChecker(const char *lang)
{
   isp_result *result;
   char *word;
   int i, oldval, clickline;
   float newval;
   char *tmp;
   FL_OBJECT *obj;
   char *language = StringCopy(lang);
   
   tmp = (isp_status&ISP_ALTLANG) ? AltLang:language;

   oldval = 0;  /* used for updating slider only when needed */
	
   /* create ispell process */
   signal(SIGCHLD, sigchldhandler);

   create_ispell_pipe(tmp);

   if (isp_pid == -1) {
      fl_show_message("\n\nThe ispell-process has died for some reason. *One* possible reason\ncould be that you do not have a dictionary file\nfor the language of this document installed. \nCheck /usr/lib/ispell or set another\ndictionary in the Spellchecker Options menu.", "", "");
      fclose(out);
      delete[] language;
      return true;
   }

   while (true) {
      word = NextWord(newval);
      if (word==NULL) break;

// What use are the escape chars if this is enabled? The NextWord() routine should
// be more intelligent so this can be avoided. (Asger Alstrup)
      if (!isalpha(*word)) continue;

      if(20*5.0*newval>oldval) {
	 oldval = (int)(20*5.0*newval);
	 fl_set_slider_value(fd_form_spell_check->slider, oldval);
      }

      result = ispell_check_word(word);
      if (isp_pid==-1) break;

      obj =  fl_check_forms();
      if (obj == fd_form_spell_check->stop) {
	 delete result;
	 delete[] word;
	 delete[] language;
	 ispell_terminate();
	 return true;
      }
      if (obj == fd_form_spell_check->done) {
	 if (fl_show_question("Do you want to stop the spellchecking?",
			      "","")) {
	    delete result;
	    delete[] word;
	    delete[] language;
	    ispell_terminate(); 
	    return false;
	 }
      }
    
      switch (result->flag) {
      case ISP_UNKNOWN:
      case ISP_MISSED:
	 SelectLastWord();
	 fl_set_object_label(fd_form_spell_check->text, word);
	 fl_set_input(fd_form_spell_check->input, word);
	 fl_clear_browser(fd_form_spell_check->browser);
	 for (i=0; i<result->count; i++) {
	    fl_add_browser_line(fd_form_spell_check->browser, result->misses[i]);
	 }

	 clickline = -1;
	 while (true) {
	    obj = fl_do_forms();
	    if (obj==fd_form_spell_check->insert) {
	       ispell_insert_word(word);
	       break;
	    }
	    if (obj==fd_form_spell_check->accept) {
	       ispell_accept_word(word);
	       break;
	    }

	    // this is wrong. What we should do is to replace the old string
	    // with the new one, move point to the beginning of the just
	    // inserted string and continue the spellcheck from that point.
	    if (obj==fd_form_spell_check->replace || 
		obj==fd_form_spell_check->input) {
	       ReplaceWord(fl_get_input(fd_form_spell_check->input));
	       ispell_insert_word(fl_get_input(fd_form_spell_check->input));
	       break;
	    }
	    if (obj==fd_form_spell_check->browser) {
		    // implements double click in the browser window.
		    // sendt to lyx@vis by Mark Burton <mark@cbl.leeds.ac.uk>
		    if (clickline ==
			fl_get_browser(fd_form_spell_check->browser)) {
			    ReplaceWord(fl_get_input(fd_form_spell_check->input));
			    ispell_insert_word(fl_get_input(fd_form_spell_check->input));
			    break;
		    }
		    clickline = fl_get_browser(fd_form_spell_check->browser);
		    fl_set_input(fd_form_spell_check->input, 
				 fl_get_browser_line(fd_form_spell_check->browser,
						     fl_get_browser(fd_form_spell_check->browser)));
	    }
	    if (obj==fd_form_spell_check->stop) {
	       delete result;
	       delete[] word;
	       delete[] language;
	       ispell_terminate();
	       return true;
	    }
	    
	    if (obj==fd_form_spell_check->done) {
	       if(fl_show_question("Do you want to stop the spellchecking?", "", "")) { 
		  delete result;
		  delete[] word;
		  delete[] language;
		  ispell_terminate();
		  // this will be done in ShowSpellChecker (Matthias)
		  // fl_hide_form(fd_form_spell_check->form_spell_check);
		  // EndOfSpellCheck();
		  return false;
	       }
	    }
	 }
      default:
	 delete result;
	 delete[] word;
      }
   }
   if(isp_pid!=-1) {
      ispell_terminate();
      fl_show_message("","Spellchecking completed!", "");
   } else {
      fl_show_message("The ispell-process has died for some reason.\nMaybe it has been killed.", "", "");
      fclose(out);
   }

   delete[] language;
   return true;
}

void sigchldhandler(int sig)
{ 
  int status ;
  // sigset_t oldmask, mask = 1 << (SIGCHLD-1);

  // we cannot afford any other child signal while handling this one
  // so we have to block it and unblock at the end of signal servicing
  // routine
  // causes problems, so we will disable it temporarily
  //sigprocmask(SIG_BLOCK, &mask, &oldmask);

  if (isp_pid>0)
    if (waitpid(isp_pid,&status,WNOHANG)==isp_pid) {
	isp_pid=-1;
	fcntl(isp_fd, F_SETFL, O_NONBLOCK); /* set the file descriptor
					       to nonblocking so we can 
					       continue */
    }
  sigchldchecker(sig);

  // set old signal block mask
  //sigprocmask(SIG_SETMASK, &oldmask, NULL);
}
