/*
 * History.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.history;

import com.google.gwt.core.client.JsArrayNumber;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.inject.Inject;

import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.TimeBufferedCommand;
import org.rstudio.core.client.command.CommandBinder;
import org.rstudio.core.client.command.Handler;
import org.rstudio.core.client.events.HasSelectionCommitHandlers;
import org.rstudio.core.client.events.SelectionCommitEvent;
import org.rstudio.core.client.events.SelectionCommitHandler;
import org.rstudio.core.client.jsonrpc.RpcObjectList;
import org.rstudio.core.client.widget.ProgressIndicator;
import org.rstudio.core.client.widget.ProgressOperation;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.ConsoleDispatcher;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.SimpleRequestCallback;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.server.VoidServerRequestCallback;
import org.rstudio.studio.client.workbench.WorkbenchView;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.model.ClientState;
import org.rstudio.studio.client.workbench.model.Session;
import org.rstudio.studio.client.workbench.model.helper.StringStateValue;
import org.rstudio.studio.client.workbench.views.BasePresenter;
import org.rstudio.studio.client.workbench.views.console.events.ConsoleResetHistoryEvent;
import org.rstudio.studio.client.workbench.views.console.events.ConsoleResetHistoryHandler;
import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent;
import org.rstudio.studio.client.workbench.views.history.History.Display.Mode;
import org.rstudio.studio.client.workbench.views.history.events.FetchCommandsEvent;
import org.rstudio.studio.client.workbench.views.history.events.FetchCommandsHandler;
import org.rstudio.studio.client.workbench.views.history.events.HistoryEntriesAddedEvent;
import org.rstudio.studio.client.workbench.views.history.events.HistoryEntriesAddedHandler;
import org.rstudio.studio.client.workbench.views.history.model.HistoryEntry;
import org.rstudio.studio.client.workbench.views.history.model.HistoryServerOperations;
import org.rstudio.studio.client.workbench.views.source.events.InsertSourceEvent;

import java.util.ArrayList;

public class History extends BasePresenter implements SelectionCommitHandler<Void>,
                                                      FetchCommandsHandler
{
   public interface SearchBoxDisplay extends HasValueChangeHandlers<String>
   {
      String getText();
      public void setText(String text);
   }

   public interface Display extends WorkbenchView,
                                    HasSelectionCommitHandlers<Void>
   {
      public enum Mode
      {
         Recent(0),
         SearchResults(1),
         CommandContext(2);

         Mode(int value)
         {
            value_ = value;
         }

         public int getValue()
         {
            return value_;
         }

         private final int value_;
      }

      void setRecentCommands(ArrayList<HistoryEntry> commands, 
                             boolean scrollToBottom);
      void addRecentCommands(ArrayList<HistoryEntry> entries, boolean top);
      
      int getRecentCommandsScrollPosition();
      void setRecentCommandsScrollPosition(int scrollPosition);
      
      ArrayList<Integer> getRecentCommandsSelectedRowIndexes();
      int getRecentCommandsRowsDisplayed();
      
      void truncateRecentCommands(int maxCommands);   
      
      ArrayList<String> getSelectedCommands();
      ArrayList<Long> getSelectedCommandIndexes();
      HandlerRegistration addFetchCommandsHandler(FetchCommandsHandler handler);
      void setMoreCommands(long moreCommands);
      SearchBoxDisplay getSearchBox();
      Mode getMode();
      void scrollToBottom();
      void focusSearch();

      void dismissSearchResults();
      void showSearchResults(String query,
                             ArrayList<HistoryEntry> entries);
      void showContext(String command,
                       ArrayList<HistoryEntry> entries,
                       long highlightOffset,
                       long highlightLength);
      void dismissContext();

      HasHistory getRecentCommandsWidget();
      HasHistory getSearchResultsWidget();
      HasHistory getCommandContextWidget();

      boolean isCommandTableFocused();
    
   
   }

   public interface Binder extends CommandBinder<Commands, History>
   {}


   class SearchCommand extends TimeBufferedCommand implements ValueChangeHandler<String>
   {
      SearchCommand(Session session)
      {
         super(200);
      }

      @Override
      protected void performAction(boolean shouldSchedulePassive)
      {
         final String query = searchQuery_;
         if (searchQuery_ != null && searchQuery_.length() > 0)
         {
            server_.searchHistoryArchive(
                  searchQuery_, COMMAND_CHUNK_SIZE,
                  new SimpleRequestCallback<RpcObjectList<HistoryEntry>>()
                  {
                     @Override
                     public void onResponseReceived(
                           RpcObjectList<HistoryEntry> response)
                     {
                        if (!query.equals(searchQuery_))
                           return;

                        ArrayList<HistoryEntry> entries = toList(response);
                        view_.showSearchResults(query, entries);
                     }
                  });
         }
      }

      public void onValueChange(ValueChangeEvent<String> event)
      {
         String query = event.getValue();
         searchQuery_ = query;
         if (searchQuery_.equals(""))
         {
            view_.dismissSearchResults();
         }
         else
         {
            nudge();
         }
      }

      public void dismissResults()
      {
         view_.dismissSearchResults();
         searchQuery_ = null;
      }

      private String searchQuery_;
   }

   @Inject
   public History(final Display view,
                  HistoryServerOperations server,
                  final GlobalDisplay globalDisplay,
                  ConsoleDispatcher consoleDispatcher,
                  EventBus events,
                  final Session session,
                  Commands commands,
                  Binder binder)
   {
      super(view);
      view_ = view;
      events_ = events;
      globalDisplay_ = globalDisplay;
      consoleDispatcher_ = consoleDispatcher;
      searchCommand_ = new SearchCommand(session);
      session_ = session;

      binder.bind(commands, this);

      view_.addSelectionCommitHandler(this);
      view_.addFetchCommandsHandler(this);

      server_ = server;
      events_.addHandler(ConsoleResetHistoryEvent.TYPE, new ConsoleResetHistoryHandler()
      {
         @Override
         public void onConsoleResetHistory(ConsoleResetHistoryEvent event)
         {
            view_.bringToFront();
            
            // convert to HistoryEntry
            ArrayList<HistoryEntry> commands = toRecentCommandsList(
                                                         event.getHistory());
            
            // determine entries to add
            int preservedScrollPos = -1;
            int startIndex = Math.max(0, commands.size() - COMMAND_CHUNK_SIZE);
            
            // if we are updating an existing context then preserve the 
            // history position and the scroll position
            if (event.getPreserveUIContext())
            {
               preservedScrollPos = view_.getRecentCommandsScrollPosition();
               
               if (historyPosition_ < commands.size())
                  startIndex = new Long(historyPosition_).intValue();
            }
            
            // set recent commands
            ArrayList<HistoryEntry> subList = new ArrayList<HistoryEntry>();
            subList.addAll(commands.subList(startIndex, commands.size()));
            boolean scrollToBottom = preservedScrollPos == -1;
            setRecentCommands(subList, scrollToBottom);
            
            // restore scroll position if requested
            if (preservedScrollPos != -1)
            {
               final int scrollPos = preservedScrollPos;
               Scheduler.get().scheduleDeferred(new ScheduledCommand()
               {
                  public void execute()
                  {
                     view_.setRecentCommandsScrollPosition(scrollPos);
                  }
               });         
            }
         }  
      });
      
      events_.addHandler(HistoryEntriesAddedEvent.TYPE, new HistoryEntriesAddedHandler()
      {
         public void onHistoryEntriesAdded(HistoryEntriesAddedEvent event)
         {
            view_.addRecentCommands(toList(event.getEntries()), false);
            view_.truncateRecentCommands(
                        session_.getSessionInfo().getConsoleHistoryCapacity());
         }
      });

      view_.getSearchBox().addValueChangeHandler(searchCommand_);

      view_.getRecentCommandsWidget().getKeyTarget().addKeyDownHandler(
            new KeyHandler(commands.historySendToConsole(), 
                           commands.historySendToSource(),
                           null, 
                           null));
      view_.getSearchResultsWidget().getKeyTarget().addKeyDownHandler(
            new KeyHandler(commands.historySendToConsole(),
                           commands.historySendToSource(),
                           commands.historyDismissResults(),
                           commands.historyShowContext()));
      view_.getCommandContextWidget().getKeyTarget().addKeyDownHandler(
            new KeyHandler(commands.historySendToConsole(),
                           commands.historySendToSource(),
                           commands.historyDismissContext(),
                           null));

      new StringStateValue("history", "query", ClientState.TEMPORARY,
                           session.getSessionInfo().getClientState())
      {
         @Override
         protected void onInit(String value)
         {
            if (value != null && value.length() != 0)
            {
               view_.getSearchBox().setText(value);
            }
         }

         @Override
         protected String getValue()
         {
            return view_.getSearchBox().getText();
         }
      };

      server_.getRecentHistory(
            COMMAND_CHUNK_SIZE,
            new ServerRequestCallback<RpcObjectList<HistoryEntry>>()
      {
         @Override
         public void onResponseReceived(RpcObjectList<HistoryEntry> response)
         {
            ArrayList<HistoryEntry> result = toRecentCommandsList(response);
            setRecentCommands(result, true);
         }

         @Override
         public void onError(ServerError error)
         {
            globalDisplay_.showErrorMessage("Error While Retrieving History",
                                           error.getUserMessage());
         }
      });
   }

   
   private void setRecentCommands(ArrayList<HistoryEntry> commands,
                                  boolean scrollToBottom)
   {  
      view_.setRecentCommands(commands, scrollToBottom);

      if (commands.size() > 0)
         historyPosition_ = commands.get(0).getIndex();
      else
         historyPosition_ = 0;
      
      view_.setMoreCommands(Math.min(historyPosition_, COMMAND_CHUNK_SIZE));
   }
   

   private class KeyHandler implements KeyDownHandler
   {
      private KeyHandler(Command accept, 
                         Command shiftAccept,
                         Command left, 
                         Command right)
      {
         this.accept_ = accept;
         this.shiftAccept_ = shiftAccept;
         this.left_ = left;
         this.right_ = right;
      }

      public void onKeyDown(KeyDownEvent event)
      {
         if (!view_.isCommandTableFocused())
            return;
         
         boolean handled = false;
         
         if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER)
         {
            if (event.isShiftKeyDown())
            {
               if (shiftAccept_ != null)
                  shiftAccept_.execute();
               handled = true;
            }
            else if (!event.isAnyModifierKeyDown())
            {
               if (accept_ != null)
                  accept_.execute();
               handled = true;
            }
         }
         else if (!event.isAnyModifierKeyDown())
         {
            switch (event.getNativeKeyCode())
            {
               case KeyCodes.KEY_ESCAPE:
               case KeyCodes.KEY_LEFT:
                  if (left_ != null)
                     left_.execute();
                  handled = true;
                  break;
               case KeyCodes.KEY_RIGHT:
                  if (right_ != null)
                     right_.execute();
                  handled = true;
                  break;
            }
         }
         
         if (handled)
         {
            event.preventDefault();
            event.stopPropagation();
         }
      }

      private final Command accept_;
      private final Command shiftAccept_;
      private final Command left_;
      private final Command right_;
   }

   @Override
   public void onSelected()
   {
      super.onSelected();
      if (view_.getMode() == Mode.Recent)
      {
         view_.scrollToBottom();
         view_.focusSearch();
      }
      
   }

   private String getSelectedCommands()
   {
      ArrayList<String> commands = view_.getSelectedCommands();
      StringBuilder cmd = new StringBuilder();
      for (String command : commands)
      {
         cmd.append(command);
         cmd.append("\n");
      }
      String commandString = cmd.toString();
      return commandString;
   }

   @Handler
   void onHistorySendToConsole()
   {
      String commandString = getSelectedCommands();
      commandString = StringUtil.chomp(commandString);
      if (commandString.length() > 0 )
         events_.fireEvent(new SendToConsoleEvent(commandString, false));
   }

   @Handler
   void onHistorySendToSource()
   {
      String commandString = getSelectedCommands();
      if (commandString.length() > 0)
         events_.fireEvent(new InsertSourceEvent(commandString, true));
   }
   
   void onSearchHistory()
   {
      globalDisplay_.showErrorMessage("Message", "onSearchHistory");
   }
   
   void onLoadHistory()
   {
      view_.bringToFront();
      
      consoleDispatcher_.chooseFileThenExecuteCommand("Load History", 
                                                      "loadhistory");
   }
   
   void onSaveHistory()
   {
      view_.bringToFront();
      
      consoleDispatcher_.saveFileAsThenExecuteCommand("Save History As", 
                                                      ".Rhistory", 
                                                      false,
                                                      "savehistory");
   }
   
   @Handler
   void onHistoryRemoveEntries()
   {   
      // get selected indexes (bail if there is no selection)
      final ArrayList<Integer> selectedRowIndexes = 
                              view_.getRecentCommandsSelectedRowIndexes();
      if (selectedRowIndexes.size() < 1)
      {
         globalDisplay_.showErrorMessage(
                              "Error", 
                              "No history entries currently selected.");
         return;
      }
      
      // bring view to front
      view_.bringToFront();
      
      globalDisplay_.showYesNoMessage(
            GlobalDisplay.MSG_QUESTION,
            "Confirm Remove Entries",
            "Are you sure you want to remove the selected entries from " +
            "the history?",
     
            new ProgressOperation() {
               public void execute(final ProgressIndicator indicator)
               {
                  indicator.onProgress("Removing items...");
                  
                  // for each selected row index we need to calculate
                  // the offset from the bottom
                  int rowCount = view_.getRecentCommandsRowsDisplayed(); 
                  JsArrayNumber bottomIndexes = (JsArrayNumber) 
                                             JsArrayNumber.createArray();
                  for (int i = 0; i<selectedRowIndexes.size(); i++)
                     bottomIndexes.push(rowCount - selectedRowIndexes.get(i) - 1);
                  
                  server_.removeHistoryItems(
                                    bottomIndexes,
                                    new VoidServerRequestCallback(indicator));
               }
            },
            
            true
         );
      
      
   }
   
   @Handler
   void onClearHistory()
   {
      view_.bringToFront();
      
      globalDisplay_.showYesNoMessage(
         GlobalDisplay.MSG_WARNING,
         "Confirm Clear History",
         "Are you sure you want to clear all history entries?",
  
         new ProgressOperation() {
            public void execute(final ProgressIndicator indicator)
            {
               indicator.onProgress("Clearing history...");
               server_.clearHistory(
                     new VoidServerRequestCallback(indicator));
            }
         },
         
         true
      );
   }

   @Handler
   void onHistoryDismissResults()
   {
      searchCommand_.dismissResults();
   }

   @Handler
   void onHistoryDismissContext()
   {
      view_.dismissContext();
   }

   @Handler
   void onHistoryShowContext()
   {
      ArrayList<Long> indexes = view_.getSelectedCommandIndexes();
      if (indexes.size() != 1)
         return;

      final String command = view_.getSelectedCommands().get(0);
      final Long min = indexes.get(0);
      final long max = indexes.get(indexes.size() - 1) + 1;
      final long start = Math.max(0, min - CONTEXT_LINES);
      final long end = max + CONTEXT_LINES;

      server_.getHistoryArchiveItems(
            start,
            end,
            new SimpleRequestCallback<RpcObjectList<HistoryEntry>>() {
               @Override
               public void onResponseReceived(RpcObjectList<HistoryEntry> response)
               {
                  ArrayList<HistoryEntry> entries = toList(response);
                  view_.showContext(command,
                                    entries,
                                    min - start,
                                    max - min);
               }
            });
   }

   private ArrayList<HistoryEntry> toList(RpcObjectList<HistoryEntry> response)
   {
      ArrayList<HistoryEntry> entries = new ArrayList<HistoryEntry>();
      for (int i = 0; i < response.length(); i++)
         entries.add(response.get(i));
      return entries;
   }
   
   private ArrayList<HistoryEntry> toRecentCommandsList(
                                             JsArrayString jsCommands)
   {
      ArrayList<HistoryEntry> commands = new ArrayList<HistoryEntry>();
      for (int i=0; i<jsCommands.length(); i++)
         commands.add(HistoryEntry.create(i, jsCommands.get(i)));
      return commands;
   }
   
   private ArrayList<HistoryEntry> toRecentCommandsList(
                                 RpcObjectList<HistoryEntry> response)
   {
      ArrayList<HistoryEntry> entries = new ArrayList<HistoryEntry>();
      for (int i = 0; i < response.length(); i++)
         entries.add(response.get(i));
      return entries;
   }
   
   

   public void onSelectionCommit(SelectionCommitEvent<Void> e)
   {
      onHistorySendToConsole();
   }

   public void onFetchCommands(FetchCommandsEvent event)
   {
      if (fetchingMoreCommands_)
         return;

      if (historyPosition_ == 0)
      {
         // This should rarely/never happen
         return;
      }

      long startIndex = Math.max(0, historyPosition_ - COMMAND_CHUNK_SIZE);
      long endIndex = historyPosition_;
      server_.getHistoryItems(startIndex, endIndex,
            new SimpleRequestCallback<RpcObjectList<HistoryEntry>>()
            {
               @Override
               public void onResponseReceived(RpcObjectList<HistoryEntry> response)
               {
                  ArrayList<HistoryEntry> entries = 
                                                toRecentCommandsList(response);
                  view_.addRecentCommands(entries, true);
                  fetchingMoreCommands_ = false;

                  if (response.length() > 0)
                     historyPosition_ = response.get(0).getIndex();
                  else
                     historyPosition_ = 0; // this shouldn't happen

                  view_.setMoreCommands(Math.min(historyPosition_,
                                                 COMMAND_CHUNK_SIZE));
               }

               @Override
               public void onError(ServerError error)
               {
                  super.onError(error);
                  fetchingMoreCommands_ = false;
               }
            });
   }
   
  

   // This field indicates how far into the history stream we have reached.
   // When this value becomes 0, that means there is no more history to go
   // fetch.
   private long historyPosition_ = 0;

   private static final int COMMAND_CHUNK_SIZE = 300;
   private static final int CONTEXT_LINES = 50;
   private boolean fetchingMoreCommands_ = false;
   private final Display view_;
   private final EventBus events_;
   private final GlobalDisplay globalDisplay_;
   private final SearchCommand searchCommand_;
   private HistoryServerOperations server_;
   private final Session session_;
   private final ConsoleDispatcher consoleDispatcher_;
}
