/*
 * SVNReviewPresenter.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.vcs.svn.dialog;

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.*;
import com.google.gwt.event.logical.shared.HasAttachHandlers;
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.json.client.JSONNumber;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.RowCountChangeEvent;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.inject.Inject;
import org.rstudio.core.client.Invalidation;
import org.rstudio.core.client.Invalidation.Token;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.WidgetHandlerRegistration;
import org.rstudio.core.client.command.CommandBinder;
import org.rstudio.core.client.command.Handler;
import org.rstudio.core.client.widget.Operation;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.SimpleRequestCallback;
import org.rstudio.studio.client.common.vcs.DiffResult;
import org.rstudio.studio.client.common.vcs.SVNServerOperations;
import org.rstudio.studio.client.common.vcs.StatusAndPath;
import org.rstudio.studio.client.server.ServerError;
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.IntStateValue;
import org.rstudio.studio.client.workbench.views.files.events.FileChangeEvent;
import org.rstudio.studio.client.workbench.views.vcs.ViewVcsConstants;
import org.rstudio.studio.client.workbench.views.vcs.common.ChangelistTable;
import org.rstudio.studio.client.workbench.views.vcs.common.ProcessCallback;
import org.rstudio.studio.client.workbench.views.vcs.common.VCSFileOpener;
import org.rstudio.studio.client.workbench.views.vcs.common.diff.*;
import org.rstudio.studio.client.workbench.views.vcs.common.events.*;
import org.rstudio.studio.client.workbench.views.vcs.common.events.DiffChunkActionEvent.Action;
import org.rstudio.studio.client.workbench.views.vcs.common.events.VcsRefreshEvent.Reason;
import org.rstudio.studio.client.workbench.views.vcs.dialog.ReviewPresenter;
import org.rstudio.studio.client.workbench.views.vcs.svn.SVNCommandHandler;
import org.rstudio.studio.client.workbench.views.vcs.svn.SVNDiffParser;
import org.rstudio.studio.client.workbench.views.vcs.svn.SVNPresenterDisplay;
import org.rstudio.studio.client.workbench.views.vcs.svn.model.SVNState;

import java.util.ArrayList;
import java.util.HashSet;

public class SVNReviewPresenter implements ReviewPresenter
{
   public interface Binder extends CommandBinder<Commands, SVNReviewPresenter> {}

   public interface Display extends IsWidget, HasAttachHandlers, SVNPresenterDisplay
   {
      ArrayList<String> getSelectedPaths();
      void setSelectedStatusAndPaths(ArrayList<StatusAndPath> selectedPaths);

      LineTablePresenter.Display getLineTableDisplay();
      ChangelistTable getChangelistTable();
      HasValue<Integer> getContextLines();

      HasClickHandlers getSwitchViewButton();

      HasClickHandlers getDiscardAllButton();

      void setData(ArrayList<ChunkOrLine> lines);

      HasClickHandlers getOverrideSizeWarningButton();
      void showSizeWarning(long sizeInBytes);
      void hideSizeWarning();

      void showContextMenu(int clientX, int clientY);

      void onShow();
   }

   private class DiscardClickHandler implements ClickHandler, Command
   {
      public DiscardClickHandler()
      {
      }

      @Override
      public void onClick(ClickEvent event)
      {
         execute();
      }

      @Override
      public void execute()
      {
         ArrayList<String> paths = view_.getSelectedPaths();

         server_.svnRevert(paths,
                           new ProcessCallback(constants_.revertCapitalized()));

         view_.getChangelistTable().moveSelectionDown();
      }
   }

   private class ApplyPatchHandler implements DiffChunkActionEvent.Handler,
                                              DiffLinesActionEvent.Handler
   {
      @Override
      public void onDiffChunkAction(DiffChunkActionEvent event)
      {
         ArrayList<DiffChunk> chunks = new ArrayList<>();
         chunks.add(event.getDiffChunk());
         doPatch(event.getAction(), event.getDiffChunk().getLines(), chunks);
      }

      @Override
      public void onDiffLinesAction(DiffLinesActionEvent event)
      {
         ArrayList<Line> lines = view_.getLineTableDisplay().getSelectedLines();
         doPatch(event.getAction(), lines, activeChunks_);
      }

      private void doPatch(Action action,
                           ArrayList<Line> lines,
                           ArrayList<DiffChunk> chunks)
      {
         if (action != Action.Discard)
            throw new IllegalArgumentException("Unhandled diff chunk action");

         applyPatch(chunks, lines, true);
      }

   }

   @Inject
   public SVNReviewPresenter(SVNServerOperations server,
                             Display view,
                             Binder binder,
                             Commands commands,
                             final EventBus events,
                             final SVNState svnState,
                             final Session session,
                             final GlobalDisplay globalDisplay,
                             VCSFileOpener vcsFileOpener)
   {
      server_ = server;
      view_ = view;
      svnState_ = svnState;

      binder.bind(commands, this);

      undiffableStatuses_.add("?");
      undiffableStatuses_.add("!");
      undiffableStatuses_.add("X");

      commandHandler_ = new SVNCommandHandler(view,
                                              globalDisplay,
                                              commands,
                                              server,
                                              svnState,
                                              vcsFileOpener);

      new WidgetHandlerRegistration(view.asWidget())
      {
         @Override
         protected HandlerRegistration doRegister()
         {
            return svnState_.addVcsRefreshHandler(new VcsRefreshEvent.Handler()
            {
               @Override
               public void onVcsRefresh(VcsRefreshEvent event)
               {
                  if (event.getReason() == Reason.VcsOperation)
                  {
                     Scheduler.get().scheduleDeferred(new ScheduledCommand()
                     {
                        @Override
                        public void execute()
                        {
                           updateDiff();

                           initialized_ = true;
                        }
                     });
                  }
               }
            }, false);
         }
      };

      new WidgetHandlerRegistration(view.asWidget())
      {
         @Override
         protected HandlerRegistration doRegister()
         {
            return events.addHandler(FileChangeEvent.TYPE, new FileChangeEvent.Handler()
            {
               @Override
               public void onFileChange(FileChangeEvent event)
               {
                  ArrayList<StatusAndPath> paths = view_.getChangelistTable()
                        .getSelectedItems();
                  if (paths.size() != 1)
                  {
                     clearDiff();
                     return;
                  }

                  StatusAndPath vcsStatus = StatusAndPath.fromInfo(
                        event.getFileChange().getFile().getSVNStatus());
                  if (paths.get(0).getRawPath() == vcsStatus.getRawPath())
                  {
                     svnState.refresh(false);
                  }
               }
            });
         }
      };

      view_.getChangelistTable().addSelectionChangeHandler(new com.google.gwt.view.client.SelectionChangeEvent.Handler()
      {
         @Override
         public void onSelectionChange(SelectionChangeEvent event)
         {
            overrideSizeWarning_ = false;
            commandHandler_.setFilesCommandsEnabled(view_.getSelectedPaths().size() > 0);
            if (initialized_)
               updateDiff();
         }
      });
      view_.getChangelistTable().addRowCountChangeHandler(new RowCountChangeEvent.Handler()
      {
         @Override
         public void onRowCountChange(RowCountChangeEvent event)
         {
            // This is necessary because during initial load, the selection
            // model has its selection set before any items are loaded into
            // the table (so therefore view_.getSelectedPaths().size() is always
            // 0, and the files commands are not enabled until selection changes
            // again). By updating the files commands' enabled state on row
            // count change as well, we can make sure they get enabled.
            commandHandler_.setFilesCommandsEnabled(view_.getSelectedPaths().size() > 0);
         }
      });

      view_.getChangelistTable().addContextMenuHandler(new ContextMenuHandler()
      {
         @Override
         public void onContextMenu(ContextMenuEvent event)
         {
            NativeEvent nativeEvent = event.getNativeEvent();
            view_.showContextMenu(nativeEvent.getClientX(),
                                  nativeEvent.getClientY());
         }
      });

      view_.getDiscardAllButton().addClickHandler(new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent event)
         {
            String message = view_.getLineTableDisplay()
                                 .getSelectedLines()
                                 .size() == 0
                           ? constants_.allChangesInFileWillBeLost()
                           : constants_.selectedChangesInFileWillBeLost();
            globalDisplay.showYesNoMessage(
                  GlobalDisplay.MSG_WARNING,
                  constants_.discardAllCapitalized(),
                  message,
                  new Operation()
                  {
                     @Override
                     public void execute()
                     {
                        new DiscardClickHandler().execute();
                     }
                  },
                  false);
         }
      });

      view_.getLineTableDisplay().addDiffChunkActionHandler(new ApplyPatchHandler());
      view_.getLineTableDisplay().addDiffLineActionHandler(new ApplyPatchHandler());

      new IntStateValue(MODULE_SVN, KEY_CONTEXT_LINES, ClientState.PERSISTENT,
                        session.getSessionInfo().getClientState())
      {
         @Override
         protected void onInit(Integer value)
         {
            if (value != null)
               view_.getContextLines().setValue(value);
         }

         @Override
         protected Integer getValue()
         {
            return view_.getContextLines().getValue();
         }
      };

      view_.getContextLines().addValueChangeHandler(new ValueChangeHandler<Integer>()
      {
         @Override
         public void onValueChange(ValueChangeEvent<Integer> event)
         {
            updateDiff();
         }
      });

      view_.getOverrideSizeWarningButton().addClickHandler(new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent event)
         {
            overrideSizeWarning_ = true;
            updateDiff();
         }
      });
   }

   private void applyPatch(ArrayList<DiffChunk> chunks,
                           ArrayList<Line> lines,
                           boolean reverse)
   {
      chunks = new ArrayList<>(chunks);

      if (reverse)
      {
         for (int i = 0; i < chunks.size(); i++)
            chunks.set(i, chunks.get(i).reverse());
         lines = Line.reverseLines(lines);
      }

      String path = view_.getChangelistTable().getSelectedPaths().get(0);

      UnifiedEmitter emitter = new UnifiedEmitter(path);
      for (DiffChunk chunk : chunks)
         emitter.addContext(chunk);
      emitter.addDiffs(lines);
      String patch = emitter.createPatch(false);

      server_.svnApplyPatch(path, patch,
                            StringUtil.notNull(currentEncoding_),
                            new SimpleRequestCallback<>());
   }

   private void updateDiff()
   {
      view_.hideSizeWarning();

      final ArrayList<StatusAndPath> paths = view_.getChangelistTable().getSelectedItems();
      if (paths.size() != 1)
      {
         clearDiff();
         return;
      }

      final StatusAndPath item = paths.get(0);

      if (item.getPath() != currentFilename_)
      {
         clearDiff();
         currentFilename_ = item.getPath();
      }

      // bail if this is an undiffable status
      if (undiffableStatuses_.contains(item.getStatus()))
         return;

      diffInvalidation_.invalidate();
      final Token token = diffInvalidation_.getInvalidationToken();

      server_.svnDiffFile(
            item.getPath(),
            view_.getContextLines().getValue(),
            overrideSizeWarning_,
            new SimpleRequestCallback<DiffResult>(constants_.diffError())
            {
               @Override
               public void onResponseReceived(DiffResult diffResult)
               {
                  if (token.isInvalid())
                     return;

                  String response = diffResult.getDecodedValue();

                  // Use lastResponse_ to prevent unnecessary flicker
                  if (response == currentResponse_)
                     return;
                  currentResponse_ = response;
                  currentEncoding_ = diffResult.getSourceEncoding();

                  SVNDiffParser parser = new SVNDiffParser(response);
                  parser.nextFilePair();

                  ArrayList<ChunkOrLine> allLines = new ArrayList<>();

                  activeChunks_.clear();
                  for (DiffChunk chunk;
                       null != (chunk = parser.nextChunk());)
                  {
                     if (!chunk.shouldIgnore())
                     {
                        activeChunks_.add(chunk);
                        allLines.add(new ChunkOrLine(chunk));
                     }

                     for (Line line : chunk.getLines())
                        allLines.add(new ChunkOrLine(line));
                  }

                  view_.getLineTableDisplay().setShowActions(
                        !"?".equals(item.getStatus()));
                  view_.setData(allLines);
               }

               @Override
               public void onError(ServerError error)
               {
                  JSONNumber size = error.getClientInfo().isNumber();
                  if (size != null)
                     view_.showSizeWarning((long) size.doubleValue());
                  else
                     super.onError(error);
               }
            });
   }

   private void clearDiff()
   {
      currentResponse_ = null;
      currentFilename_ = null;
      view_.getLineTableDisplay().clear();
   }

   @Override
   public Widget asWidget()
   {
      return view_.asWidget();
   }

   @Override
   public HandlerRegistration addSwitchViewHandler(
         final SwitchViewEvent.Handler h)
   {
      return view_.getSwitchViewButton().addClickHandler(new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent event)
         {
            h.onSwitchView(new SwitchViewEvent());
         }
      });
   }

   @Override
   public void setSelectedPaths(ArrayList<StatusAndPath> selectedPaths)
   {
      view_.setSelectedStatusAndPaths(selectedPaths);
   }

   public void onShow()
   {
      // Ensure that we're fresh
      svnState_.refresh();

      view_.onShow();
   }

   @Handler
   public void onVcsCommit()
   {
      commandHandler_.onVcsCommit();
   }

   @Handler
   public void onVcsPull()
   {
      commandHandler_.onVcsPull();
   }

   @Handler
   public void onVcsIgnore()
   {
      commandHandler_.onVcsIgnore();
   }

   private final Invalidation diffInvalidation_ = new Invalidation();
   private final SVNServerOperations server_;
   private final SVNCommandHandler commandHandler_;
   private final Display view_;
   private ArrayList<DiffChunk> activeChunks_ = new ArrayList<>();
   private String currentResponse_;
   private String currentEncoding_;
   private String currentFilename_;
   private SVNState svnState_;
   private boolean initialized_;
   private static final String MODULE_SVN = "vcs_svn";
   private static final String KEY_CONTEXT_LINES = "context_lines";

   private final HashSet<String> undiffableStatuses_ = new HashSet<>();

   private boolean overrideSizeWarning_ = false;
   private static final ViewVcsConstants constants_ = GWT.create(ViewVcsConstants.class);

}
