/*
 * Copyright 2000-2016 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 org.jetbrains.plugins.github;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Convertor;
import com.intellij.vcs.log.VcsCommitMetadata;
import git4idea.DialogManager;
import git4idea.GitCommit;
import git4idea.GitLocalBranch;
import git4idea.changes.GitChangeUtils;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.history.GitHistoryUtils;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.ui.branch.GitCompareBranchesDialog;
import git4idea.update.GitFetchResult;
import git4idea.update.GitFetcher;
import git4idea.util.GitCommitCompareInfo;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.github.api.GithubApiUtil;
import org.jetbrains.plugins.github.api.GithubFullPath;
import org.jetbrains.plugins.github.api.data.GithubBranch;
import org.jetbrains.plugins.github.api.data.GithubPullRequest;
import org.jetbrains.plugins.github.api.data.GithubRepo;
import org.jetbrains.plugins.github.api.data.GithubRepoDetailed;
import org.jetbrains.plugins.github.exceptions.GithubOperationCanceledException;
import org.jetbrains.plugins.github.ui.GithubSelectForkDialog;
import org.jetbrains.plugins.github.util.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class GithubCreatePullRequestWorker {
  private static final Logger LOG = GithubUtil.LOG;
  private static final String CANNOT_CREATE_PULL_REQUEST = "Can't Create Pull Request";

  @NotNull private final Project myProject;
  @NotNull private final Git myGit;
  @NotNull private final GitRepository myGitRepository;
  @NotNull private final GithubAuthDataHolder myAuthHolder;

  @NotNull private final GithubFullPath myPath;
  @NotNull private final String myRemoteName;
  @NotNull private final String myRemoteUrl;
  @NotNull private final String myCurrentBranch;

  @NotNull private GithubFullPath mySource;

  @NotNull private final List<ForkInfo> myForks;
  @Nullable private List<GithubFullPath> myAvailableForks;

  private GithubCreatePullRequestWorker(@NotNull Project project,
                                        @NotNull Git git,
                                        @NotNull GitRepository gitRepository,
                                        @NotNull GithubAuthDataHolder authHolder,
                                        @NotNull GithubFullPath path,
                                        @NotNull String remoteName,
                                        @NotNull String remoteUrl,
                                        @NotNull String currentBranch) {
    myProject = project;
    myGit = git;
    myGitRepository = gitRepository;
    myAuthHolder = authHolder;
    myPath = path;
    myRemoteName = remoteName;
    myRemoteUrl = remoteUrl;
    myCurrentBranch = currentBranch;

    myForks = new ArrayList<>();
  }

  @NotNull
  public String getCurrentBranch() {
    return myCurrentBranch;
  }

  @NotNull
  public List<ForkInfo> getForks() {
    return myForks;
  }

  @Nullable
  public static GithubCreatePullRequestWorker create(@NotNull final Project project, @Nullable final VirtualFile file) {
    return GithubUtil.computeValueInModal(project, "Loading data...", indicator -> {
      Git git = ServiceManager.getService(Git.class);

      GitRepository gitRepository = GithubUtil.getGitRepository(project, file);
      if (gitRepository == null) {
        GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't find git repository");
        return null;
      }
      gitRepository.update();

      Pair<GitRemote, String> remote = GithubUtil.findGithubRemote(gitRepository);
      if (remote == null) {
        GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't find GitHub remote");
        return null;
      }
      String remoteName = remote.getFirst().getName();
      String remoteUrl = remote.getSecond();

      GithubFullPath path = GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(remoteUrl);
      if (path == null) {
        GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't process remote: " + remoteUrl);
        return null;
      }

      GitLocalBranch currentBranch = gitRepository.getCurrentBranch();
      if (currentBranch == null) {
        GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "No current branch");
        return null;
      }

      GithubAuthDataHolder authHolder;
      try {
        authHolder = GithubUtil.getValidAuthDataHolderFromConfig(project, AuthLevel.LOGGED, indicator);
      }
      catch (IOException e) {
        GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e);
        return null;
      }

      GithubCreatePullRequestWorker worker =
        new GithubCreatePullRequestWorker(project, git, gitRepository, authHolder, path, remoteName, remoteUrl, currentBranch.getName());

      try {
        worker.initForks(indicator);
      }
      catch (IOException e) {
        GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e);
        return null;
      }

      return worker;
    });
  }

  private void initForks(@NotNull ProgressIndicator indicator) throws IOException {
    doLoadForksFromGithub(indicator);
    doLoadForksFromGit(indicator);
    doLoadForksFromSettings(indicator);
  }

  @Nullable
  private ForkInfo doAddFork(@NotNull GithubFullPath path,
                             @Nullable String remoteName,
                             @NotNull ProgressIndicator indicator) {
    for (ForkInfo fork : myForks) {
      if (fork.getPath().equals(path)) {
        if (fork.getRemoteName() == null && remoteName != null) {
          fork.setRemoteName(remoteName);
        }
        return fork;
      }
    }

    try {
      List<String> branches = loadBranches(path, indicator);
      String defaultBranch = doLoadDefaultBranch(path, indicator);

      ForkInfo fork = new ForkInfo(path, branches, defaultBranch);
      myForks.add(fork);
      if (remoteName != null) {
        fork.setRemoteName(remoteName);
      }
      return fork;
    }
    catch (IOException e) {
      GithubNotifications.showWarning(myProject, "Can't load branches for " + path.getFullName(), e);
      return null;
    }
  }

  @Nullable
  private ForkInfo doAddFork(@NotNull GithubRepo repo, @NotNull ProgressIndicator indicator) {
    GithubFullPath path = repo.getFullPath();
    for (ForkInfo fork : myForks) {
      if (fork.getPath().equals(path)) {
        return fork;
      }
    }

    try {
      List<String> branches = loadBranches(path, indicator);
      String defaultBranch = repo.getDefaultBranch();

      ForkInfo fork = new ForkInfo(path, branches, defaultBranch);
      myForks.add(fork);
      return fork;
    }
    catch (IOException e) {
      GithubNotifications.showWarning(myProject, "Can't load branches for " + path.getFullName(), e);
      return null;
    }
  }

  private void doLoadForksFromSettings(@NotNull ProgressIndicator indicator) throws IOException {
    GithubFullPath savedRepo = GithubProjectSettings.getInstance(myProject).getCreatePullRequestDefaultRepo();
    if (savedRepo != null) {
      doAddFork(savedRepo, null, indicator);
    }
  }

  private void doLoadForksFromGit(@NotNull ProgressIndicator indicator) {
    for (GitRemote remote : myGitRepository.getRemotes()) {
      for (String url : remote.getUrls()) {
        if (GithubUrlUtil.isGithubUrl(url)) {
          GithubFullPath path = GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(url);
          if (path != null) {
            doAddFork(path, remote.getName(), indicator);
            break;
          }
        }
      }
    }
  }

  private void doLoadForksFromGithub(@NotNull ProgressIndicator indicator) throws IOException {
    GithubRepoDetailed repo = GithubUtil.runTask(myProject, myAuthHolder, indicator, connection ->
      GithubApiUtil.getDetailedRepoInfo(connection, myPath.getUser(), myPath.getRepository()));

    doAddFork(repo, indicator);
    if (repo.getParent() != null) {
      doAddFork(repo.getParent(), indicator);
    }
    if (repo.getSource() != null) {
      doAddFork(repo.getSource(), indicator);
    }

    mySource = repo.getSource() == null ? repo.getFullPath() : repo.getSource().getFullPath();
  }

  @NotNull
  private List<String> loadBranches(@NotNull final GithubFullPath fork, @NotNull ProgressIndicator indicator) throws IOException {
    List<GithubBranch> branches = GithubUtil.runTask(myProject, myAuthHolder, indicator, connection ->
      GithubApiUtil.getRepoBranches(connection, fork.getUser(), fork.getRepository()));
    return ContainerUtil.map(branches, GithubBranch::getName);
  }

  @Nullable
  private String doLoadDefaultBranch(@NotNull final GithubFullPath fork, @NotNull ProgressIndicator indicator) throws IOException {
    GithubRepo repo = GithubUtil.runTask(myProject, myAuthHolder, indicator, connection ->
      GithubApiUtil.getDetailedRepoInfo(connection, fork.getUser(), fork.getRepository()));
    return repo.getDefaultBranch();
  }

  public void launchFetchRemote(@NotNull final ForkInfo fork) {
    if (fork.getRemoteName() == null) return;

    if (fork.getFetchTask() != null) return;
    synchronized (fork.LOCK) {
      if (fork.getFetchTask() != null) return;

      final MasterFutureTask<Void> task = new MasterFutureTask<>(new Callable<Void>() {
        @Override
        public Void call() throws Exception {
          doFetchRemote(fork);
          return null;
        }
      });
      fork.setFetchTask(task);

      ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
        @Override
        public void run() {
          task.run();
        }
      });
    }
  }

  public void launchLoadDiffInfo(@NotNull final BranchInfo branch) {
    if (branch.getForkInfo().getRemoteName() == null) return;

    if (branch.getDiffInfoTask() != null) return;
    synchronized (branch.LOCK) {
      if (branch.getDiffInfoTask() != null) return;

      launchFetchRemote(branch.getForkInfo());
      MasterFutureTask<Void> masterTask = branch.getForkInfo().getFetchTask();
      assert masterTask != null;

      final SlaveFutureTask<DiffInfo> task = new SlaveFutureTask<>(masterTask, new Callable<DiffInfo>() {
        @Override
        public DiffInfo call() throws VcsException {
          return doLoadDiffInfo(branch);
        }
      });
      branch.setDiffInfoTask(task);

      ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
        @Override
        public void run() {
          task.run();
        }
      });
    }
  }

  @Nullable
  public DiffInfo getDiffInfo(@NotNull final BranchInfo branch) throws IOException {
    if (branch.getForkInfo().getRemoteName() == null) return null;

    launchLoadDiffInfo(branch);

    assert branch.getDiffInfoTask() != null;
    try {
      return branch.getDiffInfoTask().get();
    }
    catch (InterruptedException e) {
      throw new GithubOperationCanceledException(e);
    }
    catch (ExecutionException e) {
      Throwable ex = e.getCause();
      if (ex instanceof VcsException) throw new IOException(ex);
      LOG.error(ex);
      return null;
    }
  }

  private boolean doFetchRemote(@NotNull ForkInfo fork) {
    if (fork.getRemoteName() == null) return false;

    GitFetchResult result =
      new GitFetcher(myProject, new EmptyProgressIndicator(), false).fetch(myGitRepository.getRoot(), fork.getRemoteName(), null);
    if (!result.isSuccess()) {
      GitFetcher.displayFetchResult(myProject, result, null, result.getErrors());
      return false;
    }
    return true;
  }

  @NotNull
  private DiffInfo doLoadDiffInfo(@NotNull final BranchInfo branch) throws VcsException {
    // TODO: make cancelable and abort old speculative requests (when git4idea will allow to do so)
    String currentBranch = myCurrentBranch;
    String targetBranch = branch.getForkInfo().getRemoteName() + "/" + branch.getRemoteName();

    List<GitCommit> commits1 = GitHistoryUtils.history(myProject, myGitRepository.getRoot(), ".." + targetBranch);
    List<GitCommit> commits2 = GitHistoryUtils.history(myProject, myGitRepository.getRoot(), targetBranch + "..");
    Collection<Change> diff = GitChangeUtils.getDiff(myProject, myGitRepository.getRoot(), targetBranch, myCurrentBranch, null);
    GitCommitCompareInfo info = new GitCommitCompareInfo(GitCommitCompareInfo.InfoType.BRANCH_TO_HEAD);
    info.put(myGitRepository, diff);
    info.put(myGitRepository, Couple.of(commits1, commits2));

    return new DiffInfo(info, currentBranch, targetBranch);
  }

  private void doConfigureRemote(@NotNull ForkInfo fork) {
    if (fork.getRemoteName() != null) return;

    GithubFullPath path = fork.getPath();
    String url = GithubUrlUtil.getCloneUrl(path);

    if (GithubUtil.addGithubRemote(myProject, myGitRepository, path.getUser(), url)) {
      fork.setRemoteName(path.getUser());
    }
  }

  public void configureRemote(@NotNull final ForkInfo fork) {
    GithubUtil.computeValueInModal(myProject, "Creating remote..", false, indicator -> {
      doConfigureRemote(fork);
    });
  }

  @NotNull
  public Couple<String> getDefaultDescriptionMessage(@NotNull final BranchInfo branch) {
    Couple<String> message = branch.getDefaultMessage();
    if (message != null) return message;

    if (branch.getForkInfo().getRemoteName() == null) {
      return getSimpleDefaultDescriptionMessage(branch);
    }

    return GithubUtil.computeValueInModal(myProject, "Collecting last commits...", true, indicator -> {
      String localBranch = myCurrentBranch;
      String targetBranch = branch.getForkInfo().getRemoteName() + "/" + branch.getRemoteName();
      try {
        List<VcsCommitMetadata> commits =
          GitHistoryUtils.readLastCommits(myProject, myGitRepository.getRoot(), localBranch, targetBranch);
        if (commits == null) return getSimpleDefaultDescriptionMessage(branch);

        VcsCommitMetadata localCommit = commits.get(0);
        VcsCommitMetadata targetCommit = commits.get(1);

        if (localCommit.getParents().contains(targetCommit.getId())) {
          return GithubUtil.getGithubLikeFormattedDescriptionMessage(localCommit.getFullMessage());
        }
        return getSimpleDefaultDescriptionMessage(branch);
      }
      catch (ProcessCanceledException e) {
        return getSimpleDefaultDescriptionMessage(branch);
      }
      catch (VcsException e) {
        GithubNotifications.showWarning(myProject, "Can't collect additional data", e);
        return getSimpleDefaultDescriptionMessage(branch);
      }
    });
  }

  @NotNull
  public Couple<String> getSimpleDefaultDescriptionMessage(@NotNull final BranchInfo branch) {
    Couple<String> message = Couple.of(myCurrentBranch, "");
    branch.setDefaultMessage(message);
    return message;
  }

  public boolean checkAction(@Nullable final BranchInfo branch) {
    if (branch == null) {
      GithubNotifications.showWarningDialog(myProject, CANNOT_CREATE_PULL_REQUEST, "Target branch is not selected");
      return false;
    }

    DiffInfo info;
    try {
      info = GithubUtil.computeValueInModalIO(myProject, "Collecting diff data...", indicator ->
        GithubUtil.runInterruptable(indicator, () -> getDiffInfo(branch)));
    }
    catch (IOException e) {
      GithubNotifications.showError(myProject, "Can't collect diff data", e);
      return true;
    }
    if (info == null) {
      return true;
    }

    ForkInfo fork = branch.getForkInfo();

    String localBranchName = "'" + myCurrentBranch + "'";
    String targetBranchName = "'" + fork.getRemoteName() + "/" + branch.getRemoteName() + "'";
    if (info.getInfo().getBranchToHeadCommits(myGitRepository).isEmpty()) {
      return GithubNotifications
        .showYesNoDialog(myProject, "Empty Pull Request",
                         "The branch " + localBranchName + " is fully merged to the branch " + targetBranchName + '\n' +
                         "Do you want to proceed anyway?");
    }
    if (!info.getInfo().getHeadToBranchCommits(myGitRepository).isEmpty()) {
      return GithubNotifications
        .showYesNoDialog(myProject, "Target Branch Is Not Fully Merged",
                         "The branch " + targetBranchName + " is not fully merged to the branch " + localBranchName + '\n' +
                         "Do you want to proceed anyway?");
    }

    return true;
  }

  public void createPullRequest(@NotNull final BranchInfo branch,
                                @NotNull final String title,
                                @NotNull final String description) {
    new Task.Backgroundable(myProject, "Creating Pull Request...") {
      @Override
      public void run(@NotNull ProgressIndicator indicator) {
        LOG.info("Pushing current branch");
        indicator.setText("Pushing current branch...");
        GitCommandResult result = myGit.push(myGitRepository, myRemoteName, myRemoteUrl, myCurrentBranch, true);
        if (!result.success()) {
          GithubNotifications.showError(myProject, CANNOT_CREATE_PULL_REQUEST, "Push failed:<br/>" + result.getErrorOutputAsHtmlString());
          return;
        }

        LOG.info("Creating pull request");
        indicator.setText("Creating pull request...");
        GithubPullRequest request = doCreatePullRequest(indicator, branch, title, description);
        if (request == null) {
          return;
        }

        GithubNotifications.showInfoURL(myProject, "Successfully created pull request",
                                        "Pull request #" + request.getNumber(), request.getHtmlUrl());
      }
    }.queue();
  }

  @Nullable
  private GithubPullRequest doCreatePullRequest(@NotNull ProgressIndicator indicator,
                                                @NotNull final BranchInfo branch,
                                                @NotNull final String title,
                                                @NotNull final String description) {
    final GithubFullPath forkPath = branch.getForkInfo().getPath();

    final String head = myPath.getUser() + ":" + myCurrentBranch;
    final String base = branch.getRemoteName();

    try {
      return GithubUtil.runTask(myProject, myAuthHolder, indicator, connection ->
        GithubApiUtil.createPullRequest(connection, forkPath.getUser(), forkPath.getRepository(), title, description, head, base));
    }
    catch (IOException e) {
      GithubNotifications.showError(myProject, CANNOT_CREATE_PULL_REQUEST, e);
      return null;
    }
  }

  public void showDiffDialog(@Nullable final BranchInfo branch) {
    if (branch == null) {
      GithubNotifications.showWarningDialog(myProject, "Can't Show Diff", "Target branch is not selected");
      return;
    }

    DiffInfo info;
    try {
      info = GithubUtil.computeValueInModalIO(myProject, "Collecting diff data...", indicator ->
        GithubUtil.runInterruptable(indicator, () -> getDiffInfo(branch)));
    }
    catch (IOException e) {
      GithubNotifications.showError(myProject, "Can't collect diff data", e);
      return;
    }
    if (info == null) {
      GithubNotifications.showErrorDialog(myProject, "Can't Show Diff", "Can't collect diff data");
      return;
    }

    GitCompareBranchesDialog dialog =
      new GitCompareBranchesDialog(myProject, info.getTo(), info.getFrom(), info.getInfo(), myGitRepository, true);
    dialog.show();
  }

  @Nullable
  public ForkInfo showTargetDialog() {
    if (myAvailableForks == null) {
      try {
        myAvailableForks = GithubUtil.computeValueInModal(myProject, myCurrentBranch, indicator -> getAvailableForks(indicator));
      }
      catch (ProcessCanceledException ignore) {
      }
    }

    Convertor<String, ForkInfo> getForkPath = user ->
      GithubUtil.computeValueInModal(myProject, "Access to GitHub", indicator -> findRepositoryByUser(indicator, user));

    GithubSelectForkDialog dialog = new GithubSelectForkDialog(myProject, myAvailableForks, getForkPath);
    DialogManager.show(dialog);
    if (!dialog.isOK()) {
      return null;
    }
    return dialog.getPath();
  }

  @Nullable
  private List<GithubFullPath> getAvailableForks(@NotNull ProgressIndicator indicator) {
    try {
      List<GithubRepo> forks = GithubUtil.runTask(myProject, myAuthHolder, indicator, connection ->
        GithubApiUtil.getForks(connection, mySource.getUser(), mySource.getRepository())
      );
      List<GithubFullPath> forkPaths = ContainerUtil.map(forks, GithubRepo::getFullPath);
      if (!forkPaths.contains(mySource)) return ContainerUtil.append(forkPaths, mySource);
      return forkPaths;
    }
    catch (IOException e) {
      GithubNotifications.showWarning(myProject, "Can't load available forks", e);
      return null;
    }
  }

  @Nullable
  private ForkInfo findRepositoryByUser(@NotNull final ProgressIndicator indicator, @NotNull final String user) {
    for (ForkInfo fork : myForks) {
      if (StringUtil.equalsIgnoreCase(user, fork.getPath().getUser())) {
        return fork;
      }
    }

    try {
      GithubRepo repo = GithubUtil.runTask(myProject, myAuthHolder, indicator, connection -> {
        try {
          GithubRepoDetailed target = GithubApiUtil.getDetailedRepoInfo(connection, user, mySource.getRepository());
          if (target.getSource() != null && StringUtil.equals(target.getSource().getUserName(), mySource.getUser())) {
            return target;
          }
        }
        catch (IOException ignore) {
          // such repo may not exist
        }

        return GithubApiUtil.findForkByUser(connection, mySource.getUser(), mySource.getRepository(), user);
      });

      if (repo == null) return null;
      return doAddFork(repo, indicator);
    }
    catch (IOException e) {
      GithubNotifications.showError(myProject, "Can't find repository", e);
      return null;
    }
  }

  public static class ForkInfo {
    @NotNull public final Object LOCK = new Object();

    // initial loading
    @NotNull private final GithubFullPath myPath;

    @NotNull private final String myDefaultBranch;
    @NotNull private final List<BranchInfo> myBranches;

    @Nullable private String myRemoteName;
    private boolean myProposedToCreateRemote;

    @Nullable private MasterFutureTask<Void> myFetchTask;

    public ForkInfo(@NotNull GithubFullPath path, @NotNull List<String> branches, @Nullable String defaultBranch) {
      myPath = path;
      myDefaultBranch = defaultBranch == null ? "master" : defaultBranch;
      myBranches = new ArrayList<>();
      for (String branchName : branches) {
        myBranches.add(new BranchInfo(branchName, this));
      }
    }

    @NotNull
    public GithubFullPath getPath() {
      return myPath;
    }

    @Nullable
    public String getRemoteName() {
      return myRemoteName;
    }

    @NotNull
    public String getDefaultBranch() {
      return myDefaultBranch;
    }

    @NotNull
    public List<BranchInfo> getBranches() {
      return myBranches;
    }

    public void setRemoteName(@NotNull String remoteName) {
      myRemoteName = remoteName;
    }

    public boolean isProposedToCreateRemote() {
      return myProposedToCreateRemote;
    }

    public void setProposedToCreateRemote(boolean proposedToCreateRemote) {
      myProposedToCreateRemote = proposedToCreateRemote;
    }

    @Nullable
    public MasterFutureTask<Void> getFetchTask() {
      return myFetchTask;
    }

    public void setFetchTask(@NotNull MasterFutureTask<Void> fetchTask) {
      myFetchTask = fetchTask;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      ForkInfo info = (ForkInfo)o;

      if (!myPath.equals(info.myPath)) return false;

      return true;
    }

    @Override
    public int hashCode() {
      return myPath.hashCode();
    }

    @Override
    public String toString() {
      return myPath.getUser() + ":" + myPath.getRepository();
    }
  }

  public static class BranchInfo {
    @NotNull public final Object LOCK = new Object();

    @NotNull private final ForkInfo myForkInfo;
    @NotNull private final String myRemoteName;

    @Nullable private SlaveFutureTask<DiffInfo> myDiffInfoTask;

    @Nullable private Couple<String> myDefaultMessage;

    public BranchInfo(@NotNull String remoteName, @NotNull ForkInfo fork) {
      myRemoteName = remoteName;
      myForkInfo = fork;
    }

    @NotNull
    public ForkInfo getForkInfo() {
      return myForkInfo;
    }

    @NotNull
    public String getRemoteName() {
      return myRemoteName;
    }

    @Nullable
    public SlaveFutureTask<DiffInfo> getDiffInfoTask() {
      return myDiffInfoTask;
    }

    public void setDiffInfoTask(@NotNull SlaveFutureTask<DiffInfo> diffInfoTask) {
      myDiffInfoTask = diffInfoTask;
    }

    @Nullable
    public Couple<String> getDefaultMessage() {
      return myDefaultMessage;
    }

    public void setDefaultMessage(@NotNull Couple<String> message) {
      myDefaultMessage = message;
    }

    @Override
    public String toString() {
      return myRemoteName;
    }
  }

  public static class DiffInfo {
    @NotNull private final GitCommitCompareInfo myInfo;
    @NotNull private final String myFrom;
    @NotNull private final String myTo;

    private DiffInfo(@NotNull GitCommitCompareInfo info, @NotNull String from, @NotNull String to) {
      myInfo = info;
      myFrom = from; // HEAD
      myTo = to;     // BASE
    }

    @NotNull
    public GitCommitCompareInfo getInfo() {
      return myInfo;
    }

    @NotNull
    public String getFrom() {
      return myFrom;
    }

    @NotNull
    public String getTo() {
      return myTo;
    }
  }

  public static class SlaveFutureTask<T> extends FutureTask<T> {
    @NotNull private final MasterFutureTask myMaster;

    public SlaveFutureTask(@NotNull MasterFutureTask master, @NotNull Callable<T> callable) {
      super(callable);
      myMaster = master;
    }

    @Override
    public void run() {
      if (myMaster.isDone()) {
        super.run();
      }
      else {
        if (!myMaster.addSlave(this)) {
          super.run();
        }
      }
    }

    public T safeGet() {
      try {
        return super.get();
      }
      catch (InterruptedException | ExecutionException | CancellationException e) {
        return null;
      }
    }
  }

  public static class MasterFutureTask<T> extends FutureTask<T> {
    @NotNull private final Object LOCK = new Object();
    private boolean myDone = false;

    @Nullable private List<SlaveFutureTask> mySlaves;

    public MasterFutureTask(@NotNull Callable<T> callable) {
      super(callable);
    }

    boolean addSlave(@NotNull SlaveFutureTask slave) {
      if (isDone()) {
        return false;
      }
      else {
        synchronized (LOCK) {
          if (myDone) return false;
          if (mySlaves == null) mySlaves = new ArrayList<>();
          mySlaves.add(slave);
          return true;
        }
      }
    }

    @Override
    protected void done() {
      synchronized (LOCK) {
        myDone = true;
        if (mySlaves != null) {
          for (final SlaveFutureTask slave : mySlaves) {
            runSlave(slave);
          }
          mySlaves = null;
        }
      }
    }

    protected void runSlave(@NotNull final SlaveFutureTask slave) {
      ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
        @Override
        public void run() {
          slave.run();
        }
      });
    }
  }
}
