// 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.command;

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.FilePath;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.zmlx.hg4idea.HgRevisionNumber;
import org.zmlx.hg4idea.execution.HgCommandExecutor;
import org.zmlx.hg4idea.execution.HgCommandResult;
import org.zmlx.hg4idea.util.HgChangesetUtil;
import org.zmlx.hg4idea.util.HgUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * Commands to get revision numbers. These are: parents, id, tip.
 */
public class HgWorkingCopyRevisionsCommand {

  private final Project myProject;
  private static final Logger LOG = Logger.getInstance(HgWorkingCopyRevisionsCommand.class);

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

  /**
   * Current repository revision(s).
   * @param repo repository to work on.
   * @return List of parent's revision numbers.
   * @see #parents(VirtualFile, VirtualFile, HgRevisionNumber)
   * TODO: return Pair
   */
  public @NotNull List<HgRevisionNumber> parents(@NotNull VirtualFile repo) {
    return getRevisions(repo, "parents", null, null, true);
  }

  /**
   * @see #parents(VirtualFile, VirtualFile, HgRevisionNumber)
   */
  public @NotNull Couple<HgRevisionNumber> parents(@NotNull VirtualFile repo, @Nullable VirtualFile file) {
    return parents(repo, file, null);
  }

  /**
   * Parent(s) of the given revision of the given file. If there are two of them (in the case of merge) the first element of the pair
   * is the latest parent (i.e. having greater revision number), second one is the earlier parent (having smaller revision number).
   * @param repo     repository to work on.
   * @param file     file which revision's parents we are interested in. If null, the history of the whole repository is considered.
   * @param revision revision number which parent is wanted. If null, the last revision is taken.
   * @return One or two (in case of a merge commit) parents of the given revision. Or even zero in case of a fresh repository.
   *         So one should check pair elements for null.
   */
  public @NotNull Couple<HgRevisionNumber> parents(@NotNull VirtualFile repo, @Nullable VirtualFile file, @Nullable HgRevisionNumber revision) {
    return parents(repo, VcsUtil.getFilePath(file), revision);
  }

  /**
   * @see #parents(VirtualFile, FilePath, HgRevisionNumber)
   */
  public @NotNull Couple<HgRevisionNumber> parents(@NotNull VirtualFile repo, @Nullable FilePath file) {
    return parents(repo, file, null);
  }

  /**
   * Parent(s) of the given revision of the given file. If there are two of them (in the case of merge) the first element of the pair
   * is the latest parent (i.e. having greater revision number), second one is the earlier parent (having smaller revision number).
   * @param repo     repository to work on.
   * @param file     filepath which revision's parents we are interested in. If null, the history of the whole repository is considered.
   * @param revision revision number which parent is wanted. If null, the last revision is taken.
   * @return One or two (in case of a merge commit) parents of the given revision. Or even zero in case of a fresh repository.
   *         So one should check pair elements for null.
   */
  public @NotNull Couple<HgRevisionNumber> parents(@NotNull VirtualFile repo, @Nullable FilePath file, @Nullable HgRevisionNumber revision) {
    final List<HgRevisionNumber> revisions = getRevisions(repo, "parents", file, revision, true);
    return switch (revisions.size()) {
      case 1 -> Couple.of(revisions.get(0), null);
      case 2 -> Couple.of(revisions.get(0), revisions.get(1));
      default -> Couple.of(null, null);
    };
  }

  public @Nullable HgRevisionNumber firstParent(@NotNull VirtualFile repo) {
    List<HgRevisionNumber> parents = parents(repo);
    if (parents.isEmpty()) {
      //this is possible when we have a freshly initialized mercurial repository
      return HgRevisionNumber.NULL_REVISION_NUMBER;
    }
    else {
      return parents.get(0);
    }
  }

  public @Nullable HgRevisionNumber tip(@NotNull VirtualFile repo) {
    List<HgRevisionNumber> tips = getRevisions(repo, "tip", null, null, true);
    if (tips.size() > 1) {
      throw new IllegalStateException("There cannot be multiple tips");
    }
    if(!tips.isEmpty()) {
      return tips.get(0);
    }
    else return HgRevisionNumber.NULL_REVISION_NUMBER;
  }

