/*
 * Copyright 2000-2015 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.openapi.diff.impl.dir;

import com.intellij.diff.DiffContentFactory;
import com.intellij.diff.chains.DiffRequestProducerException;
import com.intellij.diff.contents.DiffContent;
import com.intellij.diff.impl.CacheDiffRequestProcessor;
import com.intellij.diff.requests.DiffRequest;
import com.intellij.diff.requests.SimpleDiffRequest;
import com.intellij.diff.tools.util.DiffDataKeys;
import com.intellij.diff.tools.util.PrevNextDifferenceIterable;
import com.intellij.diff.util.DiffPlaces;
import com.intellij.diff.util.DiffUserDataKeysEx.ScrollToPolicy;
import com.intellij.ide.DataManager;
import com.intellij.ide.diff.DiffElement;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.diff.impl.dir.actions.DirDiffToolbarActions;
import com.intellij.openapi.diff.impl.dir.actions.RefreshDirDiffAction;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.pom.Navigatable;
import com.intellij.ui.ClickListener;
import com.intellij.ui.FilterComponent;
import com.intellij.ui.PopupHandler;
import com.intellij.ui.TableSpeedSearch;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBLoadingPanel;
import com.intellij.ui.components.JBLoadingPanelListener;
import com.intellij.ui.table.JBTable;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author Konstantin Bulenkov
 */
@SuppressWarnings({"unchecked"})
public class DirDiffPanel implements Disposable, DataProvider {
  private static final Logger LOG = Logger.getInstance(DirDiffPanel.class);

  public static final String DIVIDER_PROPERTY = "dir.diff.panel.divider.location";
  private static final int DIVIDER_PROPERTY_DEFAULT_VALUE = 200;
  private JPanel myDiffPanel;
  private JBTable myTable;
  private JPanel myComponent;
  private JSplitPane mySplitPanel;
  private TextFieldWithBrowseButton mySourceDirField;
  private TextFieldWithBrowseButton myTargetDirField;
  private JBLabel myTargetDirLabel;
  private JBLabel mySourceDirLabel;
  private JPanel myToolBarPanel;
  private JPanel myRootPanel;
  private JPanel myFilterPanel;
  private JBLabel myFilterLabel;
  private JPanel myFilesPanel;
  private JPanel myHeaderPanel;
  private FilterComponent myFilter;
  private final DirDiffTableModel myModel;
  private final DirDiffWindow myDiffWindow;
  private final MyDiffRequestProcessor myDiffRequestProcessor;
  private final PrevNextDifferenceIterable myPrevNextDifferenceIterable;
  private String oldFilter;
  public static final DataKey<DirDiffTableModel> DIR_DIFF_MODEL = DataKey.create("DIR_DIFF_MODEL");
  public static final DataKey<JTable> DIR_DIFF_TABLE = DataKey.create("DIR_DIFF_TABLE");

