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

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.merge.MergeData;
import com.intellij.openapi.vcs.merge.MergeProvider;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ArrayUtilRt;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.zmlx.hg4idea.HgBundle;
import org.zmlx.hg4idea.HgContentRevision;
import org.zmlx.hg4idea.HgFile;
import org.zmlx.hg4idea.HgRevisionNumber;
import org.zmlx.hg4idea.action.HgCommandResultNotifier;
import org.zmlx.hg4idea.command.HgResolveCommand;
import org.zmlx.hg4idea.command.HgWorkingCopyRevisionsCommand;
import org.zmlx.hg4idea.execution.HgCommandResult;
import org.zmlx.hg4idea.execution.HgPromptCommandExecutor;
import org.zmlx.hg4idea.util.HgUtil;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import static org.zmlx.hg4idea.HgNotificationIdsHolder.DEBUGANCESTOR_ERROR;

/**
 * @author Kirill Likhodedov
 */
public class HgMergeProvider implements MergeProvider {
  private static final Logger LOG = Logger.getInstance(HgMergeProvider.class.getName());
  private final Project myProject;

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

  @Override
  public @NotNull MergeData loadRevisions(final @NotNull VirtualFile file) throws VcsException {
    final MergeData mergeData = new MergeData();

    final HgWorkingCopyRevisionsCommand command = new HgWorkingCopyRevisionsCommand(myProject);
    final VirtualFile repo = HgUtil.getHgRootOrThrow(myProject, file);
    final HgFile hgFile = new HgFile(myProject, file);

    HgRevisionNumber serverRevisionNumber;
    HgRevisionNumber localRevisionNumber;
    HgRevisionNumber baseRevisionNumber = null;
    // there are two possibilities: we have checked in local changes in the selected file or we didn't.
    if (wasFileCheckedIn(repo, file)) {
      // 1. We checked in.
      // We have a merge in progress, which means we have 2 heads (parents).
      // the second one is "their" revision pulled from the parent repo,
      // first parent is the local change.
      // to retrieve the base version we get the parent of the local change, i.e. the [only] parent of the first parent.
      //Which one is local revision depends on which one is merged with,
      // i.e if you update to 17 revision and then merge it with 23, so 17 is your local and 17->parent is your base revision.
      // This may produce misunderstanding when you update your project with merging (your update firstly to next revisions  and then
      // merge with previous). see http://hgbook.red-bean.com/read/managing-releases-and-branchy-development.html
      final Couple<HgRevisionNumber> parents = command.parents(repo, file);
      serverRevisionNumber = parents.second;
      localRevisionNumber = parents.first;
      final HgContentRevision local = HgContentRevision.create(myProject, hgFile, localRevisionNumber);
      mergeData.CURRENT = local.getContentAsBytes();
      // we are sure that we have a common ancestor, because otherwise we'll get "repository is unrelated" error while pulling,
      // due to different root changesets which is prohibited.
      // Find common ancestor of two revisions : hg debugancestor rev1 rev2
      // Using quotes may produce wrong escaping errors on Unix-type systems
      List<String> arguments = new ArrayList<>();
      String localChangeset = localRevisionNumber.getChangeset();
      String serverChangeset = serverRevisionNumber.getChangeset();
      arguments.add(StringUtil.isEmptyOrSpaces(localChangeset) ? localRevisionNumber.getRevision() : localChangeset);
      arguments.add(StringUtil.isEmptyOrSpaces(serverChangeset) ? serverRevisionNumber.getRevision() : serverChangeset);
      HgCommandResult result = new HgPromptCommandExecutor(myProject).executeInCurrentThread(repo, "debugancestor", arguments);
      if (result != null) {
        String output = result.getRawOutput();
        final List<String> parts = StringUtil.split(output, ":");
        if (parts.size() < 2) {
          LOG.info("Couldn't parse result of debugancestor command execution " + arguments);
          new HgCommandResultNotifier(myProject)
            .notifyError(DEBUGANCESTOR_ERROR,
                         null,
                         HgBundle.message("hg4idea.error.debugancestor.command.execution"),
                         HgBundle.message("hg4idea.error.debugancestor.command.description"));
        }
        else {
          baseRevisionNumber = HgRevisionNumber.getInstance(parts.get(0), parts.get(1));
        }
      }
      else {
        LOG.info(HgBundle.message("hg4idea.error.debugancestor.command.execution") + arguments);
        new HgCommandResultNotifier(myProject)
          .notifyError(DEBUGANCESTOR_ERROR,
                       null,
                       HgBundle.message("hg4idea.error.debugancestor.command.execution"),
                       HgBundle.message("hg4idea.error.debugancestor.command.description"));
      }
    }
    else {
      // 2. local changes are not checked in.
      // then there is only one parent, which is server changes.
      // local changes are retrieved from the file system, they are not in the Mercurial yet.
      // base is the only parent of server changes.
      serverRevisionNumber = command.parents(repo, file).first;
      baseRevisionNumber = command.parents(repo, file, serverRevisionNumber).first;
      final File origFile = new File(file.getPath() + ".orig");
      mergeData.CURRENT = VcsUtil.getFileByteContent(origFile);
    }

    if (baseRevisionNumber != null) {
      final HgContentRevision base = HgContentRevision.create(myProject, hgFile, baseRevisionNumber);
      //if file doesn't exist in ancestor revision the base revision should be empty
      mergeData.ORIGINAL = base.getContent() != null ? base.getContentAsBytes() : ArrayUtilRt.EMPTY_BYTE_ARRAY;
    }
    else { // no base revision means that the file was added simultaneously with different content in both repositories
      mergeData.ORIGINAL = ArrayUtilRt.EMPTY_BYTE_ARRAY;
    }
    final HgContentRevision server = HgContentRevision.create(myProject, hgFile, serverRevisionNumber);
    mergeData.LAST = server.getContentAsBytes();
    file.refresh(false, false);

    return mergeData;
  }

  @Override
  public void conflictResolvedForFile(@NotNull VirtualFile file) {
    try {
      new HgResolveCommand(myProject).markResolved(HgUtil.getHgRootOrThrow(myProject, file), file);
    } catch (VcsException e) {
      LOG.error("Couldn't mark file resolved, because it is not under Mercurial root.");
    }
  }

  @Override
  public boolean isBinary(@NotNull VirtualFile file) {
    return file.getFileType().isBinary();
  }

  /**
   * Checks if the given file was checked in before the merge start.
   * @param repo repository to work on.
   * @param file file to be checked.
   * @return True if the file was checked in before merge, false if it wasn't.
   */
  private boolean wasFileCheckedIn(VirtualFile repo, VirtualFile file) {
    // in the case of merge if the file was checked in, it will have 2 parents after hg pull. If it wasn't, it would have only one parent
    final Couple<HgRevisionNumber> parents = new HgWorkingCopyRevisionsCommand(myProject).parents(repo, file);
    return parents.second != null;
  }

}
