/*
 * Copyright 2000-2009 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.vcs.changes.actions;

import com.intellij.icons.AllIcons;
import com.intellij.idea.ActionsBundle;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.diff.*;
import com.intellij.openapi.diff.impl.external.BinaryDiffTool;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsBundle;
import com.intellij.openapi.vcs.VcsDataKeys;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.BeforeAfter;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.util.*;

/**
 * @author max
 */
public class ShowDiffAction extends AnAction implements DumbAware {
  private static final Logger LOG = Logger.getInstance(ShowDiffAction.class);

  private static final String ourText = ActionsBundle.actionText("ChangesView.Diff");

  public ShowDiffAction() {
    super(ourText,
          ActionsBundle.actionDescription("ChangesView.Diff"),
          AllIcons.Actions.Diff);
  }

  public void update(AnActionEvent e) {
    Change[] changes = e.getData(VcsDataKeys.CHANGES);
    Project project = e.getData(CommonDataKeys.PROJECT);
    e.getPresentation().setEnabled(project != null && canShowDiff(changes));
  }

  protected static boolean canShowDiff(Change[] changes) {
    if (changes == null || changes.length == 0) return false;
    for (Change change : changes) {
      if (!ChangesUtil.getFilePath(change).isDirectory() || change.hasOtherLayers()) return true;
    }
    return false;
  }

  public void actionPerformed(final AnActionEvent e) {
    final Project project = e.getData(CommonDataKeys.PROJECT);
    if (project == null) return;
    if (ChangeListManager.getInstance(project).isFreezedWithNotification(null)) return;
    final Change[] changes = e.getData(VcsDataKeys.CHANGES);
    if (changes == null) return;

    final boolean needsConvertion = checkIfThereAreFakeRevisions(project, changes);
    final List<Change> changesInList = e.getData(VcsDataKeys.CHANGES_IN_LIST_KEY);

    // this trick is essential since we are under some conditions to refresh changes;
    // but we can only rely on callback after refresh
    final Runnable performer = new Runnable() {
      public void run() {
        Change[] convertedChanges;
        if (needsConvertion) {
          convertedChanges = loadFakeRevisions(project, changes);
        } else {
          convertedChanges = changes;
        }

        if (convertedChanges == null || convertedChanges.length == 0) {
          return;
        }

        List<Change> changesInListCopy = changesInList;

        int index = 0;
        if (convertedChanges.length == 1) {
          final Change selectedChange = convertedChanges[0];
          ChangeList changeList = ((ChangeListManagerImpl) ChangeListManager.getInstance(project)).getIdentityChangeList(selectedChange);
          if (changeList != null) {
            if (changesInListCopy == null) {
              changesInListCopy = new ArrayList<Change>(changeList.getChanges());
              Collections.sort(changesInListCopy, new Comparator<Change>() {
                public int compare(final Change o1, final Change o2) {
                  return ChangesUtil.getFilePath(o1).getName().compareToIgnoreCase(ChangesUtil.getFilePath(o2).getName());
                }
              });
            }
            convertedChanges = changesInListCopy.toArray(new Change[changesInListCopy.size()]);
            index = Math.max(0, ContainerUtil.indexOfIdentity(changesInListCopy, selectedChange));
          }
        }

        showDiffForChange(convertedChanges, index, project);
      }
    };

    if (needsConvertion) {
      ChangeListManager.getInstance(project).invokeAfterUpdate(performer, InvokeAfterUpdateMode.BACKGROUND_CANCELLABLE, ourText, ModalityState.current());
    }  else {
      performer.run();
    }
  }

  public static void showDiffForChange(final Change[] changes, final int index, final Project project) {
    showDiffForChange(changes, index, project, new ShowDiffUIContext(true));
  }