  public DirDiffPanel(DirDiffTableModel model, DirDiffWindow wnd) {
    myModel = model;
    myDiffWindow = wnd;
    mySourceDirField.setText(model.getSourceDir().getPath());
    myTargetDirField.setText(model.getTargetDir().getPath());
    mySourceDirField.setBorder(JBUI.Borders.emptyRight(8));
    myTargetDirField.setBorder(JBUI.Borders.emptyRight(12));
    mySourceDirLabel.setIcon(model.getSourceDir().getIcon());
    myTargetDirLabel.setIcon(model.getTargetDir().getIcon());
    myTargetDirLabel.setBorder(JBUI.Borders.emptyLeft(8));
    myModel.setTable(myTable);
    myModel.setPanel(this);
    Disposer.register(this, myModel);
    myTable.setModel(myModel);
    new TableSpeedSearch(myTable);

    final DirDiffTableCellRenderer renderer = new DirDiffTableCellRenderer();
    myTable.setExpandableItemsEnabled(false);
    myTable.setDefaultRenderer(Object.class, renderer);
    myTable.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
    final Project project = myModel.getProject();
    myTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
      @Override
      public void valueChanged(ListSelectionEvent e) {
        final int lastIndex = e.getLastIndex();
        final int firstIndex = e.getFirstIndex();
        final DirDiffElementImpl last = myModel.getElementAt(lastIndex);
        final DirDiffElementImpl first = myModel.getElementAt(firstIndex);
        if (last == null || first == null) {
          update(false);
          return;
        }
        if (last.isSeparator()) {
          final int ind = lastIndex + ((lastIndex < firstIndex) ? 1 : -1);
          myTable.getSelectionModel().addSelectionInterval(ind, ind);
        }
        else if (first.isSeparator()) {
          final int ind = firstIndex + ((firstIndex < lastIndex) ? 1 : -1);
          myTable.getSelectionModel().addSelectionInterval(ind, ind);
        }
        else {
          update(false);
        }
        myDiffWindow.setTitle(myModel.getTitle());
      }
    });
    if (model.isOperationsEnabled()) {
      new AnAction("Change diff operation") {
        @Override
        public void actionPerformed(AnActionEvent e) {
          changeOperationForSelection();
        }
      }.registerCustomShortcutSet(CustomShortcutSet.fromString("SPACE"), myTable);
      new ClickListener() {
        @Override
        public boolean onClick(@NotNull MouseEvent e, int clickCount) {
          if (e.getButton() == MouseEvent.BUTTON3) return false;
          if (myTable.getRowCount() > 0) {
            final int row = myTable.rowAtPoint(e.getPoint());
            final int col = myTable.columnAtPoint(e.getPoint());

            if (row != -1 && col == ((myTable.getColumnCount() - 1) / 2)) {
              changeOperationForSelection();
            }
          }
          return true;
        }
      }.installOn(myTable);
    }
    myTable.addKeyListener(new KeyAdapter() {
      @Override
      public void keyPressed(KeyEvent e) {
        final int keyCode = e.getKeyCode();

        int row;
        if (keyCode == KeyEvent.VK_DOWN) {
          row = getNextRow();
        }
        else if (keyCode == KeyEvent.VK_UP) {
          row = getPrevRow();
        }
        else {
          row = -1;
        }

        if (row != -1) {
          selectRow(row, e.isShiftDown());
          e.consume();
        }
      }
    });
    final TableColumnModel columnModel = myTable.getColumnModel();
    final TableColumn operationColumn = columnModel.getColumn((columnModel.getColumnCount() - 1) / 2);
    operationColumn.setMaxWidth(JBUI.scale(25));
    operationColumn.setMinWidth(JBUI.scale(25));
    for (int i = 0; i < columnModel.getColumnCount(); i++) {
      final String name = myModel.getColumnName(i);
      final TableColumn column = columnModel.getColumn(i);
      if (DirDiffTableModel.COLUMN_DATE.equals(name)) {
        column.setMaxWidth(JBUI.scale(90));
        column.setMinWidth(JBUI.scale(90));
      }
      else if (DirDiffTableModel.COLUMN_SIZE.equals(name)) {
        column.setMaxWidth(JBUI.scale(120));
        column.setMinWidth(JBUI.scale(120));
      }
    }
    final DirDiffToolbarActions actions = new DirDiffToolbarActions(myModel, myDiffPanel);
    final ActionManager actionManager = ActionManager.getInstance();
    final ActionToolbar toolbar = actionManager.createActionToolbar("DirDiff", actions, true);
    registerCustomShortcuts(actions, myTable);
    myToolBarPanel.add(toolbar.getComponent(), BorderLayout.CENTER);
    final JBLabel label = new JBLabel("Use Space button or mouse click to change operation for the selected elements." +
                                      " Enter to perform.", SwingConstants.CENTER);
    label.setForeground(UIUtil.getInactiveTextColor());
    UIUtil.applyStyle(UIUtil.ComponentStyle.MINI, label);
    DataManager.registerDataProvider(myFilesPanel, this);
    myTable.addMouseListener(new PopupHandler() {
      @Override
      public void invokePopup(Component comp, int x, int y) {
        final JPopupMenu popupMenu =
          actionManager.createActionPopupMenu("DirDiffPanel", (ActionGroup)actionManager.getAction("DirDiffMenu")).getComponent();
        popupMenu.show(comp, x, y);
      }
    });
    myFilesPanel.add(label, BorderLayout.SOUTH);
    final JBLoadingPanel loadingPanel = new JBLoadingPanel(new BorderLayout(), wnd.getDisposable());
    loadingPanel.addListener(new JBLoadingPanelListener.Adapter() {
      boolean showHelp = true;

      @Override
      public void onLoadingFinish() {
        if (showHelp && myModel.isOperationsEnabled() && myModel.getRowCount() > 0) {
          final long count = PropertiesComponent.getInstance().getOrInitLong("dir.diff.space.button.info", 0);
          if (count < 3) {
            JBPopupFactory.getInstance().createBalloonBuilder(new JLabel(" Use Space button to change operation"))
              .setFadeoutTime(5000)
              .setContentInsets(JBUI.insets(15))
              .createBalloon().show(new RelativePoint(myTable, new Point(myTable.getWidth() / 2, 0)), Balloon.Position.above);
            PropertiesComponent.getInstance().setValue("dir.diff.space.button.info", String.valueOf(count + 1));
          }
        }
        showHelp = false;
      }
    });
    loadingPanel.add(myComponent, BorderLayout.CENTER);
    myTable.putClientProperty(myModel.DECORATOR, loadingPanel);
    myTable.addComponentListener(new ComponentAdapter() {
      @Override
      public void componentShown(ComponentEvent e) {
        myTable.removeComponentListener(this);
        myModel.reloadModel(false);
      }
    });
    myRootPanel.removeAll();
    myRootPanel.add(loadingPanel, BorderLayout.CENTER);
    myFilter = new FilterComponent("dir.diff.filter", 15, false) {
      @Override
      public void filter() {
        fireFilterUpdated();
      }

      @Override
      protected void onEscape(KeyEvent e) {
        e.consume();
        focusTable();
      }

      @Override
      protected JComponent getPopupLocationComponent() {
        return UIUtil.findComponentOfType(super.getPopupLocationComponent(), JTextComponent.class);
      }
    };

    myModel.addModelListener(new DirDiffModelListener() {
      @Override
      public void updateStarted() {
        myFilter.setEnabled(false);
      }

      @Override
      public void updateFinished() {
        myFilter.setEnabled(true);
      }
    });
    myFilter.getTextEditor().setColumns(10);
    myFilter.setFilter(myModel.getSettings().getFilter());
    //oldFilter = myFilter.getText();
    oldFilter = myFilter.getFilter();
    myFilterPanel.add(myFilter, BorderLayout.CENTER);
    myFilterLabel.setLabelFor(myFilter);
    final Callable<DiffElement> srcChooser = myModel.getSourceDir().getElementChooser(project);
    final Callable<DiffElement> trgChooser = myModel.getTargetDir().getElementChooser(project);
    mySourceDirField.setEditable(false);
    myTargetDirField.setEditable(false);

    if (srcChooser != null && myModel.getSettings().enableChoosers) {
      mySourceDirField.setButtonEnabled(true);
      mySourceDirField.addActionListener(new AbstractAction() {
        @Override
        public void actionPerformed(ActionEvent e) {
          try {
            final Callable<DiffElement> chooser = myModel.getSourceDir().getElementChooser(project);
            if (chooser == null) return;
            final DiffElement newElement = chooser.call();
            if (newElement != null) {
              if (!StringUtil.equals(mySourceDirField.getText(), newElement.getPath())) {
                myModel.setSourceDir(newElement);
                mySourceDirField.setText(newElement.getPath());
                String shortcutsText = KeymapUtil.getShortcutsText(RefreshDirDiffAction.REFRESH_SHORTCUT.getShortcuts());
                myModel.clearWithMessage("Source or Target has been changed." +
                                         " Please run Refresh (" + shortcutsText + ")");
              }
            }
          }
          catch (Exception ignored) {
          }
        }
      });
    }
    else {
      Dimension preferredSize = mySourceDirField.getPreferredSize();
      mySourceDirField.setButtonEnabled(false);
      mySourceDirField.getButton().setVisible(false);
      mySourceDirField.setPreferredSize(preferredSize);
    }

    if (trgChooser != null && myModel.getSettings().enableChoosers) {
      myTargetDirField.setButtonEnabled(true);
      myTargetDirField.addActionListener(new AbstractAction() {
        @Override
        public void actionPerformed(ActionEvent e) {
          try {
            final Callable<DiffElement> chooser = myModel.getTargetDir().getElementChooser(project);
            if (chooser == null) return;
            final DiffElement newElement = chooser.call();
            if (newElement != null) {
              myModel.setTargetDir(newElement);
              myTargetDirField.setText(newElement.getPath());
            }
          }
          catch (Exception ignored) {
          }
        }
      });
    }
    else {
      Dimension preferredSize = myTargetDirField.getPreferredSize();
      myTargetDirField.setButtonEnabled(false);
      myTargetDirField.getButton().setVisible(false);
      myTargetDirField.setPreferredSize(preferredSize);
    }

    myDiffRequestProcessor = new MyDiffRequestProcessor(project);
    Disposer.register(this, myDiffRequestProcessor);
    myDiffPanel.add(myDiffRequestProcessor.getComponent(), BorderLayout.CENTER);

    myPrevNextDifferenceIterable = new MyPrevNextDifferenceIterable();
  }

  private int getNextRow() {
    if (myTable.getSelectedRows().length == 0) return -1;
    int rowCount = myTable.getRowCount();
    int row = myTable.getSelectionModel().getLeadSelectionIndex();

    while (true) {
      if (row >= rowCount) return -1;
      row++;
      DirDiffElementImpl element = myModel.getElementAt(row);
      if (element == null) return -1;
      if (!element.isSeparator()) break;
    }

    return row;
  }

  private int getPrevRow() {
    if (myTable.getSelectedRows().length == 0) return -1;
    int row = myTable.getSelectionModel().getLeadSelectionIndex();

    while (true) {
      if (row <= 0) return -1;
      row--;
      DirDiffElementImpl element = myModel.getElementAt(row);
      if (element == null) return -1;
      if (!element.isSeparator()) break;
    }

    return row;
  }

  private void selectRow(int row, boolean extend) {
    if (row == -1) return;
    DirDiffElementImpl element = myModel.getElementAt(row);
    if (element == null || element.isSeparator()) return;
    myTable.changeSelection(row, (myModel.getColumnCount() - 1) / 2, false, extend);
  }

  public AnAction[] getActions() {
    return new DirDiffToolbarActions(myModel, myDiffPanel).getChildren(null);
  }

  public JComponent extractFilterPanel() {
    myHeaderPanel.setVisible(false);
    return myFilterPanel;
  }

  private void changeOperationForSelection() {
    for (int row : myTable.getSelectedRows()) {
      if (row != -1) {
        final DirDiffElementImpl element = myModel.getElementAt(row);
        if (element != null) {
          element.setNextOperation();
          myModel.fireTableRowsUpdated(row, row);
        }
      }
    }
  }

  public void update(boolean force) {
    myDiffRequestProcessor.updateRequest(force);
  }

  private void registerCustomShortcuts(DirDiffToolbarActions actions, JComponent component) {
    for (AnAction action : actions.getChildren(null)) {
      if (action instanceof ShortcutProvider) {
        final ShortcutSet shortcut = ((ShortcutProvider)action).getShortcut();
        if (shortcut != null) {
          action.registerCustomShortcutSet(shortcut, component);
        }
      }
    }
  }

  public void focusTable() {
    final Project project = myModel.getProject();
    final IdeFocusManager focusManager = project == null || project.isDefault() ?
                                         IdeFocusManager.getGlobalInstance() :
                                         IdeFocusManager.getInstance(project);
    focusManager.requestFocus(myTable, true);
  }

  public String getFilter() {
    return myFilter.getFilter();
  }

  private void fireFilterUpdated() {
    final String newFilter = myFilter.getFilter();
    if (!StringUtil.equals(oldFilter, newFilter)) {
      oldFilter = newFilter;
      myModel.getSettings().setFilter(newFilter);
      myModel.applySettings();
    }
  }

  public JComponent getPanel() {
    return myRootPanel;
  }

  public JBTable getTable() {
    return myTable;
  }

  public void dispose() {
    myModel.stopUpdating();
    PropertiesComponent.getInstance().setValue(DIVIDER_PROPERTY, mySplitPanel.getDividerLocation(), DIVIDER_PROPERTY_DEFAULT_VALUE);
  }

  private void createUIComponents() {
    mySourceDirField = new TextFieldWithBrowseButton(null, this);
    myTargetDirField = new TextFieldWithBrowseButton(null, this);

    final AtomicBoolean callUpdate = new AtomicBoolean(true);
    myRootPanel = new JPanel(new BorderLayout()) {
      @Override
      protected void paintChildren(Graphics g) {
        super.paintChildren(g);
        if (callUpdate.get()) {
          callUpdate.set(false);
          myModel.reloadModel(false);
        }
      }
    };
  }

  public void setupSplitter() {
    int value = PropertiesComponent.getInstance().getInt(DIVIDER_PROPERTY, DIVIDER_PROPERTY_DEFAULT_VALUE);
    mySplitPanel.setDividerLocation(Integer.valueOf(value));
  }

  @Override
  public Object getData(@NonNls String dataId) {
    if (CommonDataKeys.PROJECT.is(dataId)) {
      return myModel.getProject();
    }
    else if (DIR_DIFF_MODEL.is(dataId)) {
      return myModel;
    }
    else if (DIR_DIFF_TABLE.is(dataId)) {
      return myTable;
    }
    else if (DiffDataKeys.NAVIGATABLE_ARRAY.is(dataId)) {
      return getNavigatableArray();
    }
    else if (DiffDataKeys.PREV_NEXT_DIFFERENCE_ITERABLE.is(dataId)) {
      return myPrevNextDifferenceIterable;
    }
    return null;
  }

  @Nullable
  private Navigatable[] getNavigatableArray() {
    Project project = myModel.getProject();
    List<DirDiffElementImpl> elements = myModel.getSelectedElements();
    List<Navigatable> navigatables = new ArrayList<>();
    for (DirDiffElementImpl element : elements) {
      DiffElement source = element.getSource();
      DiffElement target = element.getTarget();
      Navigatable navigatable1 = source != null ? source.getNavigatable(project) : null;
      Navigatable navigatable2 = target != null ? target.getNavigatable(project) : null;
      if (navigatable1 != null) navigatables.add(navigatable1);
      if (navigatable2 != null) navigatables.add(navigatable2);
    }
    return ContainerUtil.toArray(navigatables, new Navigatable[navigatables.size()]);
  }

  private class MyPrevNextDifferenceIterable implements PrevNextDifferenceIterable {
    @Override
    public boolean canGoPrev() {
      return getPrevRow() != -1;
    }

    @Override
    public boolean canGoNext() {
      return getNextRow() != -1;
    }

    @Override
    public void goPrev() {
      selectRow(getPrevRow(), false);
    }

    @Override
    public void goNext() {
      selectRow(getNextRow(), false);
    }
  }

  private class MyDiffRequestProcessor extends CacheDiffRequestProcessor<ElementWrapper> {
    public MyDiffRequestProcessor(@Nullable Project project) {
      super(project, DiffPlaces.DIR_DIFF);
    }

    @Nullable
    @Override
    protected String getRequestName(@NotNull ElementWrapper element) {
      return null;
    }

    @Override
    protected ElementWrapper getCurrentRequestProvider() {
      DirDiffElementImpl element = myModel.getElementAt(myTable.getSelectedRow());
      return element != null ? new ElementWrapper(element) : null;
    }

    @NotNull
    @Override
    protected DiffRequest loadRequest(@NotNull ElementWrapper element, @NotNull ProgressIndicator indicator)
      throws ProcessCanceledException, DiffRequestProducerException {
      final Project project = myModel.getProject();
      DiffElement sourceElement = element.sourceElement;
      DiffElement targetElement = element.targetElement;

      DiffContent sourceContent = sourceElement != null ? sourceElement.createDiffContent(project, indicator) :
                                  DiffContentFactory.getInstance().createEmpty();
      DiffContent targetContent = targetElement != null ? targetElement.createDiffContent(project, indicator) :
                                  DiffContentFactory.getInstance().createEmpty();

      return new SimpleDiffRequest(null, sourceContent, targetContent, null, null);
    }

    //
    // Navigation
    //

    @Override
    protected boolean hasNextChange() {
      return getNextRow() != -1;
    }

    @Override
    protected boolean hasPrevChange() {
      return getPrevRow() != -1;
    }

    @Override
    protected void goToNextChange(boolean fromDifferences) {
      selectRow(getNextRow(), false);
      updateRequest(false, fromDifferences ? ScrollToPolicy.FIRST_CHANGE : null);
    }

    @Override
    protected void goToPrevChange(boolean fromDifferences) {
      selectRow(getPrevRow(), false);
      updateRequest(false, fromDifferences ? ScrollToPolicy.LAST_CHANGE : null);
    }

    @Override
    protected boolean isNavigationEnabled() {
      return myModel.getRowCount() > 0;
    }
  }

  private static class ElementWrapper {
    @Nullable public final DiffElement sourceElement;
    @Nullable public final DiffElement targetElement;

    public ElementWrapper(@NotNull DirDiffElementImpl element) {
      sourceElement = element.getSource();
      targetElement = element.getTarget();
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      ElementWrapper wrapper = (ElementWrapper)o;

      if (sourceElement != null ? !sourceElement.equals(wrapper.sourceElement) : wrapper.sourceElement != null) return false;
      if (targetElement != null ? !targetElement.equals(wrapper.targetElement) : wrapper.targetElement != null) return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result = sourceElement != null ? sourceElement.hashCode() : 0;
      result = 31 * result + (targetElement != null ? targetElement.hashCode() : 0);
      return result;
    }
  }
}
