/*
 * Copyright 2000-2016 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.codeInspection.ex;

import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInspection.CommonProblemDescriptor;
import com.intellij.codeInspection.InspectionManager;
import com.intellij.codeInspection.InspectionsBundle;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.reference.RefElement;
import com.intellij.codeInspection.reference.RefEntity;
import com.intellij.codeInspection.reference.RefManagerImpl;
import com.intellij.codeInspection.ui.InspectionResultsView;
import com.intellij.codeInspection.ui.InspectionTree;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.CustomComponentAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.ui.ClickListener;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.util.SequentialModalProgressTask;
import com.intellij.util.ui.UIUtil;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import java.awt.event.MouseEvent;
import java.util.*;

/**
 * @author max
 */
public class QuickFixAction extends AnAction implements CustomComponentAction {
  private static final Logger LOG = Logger.getInstance("#" + QuickFixAction.class.getName());

  public static final QuickFixAction[] EMPTY = new QuickFixAction[0];
  protected final InspectionToolWrapper myToolWrapper;

  protected static InspectionResultsView getInvoker(AnActionEvent e) {
    return InspectionResultsView.DATA_KEY.getData(e.getDataContext());
  }

  protected QuickFixAction(String text, @NotNull InspectionToolWrapper toolWrapper) {
    this(text, AllIcons.Actions.CreateFromUsage, null, toolWrapper);
  }

  protected QuickFixAction(String text, Icon icon, KeyStroke keyStroke, @NotNull InspectionToolWrapper toolWrapper) {
    super(text, null, icon);
    myToolWrapper = toolWrapper;
    if (keyStroke != null) {
      registerCustomShortcutSet(new CustomShortcutSet(keyStroke), null);
    }
  }

  @Override
  public void update(AnActionEvent e) {
    final InspectionResultsView view = getInvoker(e);
    if (view == null) {
      e.getPresentation().setEnabled(false);
      return;
    }

    e.getPresentation().setVisible(false);
    e.getPresentation().setEnabled(false);

    final InspectionTree tree = view.getTree();
    final InspectionToolWrapper toolWrapper = tree.getSelectedToolWrapper(true);
    if (!view.isSingleToolInSelection() || toolWrapper != myToolWrapper) {
      return;
    }

    if (!isProblemDescriptorsAcceptable() && tree.getSelectedElements().length > 0 ||
        isProblemDescriptorsAcceptable() && tree.getSelectedDescriptors().length > 0) {
      e.getPresentation().setVisible(true);
      e.getPresentation().setEnabled(true);
    }
  }

  protected boolean isProblemDescriptorsAcceptable() {
    return false;
  }

  public String getText() {
    return getTemplatePresentation().getText();
  }

