package com.intellij.remoteServer.util;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.changes.ui.CommitChangeListDialog;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.remoteServer.agent.util.CloudAgentLoggingHandler;
import com.intellij.remoteServer.agent.util.CloudGitApplication;
import com.intellij.remoteServer.configuration.deployment.DeploymentSource;
import com.intellij.remoteServer.runtime.deployment.DeploymentLogManager;
import com.intellij.remoteServer.runtime.deployment.DeploymentTask;
import com.intellij.util.ArrayUtil;
import com.intellij.util.concurrency.Semaphore;
import git4idea.GitPlatformFacade;
import git4idea.GitUtil;
import git4idea.actions.GitInit;
import git4idea.commands.*;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import git4idea.util.GitFileUtils;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
 * @author michael.golubev
 */
public class CloudGitDeploymentRuntime extends CloudDeploymentRuntime {

  private static final Logger LOG = Logger.getInstance("#" + CloudGitDeploymentRuntime.class.getName());

  private static final String COMMIT_MESSAGE = "Deploy";

  private static final CommitSession NO_COMMIT = new CommitSession() {

    @Nullable
    @Override
    public JComponent getAdditionalConfigurationUI() {
      return null;
    }

    @Nullable
    @Override
    public JComponent getAdditionalConfigurationUI(Collection<Change> changes, String commitMessage) {
      return null;
    }

    @Override
    public boolean canExecute(Collection<Change> changes, String commitMessage) {
      return true;
    }

    @Override
    public void execute(Collection<Change> changes, String commitMessage) {

    }

    @Override
    public void executionCanceled() {

    }

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

  private static final List<CommitExecutor> ourCommitExecutors = Arrays.asList(
    new CommitExecutor() {

      @Nls
      @Override
      public String getActionText() {
        return "Commit and Push";
      }

      @NotNull
      @Override
      public CommitSession createCommitSession() {
        return CommitSession.VCS_COMMIT;
      }
    },
    new CommitExecutorBase() {

      @Nls
      @Override
      public String getActionText() {
        return "Push without Commit";
      }

      @NotNull
      @Override
      public CommitSession createCommitSession() {
        return NO_COMMIT;
      }

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

  private final GitRepositoryManager myGitRepositoryManager;
  private final Git myGit;

  private final VirtualFile myContentRoot;
  private final File myRepositoryRootFile;

  private final String myDefaultRemoteName;
  private final ChangeListManagerEx myChangeListManager;
  private String myRemoteName;
  private final String myCloudName;

  private GitRepository myRepository;

  public CloudGitDeploymentRuntime(CloudMultiSourceServerRuntimeInstance serverRuntime,
                                   DeploymentSource source,
                                   File repositoryRoot,
                                   DeploymentTask<? extends CloudDeploymentNameConfiguration> task,
                                   DeploymentLogManager logManager,
                                   String defaultRemoteName,
                                   String cloudName) throws ServerRuntimeException {
    super(serverRuntime, source, task, logManager);

    myDefaultRemoteName = defaultRemoteName;
    myCloudName = cloudName;

    myRepositoryRootFile = repositoryRoot;

    VirtualFile contentRoot = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(myRepositoryRootFile);
    LOG.assertTrue(contentRoot != null, "Repository root is not found");
    myContentRoot = contentRoot;

    Project project = getProject();
    myGitRepositoryManager = GitUtil.getRepositoryManager(project);
    myGit = ServiceManager.getService(Git.class);
    if (myGit == null) {
      throw new ServerRuntimeException("Can't initialize GIT");
    }
    GitPlatformFacade gitPlatformFacade = ServiceManager.getService(GitPlatformFacade.class);
    myChangeListManager = gitPlatformFacade.getChangeListManager(project);
  }

  @Override
  public CloudGitApplication deploy() throws ServerRuntimeException {
    CloudGitApplication application = findOrCreateApplication();
    deployApplication(application);
    return application;
  }

  private void deployApplication(CloudGitApplication application) throws ServerRuntimeException {
    boolean firstDeploy = findRepository() == null;

    GitRepository repository = findOrCreateRepository();
    addOrResetGitRemote(application, repository);

    final LocalChangeList activeChangeList = myChangeListManager.getDefaultChangeList();

    if (activeChangeList != null && !firstDeploy) {
      commitWithChangesDialog(activeChangeList);
    }
    else {
      add();
      commit();
    }
    repository.update();
    pushApplication(application);
  }

  protected void commitWithChangesDialog(final @NotNull LocalChangeList activeChangeList)
    throws ServerRuntimeException {

    Collection<Change> changes = activeChangeList.getChanges();
    final List<Change> relevantChanges = new ArrayList<Change>();
    for (Change change : changes) {
      if (isRelevant(change.getBeforeRevision()) || isRelevant(change.getAfterRevision())) {
        relevantChanges.add(change);
      }
    }

    final Semaphore commitSemaphore = new Semaphore();
    commitSemaphore.down();

    final Ref<Boolean> commitSucceeded = new Ref<Boolean>(false);
    Boolean commitStarted = runOnEdt(new Computable<Boolean>() {

      @Override
      public Boolean compute() {
        return CommitChangeListDialog.commitChanges(getProject(),
                                                    relevantChanges,
                                                    activeChangeList,
                                                    ourCommitExecutors,
                                                    false,
                                                    COMMIT_MESSAGE,
                                                    new CommitResultHandler() {

                                                      @Override
                                                      public void onSuccess(@NotNull String commitMessage) {
                                                        commitSucceeded.set(true);
                                                        commitSemaphore.up();
                                                      }

                                                      @Override
                                                      public void onFailure() {
                                                        commitSemaphore.up();
                                                      }
                                                    },
                                                    false);
      }
    });
    if (commitStarted != null && commitStarted) {
      commitSemaphore.waitFor();
      if (!commitSucceeded.get()) {
        getRepository().update();
        throw new ServerRuntimeException("Commit failed");
      }
    }
    else {
      throw new ServerRuntimeException("Deploy interrupted");
    }
  }

  private boolean isRelevant(ContentRevision contentRevision) throws ServerRuntimeException {
    if (contentRevision == null) {
      return false;
    }
    GitRepository repository = getRepository();
    VirtualFile affectedFile = contentRevision.getFile().getVirtualFile();
    return affectedFile != null && VfsUtilCore.isAncestor(repository.getRoot(), affectedFile, false);
  }

  private static <T> T runOnEdt(final Computable<T> computable) {
    final Ref<T> result = new Ref<T>();
    ApplicationManager.getApplication().invokeAndWait(new Runnable() {

      @Override
      public void run() {
        result.set(computable.compute());
      }
    }, ModalityState.any());
    return result.get();
  }

  public boolean isDeployed() throws ServerRuntimeException {
    return findApplication() != null;
  }

  public CloudGitApplication findOrCreateApplication() throws ServerRuntimeException {
    CloudGitApplication application = findApplication();
    if (application == null) {
      application = createApplication();
    }
    return application;
  }

  public void addOrResetGitRemote(CloudGitApplication application, GitRepository repository) throws ServerRuntimeException {
    String gitUrl = application.getGitUrl();
    if (myRemoteName == null) {
      for (GitRemote gitRemote : repository.getRemotes()) {
        if (gitRemote.getUrls().contains(gitUrl)) {
          myRemoteName = gitRemote.getName();
          return;
        }
      }
    }
    GitRemote gitRemote = GitUtil.findRemoteByName(repository, getRemoteName());
    if (gitRemote == null) {
      addGitRemote(application);
    }
    else if (!gitRemote.getUrls().contains(gitUrl)) {
      resetGitRemote(application);
    }
  }

  public GitRepository findOrCreateRepository() throws ServerRuntimeException {
    GitRepository repository = findRepository();
    if (repository == null) {
      getLoggingHandler().println("Initializing git repository...");
      GitCommandResult gitInitResult = getGit().init(getProject(), getRepositoryRoot(), createGitLineHandlerListener());
      checkGitResult(gitInitResult);

      refreshApplicationRepository();

      repository = getRepository();
    }
    return repository;
  }

  public void downloadExistingApplication() throws ServerRuntimeException {
    new CloneJobWithRemote().cloneToModule(getApplication().getGitUrl());
    getRepository().update();
    refreshContentRoot();
  }

  protected Git getGit() {
    return myGit;
  }

  protected VirtualFile getRepositoryRoot() {
    return myContentRoot;
  }

  protected File getRepositoryRootFile() {
    return myRepositoryRootFile;
  }

  protected static void checkGitResult(GitCommandResult commandResult) throws ServerRuntimeException {
    if (!commandResult.success()) {
      Throwable exception = commandResult.getException();
      if (exception != null) {
        LOG.info(exception);
        throw new ServerRuntimeException(exception);
      }
      else {
        throw new ServerRuntimeException(commandResult.getErrorOutputAsJoinedString());
      }
    }
  }

  protected GitLineHandlerListener createGitLineHandlerListener() {
    return new GitLineHandlerAdapter() {

      @Override
      public void onLineAvailable(String line, Key outputType) {
        getLoggingHandler().println(line);
      }
    };
  }

  @Override
  protected CloudAgentLoggingHandler getLoggingHandler() {
    return super.getLoggingHandler();
  }

  protected void addGitRemote(CloudGitApplication application) throws ServerRuntimeException {
    doGitRemote(getRemoteName(), application, "add", CloudBundle.getText("failed.add.remote", getRemoteName()));
  }

  protected void resetGitRemote(CloudGitApplication application) throws ServerRuntimeException {
    doGitRemote(getRemoteName(), application, "set-url", CloudBundle.getText("failed.reset.remote", getRemoteName()));
  }

  protected void doGitRemote(String remoteName,
                             CloudGitApplication application,
                             String subCommand,
                             String failMessage)
    throws ServerRuntimeException {
    try {
      final GitSimpleHandler handler = new GitSimpleHandler(getProject(), myContentRoot, GitCommand.REMOTE);
      handler.setSilent(false);
      handler.addParameters(subCommand, remoteName, application.getGitUrl());
      handler.run();
      getRepository().update();
      if (handler.getExitCode() != 0) {
        throw new ServerRuntimeException(failMessage);
      }
    }
    catch (VcsException e) {
      throw new ServerRuntimeException(e);
    }
  }

  @Nullable
  protected GitRepository findRepository() {
    if (myRepository != null) {
      return myRepository;
    }
    myRepository = myGitRepositoryManager.getRepositoryForRoot(myContentRoot);
    return myRepository;
  }

  protected void refreshApplicationRepository() {
    GitInit.refreshAndConfigureVcsMappings(getProject(), getRepositoryRoot(), getRepositoryRootFile().getAbsolutePath());
  }

  protected void pushApplication(@NotNull CloudGitApplication application) throws ServerRuntimeException {
    push(application, getRepository(), getRemoteName());
  }

  protected void push(@NotNull CloudGitApplication application, @NotNull GitRepository repository, @NotNull String remote)
    throws ServerRuntimeException {
    GitCommandResult gitPushResult
      = getGit().push(repository, remote, application.getGitUrl(), "master:master", false, createGitLineHandlerListener());
    checkGitResult(gitPushResult);
  }

  @NotNull
  protected GitRepository getRepository() throws ServerRuntimeException {
    GitRepository repository = findRepository();
    if (repository == null) {
      throw new ServerRuntimeException("Unable to find GIT repository for module root: " + myContentRoot);
    }
    return repository;
  }

  protected void fetch() throws ServerRuntimeException {
    final VirtualFile contentRoot = getRepositoryRoot();
    GitRepository repository = getRepository();
    final GitLineHandler fetchHandler = new GitLineHandler(getProject(), contentRoot, GitCommand.FETCH);
    fetchHandler.setUrl(getApplication().getGitUrl());
    fetchHandler.setSilent(false);
    fetchHandler.addParameters(getRemoteName());
    fetchHandler.addLineListener(createGitLineHandlerListener());
    performRemoteGitTask(fetchHandler, CloudBundle.getText("fetching.application", getCloudName()));

    repository.update();
  }

  protected void add() throws ServerRuntimeException {
    try {
      GitFileUtils.addFiles(getProject(), myContentRoot, myContentRoot);
    }
    catch (VcsException e) {
      throw new ServerRuntimeException(e);
    }
  }

  protected void commit() throws ServerRuntimeException {
    commit(COMMIT_MESSAGE);
  }

  protected void commit(String message) throws ServerRuntimeException {
    try {
      if (GitUtil.hasLocalChanges(true, getProject(), myContentRoot)) {
        GitSimpleHandler handler = new GitSimpleHandler(getProject(), myContentRoot, GitCommand.COMMIT);
        handler.setSilent(false);
        handler.setStdoutSuppressed(false);
        handler.addParameters("-m", message);
        handler.endOptions();
        handler.run();
      }
    }
    catch (VcsException e) {
      throw new ServerRuntimeException(e);
    }
  }

  protected void performRemoteGitTask(final GitLineHandler handler, String title) throws ServerRuntimeException {
    final GitTask task = new GitTask(getProject(), handler, title);
    task.setProgressAnalyzer(new GitStandardProgressAnalyzer());

    final Semaphore semaphore = new Semaphore();
    semaphore.down();

    final Ref<ServerRuntimeException> errorRef = new Ref<ServerRuntimeException>();

    ApplicationManager.getApplication().invokeLater(new Runnable() {

      @Override
      public void run() {
        task.execute(false, false, new GitTaskResultHandlerAdapter() {

          @Override
          protected void run(GitTaskResult result) {
            super.run(result);
            semaphore.up();
          }

          @Override
          protected void onFailure() {
            for (VcsException error : handler.errors()) {
              getLoggingHandler().println(error.toString());
              if (errorRef.isNull()) {
                errorRef.set(new ServerRuntimeException(error));
              }
            }
          }
        });
      }
    });

    semaphore.waitFor();
    if (!errorRef.isNull()) {
      throw errorRef.get();
    }
  }

  protected void refreshContentRoot() {
    ApplicationManager.getApplication().invokeLater(new Runnable() {

      @Override
      public void run() {
        getRepositoryRoot().refresh(false, true);
      }
    });
  }

  public void fetchAndRefresh() throws ServerRuntimeException {
    fetch();
    refreshContentRoot();
  }

  private String getRemoteName() {
    if (myRemoteName == null) {
      myRemoteName = myDefaultRemoteName;
    }
    return myRemoteName;
  }

  private String getCloudName() {
    return myCloudName;
  }

  protected CloudGitApplication findApplication() throws ServerRuntimeException {
    return getAgentTaskExecutor().execute(new Computable<CloudGitApplication>() {

      @Override
      public CloudGitApplication compute() {
        return getDeployment().findApplication();
      }
    });
  }

  protected CloudGitApplication getApplication() throws ServerRuntimeException {
    CloudGitApplication application = findApplication();
    if (application == null) {
      throw new ServerRuntimeException("Can't find the application: " + getApplicationName());
    }
    return application;
  }

  protected CloudGitApplication createApplication() throws ServerRuntimeException {
    return getAgentTaskExecutor().execute(new Computable<CloudGitApplication>() {

      @Override
      public CloudGitApplication compute() {
        return getDeployment().createApplication();
      }
    });
  }

  public CloudGitApplication findApplication4Repository() throws ServerRuntimeException {
    final List<String> repositoryUrls = new ArrayList<String>();
    for (GitRemote remote : getRepository().getRemotes()) {
      for (String url : remote.getUrls()) {
        repositoryUrls.add(url);
      }
    }

    return getAgentTaskExecutor().execute(new Computable<CloudGitApplication>() {

      @Override
      public CloudGitApplication compute() {
        return getDeployment().findApplication4Repository(ArrayUtil.toStringArray(repositoryUrls));
      }
    });
  }

  public class CloneJob {

    public File cloneToTemp(String gitUrl) throws ServerRuntimeException {
      File cloneDir;
      try {
        cloneDir = FileUtil.createTempDirectory("cloud", "clone");
      }
      catch (IOException e) {
        throw new ServerRuntimeException(e);
      }

      File cloneDirParent = cloneDir.getParentFile();
      String cloneDirName = cloneDir.getName();

      doClone(cloneDirParent, cloneDirName, gitUrl);
      return cloneDir;
    }

    public void cloneToModule(String gitUrl) throws ServerRuntimeException {
      File cloneDir = cloneToTemp(gitUrl);

      try {
        FileUtil.copyDir(cloneDir, getRepositoryRootFile());
      }
      catch (IOException e) {
        throw new ServerRuntimeException(e);
      }

      refreshApplicationRepository();
    }

    public void doClone(File cloneDirParent, String cloneDirName, String gitUrl) throws ServerRuntimeException {
      GitCommandResult gitCloneResult
        = getGit().clone(getProject(), cloneDirParent, gitUrl, cloneDirName, createGitLineHandlerListener());
      checkGitResult(gitCloneResult);
    }
  }

  public class CloneJobWithRemote extends CloneJob {

    public void doClone(File cloneDirParent, String cloneDirName, String gitUrl) throws ServerRuntimeException {
      final GitLineHandler handler = new GitLineHandler(getProject(), cloneDirParent, GitCommand.CLONE);
      handler.setSilent(false);
      handler.setStdoutSuppressed(false);
      handler.setUrl(gitUrl);
      handler.addParameters("--progress");
      handler.addParameters(gitUrl);
      handler.addParameters(cloneDirName);
      handler.addParameters("-o");
      handler.addParameters(getRemoteName());
      handler.addLineListener(createGitLineHandlerListener());
      performRemoteGitTask(handler, CloudBundle.getText("cloning.existing.application", getCloudName()));
    }
  }
}
