// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package git4idea.update;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.AbstractVcsHelper;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.openapi.vfs.VirtualFile;
import git4idea.GitLocalBranch;
import git4idea.GitRevisionNumber;
import git4idea.GitUtil;
import git4idea.GitVcs;
import git4idea.branch.GitBranchPair;
import git4idea.commands.Git;
import git4idea.commands.GitCommand;
import git4idea.commands.GitLineHandler;
import git4idea.config.GitConfigUtil;
import git4idea.config.GitVersionSpecialty;
import git4idea.config.UpdateMethod;
import git4idea.merge.MergeChangeCollector;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import org.jetbrains.annotations.NotNull;

import static git4idea.GitUtil.HEAD;
import static git4idea.config.UpdateMethod.MERGE;
import static git4idea.config.UpdateMethod.REBASE;

/**
 * Updates a single repository via merge or rebase.
 * @see GitRebaseUpdater
 * @see GitMergeUpdater
 */
public abstract class GitUpdater {
  private static final Logger LOG = Logger.getInstance(GitUpdater.class);

  protected final @NotNull Project myProject;
  protected final @NotNull Git myGit;
  protected final @NotNull VirtualFile myRoot;
  protected final @NotNull GitRepository myRepository;
  protected final @NotNull ProgressIndicator myProgressIndicator;
  protected final @NotNull UpdatedFiles myUpdatedFiles;
  protected final @NotNull AbstractVcsHelper myVcsHelper;
  protected final @NotNull GitRepositoryManager myRepositoryManager;
  protected final GitVcs myVcs;

  protected GitRevisionNumber myBefore; // The revision that was before update

  protected GitUpdater(@NotNull Project project,
                       @NotNull Git git,
                       @NotNull GitRepository repository,
                       @NotNull ProgressIndicator progressIndicator,
                       @NotNull UpdatedFiles updatedFiles) {
    myProject = project;
    myGit = git;
    myRoot = repository.getRoot();
    myRepository = repository;
    myProgressIndicator = progressIndicator;
    myUpdatedFiles = updatedFiles;
    myVcsHelper = AbstractVcsHelper.getInstance(project);
    myVcs = GitVcs.getInstance(project);
    myRepositoryManager = GitUtil.getRepositoryManager(myProject);
  }

  /**
   * Returns proper updater based on the update policy (merge or rebase) selected by user or stored in his .git/config
   * @return {@link GitMergeUpdater} or {@link GitRebaseUpdater}.
   */
  public static @NotNull GitUpdater getUpdater(@NotNull Project project,
                                      @NotNull Git git,
                                      @NotNull GitBranchPair trackedBranches,
                                      @NotNull GitRepository repository,
                                      @NotNull ProgressIndicator progressIndicator,
                                      @NotNull UpdatedFiles updatedFiles,
                                      @NotNull UpdateMethod updateMethod) {
    if (updateMethod == UpdateMethod.BRANCH_DEFAULT) {
      updateMethod = resolveUpdateMethod(repository);
    }
    if (updateMethod == UpdateMethod.RESET) {
      return new GitResetUpdater(project, git, repository, trackedBranches, progressIndicator, updatedFiles);
    }
    return updateMethod == REBASE ?
           new GitRebaseUpdater(project, git, repository, trackedBranches, progressIndicator, updatedFiles) :
           new GitMergeUpdater(project, git, repository, trackedBranches, progressIndicator, updatedFiles);
  }

  public static @NotNull UpdateMethod resolveUpdateMethod(@NotNull GitRepository repository) {
    Project project = repository.getProject();
    GitLocalBranch branch = repository.getCurrentBranch();
    if (branch != null) {
      String branchName = branch.getName();
      try {
        String rebaseValue = GitConfigUtil.getValue(project, repository.getRoot(), "branch." + branchName + ".rebase");
        if (rebaseValue != null) {
          if (isRebaseValue(rebaseValue)) {
            return REBASE;
          }
          if (GitConfigUtil.getBooleanValue(rebaseValue) == Boolean.FALSE) {
            // explicit override of a more generic pull.rebase config value
            return MERGE;
          }
          LOG.warn("Unknown value for branch." + branchName + ".rebase: " + rebaseValue);
        }
      }
      catch (VcsException e) {
        LOG.warn("Couldn't get git config branch." + branchName + ".rebase");
      }
    }

    if (GitVersionSpecialty.KNOWS_PULL_REBASE.existsIn(project)) {
      try {
        String pullRebaseValue = GitConfigUtil.getValue(project, repository.getRoot(), "pull.rebase");
        if (pullRebaseValue != null && isRebaseValue(pullRebaseValue)) return REBASE;
      }
      catch (VcsException e) {
        LOG.warn("Couldn't get git config pull.rebase");
      }
    }

    return MERGE;
  }

  private static boolean isRebaseValue(@NotNull String configValue) {
    return GitConfigUtil.getBooleanValue(configValue) == Boolean.TRUE ||
           configValue.equalsIgnoreCase("interactive") ||
           configValue.equalsIgnoreCase("preserve"); // 'yes' is not specified in the man, but actually works
  }

  public @NotNull GitUpdateResult update() throws VcsException {
    markStart(myRepository);
    try {
      GitUpdateResult result = doUpdate();
      myRepository.update();
      return result;
    }
    finally {
      markEnd(myRepository);
    }
  }

  /**
   * Checks the repository if local changes need to be saved before update.
   * For rebase local changes need to be saved always, 
   * for merge - only in the case if merge affects the same files or there is something in the index.
   * @return true if local changes from this root need to be saved, false if not.
   */
  public abstract boolean isSaveNeeded();

  /**
   * Checks if update is needed, i.e. if there are remote changes that weren't merged into the current branch.
   * @return true if update is needed, false otherwise.
   */
  public boolean isUpdateNeeded(@NotNull GitBranchPair branchPair) throws VcsException {
    String remoteBranch = branchPair.getTarget().getName();
    if (!hasRemoteChanges(remoteBranch)) {
      LOG.info("isUpdateNeeded: No remote changes, update is not needed");
      return false;
    }
    return true;
  }

  /**
   * Performs update (via rebase or merge - depending on the implementing classes).
   */
  protected abstract @NotNull GitUpdateResult doUpdate();

  protected void markStart(GitRepository repository) throws VcsException {
    // remember the current position
    myBefore = GitRevisionNumber.resolve(myProject, repository.getRoot(), "HEAD");
  }

  protected void markEnd(GitRepository repository) throws VcsException {
    // find out what have changed, this is done even if the process was cancelled.
    new MergeChangeCollector(myProject, repository, myBefore).collect(myUpdatedFiles);
  }

  protected boolean hasRemoteChanges(@NotNull String remoteBranch) throws VcsException {
    GitLineHandler handler = new GitLineHandler(myProject, myRoot, GitCommand.REV_LIST);
    handler.setSilent(true);
    handler.addParameters("-1");
    handler.addParameters(HEAD + ".." + remoteBranch);
    String output = myGit.runCommand(handler).getOutputOrThrow();
    return !output.isEmpty();
  }
}