  @Override
  public void actionPerformed(final AnActionEvent e) {
    final InspectionResultsView view = getInvoker(e);
    final InspectionTree tree = view.getTree();
    try {
      Ref<CommonProblemDescriptor[]> descriptors = Ref.create();
      Set<VirtualFile> readOnlyFiles = new THashSet<>();
      if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> ReadAction.run(() -> {
        final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
        indicator.setText("Checking problem descriptors...");
        descriptors.set(tree.getSelectedDescriptors(true, readOnlyFiles, false, false));
      }), InspectionsBundle.message("preparing.for.apply.fix"), true, e.getProject())) {
        return;
      }
      if (isProblemDescriptorsAcceptable() && descriptors.get().length > 0) {
        doApplyFix(view.getProject(), descriptors.get(), readOnlyFiles, tree.getContext());
      } else {
        doApplyFix(getSelectedElements(view), view);
      }
    } finally {
      view.setApplyingFix(false);
    }
  }


  protected void applyFix(@NotNull Project project,
                          @NotNull GlobalInspectionContextImpl context,
                          @NotNull CommonProblemDescriptor[] descriptors,
                          @NotNull Set<PsiElement> ignoredElements) {
  }

  private void doApplyFix(@NotNull final Project project,
                          @NotNull final CommonProblemDescriptor[] descriptors,
                          @NotNull Set<VirtualFile> readOnlyFiles,
                          @NotNull final GlobalInspectionContextImpl context) {
    if (!FileModificationService.getInstance().prepareVirtualFilesForWrite(project, readOnlyFiles)) return;
    
    final RefManagerImpl refManager = (RefManagerImpl)context.getRefManager();
    final boolean initial = refManager.isInProcess();

    refManager.inspectionReadActionFinished();

    try {
      final Set<PsiElement> ignoredElements = new HashSet<>();
      performFixesInBatch(project, descriptors, context, ignoredElements);

      refreshViews(project, ignoredElements, myToolWrapper);
    }
    finally { //to make offline view lazy
      if (initial) refManager.inspectionReadActionStarted();
    }
  }

  protected void performFixesInBatch(@NotNull Project project,
                                     @NotNull CommonProblemDescriptor[] descriptors,
                                     @NotNull GlobalInspectionContextImpl context,
                                     Set<PsiElement> ignoredElements) {
    final String templatePresentationText = getTemplatePresentation().getText();
    assert templatePresentationText != null;
    CommandProcessor.getInstance().executeCommand(project, () -> {
      CommandProcessor.getInstance().markCurrentCommandAsGlobal(project);
      final SequentialModalProgressTask progressTask =
        new SequentialModalProgressTask(project, templatePresentationText, true);
      progressTask.setMinIterationTime(200);
      progressTask.setTask(new PerformFixesTask(project, descriptors, ignoredElements, progressTask, context));
      ProgressManager.getInstance().run(progressTask);
    }, templatePresentationText, null);
  }

  private void doApplyFix(@NotNull final RefEntity[] refElements, @NotNull InspectionResultsView view) {
    final RefManagerImpl refManager = (RefManagerImpl)view.getGlobalInspectionContext().getRefManager();

    final boolean initial = refManager.isInProcess();

    refManager.inspectionReadActionFinished();

    try {
      final boolean[] refreshNeeded = {false};
      if (refElements.length > 0) {
        final Project project = refElements[0].getRefManager().getProject();
        CommandProcessor.getInstance().executeCommand(project, () -> {
          CommandProcessor.getInstance().markCurrentCommandAsGlobal(project);
          ApplicationManager.getApplication().runWriteAction(() -> {
            refreshNeeded[0] = applyFix(refElements);
          });
        }, getTemplatePresentation().getText(), null);
      }
      if (refreshNeeded[0]) {
        refreshViews(view.getProject(), refElements, myToolWrapper);
      }
    }
    finally {  //to make offline view lazy
      if (initial) refManager.inspectionReadActionStarted();
    }
  }

  public static void removeElements(@NotNull RefEntity[] refElements, @NotNull Project project, @NotNull InspectionToolWrapper toolWrapper) {
    refreshViews(project, refElements, toolWrapper);
    final ArrayList<RefElement> deletedRefs = new ArrayList<>(1);
    for (RefEntity refElement : refElements) {
      if (!(refElement instanceof RefElement)) continue;
      refElement.getRefManager().removeRefElement((RefElement)refElement, deletedRefs);
    }
  }

  private static Set<VirtualFile> getReadOnlyFiles(@NotNull RefEntity[] refElements) {
    Set<VirtualFile> readOnlyFiles = new THashSet<>();
    for (RefEntity refElement : refElements) {
      PsiElement psiElement = refElement instanceof RefElement ? ((RefElement)refElement).getElement() : null;
      if (psiElement == null || psiElement.getContainingFile() == null) continue;
      readOnlyFiles.add(psiElement.getContainingFile().getVirtualFile());
    }
    return readOnlyFiles;
  }

  private static RefEntity[] getSelectedElements(InspectionResultsView view) {
    if (view == null) return new RefElement[0];
    List<RefEntity> selection = new ArrayList<>(Arrays.asList(view.getTree().getSelectedElements()));
    PsiDocumentManager.getInstance(view.getProject()).commitAllDocuments();
    Collections.sort(selection, (o1, o2) -> {
      if (o1 instanceof RefElement && o2 instanceof RefElement) {
        RefElement r1 = (RefElement)o1;
        RefElement r2 = (RefElement)o2;
        final PsiElement element1 = r1.getElement();
        final PsiElement element2 = r2.getElement();
        final PsiFile containingFile1 = element1.getContainingFile();
        final PsiFile containingFile2 = element2.getContainingFile();
        if (containingFile1 == containingFile2) {
          int i1 = element1.getTextOffset();
          int i2 = element2.getTextOffset();
          if (i1 < i2) {
            return 1;
          }
          if (i1 > i2){
            return -1;
          }
          return 0;
        }
        return containingFile1.getName().compareTo(containingFile2.getName());
      }
      if (o1 instanceof RefElement) {
        return 1;
      }
      if (o2 instanceof RefElement) {
        return -1;
      }
      return o1.getName().compareTo(o2.getName());
    });

    return selection.toArray(new RefEntity[selection.size()]);
  }

  private static void refreshViews(@NotNull Project project, @NotNull Set<PsiElement> selectedElements, @NotNull InspectionToolWrapper toolWrapper) {
    InspectionManagerEx managerEx = (InspectionManagerEx)InspectionManager.getInstance(project);
    final Set<GlobalInspectionContextImpl> runningContexts = managerEx.getRunningContexts();
    for (GlobalInspectionContextImpl context : runningContexts) {
      for (PsiElement element : selectedElements) {
        context.ignoreElement(toolWrapper.getTool(), element);
      }
      context.refreshViews();
    }
  }

  protected static void refreshViews(@NotNull Project project, @NotNull RefEntity[] refElements, @NotNull InspectionToolWrapper toolWrapper) {
    final Set<PsiElement> ignoredElements = new HashSet<>();
    for (RefEntity element : refElements) {
      final PsiElement psiElement = element instanceof RefElement ? ((RefElement)element).getElement() : null;
      if (psiElement != null && psiElement.isValid()) {
        ignoredElements.add(psiElement);
      }
    }
    refreshViews(project, ignoredElements, toolWrapper);
  }

  /**
   * @return true if immediate UI update needed.
   */
  protected boolean applyFix(@NotNull RefEntity[] refElements) {
    Set<VirtualFile> readOnlyFiles = getReadOnlyFiles(refElements);
    if (!readOnlyFiles.isEmpty()) {
      final Project project = refElements[0].getRefManager().getProject();
      final ReadonlyStatusHandler.OperationStatus operationStatus = ReadonlyStatusHandler.getInstance(project).ensureFilesWritable(
        VfsUtilCore.toVirtualFileArray(readOnlyFiles));
      if (operationStatus.hasReadonlyFiles()) return false;
    }
    return true;
  }

  @Override
  public JComponent createCustomComponent(Presentation presentation) {
    final JButton button = new JButton(presentation.getText());
    Icon icon = presentation.getIcon();
    if (icon == null) {
      icon = AllIcons.Actions.CreateFromUsage;
    }
    button.setEnabled(presentation.isEnabled());
    button.setIcon(IconLoader.getTransparentIcon(icon, 0.75f));
    new ClickListener() {
      @Override
      public boolean onClick(@NotNull MouseEvent event, int clickCount) {
        final ActionToolbar toolbar = UIUtil.getParentOfType(ActionToolbar.class, button);
        actionPerformed(AnActionEvent.createFromAnAction(QuickFixAction.this,
                                                         event,
                                                         ActionPlaces.UNKNOWN,
                                                         toolbar == null ? DataManager.getInstance().getDataContext(button) : toolbar.getToolbarDataContext()));
        return true;
      }
    }.installOn(button);
    JPanel panel = new JPanel();
    panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
    panel.setBorder(IdeBorderFactory.createEmptyBorder(7, 0, 8, 0));
    panel.add(button);
    return panel;
  }

  private class PerformFixesTask extends PerformFixesModalTask {
    @NotNull private final GlobalInspectionContextImpl myContext;
    @NotNull
    private final Set<PsiElement> myIgnoredElements;

    PerformFixesTask(@NotNull Project project,
                     @NotNull CommonProblemDescriptor[] descriptors,
                     @NotNull Set<PsiElement> ignoredElements,
                     @NotNull SequentialModalProgressTask task,
                     @NotNull GlobalInspectionContextImpl context) {
      super(project, descriptors, task);
      myContext = context;
      myIgnoredElements = ignoredElements;
    }

    @Override
    protected void applyFix(Project project, CommonProblemDescriptor descriptor) {
      if (descriptor instanceof ProblemDescriptor && 
          ((ProblemDescriptor)descriptor).getStartElement() == null &&
          ((ProblemDescriptor)descriptor).getEndElement() == null) {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Invalidated psi for " + descriptor);
        }
        return;
      }

      try {
        QuickFixAction.this.applyFix(myProject, myContext, new CommonProblemDescriptor[]{descriptor}, myIgnoredElements);
      }
      catch (ProcessCanceledException e) {
        throw e;
      }
      catch (Throwable e) {
        LOG.error(e);
      }
    }
  }
}
