/*
 * GitReviewPanel.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.vcs.git.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.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.AppCommand;
import org.rstudio.core.client.command.KeyboardShortcut;
import org.rstudio.core.client.widget.*;
import org.rstudio.studio.client.common.filetypes.FileTypeRegistry;
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.CheckoutBranchToolbarButton;
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.git.dialog.GitReviewPresenter.Display;
import org.rstudio.studio.client.workbench.views.vcs.git.GitChangelistTablePresenter;

import java.util.ArrayList;

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

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

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

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

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

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

      @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.png")
      ImageResource blankFileIcon();
   }

   interface Styles extends SharedStyles
   {
      String diffToolbar();

      String stagedLabel();
      String staged();

      String unstaged();

      String diffViewOptions();

      String commitMessage();
      String commitButton();

      String splitPanelCommit();
   }

   @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).equals(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, GitReviewPanel>
   {
   }

   @Inject
   public GitReviewPanel(GitChangelistTablePresenter changelist,
                         LineTableView diffPane,
                         final Commands commands,
                         FileTypeRegistry fileTypeRegistry,
                         CheckoutBranchToolbarButton branchToolbarButton)
   {
      fileTypeRegistry_ = fileTypeRegistry;
      splitPanel_ = new SplitLayoutPanel(4);
      splitPanelCommit_ = new SplitLayoutPanel(4);

      commitButton_ = new ThemedButton("Commit");
      commitButton_.addStyleName(RES.styles().commitButton());

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

      overrideSizeWarning_ = new SizeWarningWidget("diff");

      changelist.setSelectFirstItemByDefault(true);

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

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

      switchViewButton_ = new LeftRightToggleButton("Changes", "History", true);
      topToolbar_.addLeftWidget(switchViewButton_);

      topToolbar_.addLeftWidget(branchToolbarButton);
      
      topToolbar_.addLeftSeparator();
      
      topToolbar_.addLeftWidget(new ToolbarButton(
            null, commands.vcsRefresh().getImageResource(),
            new ClickHandler() {
               @Override
               public void onClick(ClickEvent event)
               {
                  changelist_.showProgress();
                  commands.vcsRefresh().execute();
               }
            }));
      
      topToolbar_.addLeftSeparator();

      stageFilesButton_ = topToolbar_.addLeftWidget(new ToolbarButton(
            "Stage",
            RES.stage(),
            (ClickHandler) null));

      topToolbar_.addLeftSeparator();

      revertFilesButton_ = topToolbar_.addLeftWidget(new ToolbarButton(
            "Revert",
            commands.vcsRevert().getImageResource(),
            (ClickHandler) null));

      ignoreButton_ = topToolbar_.addLeftWidget(new ToolbarButton(
            "Ignore", RES.ignore(), (ClickHandler) null));

      
      topToolbar_.addRightWidget(commands.vcsPull().createToolbarButton());

      topToolbar_.addRightSeparator();

      topToolbar_.addRightWidget(commands.vcsPush().createToolbarButton());
  
      diffToolbar_.addStyleName(RES.styles().toolbar());
      diffToolbar_.addStyleName(RES.styles().diffToolbar());

      toolbarWrapper_.setCellWidth(diffToolbar_, "100%");

      stageAllButton_ = diffToolbar_.addLeftWidget(new ToolbarButton(
            "Stage All", RES.stage(), (ClickHandler) null));
      diffToolbar_.addLeftSeparator();
      discardAllButton_ = diffToolbar_.addLeftWidget(new ToolbarButton(
            "Discard All", RES.discard(), (ClickHandler) null));

      unstageAllButton_ = diffToolbar_.addLeftWidget(new ToolbarButton(
            "Unstage All", RES.discard(), (ClickHandler) null));
      unstageAllButton_.setVisible(false);

      unstagedCheckBox_.addValueChangeHandler(new ValueChangeHandler<Boolean>()
      {
         @Override
         public void onValueChange(ValueChangeEvent<Boolean> e)
         {
            ValueChangeEvent.fire(stagedCheckBox_, stagedCheckBox_.getValue());
         }
      });

      stagedCheckBox_.addValueChangeHandler(new ValueChangeHandler<Boolean>()
      {
         @Override
         public void onValueChange(ValueChangeEvent<Boolean> e)
         {
            stageAllButton_.setVisible(!e.getValue());
            discardAllButton_.setVisible(!e.getValue());
            unstageAllButton_.setVisible(e.getValue());
            diffToolbar_.invalidateSeparators();
         }
      });
      
      commitMessage_.getElement().setAttribute("spellcheck", "false");

      listBoxAdapter_ = new ListBoxAdapter(contextLines_);

      FontSizer.applyNormalFontSize(commitMessage_);

      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 scoll height.
      return Math.max(
            getLineScroll(panel),
            panel.getElement().getClientHeight() - getLineScroll(panel));
   }

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

   @Override
   public HasClickHandlers getStageFilesButton()
   {
      return stageFilesButton_;
   }

   @Override
   public HasClickHandlers getRevertFilesButton()
   {
      return revertFilesButton_;
   }
   
   @Override
   public void setFilesCommandsEnabled(boolean enabled)
   {
      stageFilesButton_.setEnabled(enabled);
      revertFilesButton_.setEnabled(enabled);
      ignoreButton_.setEnabled(enabled);
      
   }

   @Override
   public HasClickHandlers getIgnoreButton()
   {
      return ignoreButton_;
   }

   @Override
   public HasClickHandlers getStageAllButton()
   {
      return stageAllButton_;
   }

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

   @Override
   public HasClickHandlers getUnstageAllButton()
   {
      return unstageAllButton_;
   }

   @Override
   public void setShowActions(boolean showActions)
   {
      diffToolbar_.setVisible(showActions);
      lines_.setShowActions(showActions);
   }

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

      getLineTableDisplay().setData(lines, patchMode);

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

   @Override
   public HasText getCommitMessage()
   {
      return commitMessage_;
   }

   @Override
   public HasClickHandlers getCommitButton()
   {
      return commitButton_;
   }

   @Override
   public HasValue<Boolean> getCommitIsAmend()
   {
      return commitIsAmend_;
   }

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

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

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

   @Override
   public HasValue<Boolean> getStagedCheckBox()
   {
      return stagedCheckBox_;
   }

   @Override
   public HasValue<Boolean> getUnstagedCheckBox()
   {
      return unstagedCheckBox_;
   }

   @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,
                               Command openSelectedCommand)
   {
      final ToolbarPopupMenu menu = new ToolbarPopupMenu();
      
      MenuItem stageMenu = new MenuItem(
           AppCommand.formatMenuLabel(RES.stage(), "Stage", ""),
           true,
           new Command() {
              @Override
              public void execute()
              {
                 stageFilesButton_.click();
              }
              
           });
      if (stageFilesButton_.isEnabled())
      {
         menu.addItem(stageMenu);
         menu.addSeparator();
      }
     
    
      
     MenuItem revertMenu = new MenuItem(
           AppCommand.formatMenuLabel(RES.discard(), "Revert...", ""),
           true,
           new Command() {
              @Override
              public void execute()
              {
                 revertFilesButton_.click();
              }
              
           });
      if (revertFilesButton_.isEnabled())
         menu.addItem(revertMenu);
      
      MenuItem ignoreMenu = new MenuItem(
            AppCommand.formatMenuLabel(RES.ignore(), "Ignore...", ""),
            true,
            new Command() {
               @Override
               public void execute()
               {
                  ignoreButton_.click();
               }
               
            });
       if (ignoreButton_.isEnabled())
          menu.addItem(ignoreMenu);
     
      menu.addSeparator();
      MenuItem openMenu = new MenuItem(
                           AppCommand.formatMenuLabel(null, "Open File", ""),
                           true, 
                           openSelectedCommand);
      menu.addItem(openMenu);
     
      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)
   SplitLayoutPanel splitPanelCommit_;
   @UiField(provided = true)
   ChangelistTable changelist_;
   @UiField(provided = true)
   ThemedButton commitButton_;
   @UiField
   RadioButton stagedCheckBox_;
   @UiField
   RadioButton unstagedCheckBox_;
   @UiField(provided = true)
   LineTableView lines_;
   @UiField
   ListBox contextLines_;
   @UiField
   Toolbar topToolbar_;
   @UiField
   Toolbar diffToolbar_;
   @UiField
   TextArea commitMessage_;
   @UiField
   CheckBox commitIsAmend_;
   @UiField
   ScrollPanel diffScroll_;
   @UiField
   FlowPanel diffViewOptions_;
   @UiField
   HorizontalPanel toolbarWrapper_;

   private ListBoxAdapter listBoxAdapter_;

   private ToolbarButton stageFilesButton_;
   private ToolbarButton revertFilesButton_;
   private ToolbarButton ignoreButton_;
   private ToolbarButton stageAllButton_;
   private ToolbarButton discardAllButton_;
   private ToolbarButton unstageAllButton_;
   @SuppressWarnings("unused")
   private final FileTypeRegistry fileTypeRegistry_;
   private LeftRightToggleButton switchViewButton_;

   private SizeWarningWidget overrideSizeWarning_;

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