// Copyright 2008-2010 Victor Iacoban
//
// 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 org.zmlx.hg4idea.provider;

import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.annotate.ShowAllAffectedGenericAction;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.history.*;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ui.ColumnInfo;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.zmlx.hg4idea.HgBundle;
import org.zmlx.hg4idea.HgFile;
import org.zmlx.hg4idea.HgFileRevision;
import org.zmlx.hg4idea.HgRevisionNumber;
import org.zmlx.hg4idea.command.HgLogCommand;
import org.zmlx.hg4idea.command.HgWorkingCopyRevisionsCommand;
import org.zmlx.hg4idea.execution.HgCommandResult;
import org.zmlx.hg4idea.log.HgBaseLogParser;
import org.zmlx.hg4idea.log.HgFileRevisionLogParser;
import org.zmlx.hg4idea.log.HgHistoryUtil;
import org.zmlx.hg4idea.util.HgChangesetUtil;
import org.zmlx.hg4idea.util.HgUtil;
import org.zmlx.hg4idea.util.HgVersion;

import javax.swing.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static com.intellij.util.containers.ContainerUtil.getFirstItem;
import static java.util.Objects.requireNonNull;
import static org.zmlx.hg4idea.HgNotificationIdsHolder.LOG_CMD_EXEC_ERROR;

public class HgHistoryProvider implements VcsHistoryProvider {

  private final Project myProject;

  public HgHistoryProvider(Project project) {
    myProject = project;
  }

  @Override
  public VcsDependentHistoryComponents getUICustomization(VcsHistorySession session,
                                                          JComponent forShortcutRegistration) {
    return VcsDependentHistoryComponents.createOnlyColumns(ColumnInfo.EMPTY_ARRAY);
  }

  @Override
  public AnAction[] getAdditionalActions(Runnable runnable) {
    return new AnAction[]{ShowAllAffectedGenericAction.getInstance(),
      ActionManager.getInstance().getAction(VcsActions.ACTION_COPY_REVISION_NUMBER)};
  }

  @Override
  public boolean isDateOmittable() {
    return false;
  }

  @Override
  public String getHelpId() {
    return null;
  }

  @Override
  public VcsHistorySession createSessionFor(FilePath filePath) {
    final VirtualFile vcsRoot = VcsUtil.getVcsRootFor(myProject, filePath);
    if (vcsRoot == null) {
      return null;
    }
    final List<VcsFileRevision> revisions = new ArrayList<>(getHistory(filePath, vcsRoot, myProject));
    return createAppendableSession(vcsRoot, filePath, revisions,
                                   revisions.isEmpty() ? null : requireNonNull(getFirstItem(revisions)).getRevisionNumber());
  }

  @Override
  public void reportAppendableHistory(FilePath filePath, final VcsAppendableHistorySessionPartner partner) throws VcsException {
    final VirtualFile vcsRoot = HgUtil.getHgRootOrThrow(myProject, filePath);

    final List<HgFileRevision> history = getHistory(filePath, vcsRoot, myProject);
    if (history.isEmpty()) return;

    final VcsAbstractHistorySession emptySession = createAppendableSession(vcsRoot, filePath, Collections.emptyList(), null);
    partner.reportCreatedEmptySession(emptySession);

    for (HgFileRevision hgFileRevision : history) {
      partner.acceptRevision(hgFileRevision);
    }
  }

  private @NotNull VcsAbstractHistorySession createAppendableSession(@NotNull VirtualFile vcsRoot,
                                                                     @NotNull FilePath filePath,
                                                                     @NotNull List<VcsFileRevision> revisions,
                                                                     @Nullable VcsRevisionNumber number) {
    return new VcsAbstractHistorySession(revisions, number) {
      @Override
      protected @Nullable VcsRevisionNumber calcCurrentRevisionNumber() {
        if (filePath.isDirectory()) return new HgWorkingCopyRevisionsCommand(myProject).firstParent(vcsRoot);
        return new HgWorkingCopyRevisionsCommand(myProject).parents(vcsRoot, filePath).first;
      }

      @Override
      public VcsHistorySession copy() {
        return createAppendableSession(vcsRoot, filePath, getRevisionList(), getCurrentRevisionNumber());
      }
    };
  }

