/*
 * SVNReviewPanel.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.Document;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.ImageResource.RepeatStyle;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.ui.*;
import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
import com.google.inject.Inject;
import org.rstudio.core.client.WidgetHandlerRegistration;
import org.rstudio.core.client.command.KeyboardShortcut;
import org.rstudio.core.client.resources.ImageResource2x;
import org.rstudio.core.client.widget.FormLabel;
import org.rstudio.core.client.widget.LeftRightToggleButton;
import org.rstudio.core.client.widget.Toolbar;
import org.rstudio.core.client.widget.ToolbarButton;
import org.rstudio.core.client.widget.ToolbarPopupMenu;
import org.rstudio.studio.client.common.vcs.GitServerOperations.PatchMode;
import org.rstudio.studio.client.common.vcs.StatusAndPath;
import org.rstudio.studio.client.workbench.commands.Commands;
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.diff.ChunkOrLine;
import org.rstudio.studio.client.workbench.views.vcs.common.diff.LineTablePresenter;
import org.rstudio.studio.client.workbench.views.vcs.common.diff.LineTableView;
import org.rstudio.studio.client.workbench.views.vcs.dialog.SharedStyles;
import org.rstudio.studio.client.workbench.views.vcs.dialog.SizeWarningWidget;
import org.rstudio.studio.client.workbench.views.vcs.svn.SVNChangelistTablePresenter;
import org.rstudio.studio.client.workbench.views.vcs.svn.dialog.SVNReviewPresenter.Display;

import java.util.ArrayList;

public class SVNReviewPanel extends ResizeComposite implements Display
{
   interface Resources extends ClientBundle
   {
      @Source("SVNReviewPanel.css")
      Styles styles();

      @ImageOptions(repeatStyle = RepeatStyle.Horizontal)
      @Source("../../dialog/images/toolbarTile.png")
      ImageResource toolbarTile();

      @Source("../../dialog/images/stageAllFiles_2x.png")
      ImageResource stageAllFiles2x();

      @Source("../../dialog/images/discard_2x.png")
      ImageResource discard2x();

      @Source("../../dialog/images/stage_2x.png")
      ImageResource stage2x();

      @Source("../../dialog/images/splitterTileV.png")
      @ImageOptions(repeatStyle = RepeatStyle.Horizontal)
      ImageResource splitterTileV();

      @Source("../../dialog/images/splitterTileH.png")
      @ImageOptions(repeatStyle = RepeatStyle.Vertical)
      ImageResource splitterTileH();

      @Source("../../dialog/images/blankFileIcon_2x.png")
      ImageResource blankFileIcon2x();
   }

   interface Styles extends SharedStyles
   {
      String contextLabel();
      String diffToolbar();
      String diffViewOptions();
      String diffContextLines();
   }

   @SuppressWarnings("unused")
   private static class ClickCommand implements HasClickHandlers, Command
   {
      @Override
      public void execute()
      {
         ClickEvent.fireNativeEvent(
               Document.get()
                     .createClickEvent(0,
                                       0,
                                       0,
                                       0,
                                       0,
                                       false,
                                       false,
                                       false,
                                       false),
               this);
      }

      @Override
      public HandlerRegistration addClickHandler(ClickHandler handler)
      {
         return handlerManager_.addHandler(ClickEvent.getType(), handler);
      }

      @Override
      public void fireEvent(GwtEvent<?> event)
      {
         handlerManager_.fireEvent(event);
      }

      private final HandlerManager handlerManager_ = new HandlerManager(this);
   }

   private static class ListBoxAdapter implements HasValue<Integer>
   {
      private ListBoxAdapter(ListBox listBox)
      {
         listBox_ = listBox;
         listBox_.addChangeHandler(new ChangeHandler()
         {
            @Override
            public void onChange(ChangeEvent event)
            {
               ValueChangeEvent.fire(ListBoxAdapter.this, getValue());
            }
         });
      }

      @Override
      public Integer getValue()
      {
         return Integer.parseInt(
               listBox_.getValue(listBox_.getSelectedIndex()));
      }

      @Override
      public void setValue(Integer value)
      {
         setValue(value, true);
      }

      @Override
      public void setValue(Integer value, boolean fireEvents)
      {
         String valueStr = value.toString();
         for (int i = 0; i < listBox_.getItemCount(); i++)
         {
            if (listBox_.getValue(i) == valueStr)
            {
               listBox_.setSelectedIndex(i);
               break;
            }
         }
      }

      @Override
      public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Integer> handler)
      {
         return handlers_.addHandler(ValueChangeEvent.getType(), handler);
      }

      @Override
      public void fireEvent(GwtEvent<?> event)
      {
         handlers_.fireEvent(event);
      }

      private final ListBox listBox_;
      private final HandlerManager handlers_ = new HandlerManager(this);
   }

   interface Binder extends UiBinder<Widget, SVNReviewPanel>
   {
   }

   @Inject
   public SVNReviewPanel(SVNChangelistTablePresenter changelist,
                         LineTableView diffPane,
                         Commands commands)
   {
      commands_ = commands;
      splitPanel_ = new SplitLayoutPanel(4);

      changelist_ = changelist.getView();
      lines_ = diffPane;
      lines_.getElement().setTabIndex(-1);
      lines_.hideStageCommands();

      overrideSizeWarning_ = new SizeWarningWidget(constants_.diff());

      topToolbar_ = new Toolbar(constants_.svnReview());
      diffToolbar_ = new Toolbar(constants_.svnDiff());

      changelist.setSelectFirstItemByDefault(true);

      Widget widget = GWT.<Binder>create(Binder.class).createAndBindUi(this);
      initWidget(widget);

      topToolbar_.addStyleName(RES.styles().toolbar());
      topToolbar_.getWrapper().addStyleName(RES.styles().toolbarInnerWrapper());

      switchViewButton_ = new LeftRightToggleButton(constants_.changesCapitalized(), constants_.historyCapitalized(), true);
      switchViewButton_.getElement().getStyle().setMarginRight(8, Unit.PX);
      topToolbar_.addLeftWidget(switchViewButton_);

      topToolbar_.addLeftSeparator();

      topToolbar_.addLeftWidget(new ToolbarButton(
            constants_.refreshCapitalized(), ToolbarButton.NoTitle, commands.vcsRefresh().getImageResource(),
            new ClickHandler() {
               @Override
               public void onClick(ClickEvent event)
               {
                  changelist_.showProgress();
                  commands_.vcsRefresh().execute();
               }
            }));

      topToolbar_.addLeftSeparator();

      topToolbar_.addLeftWidget(commands.vcsAddFiles().createToolbarButton());
      topToolbar_.addLeftWidget(commands.vcsRemoveFiles().createToolbarButton());
      topToolbar_.addLeftSeparator();
      topToolbar_.addLeftWidget(commands.vcsRevert().createToolbarButton());
      topToolbar_.addLeftWidget(commands.vcsIgnore().createToolbarButton());
      topToolbar_.addLeftSeparator();
      topToolbar_.addLeftWidget(commands.vcsResolve().createToolbarButton());
      topToolbar_.addLeftSeparator();
      topToolbar_.addLeftWidget(commands.vcsCommit().createToolbarButton());


      commands.vcsPull().setButtonLabel(constants_.updateCapitalized());
      commands.vcsPull().setMenuLabel(constants_.updateCapitalized());
      topToolbar_.addRightWidget(commands.vcsPull().createToolbarButton());

      diffToolbar_.addStyleName(RES.styles().toolbar());
      diffToolbar_.addStyleName(RES.styles().diffToolbar());
      diffToolbar_.getWrapper().addStyleName(RES.styles().diffToolbarInnerWrapper());

      diffToolbar_.addLeftSeparator();
      discardAllButton_ = diffToolbar_.addLeftWidget(new ToolbarButton(
            constants_.discardAllCapitalized(), ToolbarButton.NoTitle,  new ImageResource2x(RES.discard2x())));

      contextLines_.addStyleName(RES.styles().diffContextLines());
      lblContext_.setFor(contextLines_);
      listBoxAdapter_ = new ListBoxAdapter(contextLines_);

      new WidgetHandlerRegistration(this)
      {
         @Override
         protected HandlerRegistration doRegister()
         {
            return Event.addNativePreviewHandler(new NativePreviewHandler()
            {
               @Override
               public void onPreviewNativeEvent(NativePreviewEvent event)
               {
                  NativeEvent nativeEvent = event.getNativeEvent();
                  if (event.getTypeInt() == Event.ONKEYDOWN
                      && KeyboardShortcut.getModifierValue(nativeEvent) == KeyboardShortcut.CTRL)
                  {
                     switch (nativeEvent.getKeyCode())
                     {
                        case KeyCodes.KEY_DOWN:
                           nativeEvent.preventDefault();
                           scrollBy(diffScroll_, getLineScroll(diffScroll_), 0);
                           break;
                        case KeyCodes.KEY_UP:
                           nativeEvent.preventDefault();
                           scrollBy(diffScroll_,
                                    -getLineScroll(diffScroll_),
                                    0);
                           break;
                        case KeyCodes.KEY_PAGEDOWN:
                           nativeEvent.preventDefault();
                           scrollBy(diffScroll_, getPageScroll(diffScroll_), 0);
                           break;
                        case KeyCodes.KEY_PAGEUP:
                           nativeEvent.preventDefault();
                           scrollBy(diffScroll_,
                                    -getPageScroll(diffScroll_),
                                    0);
                           break;
                     }
                  }
               }
            });
         }
      };
   }

   private void scrollBy(ScrollPanel scrollPanel, int vscroll, int hscroll)
   {
      if (vscroll != 0)
      {
         scrollPanel.setVerticalScrollPosition(
               Math.max(0, scrollPanel.getVerticalScrollPosition() + vscroll));
      }

      if (hscroll != 0)
      {
         scrollPanel.setHorizontalScrollPosition(
               Math.max(0, scrollPanel.getHorizontalScrollPosition() + hscroll));
      }
   }

   private int getLineScroll(ScrollPanel panel)
   {
      return 30;
   }

   private int getPageScroll(ScrollPanel panel)
   {
      // Return slightly less than the client height (so there's overlap between
      // one screen and the next) but never less than the line scroll height.
      return Math.max(
            getLineScroll(panel),
            panel.getElement().getClientHeight() - getLineScroll(panel));
   }

   @Override
   public HasClickHandlers getSwitchViewButton()
   {
      return switchViewButton_;
   }

   @Override
   public HasClickHandlers getDiscardAllButton()
   {
      return discardAllButton_;
   }


   @Override
   public void setData(ArrayList<ChunkOrLine> lines)
   {
      int vscroll = diffScroll_.getVerticalScrollPosition();
      int hscroll = diffScroll_.getHorizontalScrollPosition();

      getLineTableDisplay().setData(lines, PatchMode.Working);

      diffScroll_.setVerticalScrollPosition(vscroll);
      diffScroll_.setHorizontalScrollPosition(hscroll);
   }

   @Override
   public ArrayList<String> getSelectedPaths()
   {
      return changelist_.getSelectedPaths();
   }

   @Override
   public ArrayList<StatusAndPath> getSelectedItems()
   {
      return changelist_.getSelectedItems();
   }

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

   @Override
   public LineTablePresenter.Display getLineTableDisplay()
   {
      return lines_;
   }

   @Override
   public ChangelistTable getChangelistTable()
   {
      return changelist_;
   }

   @Override
   public HasValue<Integer> getContextLines()
   {
      return listBoxAdapter_;
   }

   @Override
   public HasClickHandlers getOverrideSizeWarningButton()
   {
      return overrideSizeWarning_;
   }

   @Override
   public void showSizeWarning(long sizeInBytes)
   {
      overrideSizeWarning_.setSize(sizeInBytes);
      diffScroll_.setWidget(overrideSizeWarning_);
   }

   @Override
   public void hideSizeWarning()
   {
      diffScroll_.setWidget(lines_);
   }

   @Override
   public void showContextMenu(final int clientX,
                               final int clientY)
   {
      final ToolbarPopupMenu menu = new ToolbarPopupMenu();

      menu.addItem(commands_.vcsAddFiles().createMenuItem(false));
      menu.addItem(commands_.vcsRemoveFiles().createMenuItem(false));
      menu.addSeparator();
      menu.addItem(commands_.vcsRevert().createMenuItem(false));
      menu.addItem(commands_.vcsIgnore().createMenuItem(false));
      menu.addSeparator();
      menu.addItem(commands_.vcsResolve().createMenuItem(false));
      menu.addSeparator();
      menu.addItem(commands_.vcsOpen().createMenuItem(false));

      menu.setPopupPositionAndShow(new PositionCallback() {
         @Override
         public void setPosition(int offsetWidth, int offsetHeight)
         {
            menu.setPopupPosition(clientX, clientY);
         }
      });
   }

   @Override
   public void onShow()
   {
      Scheduler.get().scheduleDeferred(new ScheduledCommand()
      {
         @Override
         public void execute()
         {
            changelist_.focus();
         }
      });
   }

   @UiField(provided = true)
   SplitLayoutPanel splitPanel_;
   @UiField(provided = true)
   ChangelistTable changelist_;
   @UiField(provided = true)
   LineTableView lines_;
   @UiField
   FormLabel lblContext_;
   @UiField
   ListBox contextLines_;
   @UiField(provided = true)
   Toolbar topToolbar_;
   @UiField(provided = true)
   Toolbar diffToolbar_;
   @UiField
   ScrollPanel diffScroll_;

   private final Commands commands_;

   private ListBoxAdapter listBoxAdapter_;

   private ToolbarButton discardAllButton_;

   private LeftRightToggleButton switchViewButton_;

   private SizeWarningWidget overrideSizeWarning_;

   private static final Resources RES = GWT.create(Resources.class);
   static {
      RES.styles().ensureInjected();
   }
   private static final ViewVcsConstants constants_ = GWT.create(ViewVcsConstants.class);
}