/*
 * CppCompletionManager.java
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, 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.cpp;


import org.rstudio.core.client.CommandWith2Args;
import org.rstudio.core.client.HandlerRegistrations;
import org.rstudio.core.client.Invalidation;
import org.rstudio.core.client.command.KeyboardHelper;
import org.rstudio.core.client.command.KeyboardShortcut;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.common.filetypes.FileTypeRegistry;
import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
import org.rstudio.studio.client.workbench.snippets.SnippetHelper;
import org.rstudio.studio.client.workbench.views.console.shell.assist.CompletionManager;
import org.rstudio.studio.client.workbench.views.console.shell.assist.CompletionUtils;
import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorSelection;
import org.rstudio.studio.client.workbench.views.source.ViewsSourceConstants;
import org.rstudio.studio.client.workbench.views.source.editors.text.AceEditor;
import org.rstudio.studio.client.workbench.views.source.editors.text.DocDisplay;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.PasteEvent;
import org.rstudio.studio.client.workbench.views.source.model.CppServerOperations;
import org.rstudio.studio.client.workbench.views.source.model.CppSourceLocation;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Timer;
import com.google.inject.Inject;

public class CppCompletionManager implements CompletionManager
{
   @Override
   public void onPaste(PasteEvent event)
   {
      hideCompletionPopup();
   }

   public CppCompletionManager(DocDisplay docDisplay,
                               InitCompletionFilter initFilter,
                               CppCompletionContext completionContext)
   {
      RStudioGinjector.INSTANCE.injectMembers(this);
      docDisplay_ = docDisplay;
      initFilter_ = initFilter;
      completionContext_ = completionContext;
      snippets_ = new SnippetHelper((AceEditor) docDisplay_, completionContext.getDocPath());
      handlers_ = new HandlerRegistrations();
      addHandlers();
   }

   private void addHandlers()
   {
      handlers_.removeHandler();

      handlers_.add(docDisplay_.addClickHandler(new ClickHandler()
      {
         public void onClick(ClickEvent event)
         {
            terminateCompletionRequest();
         }
      }));
   }

   @Inject
   void initialize(CppServerOperations server,
                   FileTypeRegistry fileTypeRegistry,
                   UserPrefs uiPrefs)
   {
      server_ = server;
      fileTypeRegistry_ = fileTypeRegistry;
      userPrefs_ = uiPrefs;
      suggestionTimer_ = new SuggestionTimer(this, userPrefs_);
   }

   // close the completion popup (if any)
   @Override
   public void close()
   {
      terminateCompletionRequest();
   }

   @Override
   public void attach()
   {
      addHandlers();
   }

   @Override
   public void detach()
   {
      handlers_.removeHandler();
      snippets_.detach();
   }

   // perform completion at the current cursor location
   @Override
   public void codeCompletion()
   {
      if (shouldComplete(null))
      {
         suggestCompletions(true);
      }
   }

   // go to help at the current cursor location
   @Override
   public void goToHelp()
   {
      // no implementation here yet since we don't have access
      // to C/C++ help (we could implement this via using libclang
      // to parse doxygen though)
   }

   // find the definition of the function at the current cursor location
   @Override
   public void goToDefinition()
   {
      completionContext_.cppCompletionOperation(new CppCompletionOperation(){

         @Override
         public void execute(String docPath, int line, int column)
         {
            server_.goToCppDefinition(
                  docPath,
                  line,
                  column,
                  new CppCompletionServerRequestCallback<CppSourceLocation>(
                        constants_.findingDefinition()) {
                     @Override
                     public void onSuccess(CppSourceLocation loc)
                     {
                        if (loc != null)
                        {
                           fileTypeRegistry_.editFile(loc.getFile(),
                                 loc.getPosition());
                        }
                     }
                  });
         }

      });
   }

   // return false to indicate key not handled
   @Override
   public boolean previewKeyDown(NativeEvent event)
   {
      suggestionTimer_.cancel();

      // if there is no completion request active then
      // check for a key-combo that triggers completion or
      // navigation / help
      int modifier = KeyboardShortcut.getModifierValue(event);
      if ((request_ == null) || request_.isTerminated())
      {
         // check for user completion key combo
         if (CompletionUtils.isCompletionRequest(event, modifier) &&
             shouldComplete(event))
         {
            return suggestCompletions(true);
         }
         else if (event.getKeyCode() == KeyCodes.KEY_TAB &&
                  modifier == KeyboardShortcut.SHIFT)
         {
            return snippets_.attemptSnippetInsertion(true);
         }
         else if (event.getKeyCode() == KeyCodes.KEY_F1
                  && modifier == KeyboardShortcut.NONE)
         {
            goToHelp();
            return true;
         }
         else if (event.getKeyCode() == KeyCodes.KEY_F2
                  && modifier == KeyboardShortcut.NONE)
         {
            goToDefinition();
            return true;
         }
         else
         {
            return false;
         }
      }
      // otherwise handle keys within the completion popup
      else
      {
         // get the key code
         int keyCode = event.getKeyCode();

         // bail on modifier keys
         if (KeyboardHelper.isModifierKey(keyCode))
            return false;

         // if there is no popup then bail
         CppCompletionPopupMenu popup = getCompletionPopup();
         if ((popup == null) || !popup.isVisible())
            return false;

         // allow emacs-style navigation of popup entries
         if (modifier == KeyboardShortcut.CTRL)
         {
            switch (keyCode)
            {
            case KeyCodes.KEY_P: return popup.selectPrev();
            case KeyCodes.KEY_N: return popup.selectNext();
            }
         }

         // backspace triggers completion if the popup is visible
         if (keyCode == KeyCodes.KEY_BACKSPACE)
         {
            deferredSuggestCompletions(false, false);
            return false;
         }

         // tab accepts the current selection (popup handles Enter)
         else if (keyCode == KeyCodes.KEY_TAB)
         {
            popup.acceptSelected();
            return true;
         }

         // allow '.' when showing file completions
         else if (popup.getCompletionPosition().getScope() == CompletionPosition.Scope.File &&
                  KeyboardHelper.isPeriodKeycode(keyCode))
         {
            return false;
         }

         // non c++ identifier keys (that aren't navigational) close the popup
         else if (!CppCompletionUtils.isCppIdentifierKey(event))
         {
            terminateCompletionRequest();
            return false;
         }

         // otherwise leave it alone
         else
         {
            return false;
         }
      }
   }

   // return false to indicate key not handled
   @Override
   public boolean previewKeyPress(char c)
   {
      suggestionTimer_.cancel();

      // don't do implicit completions if the user has set completion to manual
      // (but always do them if the completion popup is visible)
      if (userPrefs_.codeCompletion().getValue() != UserPrefs.CODE_COMPLETION_MANUAL ||
            isCompletionPopupVisible())
      {
         deferredSuggestCompletions(false, true);
      }

      return false;
   }

   private void deferredSuggestCompletions(final boolean explicit,
                                           final boolean canDelay)
   {
      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
         @Override
         public void execute()
         {
            suggestCompletions(explicit, canDelay);
         }
      });
   }

   private boolean suggestCompletions(final boolean explicit)
   {
      return suggestCompletions(explicit, false);
   }

   private boolean suggestCompletions(final boolean explicit, boolean canDelay)
   {
      suggestionTimer_.cancel();

      // check for completions disabled
      if (!completionContext_.isCompletionEnabled())
         return false;

      // check for no selection
      InputEditorSelection selection = docDisplay_.getSelection();
      if (selection == null)
         return false;

      // check for contiguous selection
      if (!docDisplay_.isSelectionCollapsed())
         return false;

      // calculate explicit value for getting completion position (if a
      // previous request was explicit then count this as explicit)
      boolean positionExplicit = explicit ||
                                 ((request_ != null) && request_.isExplicit());

      // see if we even have a completion position
      boolean alwaysComplete =
            UserPrefs.ASSISTANT_NONE.equals(userPrefs_.assistant().getValue()) &&
            userPrefs_.codeCompletion().getValue() == UserPrefs.CODE_COMPLETION_ALWAYS;
      
      int autoChars = userPrefs_.codeCompletionCharacters().getValue();
      final CompletionPosition completionPosition =
            CppCompletionUtils.getCompletionPosition(docDisplay_,
                                                     positionExplicit,
                                                     alwaysComplete,
                                                     autoChars);

      if (completionPosition == null)
      {
         terminateCompletionRequest();
         return false;
      }
      else if ((request_ != null) &&
               !request_.isTerminated() &&
               request_.getCompletionPosition().isSupersetOf(completionPosition))
      {
         request_.updateUI(false);
      }
      else if (canDelay &&
               completionPosition.getScope() == CompletionPosition.Scope.Global)
      {
         suggestionTimer_.schedule(completionPosition);
      }
      else
      {
         performCompletionRequest(completionPosition, explicit);
      }

      return true;
   }

   private void performCompletionRequest(
         final CompletionPosition completionPosition, final boolean explicit)
   {
      terminateCompletionRequest();

      final Invalidation.Token invalidationToken =
            completionRequestInvalidation_.getInvalidationToken();

      completionContext_.withUpdatedDoc(new CommandWith2Args<String, String>() {

         @Override
         public void execute(String docPath, String docId)
         {
            if (invalidationToken.isInvalid())
               return;

            request_ = new CppCompletionRequest(
               docPath,
               docId,
               completionPosition,
               docDisplay_,
               invalidationToken,
               explicit,
               CppCompletionManager.this,
               new Command() {
                  @Override
                  public void execute()
                  {
                     suggestionTimer_.cancel();
                  }
               });
         }
      });
   }

   private static class SuggestionTimer
   {
      SuggestionTimer(CppCompletionManager manager, UserPrefs uiPrefs)
      {
         manager_ = manager;
         userPrefs_ = uiPrefs;
         timer_ = new Timer()
         {
            @Override
            public void run()
            {
               manager_.performCompletionRequest(completionPosition_, false);
            }
         };
      }

      public void schedule(CompletionPosition completionPosition)
      {
         completionPosition_ = completionPosition;
         timer_.schedule(userPrefs_.codeCompletionDelay().getValue());
      }

      public void cancel()
      {
         timer_.cancel();
      }

      private final CppCompletionManager manager_;
      private final UserPrefs userPrefs_;
      private final Timer timer_;
      private CompletionPosition completionPosition_;
   }


   private CppCompletionPopupMenu getCompletionPopup()
   {
      CppCompletionPopupMenu popup = request_ != null ?
            request_.getCompletionPopup() : null;
      return popup;
   }

   private void hideCompletionPopup()
   {
      CppCompletionPopupMenu popup = getCompletionPopup();
      if (popup != null)
         popup.hide();
   }

   private boolean isCompletionPopupVisible()
   {
      CppCompletionPopupMenu popup = getCompletionPopup();
      return (popup != null) && popup.isVisible();
   }

   private void terminateCompletionRequest()
   {
      suggestionTimer_.cancel();
      completionRequestInvalidation_.invalidate();
      if (request_ != null)
      {
         request_.terminate();
         request_ = null;
      }
   }

   private boolean shouldComplete(NativeEvent event)
   {
      return initFilter_ == null || initFilter_.shouldComplete(event);
   }

   private CppServerOperations server_;
   private UserPrefs userPrefs_;
   private FileTypeRegistry fileTypeRegistry_;
   private final DocDisplay docDisplay_;
   private final CppCompletionContext completionContext_;
   private CppCompletionRequest request_;
   private SuggestionTimer suggestionTimer_;
   private final InitCompletionFilter initFilter_;
   private final Invalidation completionRequestInvalidation_ = new Invalidation();
   private final SnippetHelper snippets_;

   private final HandlerRegistrations handlers_;
   private static final ViewsSourceConstants constants_ = GWT.create(ViewsSourceConstants.class);

}