  public static List<HgFileRevision> getHistory(@NotNull FilePath filePath,
                                                @NotNull VirtualFile vcsRoot,
                                                @NotNull Project project) {
    VcsConfiguration vcsConfiguration = VcsConfiguration.getInstance(project);
    return getHistory(filePath, vcsRoot, project, null, vcsConfiguration.LIMIT_HISTORY ? vcsConfiguration.MAXIMUM_HISTORY_ROWS : -1);
  }


  public static List<HgFileRevision> getHistory(@NotNull FilePath filePath,
                                                @NotNull VirtualFile vcsRoot,
                                                @NotNull Project project,
                                                @Nullable HgRevisionNumber revisionNumber,
                                                int limit) {
    try {
      return getHistoryOrFail(filePath, vcsRoot, project, revisionNumber, limit);
    }
    catch (VcsException e) {
      VcsNotifier.getInstance(project).notifyError(LOG_CMD_EXEC_ERROR,
                                                   HgBundle.message("hg4idea.error.log.command.execution"),
                                                   e.getMessage());
      return Collections.emptyList();
    }
  }

  public static List<HgFileRevision> getHistoryOrFail(@NotNull FilePath filePath,
                                                      @NotNull VirtualFile vcsRoot,
                                                      @NotNull Project project,
                                                      @Nullable HgRevisionNumber revisionNumber,
                                                      int limit) throws VcsException {
 /*  The standard way to get history following renames is to call hg log --follow. However:
  1. It is broken in case of uncommitted rename (i.e. if the file is currently renamed in the working dir):
    in this case we use a special python template "follow(path)" which handles this case.
  2. We don't use this python "follow(path)" function for all cases, because it is fully supported only since hg 2.6,
   and it is a bit slower and possibly less reliable than plain --follow parameter.
  3. It doesn't work with "-r": in this case --follow is simply ignored (hg commit 24208:8b4b9ee6001a).
   As a workaround we could use the same follow(path) python, but this function requires current name of the file,
    which is unknown in case of "-r", and identifying it would be very slow.

    As a result we don't follow renames in annotate called from diff or from an old revision, which we can survive.
*/
    FilePath originalFilePath = HgUtil.getOriginalFileName(filePath, ChangeListManager.getInstance(project));
    if (revisionNumber == null && !filePath.isDirectory() && !filePath.equals(originalFilePath)) {
      // uncommitted renames detected
      return getHistoryForUncommittedRenamedOrFail(originalFilePath, vcsRoot, project, limit);
    }
    final HgLogCommand logCommand = new HgLogCommand(project);
    logCommand.setFollowCopies(!filePath.isDirectory());
    logCommand.setIncludeRemoved(true);
    List<String> args = new ArrayList<>();
    if (revisionNumber != null) {
      args.add("--rev");
      args.add("reverse(0::" + revisionNumber.getChangeset() + ")");
    }
    return logCommand.executeOrFail(new HgFile(vcsRoot, filePath), limit, false, args);
  }

  /**
   * Workaround for getting follow file history in case of uncommitted move/rename change
   */
  private static List<HgFileRevision> getHistoryForUncommittedRenamedOrFail(@NotNull FilePath originalHgFilePath,
                                                                            @NotNull VirtualFile vcsRoot,
                                                                            @NotNull Project project,
                                                                            int limit) throws VcsException {
    HgFile originalHgFile = new HgFile(vcsRoot, originalHgFilePath);
    final HgLogCommand logCommand = new HgLogCommand(project);
    logCommand.setIncludeRemoved(true);
    final HgVersion version = logCommand.getVersion();
    String[] templates = HgBaseLogParser.constructFullTemplateArgument(false, version);
    String template = HgChangesetUtil.makeTemplate(templates);
    List<String> argsForCmd = new ArrayList<>();
    String relativePath = originalHgFile.getRelativePath();
    argsForCmd.add("--rev");
    argsForCmd
      .add(String.format("reverse(follow(%s))", relativePath != null ? "'" + FileUtil.toSystemIndependentName(relativePath) + "'" : ""));
    HgCommandResult result = logCommand.execute(vcsRoot, template, limit, relativePath != null ? null : originalHgFile, argsForCmd);
    return HgHistoryUtil.getCommitRecordsOrFail(project, result, new HgFileRevisionLogParser(project, originalHgFile, version));
  }

  @Override
  public boolean supportsHistoryForDirectories() {
    return true;
  }

  @Override
  public DiffFromHistoryHandler getHistoryDiffHandler() {
    return new HgDiffFromHistoryHandler(myProject);
  }

  @Override
  public boolean canShowHistoryFor(@NotNull VirtualFile file) {
    return true;
  }
}