  /**
   * Returns the result of 'hg id' execution, i.e. current state of the repository.
   * @return one or two revision numbers. Two revisions is the case of unresolved merge. In other cases there are only one revision.
   */
  public @NotNull Couple<HgRevisionNumber> identify(@NotNull VirtualFile repo) {
    HgCommandExecutor commandExecutor = new HgCommandExecutor(myProject);
    commandExecutor.setSilent(true);
    HgCommandResult result = commandExecutor.executeInCurrentThread(repo, "identify", Arrays.asList("--num", "--id"));
    if (result == null) {
      return Couple.of(HgRevisionNumber.NULL_REVISION_NUMBER, null);
    }

    final List<String> lines = result.getOutputLines();
    if (!lines.isEmpty()) {
      List<String> parts = StringUtil.split(lines.get(0), " ");
      if (parts.size() >= 2) {
        String changesets = parts.get(0);
        String revisions = parts.get(1);
        if (changesets.indexOf('+') != changesets.lastIndexOf('+')) {
          // in the case of unresolved merge we have 2 revisions at once, both current, so with "+"
          // 9f2e6c02913c+b311eb4eb004+ 186+183+
          List<String> chsets = StringUtil.split(changesets, "+");
          List<String> revs = StringUtil.split(revisions, "+");
          return Couple.of(HgRevisionNumber.getInstance(revs.get(0) + "+", chsets.get(0) + "+"),
                           HgRevisionNumber.getInstance(revs.get(1) + "+", chsets.get(1) + "+"));
        } else {
          return Couple.of(HgRevisionNumber.getInstance(revisions, changesets), null);
        }
      }
    }
    return Couple.of(HgRevisionNumber.NULL_REVISION_NUMBER, null);
  }

  /**
   * Returns the list of revisions returned by one mercurial commands (parents, identify, tip).
   * Executed a command on the whole repository or on the given file.
   * During a merge, the returned list contains 2 revision numbers. The order of these numbers is
   * important: the first parent was the parent of the working directory from <em>before</em>
   * the merge, the second parent is the changeset that was merged in.
   * @param repo     repository to execute on.
   * @param command  command to execute.
   * @param file     file which revisions are wanted. If <code><b>null</b></code> then repository revisions are considered.
   * @param revision revision to execute on. If <code><b>null</b></code> then executed without the '-r' parameter, i.e. on the latest revision.
   * @param silent   pass true if this command shouldn't be mentioned in the VCS console.
   * @return List of revisions.
   */
  public @NotNull List<HgRevisionNumber> getRevisions(@NotNull VirtualFile repo,
                                              @NotNull @NonNls String command,
                                              @Nullable FilePath file,
                                              @Nullable HgRevisionNumber revision,
                                              boolean silent) {
    final List<String> args = new LinkedList<>();
    args.add("--template");
    args.add(HgChangesetUtil.makeTemplate("{rev}", "{node}"));
    if (revision != null) {
      args.add("-r");
      args.add(revision.getChangeset());
    }
    if (file != null) { // NB: this must be the last argument
      args.add(HgUtil.getOriginalFileName(file, ChangeListManager.getInstance(myProject)).getPath());
    }
    final HgCommandExecutor executor = new HgCommandExecutor(myProject);
    executor.setSilent(silent);
    final HgCommandResult result = executor.executeInCurrentThread(repo, command, args);

    if (result == null) {
      return new ArrayList<>(0);
    }

    final List<String> lines = new ArrayList<>();
    for (String line : result.getRawOutput().split(HgChangesetUtil.CHANGESET_SEPARATOR)) {
      if (!line.trim().isEmpty()) {     // filter out empty lines
        lines.add(line);
      }
    }
    if (lines.isEmpty()) {
      return new ArrayList<>();
    }

    final List<HgRevisionNumber> revisions = new ArrayList<>(lines.size());
    for(String line: lines) {
      final List<String> parts = StringUtil.split(line, HgChangesetUtil.ITEM_SEPARATOR);
      if (parts.size() < 2) {
        LOG.error("getRevisions output parse error in line [" + line + "]\n All lines: \n" + lines);
        continue;
      }
      revisions.add(HgRevisionNumber.getInstance(parts.get(0), parts.get(1)));
    }
    return revisions;
  }
}
