/* AbiWord
 * Copyright (C) 1998,1999 AbiSource, Inc.
 * 
 * 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.
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "ut_assert.h"
#include "ut_string.h"
#include "ut_hash.h"
#include "ut_debugmsg.h"

#include "xap_Dialog_Id.h"
#include "xap_DialogFactory.h"
#include "xap_Dlg_MessageBox.h"
#include "xap_App.h"

#include "ap_FrameData.h"
#include "fl_DocLayout.h"
#include "xap_Prefs.h"
#include "ap_Prefs.h"

#include "ap_Dialog_Spell.h"

#include "xap_EncodingManager.h"

#if 1
// todo: remove this requirement on INPUTWORDLEN for pspell builds
#include "ispell.h"
#endif

AP_Dialog_Spell::AP_Dialog_Spell(XAP_DialogFactory * pDlgFactory, XAP_Dialog_Id id)
  : XAP_Dialog_NonPersistent(pDlgFactory,id)
{
   m_iWordOffset = 0;
   m_iWordLength = -1;

   m_iSentenceStart = 0;
   m_iSentenceEnd = 0;
   
   m_Suggestions.count = 0;
   m_Suggestions.score = NULL;
   m_Suggestions.word = NULL;

   m_pBlockBuf = NULL;
   m_pView = NULL;
   m_pSection = NULL;
   m_pBlock = NULL;
   m_pChangeAll = NULL;
   m_pIgnoreAll = NULL;

   m_bCancelled = false;
}

AP_Dialog_Spell::~AP_Dialog_Spell(void)
{
	if (m_pView) 
	{
		if (!m_pView->isSelectionEmpty())
			m_pView->cmdUnselectSelection();

		m_pView->moveInsPtTo( m_iOrigInsPoint );
	}

	DELETEP(m_pBlockBuf);
	UT_HASH_PURGEDATA(UT_UCSChar*,(*m_pChangeAll));
	DELETEP(m_pChangeAll);
	DELETEP(m_pIgnoreAll);

	_purgeSuggestions();
}

void AP_Dialog_Spell::_purgeSuggestions(void)
{
	for (int i = 0; i < m_Suggestions.count; i++)
		FREEP(m_Suggestions.word[i]);

	FREEP(m_Suggestions.word);
	FREEP(m_Suggestions.score);
	m_Suggestions.count = 0;
}

void AP_Dialog_Spell::runModal(XAP_Frame * pFrame)
{
   UT_ASSERT(pFrame);
   m_pFrame = pFrame;
			  
   AP_FrameData * frameData = (AP_FrameData*) m_pFrame->getFrameData();
   m_pDoc = frameData->m_pDocLayout->getDocument();
   m_pView = frameData->m_pDocLayout->getView();
   m_iOrigInsPoint = m_pView->getPoint();
   m_pSection = frameData->m_pDocLayout->getFirstSection();
   m_pBlock = m_pSection->getFirstBlock();
			   
   m_pBlockBuf = new UT_GrowBuf(1024);
   bool bRes = m_pBlock->getBlockBuf(m_pBlockBuf);
   UT_ASSERT(bRes);
   
   m_pChangeAll = new UT_HashTable(7); // is 7 buckets adequate? too much?
   m_pIgnoreAll = new UT_HashTable(7);

   UT_DEBUGMSG(("modal spell dialog: xp init complete\n"));
}

bool AP_Dialog_Spell::nextMisspelledWord(void)
{
   UT_ASSERT(m_pBlockBuf);

   UT_UCSChar* pBlockText = m_pBlockBuf->getPointer(0);
   UT_uint32 iBlockLength = m_pBlockBuf->getLength();

   UT_ASSERT(m_pView && m_pView->getLayout() );	
   bool checkCaps = m_pView->getLayout()->getSpellCheckCaps();

   // loop until a misspelled word or end of document is hit
   for (;;) {

      // do we need to move to the next block?
      if (m_iWordOffset >= iBlockLength) {

	 // since we're done with this current block, put it
	 // in the block spell queue so squiggles will be updated
	
	 FL_DocLayout * docLayout = m_pSection->getDocLayout();

	 // Makes this honor spelling prefs
	 XAP_App * pApp = m_pFrame->getApp();
	 UT_ASSERT(pApp);
	 XAP_Prefs * pPrefs = pApp->getPrefs();
	 UT_ASSERT(pPrefs);
	 
	 XAP_PrefsScheme *pPrefsScheme = pPrefs->getCurrentScheme();
	 UT_ASSERT(pPrefsScheme);

	 bool b = false;
	 pPrefs->getPrefsValueBool((XML_Char*)AP_PREF_KEY_AutoSpellCheck, &b);

	 if (b)
	   docLayout->queueBlockForBackgroundCheck(FL_DocLayout::bgcrSpelling, m_pBlock);
	 
	 m_pBlock = m_pBlock->getNext();

	 // next section, too?
	 if (m_pBlock == NULL) {
	 
	    m_pSection = (fl_DocSectionLayout*) m_pSection->getNext();
	    if (m_pSection == NULL)
	      return false; // end of document
	    m_pBlock = m_pSection->getFirstBlock();
	 }
	 
	 // update the buffer with our new block
	 m_pBlockBuf->truncate(0);
	 bool bRes = m_pBlock->getBlockBuf(m_pBlockBuf);
	 UT_ASSERT(bRes);
	     
	 m_iWordOffset = 0;
	 m_iSentenceStart = 0;
	 m_iSentenceEnd = 0;
	 iBlockLength = m_pBlockBuf->getLength();
	 pBlockText = m_pBlockBuf->getPointer(0);

      }
	
      // scan block for misspelled words

      // now start checking
      bool bFound;
      bool bAllUpperCase;
	
      while (m_iWordOffset < iBlockLength) {

	 // skip delimiters...
	 while (m_iWordOffset < iBlockLength)
	 {
		 UT_UCSChar followChar;
		 followChar = ((m_iWordOffset + 1) < iBlockLength)  ?  pBlockText[m_iWordOffset+1]  :  UCS_UNKPUNK;
		 if (!UT_isWordDelimiter( pBlockText[m_iWordOffset], followChar)) break;
		 m_iWordOffset++;
	 }
	     
	 // ignore initial quote
	 if (pBlockText[m_iWordOffset] == '\'')
	   m_iWordOffset++;
	     
	 if (m_iWordOffset < iBlockLength) {
	    
	    // we're at the start of a word. find end of word...
	    bAllUpperCase = true;
	    bFound = false;
	    m_iWordLength = 0;
	    while ((!bFound) && (m_iWordOffset + m_iWordLength) < iBlockLength) {

			UT_UCSChar followChar;
			followChar = ((m_iWordOffset + m_iWordLength + 1) < iBlockLength)  ?  pBlockText[m_iWordOffset + m_iWordLength + 1]  :  UCS_UNKPUNK;
	       if ( true == UT_isWordDelimiter( pBlockText[m_iWordOffset + m_iWordLength], followChar)) {

		  bFound = true;

	       } else {

		  if (bAllUpperCase)
		    bAllUpperCase = UT_UCS_isupper(pBlockText[m_iWordOffset + m_iWordLength]);
			    
		  m_iWordLength++;

	       }
	    }
		  
	    // ignore terminal quote
	    if (pBlockText[m_iWordOffset + m_iWordLength - 1] == '\'')
	      m_iWordLength--;
		  
	    // for some reason, the spell checker fails on all 1-char words & really big ones
	    // -this is a limitation in the underlying default checker ispell --JB
	    if ((m_iWordLength > 1) &&
		XAP_EncodingManager::instance->noncjk_letters(pBlockText+m_iWordOffset, m_iWordLength) && 
		(!checkCaps || !bAllUpperCase) &&             // TODO: iff relevant Option is set
		(!UT_UCS_isdigit(pBlockText[m_iWordOffset]) &&
		 (m_iWordLength < INPUTWORDLEN))) {
	    
	       // try testing our current change all lists
		  if (!inChangeAll()) {
		    
		     // try ignore all list and user dictionaries here, too
		     XAP_App * pApp = m_pFrame->getApp();

			 UT_UCSChar theWord[INPUTWORDLEN + 1];
			 //UT_DEBUGMSG(("char: %s", pBlockText[m_iWordOffset]));
			 // convert smart quote apostrophe to ASCII single quote to be compatible with ispell
			 for (int ldex=0; ldex<m_iWordLength; ++ldex)
			 {
				 UT_UCSChar currentChar;
				 currentChar = pBlockText[m_iWordOffset + ldex];
				 if (currentChar == UCS_RQUOTE) currentChar = '\'';
				 theWord[ldex] = currentChar;
			 }
			 UT_DEBUGMSG(("word: %s\n", theWord));
			 if (!m_pDoc->isIgnore(theWord, m_iWordLength) &&
			     !pApp->isWordInDict(theWord, m_iWordLength) &&
			     !SpellCheckNWord16(theWord, m_iWordLength)) {
		  
			// unknown word...
			// prepare list of possibilities
			SpellCheckSuggestNWord16(theWord, m_iWordLength,
						 &m_Suggestions);
			    
			// update sentence boundaries (so we can display
			// the misspelled word in proper context)
			if (m_iWordOffset + m_iWordLength > m_iSentenceEnd ||
			    m_iWordOffset < m_iSentenceStart) {
			   _updateSentenceBoundaries();
			}

			UT_DEBUGMSG(("misspelled word found\n"));
		     
			// return to caller
			return true;
			// we have all the important state information in class variables,
			// so the next call to this function will pick up at the same place.
			// this also means we'll check whatever changes they make to this word.
		     }
		  } else {
		     // we changed the word, and the block buffer has
		     // been updated, so reload the pointer and length
		     pBlockText = m_pBlockBuf->getPointer(0);
		     iBlockLength = m_pBlockBuf->getLength();
		     // the offset shouldn't change
		  }
	    }
		
	    // correctly spelled, so continue on
	    m_iWordOffset += (m_iWordLength + 1);
	 }
      }
   }
}

bool AP_Dialog_Spell::makeWordVisible(void)
{
   UT_DEBUGMSG(("making misspelled word visible in main window\n"));
   
   if (!m_pView->isSelectionEmpty())
     m_pView->cmdUnselectSelection();
   m_pView->moveInsPtTo( (PT_DocPosition) (m_pBlock->getPosition() + m_iWordOffset) );
   m_pView->extSelHorizontal(true, (UT_uint32) m_iWordLength);
   m_pView->updateScreen();
   
   return true;
}

bool AP_Dialog_Spell::addIgnoreAll(void)
{
	UT_UCSChar * pBuf = (UT_UCSChar *) m_pBlockBuf->getPointer(m_iWordOffset);
	return m_pDoc->appendIgnore(pBuf,m_iWordLength);
}

void AP_Dialog_Spell::ignoreWord(void)
{
   // skip past the current word
   m_iWordOffset += (m_iWordLength + 1);
}

// changing words

bool AP_Dialog_Spell::inChangeAll(void)
{
   UT_UCSChar * bufferUnicode = _getCurrentWord();
   UT_ASSERT(bufferUnicode);
   char * bufferNormal = (char *) calloc(UT_UCS_strlen(bufferUnicode) + 1, sizeof(char));
   UT_UCS_strcpy_to_char(bufferNormal, bufferUnicode);
   FREEP(bufferUnicode);
   UT_HashEntry * ent = m_pChangeAll->findEntry(bufferNormal);
   FREEP(bufferNormal);

   if (ent == NULL) return false;
   else {
      makeWordVisible();
      bool bRes = changeWordWith( (UT_UCSChar*) (ent->pData) ); 
      return bRes;
   }
}

bool AP_Dialog_Spell::addChangeAll(UT_UCSChar * newword)
{
   UT_UCSChar * bufferUnicode = _getCurrentWord();
   char * bufferNormal = (char *) calloc(UT_UCS_strlen(bufferUnicode) + 1, sizeof(char));
   UT_UCS_strcpy_to_char(bufferNormal, bufferUnicode);
   FREEP(bufferUnicode);

   // make a copy of the word for storage
   UT_UCSChar * newword2 = (UT_UCSChar*) calloc(UT_UCS_strlen(newword) + 1, sizeof(UT_UCSChar));
   UT_UCS_strcpy(newword2, newword);
   
   UT_sint32 iRes = m_pChangeAll->addEntry(bufferNormal, NULL, (void*) newword2);

   FREEP(bufferNormal);
   
   if (iRes < 0) return false;
   else return true;
}

bool AP_Dialog_Spell::changeWordWith(UT_UCSChar * newword)
{
   bool result = true;

   UT_DEBUGMSG(("changing word\n"));
   UT_DEBUGMSG(("SAM: gp: %d\n", m_pView->getPoint()));

   // very small hack to fix bug #597 - seems
   // that the focus gets shifted to the textbox instead
   // of the document, so isSelectionEmpty() returns true
   makeWordVisible ();
   m_iWordLength = UT_UCS_strlen(newword);

#ifdef DEBUG   
   UT_UCSChar * p;
   p = newword;
   UT_DEBUGMSG(("SAM : The new word is \n"));
   for(int i=0;i<m_iWordLength;i++)
   {
	   UT_DEBUGMSG(("%c\n", (char)p[i]));
   }
#endif

   result = m_pView->cmdCharInsert(newword, m_iWordLength);
   m_pView->updateScreen();
   
   // reload block into buffer, as we just changed it
   m_pBlockBuf->truncate(0);
   bool bRes = m_pBlock->getBlockBuf(m_pBlockBuf);
   UT_ASSERT(bRes);
                 
   return result;
}

bool AP_Dialog_Spell::addToDict(void)
{
	UT_UCSChar * pBuf = (UT_UCSChar *) m_pBlockBuf->getPointer(m_iWordOffset);
	XAP_App * pApp = m_pFrame->getApp();

	// add word to the current custom dictionary
	return pApp->addWordToDict(pBuf,m_iWordLength);
}

UT_UCSChar * AP_Dialog_Spell::_getCurrentWord(void)
{
   UT_UCSChar * word = (UT_UCSChar*) calloc(m_iWordLength + 1, sizeof(UT_UCSChar));
   if (word == NULL) return NULL;

   UT_uint16 * pBuf = m_pBlockBuf->getPointer(m_iWordOffset);
   
   for (UT_sint32 i = 0; i < m_iWordLength; i++) word[i] = (UT_UCSChar) pBuf[i];

   return word;
}

UT_UCSChar * AP_Dialog_Spell::_getPreWord(void)
{
   UT_sint32 len = m_iWordOffset - m_iSentenceStart;
   UT_UCSChar * preword = (UT_UCSChar*) calloc(len + 1, sizeof(UT_UCSChar));
   if (preword == NULL) return NULL;
   
   if (len) {
      UT_uint16 * pBuf = m_pBlockBuf->getPointer(m_iSentenceStart);
   
      for (UT_sint32 i = 0; i < len; i++) preword[i] = (UT_UCSChar) pBuf[i];
   }
   
   return preword;
}

UT_UCSChar * AP_Dialog_Spell::_getPostWord(void)
{
   UT_sint32 len = m_iSentenceEnd - (m_iWordOffset + m_iWordLength) + 1;
   UT_UCSChar * postword = (UT_UCSChar*) calloc(len + 1, sizeof(UT_UCSChar));
   if (postword == NULL) return NULL;
   
   if (len) {
      UT_uint16 * pBuf = m_pBlockBuf->getPointer(m_iWordOffset+m_iWordLength);
   
      for (UT_sint32 i = 0; i < len; i++) postword[i] = (UT_UCSChar) pBuf[i];
   }
   
   return postword;
}


// TODO  This function finds the beginning and end of a sentence enclosing
// TODO  the current misspelled word. Right now, it starts from the word
// TODO  and works forward/backward until finding [.!?] or EOB
// TODO  This needs to be improved badly. However, I can't think of a 
// TODO  algorithm to do so -- especially not one which could work with
// TODO  other languages very well...
// TODO  Anyone have something better?

void AP_Dialog_Spell::_updateSentenceBoundaries(void)
{
   
   UT_UCSChar* pBlockText = m_pBlockBuf->getPointer(0);
   UT_uint32 iBlockLength = m_pBlockBuf->getLength();
   
   // go back until a period is found
   m_iSentenceStart = m_iWordOffset;
   while (m_iSentenceStart > 0) {
      if (pBlockText[m_iSentenceStart] == '.' ||
	  pBlockText[m_iSentenceStart] == '?' ||
	  pBlockText[m_iSentenceStart] == '!') {
	 m_iSentenceStart++;
	 break;
      }
      m_iSentenceStart--;
   }
   
   // skip back past any whitespace
   while (pBlockText[m_iSentenceStart] == ' ' ||
	  pBlockText[m_iSentenceStart] == UCS_TAB ||
	  pBlockText[m_iSentenceStart] == UCS_LF ||
	  pBlockText[m_iSentenceStart] == UCS_VTAB ||
	  pBlockText[m_iSentenceStart] == UCS_FF) m_iSentenceStart++;
   
   // go forward until a period is found
   m_iSentenceEnd = m_iWordOffset + m_iWordLength;
   while (m_iSentenceEnd < iBlockLength) {
      if (pBlockText[m_iSentenceEnd] == '.' ||
	  pBlockText[m_iSentenceEnd] == '?' ||
	  pBlockText[m_iSentenceEnd] == '!') break;
      m_iSentenceEnd++;
   }
   if (m_iSentenceEnd == iBlockLength) m_iSentenceEnd--;

}
