/*
 * 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.diff.tools.util.base;

import com.intellij.diff.DiffContext;
import com.intellij.diff.contents.DiffContent;
import com.intellij.diff.contents.DocumentContent;
import com.intellij.diff.contents.EmptyContent;
import com.intellij.diff.requests.ContentDiffRequest;
import com.intellij.diff.tools.util.FoldingModelSupport;
import com.intellij.diff.tools.util.base.TextDiffSettingsHolder.TextDiffSettings;
import com.intellij.diff.util.DiffUserDataKeys;
import com.intellij.diff.util.DiffUserDataKeysEx;
import com.intellij.diff.util.DiffUtil;
import com.intellij.icons.AllIcons;
import com.intellij.internal.statistic.UsageTrigger;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ComboBoxAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.actions.EditorActionUtil;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.editor.event.EditorMouseEvent;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.util.Condition;
import com.intellij.ui.ToggleActionButton;
import com.intellij.util.ArrayUtil;
import com.intellij.util.EditorPopupHandler;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.intellij.util.containers.ContainerUtil.list;

public class TextDiffViewerUtil {
  private static final Logger LOG = Logger.getInstance(TextDiffViewerUtil.class);

  @NotNull
  public static List<AnAction> createEditorPopupActions() {
    List<AnAction> result = new ArrayList<>();
    result.add(ActionManager.getInstance().getAction("CompareClipboardWithSelection"));

    result.add(Separator.getInstance());
    ContainerUtil.addAll(result, ((ActionGroup)ActionManager.getInstance().getAction(IdeActions.GROUP_DIFF_EDITOR_POPUP)).getChildren(null));

    return result;
  }

  @NotNull
  public static FoldingModelSupport.Settings getFoldingModelSettings(@NotNull DiffContext context) {
    TextDiffSettings settings = getTextSettings(context);
    return new FoldingModelSupport.Settings(settings.getContextRange(), settings.isExpandByDefault());
  }

  @NotNull
  public static TextDiffSettings getTextSettings(@NotNull DiffContext context) {
    TextDiffSettings settings = context.getUserData(TextDiffSettings.KEY);
    if (settings == null) {
      settings = TextDiffSettings.getSettings(context.getUserData(DiffUserDataKeys.PLACE));
      context.putUserData(TextDiffSettings.KEY, settings);
      if (DiffUtil.isUserDataFlagSet(DiffUserDataKeys.DO_NOT_IGNORE_WHITESPACES, context)) {
        settings.setIgnorePolicy(IgnorePolicy.DEFAULT);
      }
    }
    return settings;
  }

  @NotNull
  public static boolean[] checkForceReadOnly(@NotNull DiffContext context, @NotNull ContentDiffRequest request) {
    int contentCount = request.getContents().size();
    boolean[] result = new boolean[contentCount];

    if (DiffUtil.isUserDataFlagSet(DiffUserDataKeys.FORCE_READ_ONLY, request, context)) {
      Arrays.fill(result, true);
      return result;
    }

    boolean[] data = request.getUserData(DiffUserDataKeys.FORCE_READ_ONLY_CONTENTS);
    if (data != null && data.length == contentCount) {
      return data;
    }

    return result;
  }

  public static void installDocumentListeners(@NotNull DocumentListener listener,
                                              @NotNull List<Document> documents,
                                              @NotNull Disposable disposable) {
    for (Document document : ContainerUtil.newHashSet(documents)) {
      document.addDocumentListener(listener, disposable);
    }
  }

  public static void checkDifferentDocuments(@NotNull ContentDiffRequest request) {
    // Actually, this should be a valid case. But it has little practical sense and will require explicit checks everywhere.
    // Some listeners will be processed once instead of 2 times, some listeners will cause illegal document modifications.
    List<DiffContent> contents = request.getContents();

    boolean sameDocuments = false;
    for (int i = 0; i < contents.size(); i++) {
      for (int j = i + 1; j < contents.size(); j++) {
        DiffContent content1 = contents.get(i);
        DiffContent content2 = contents.get(j);
        if (!(content1 instanceof DocumentContent)) continue;
        if (!(content2 instanceof DocumentContent)) continue;
        sameDocuments |= ((DocumentContent)content1).getDocument() == ((DocumentContent)content2).getDocument();
      }
    }

    if (sameDocuments) {
      StringBuilder message = new StringBuilder();
      message.append("DiffRequest with same documents detected\n");
      message.append(request.toString()).append("\n");
      for (DiffContent content : contents) {
        message.append(content.toString()).append("\n");
      }
      LOG.warn(new Throwable(message.toString()));
    }
  }

  public static boolean areEqualLineSeparators(@NotNull List<? extends DiffContent> contents) {
    return areEqualDocumentContentProperties(contents, DocumentContent::getLineSeparator);
  }

  public static boolean areEqualCharsets(@NotNull List<? extends DiffContent> contents) {
    boolean sameCharset = areEqualDocumentContentProperties(contents, DocumentContent::getCharset);
    boolean sameBOM = areEqualDocumentContentProperties(contents, DocumentContent::hasBom);
    return sameCharset && sameBOM;
  }

  private static <T> boolean areEqualDocumentContentProperties(@NotNull List<? extends DiffContent> contents,
                                                               @NotNull Function<DocumentContent, T> propertyGetter) {
    List<T> properties = ContainerUtil.mapNotNull(contents, (content) -> {
      if (content instanceof EmptyContent) return null;
      return propertyGetter.fun((DocumentContent)content);
    });

    if (properties.size() < 2) return true;
    return ContainerUtil.newHashSet(properties).size() == 1;
  }

  //
  // Actions
  //

  public static abstract class ComboBoxSettingAction<T> extends ComboBoxAction implements DumbAware {
    private DefaultActionGroup myActions;

    @Override
    public void update(AnActionEvent e) {
      Presentation presentation = e.getPresentation();
      presentation.setText(getText(getValue()));
    }

    @NotNull
    @Override
    protected DefaultActionGroup createPopupActionGroup(JComponent button) {
      return getActions();
    }

    @NotNull
    public DefaultActionGroup getActions() {
      if (myActions == null) {
        myActions = new DefaultActionGroup();
        for (T setting : getAvailableOptions()) {
          myActions.add(new MyAction(setting));
        }
      }
      return myActions;
    }

    @NotNull
    protected abstract List<T> getAvailableOptions();

    @NotNull
    protected abstract T getValue();

    protected abstract void setValue(@NotNull T option);

    @NotNull
    protected abstract String getText(@NotNull T option);

    private class MyAction extends AnAction implements DumbAware {
      @NotNull private final T myOption;

      public MyAction(@NotNull T option) {
        super(getText(option));
        myOption = option;
      }

      @Override
      public void actionPerformed(@NotNull AnActionEvent e) {
        setValue(myOption);
      }
    }
  }

  private static abstract class EnumPolicySettingAction<T extends Enum> extends TextDiffViewerUtil.ComboBoxSettingAction<T> {
    @NotNull private final T[] myPolicies;

    public EnumPolicySettingAction(@NotNull T[] policies) {
      assert policies.length > 0;
      myPolicies = policies;
    }

    @Override
    public void update(AnActionEvent e) {
      super.update(e);
      e.getPresentation().setEnabledAndVisible(myPolicies.length > 1);
    }

    @NotNull
    @Override
    protected List<T> getAvailableOptions() {
      //noinspection unchecked
      return ContainerUtil.sorted(Arrays.asList(myPolicies));
    }

    @NotNull
    @Override
    public T getValue() {
      T value = getStoredValue();
      if (ArrayUtil.contains(value, myPolicies)) return value;

      List<T> substitutes = getValueSubstitutes(value);
      for (T substitute : substitutes) {
        if (ArrayUtil.contains(substitute, myPolicies)) return substitute;
      }

      return myPolicies[0];
    }

    @NotNull
    protected abstract T getStoredValue();

    @NotNull
    protected abstract List<T> getValueSubstitutes(@NotNull T value);
  }

  public static class HighlightPolicySettingAction extends EnumPolicySettingAction<HighlightPolicy> {
    @NotNull protected final TextDiffSettings mySettings;

    public HighlightPolicySettingAction(@NotNull TextDiffSettings settings,
                                        @NotNull HighlightPolicy... policies) {
      super(policies);
      mySettings = settings;
    }

    @Override
    protected void setValue(@NotNull HighlightPolicy option) {
      if (getValue() == option) return;
      UsageTrigger.trigger("diff.TextDiffSettings.HighlightPolicy." + option.name());
      mySettings.setHighlightPolicy(option);
    }

    @NotNull
    @Override
    protected HighlightPolicy getStoredValue() {
      return mySettings.getHighlightPolicy();
    }

    @NotNull
    @Override
    protected List<HighlightPolicy> getValueSubstitutes(@NotNull HighlightPolicy value) {
      if (value == HighlightPolicy.BY_WORD_SPLIT) {
        return list(HighlightPolicy.BY_WORD);
      }
      if (value == HighlightPolicy.DO_NOT_HIGHLIGHT) {
        return list(HighlightPolicy.BY_LINE);
      }
      return list(HighlightPolicy.BY_WORD);
    }

    @NotNull
    @Override
    protected String getText(@NotNull HighlightPolicy option) {
      return option.getText();
    }
  }

  public static class IgnorePolicySettingAction extends EnumPolicySettingAction<IgnorePolicy> {
    @NotNull protected final TextDiffSettings mySettings;

    public IgnorePolicySettingAction(@NotNull TextDiffSettings settings,
                                     @NotNull IgnorePolicy... policies) {
      super(policies);
      mySettings = settings;
    }

    @Override
    protected void setValue(@NotNull IgnorePolicy option) {
      if (getValue() == option) return;
      UsageTrigger.trigger("diff.TextDiffSettings.IgnorePolicy." + option.name());
      mySettings.setIgnorePolicy(option);
    }

    @NotNull
    @Override
    protected IgnorePolicy getStoredValue() {
      return mySettings.getIgnorePolicy();
    }

    @NotNull
    @Override
    protected List<IgnorePolicy> getValueSubstitutes(@NotNull IgnorePolicy value) {
      if (value == IgnorePolicy.IGNORE_WHITESPACES_CHUNKS) {
        return list(IgnorePolicy.IGNORE_WHITESPACES);
      }
      if (value == IgnorePolicy.FORMATTING) {
        return list(IgnorePolicy.TRIM_WHITESPACES);
      }
      return list(IgnorePolicy.DEFAULT);
    }

    @NotNull
    @Override
    protected String getText(@NotNull IgnorePolicy option) {
      return option.getText();
    }
  }

  public static class ToggleAutoScrollAction extends ToggleActionButton implements DumbAware {
    @NotNull protected final TextDiffSettings mySettings;

    public ToggleAutoScrollAction(@NotNull TextDiffSettings settings) {
      super("Synchronize Scrolling", AllIcons.Actions.SynchronizeScrolling);
      mySettings = settings;
    }

    @Override
    public boolean isSelected(AnActionEvent e) {
      return mySettings.isEnableSyncScroll();
    }

    @Override
    public void setSelected(AnActionEvent e, boolean state) {
      mySettings.setEnableSyncScroll(state);
    }
  }

  public static abstract class ToggleExpandByDefaultAction extends ToggleActionButton implements DumbAware {
    @NotNull protected final TextDiffSettings mySettings;

    public ToggleExpandByDefaultAction(@NotNull TextDiffSettings settings) {
      super("Collapse unchanged fragments", AllIcons.Actions.Collapseall);
      mySettings = settings;
    }

    @Override
    public boolean isVisible() {
      return mySettings.getContextRange() != -1;
    }

    @Override
    public boolean isSelected(AnActionEvent e) {
      return !mySettings.isExpandByDefault();
    }

    @Override
    public void setSelected(AnActionEvent e, boolean state) {
      boolean expand = !state;
      if (mySettings.isExpandByDefault() == expand) return;
      mySettings.setExpandByDefault(expand);
      expandAll(expand);
    }

    protected abstract void expandAll(boolean expand);
  }

  public static abstract class ReadOnlyLockAction extends ToggleAction implements DumbAware {
    @NotNull protected final DiffContext myContext;
    @NotNull protected final TextDiffSettings mySettings;

    public ReadOnlyLockAction(@NotNull DiffContext context) {
      super("Disable editing", null, AllIcons.Nodes.Padlock);
      myContext = context;
      mySettings = getTextSettings(context);
    }

    protected void applyDefaults() {
      if (isVisible()) { // apply default state
        setSelected(null, isSelected(null));
      }
    }

    @Override
    public void update(@NotNull AnActionEvent e) {
      if (!isVisible()) {
        e.getPresentation().setEnabledAndVisible(false);
      }
      else {
        super.update(e);
      }
    }

    @Override
    public boolean isSelected(AnActionEvent e) {
      return mySettings.isReadOnlyLock();
    }

    @Override
    public void setSelected(AnActionEvent e, boolean state) {
      mySettings.setReadOnlyLock(state);
      doApply(state);
    }

    private boolean isVisible() {
      return myContext.getUserData(DiffUserDataKeysEx.SHOW_READ_ONLY_LOCK) == Boolean.TRUE && canEdit();
    }

    protected abstract void doApply(boolean readOnly);

    protected abstract boolean canEdit();
  }

  public static class EditorReadOnlyLockAction extends ReadOnlyLockAction {
    private final List<? extends EditorEx> myEditableEditors;

    public EditorReadOnlyLockAction(@NotNull DiffContext context, @NotNull List<? extends EditorEx> editableEditors) {
      super(context);
      myEditableEditors = editableEditors;
      applyDefaults();
    }

    @Override
    protected void doApply(boolean readOnly) {
      for (EditorEx editor : myEditableEditors) {
        editor.setViewer(readOnly);
      }
    }

    @Override
    protected boolean canEdit() {
      return !myEditableEditors.isEmpty();
    }
  }

  @NotNull
  public static List<? extends EditorEx> getEditableEditors(@NotNull List<? extends EditorEx> editors) {
    return ContainerUtil.filter(editors, new Condition<EditorEx>() {
      @Override
      public boolean value(EditorEx editor) {
        return !editor.isViewer();
      }
    });
  }

  public static class EditorFontSizeSynchronizer implements PropertyChangeListener {
    @NotNull private final List<? extends EditorEx> myEditors;

    private boolean myDuringUpdate = false;

    public EditorFontSizeSynchronizer(@NotNull List<? extends EditorEx> editors) {
      myEditors = editors;
    }

    public void install(@NotNull Disposable disposable) {
      if (myEditors.size() < 2) return;
      for (EditorEx editor : myEditors) {
        editor.addPropertyChangeListener(this, disposable);
      }
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
      if (myDuringUpdate) return;

      if (!EditorEx.PROP_FONT_SIZE.equals(evt.getPropertyName())) return;
      if (evt.getOldValue().equals(evt.getNewValue())) return;
      int fontSize = ((Integer)evt.getNewValue()).intValue();

      for (EditorEx editor : myEditors) {
        if (evt.getSource() != editor) updateEditor(editor, fontSize);
      }
    }

    public void updateEditor(@NotNull EditorEx editor, int fontSize) {
      try {
        myDuringUpdate = true;
        editor.setFontSize(fontSize);
      }
      finally {
        myDuringUpdate = false;
      }
    }
  }

  public static class EditorActionsPopup extends EditorPopupHandler {
    @NotNull private final List<? extends AnAction> myEditorPopupActions;

    public EditorActionsPopup(@NotNull List<? extends AnAction> editorPopupActions) {
      myEditorPopupActions = editorPopupActions;
    }

    public void install(@NotNull List<? extends EditorEx> editors) {
      for (EditorEx editor : editors) {
        editor.addEditorMouseListener(this);
        editor.setContextMenuGroupId(null); // disabling default context menu
      }
    }

    @Override
    public void invokePopup(final EditorMouseEvent event) {
      if (myEditorPopupActions.isEmpty()) return;
      ActionGroup group = new DefaultActionGroup(myEditorPopupActions);
      EditorPopupHandler handler = EditorActionUtil.createEditorPopupHandler(group);
      handler.invokePopup(event);
    }
  }
}
