/*
 * CheckSpelling.java
 *
 * Copyright (C) 2009-12 by RStudio, Inc.
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.studio.client.workbench.views.source.editors.text.spelling;

import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.HasCloseHandlers;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.PopupPanel;
import org.rstudio.core.client.Debug;
import org.rstudio.core.client.Rectangle;
import org.rstudio.core.client.ResultCallback;
import org.rstudio.core.client.js.JsUtil;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.SimpleRequestCallback;
import org.rstudio.studio.client.common.spelling.SpellChecker;
import org.rstudio.studio.client.common.spelling.model.SpellCheckerResult;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.server.Void;
import org.rstudio.studio.client.workbench.views.source.editors.text.DocDisplay;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Anchor;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Range;

import java.util.ArrayList;
import java.util.HashMap;

public class CheckSpelling
{
   public interface Display extends HasCloseHandlers<PopupPanel>
   {
      HasClickHandlers getAddButton();
      HasClickHandlers getIgnoreAllButton();
      HasClickHandlers getSkipButton();
      HasClickHandlers getChangeButton();
      HasClickHandlers getChangeAllButton();

      HasText getMisspelledWord();
      HasText getReplacement();
      void setSuggestions(String[] values);
      void clearSuggestions();
      HasChangeHandlers getSuggestionList();
      String getSelectedSuggestion();

      void focusReplacement();

      void showModal();
      boolean isShowing();
      void closeDialog();

      void showProgress();
      void hideProgress();

      void setEditorSelectionBounds(Rectangle bounds);
   }

   public interface ProgressDisplay
   {
      void show();
      void hide();
      boolean isShowing();
      HasClickHandlers getCancelButton();
   }

   public CheckSpelling(SpellChecker spellChecker,
                        DocDisplay docDisplay,
                        Display view,
                        ProgressDisplay progressDisplay,
                        ResultCallback<Void, Exception> callback)
   {
      spellChecker_ = spellChecker;
      docDisplay_ = docDisplay;
      view_ = view;
      progressDisplay_ = progressDisplay;
      callback_ = callback;

      currentPos_ = docDisplay_.getSelectionStart();
      initialCursorPos_ = docDisplay_.createAnchor(currentPos_);
      wrapped_ = false;

      view_.getChangeButton().addClickHandler(new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent event)
         {
            doReplacement(view_.getReplacement().getText());
            findNextMisspelling();
         }
      });

      view_.getChangeAllButton().addClickHandler(new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent event)
         {
            if (!view_.getMisspelledWord().getText().equals(view_.getReplacement().getText()))
            {
               changeAll_.put(view_.getMisspelledWord().getText(),
                              view_.getReplacement().getText());
            }
            doReplacement(view_.getReplacement().getText());
            findNextMisspelling();
         }
      });

      view_.getSkipButton().addClickHandler(new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent event)
         {
            currentPos_ = docDisplay_.getCursorPosition();
            findNextMisspelling();
         }
      });

      view_.getIgnoreAllButton().addClickHandler(new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent event)
         {
            spellChecker_.addIgnoredWord(view_.getMisspelledWord().getText());
            currentPos_ = docDisplay_.getCursorPosition();
            findNextMisspelling();
         }
      });

      view_.getAddButton().addClickHandler(new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent event)
         {
            spellChecker_.addToUserDictionary(
                  view_.getMisspelledWord().getText());
            currentPos_ = docDisplay_.getCursorPosition();
            findNextMisspelling();
         }
      });

      view_.getSuggestionList().addChangeHandler(new ChangeHandler()
      {
         @Override
         public void onChange(ChangeEvent event)
         {
            String replacement = view_.getSelectedSuggestion();
            if (replacement != null)
               view_.getReplacement().setText(replacement);
         }
      });

      view_.addCloseHandler(new CloseHandler<PopupPanel>()
      {
         @Override
         public void onClose(CloseEvent<PopupPanel> popupPanelCloseEvent)
         {
            cancel();
         }
      });

      progressDisplay_.getCancelButton().addClickHandler(new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent event)
         {
            cancel();
            progressDisplay_.hide();
         }
      });

      progressDisplay_.show();

      findNextMisspelling();
   }

   private void cancel()
   {
      canceled_ = true;
      callback_.onCancelled();
   }

   private void doReplacement(String replacement)
   {
      docDisplay_.replaceSelection(replacement);
      // Spell check what we just replaced
      currentPos_ = docDisplay_.getSelectionStart();
   }

   private void findNextMisspelling()
   {
      try
      {
         if (checkForCancel())
            return;

         showProgress();

         Iterable<Range> wordSource = docDisplay_.getWords(
               docDisplay_.getFileType().getTokenPredicate(),
               docDisplay_.getFileType().getCharPredicate(),
               currentPos_,
               wrapped_ ? initialCursorPos_.getPosition() : null);

         final ArrayList<String> words = new ArrayList<String>();
         final ArrayList<Range> wordRanges = new ArrayList<Range>();

         for (Range r : wordSource)
         {
            // Don't worry about pathologically long words
            if (r.getEnd().getColumn() - r.getStart().getColumn() > 250)
               continue;

            wordRanges.add(r);
            words.add(docDisplay_.getTextForRange(r));

            // Check a maximum of N words at a time
            if (wordRanges.size() == 100)
               break;
         }

         if (wordRanges.size() > 0)
         {
            spellChecker_.checkSpelling(words, new SimpleRequestCallback<SpellCheckerResult>()
            {
               @Override
               public void onResponseReceived(SpellCheckerResult response)
               {
                  if (checkForCancel())
                     return;

                  for (int i = 0; i < words.size(); i++)
                  {
                     if (response.getIncorrect().contains(words.get(i)))
                     {
                        handleMisspelledWord(wordRanges.get(i));
                        return;
                     }
                  }

                  currentPos_ = wordRanges.get(wordRanges.size()-1).getEnd();
                  // Everything spelled correctly, continue
                  Scheduler.get().scheduleDeferred(new ScheduledCommand()
                  {
                     @Override
                     public void execute()
                     {
                        findNextMisspelling();
                     }
                  });
               }
            });
         }
         else
         {
            // No misspellings
            if (wrapped_)
            {
               close();
               RStudioGinjector.INSTANCE.getGlobalDisplay().showMessage(
                     GlobalDisplay.MSG_INFO,
                     "Check Spelling",
                     "Spell check is complete.");
               callback_.onSuccess(Void.create());
            }
            else
            {
               wrapped_ = true;
               currentPos_ = Position.create(0, 0);
               findNextMisspelling();
            }
         }
      }
      catch (Exception e)
      {
         Debug.log(e.toString());
         close();
         RStudioGinjector.INSTANCE.getGlobalDisplay().showErrorMessage(
               "Check Spelling",
               "An error has occurred:\n\n" + e.getMessage());
         callback_.onFailure(e);
      }
   }

   private void close()
   {
      progressDisplay_.hide();
      view_.closeDialog();
   }

   private boolean checkForCancel()
   {
      return canceled_;
   }

   private void showProgress()
   {
      if (view_.isShowing())
         view_.showProgress();
   }

   private void showDialog(Rectangle selectedWordBounds)
   {
      if (progressDisplay_.isShowing())
         progressDisplay_.hide();

      view_.setEditorSelectionBounds(selectedWordBounds);
      if (!view_.isShowing())
         view_.showModal();
      view_.hideProgress();
   }

   private void handleMisspelledWord(Range range)
   {
      try
      {
         docDisplay_.setSelectionRange(range);
         docDisplay_.moveCursorNearTop();
         view_.clearSuggestions();
         view_.getReplacement().setText("");

         final String word = docDisplay_.getTextForRange(range);

         if (changeAll_.containsKey(word))
         {
            doReplacement(changeAll_.get(word));
            findNextMisspelling();
            return;
         }

         view_.getMisspelledWord().setText(word);

         // This fixed delay is regrettable but necessary as it can take some
         // time for Ace's scrolling logic to actually execute (i.e. the next
         // time the renderloop runs). If we don't wait, then misspelled words
         // at the end of the document will result in misreported cursor bounds,
         // meaning we'll be avoiding a completely incorrect region.
         Scheduler.get().scheduleFixedDelay(new RepeatingCommand()
         {
            @Override
            public boolean execute()
            {
               showDialog(docDisplay_.getCursorBounds());

               view_.focusReplacement();

               spellChecker_.suggestionList(word, new ServerRequestCallback<JsArrayString>()
               {
                  @Override
                  public void onResponseReceived(
                        JsArrayString response)
                  {
                     String[] suggestions = JsUtil.toStringArray(response);
                     view_.setSuggestions(suggestions);
                     if (suggestions.length > 0)
                     {
                        view_.getReplacement().setText(suggestions[0]);
                        view_.focusReplacement();
                     }
                  }

                  @Override
                  public void onError(ServerError error)
                  {
                     Debug.logError(error);
                  }
               });

               return false;
            }
         }, 100);
      }
      catch (Exception e)
      {
         Debug.log(e.toString());
         close();
         RStudioGinjector.INSTANCE.getGlobalDisplay().showErrorMessage(
               "Check Spelling",
               "An error has occurred:\n\n" + e.getMessage());
         callback_.onFailure(e);
}
   }

   private final SpellChecker spellChecker_;
   private final DocDisplay docDisplay_;
   private final Display view_;
   private final ProgressDisplay progressDisplay_;
   private final ResultCallback<org.rstudio.studio.client.server.Void, Exception> callback_;
   private final Anchor initialCursorPos_;

   private final HashMap<String, String> changeAll_ = new HashMap<String, String>();

   private Position currentPos_;

   private boolean wrapped_;
   private boolean canceled_;
}