  private static boolean checkIfThereAreFakeRevisions(final Project project, final Change[] changes) {
    boolean needsConversion = false;
    for(Change change: changes) {
      final ContentRevision beforeRevision = change.getBeforeRevision();
      final ContentRevision afterRevision = change.getAfterRevision();
      if (beforeRevision instanceof FakeRevision) {
        VcsDirtyScopeManager.getInstance(project).fileDirty(beforeRevision.getFile());
        needsConversion = true;
      }
      if (afterRevision instanceof FakeRevision) {
        VcsDirtyScopeManager.getInstance(project).fileDirty(afterRevision.getFile());
        needsConversion = true;
      }
    }
    return needsConversion;
  }

  @Nullable
  private static Change[] loadFakeRevisions(final Project project, final Change[] changes) {
    List<Change> matchingChanges = new ArrayList<Change>();
    for(Change change: changes) {
      matchingChanges.addAll(ChangeListManager.getInstance(project).getChangesIn(ChangesUtil.getFilePath(change)));
    }
    return matchingChanges.toArray(new Change[matchingChanges.size()]);
  }

  public static void showDiffForChange(final Iterable<Change> changes, final Condition<Change> selectionChecker,
                                       final Project project, @NotNull ShowDiffUIContext context) {
    int newIndex = -1;
    ChangeForDiffConvertor convertor = new ChangeForDiffConvertor(project, true);
    final List<DiffRequestPresentable> changeList = ContainerUtil.newArrayList();
    for (Change change : changes) {
      if (! directoryOrBinary(change)) {    //todo
        DiffRequestPresentable presentable = convertor.convert(change);
        if (presentable != null) {
          if ((newIndex == -1) && selectionChecker.value(change)) {
            newIndex = changeList.size();
          }
          changeList.add(presentable);
        }
      }
    }
    if (changeList.isEmpty()) {
      return;
    }
    if (newIndex < 0) {
      newIndex = 0;
    }

    showDiffImpl(project, changeList, newIndex, context);
  }

  public static void showDiffForChange(final Change[] changes, int index, final Project project, @NotNull ShowDiffUIContext context) {
    final Change selected = index >= 0 ? changes[index] : null;
    /*if (isBinaryDiff(project, changes, index)) {
      showBinaryDiff(project, changes[index]);
      return;
    }*/
    showDiffForChange(Arrays.asList(changes), new Condition<Change>() {
                        @Override
                        public boolean value(final Change change) {
                          return selected != null && selected.equals(change);
                        }
                      }, project, context);
  }

  private static FileContent createBinaryFileContent(final Project project, final ContentRevision contentRevision, final String fileName)
    throws VcsException, IOException {
    final FileContent fileContent;
    if (contentRevision == null) {
      fileContent = FileContent.createFromTempFile(project,
                                              fileName,
                                              fileName,
                                              ArrayUtil.EMPTY_BYTE_ARRAY);
    } else {
      byte[] content = ((BinaryContentRevision)contentRevision).getBinaryContent();
      fileContent = FileContent.createFromTempFile(project,
                                              contentRevision.getFile().getName(),
                                              contentRevision.getFile().getName(),
                                              content == null ? ArrayUtil.EMPTY_BYTE_ARRAY : content);
    }
    return fileContent;
  }

  private static SimpleDiffRequest createBinaryDiffRequest(final Project project, final Change change) throws VcsException {
    final FilePath filePath = ChangesUtil.getFilePath(change);
    final SimpleDiffRequest request = new SimpleDiffRequest(project, filePath.getPath());
    try {
      request.setContents(createBinaryFileContent(project, change.getBeforeRevision(), filePath.getName()),
                          createBinaryFileContent(project, change.getAfterRevision(), filePath.getName()));
      return request;
    }
    catch (IOException e) {
      throw new VcsException(e);
    }
  }

  public static BeforeAfter<DiffContent> createBinaryDiffContents(final Project project, final Change change) throws VcsException {
    final FilePath filePath = ChangesUtil.getFilePath(change);
    try {
      return new BeforeAfter<DiffContent>(createBinaryFileContent(project, change.getBeforeRevision(), filePath.getName()),
                          createBinaryFileContent(project, change.getAfterRevision(), filePath.getName()));
    }
    catch (IOException e) {
      throw new VcsException(e);
    }
  }

  private static void showBinaryDiff(Project project, Change change) {
    try {
      final SimpleDiffRequest request = createBinaryDiffRequest(project, change);
      if (DiffManager.getInstance().getDiffTool().canShow(request)) {
        DiffManager.getInstance().getDiffTool().show(request);
      }
    }
    catch (VcsException e) {
      Messages.showWarningDialog(e.getMessage(), "Show Diff");
      LOG.info(e);
    }
  }

  private static boolean isBinaryDiff(Project project, Change[] changes, int index) {
    if (index >= 0 && index < changes.length) {
      final Change change = changes[index];
      return isBinaryChangeAndCanShow(project, change);
    }
    return false;
  }

  public static boolean isBinaryChange(Change change) {
    if (change.hasOtherLayers()) return false;  // +-
    final ContentRevision bRev = change.getBeforeRevision();
    final ContentRevision aRev = change.getAfterRevision();

    return (aRev == null || aRev instanceof BinaryContentRevision)
           && (bRev == null || bRev instanceof BinaryContentRevision);
  }

  public static boolean isBinaryChangeAndCanShow(Project project, Change change) {
    // todo bug here would appear when there would be another diff providers for bimary revisions
    return isBinaryChange(change) && (change.getAfterRevision() == null || BinaryDiffTool.canShow(project, change.getVirtualFile()));
  }

  public static void showDiffImpl(final Project project, @NotNull List<DiffRequestPresentable> changeList, int index,
                                  @NotNull final ShowDiffUIContext context) {
    final ChangeDiffRequest request = new ChangeDiffRequest(project, changeList, context.getActionsFactory(), context.isShowFrame());
    final DiffTool tool = DiffManager.getInstance().getDiffTool();
    final DiffRequest simpleRequest;
    try {
      request.quickCheckHaveStuff();
      simpleRequest = request.init(index);
    }
    catch (VcsException e) {
      Messages.showWarningDialog(e.getMessage(), "Show Diff");
      LOG.info(e);
      return;
    }

    if (simpleRequest != null) {
      final DiffNavigationContext navigationContext = context.getDiffNavigationContext();
      if (navigationContext != null) {
        simpleRequest.passForDataContext(DiffTool.SCROLL_TO_LINE, navigationContext);
      }
      tool.show(simpleRequest);
    }
  }

  private static boolean directoryOrBinary(final Change change) {
    // todo instead for repository tab, filter directories (? ask remotely ? non leaf nodes)
    /*if ((change.getBeforeRevision() instanceof BinaryContentRevision) || (change.getAfterRevision() instanceof BinaryContentRevision)) {
      changesList.remove(i);
      continue;
    }*/
    final FilePath path = ChangesUtil.getFilePath(change);
    if (path.isDirectory()) {
      return ! change.hasOtherLayers();
    }
    /*final FileType type = path.getFileType();
    if ((! FileTypes.UNKNOWN.equals(type)) && (type.isBinary())) {
      return true;
    }*/
    return false;
  }

  private static boolean checkNotifyBinaryDiff(final Change selectedChange) {
    final ContentRevision beforeRevision = selectedChange.getBeforeRevision();
    final ContentRevision afterRevision = selectedChange.getAfterRevision();
    if (beforeRevision instanceof BinaryContentRevision &&
        afterRevision instanceof BinaryContentRevision) {
      try {
        byte[] beforeContent = ((BinaryContentRevision)beforeRevision).getBinaryContent();
        byte[] afterContent = ((BinaryContentRevision)afterRevision).getBinaryContent();
        if (Arrays.equals(beforeContent, afterContent)) {
          Messages.showInfoMessage(VcsBundle.message("message.text.binary.versions.are.identical"), VcsBundle.message("message.title.diff"));
        } else {
          Messages.showInfoMessage(VcsBundle.message("message.text.binary.versions.are.different"), VcsBundle.message("message.title.diff"));
        }
      }
      catch (VcsException e) {
        Messages.showInfoMessage(e.getMessage(), VcsBundle.message("message.title.diff"));
      }
      return true;
    }
    return false;
  }
}
