/*
 * Copyright 2018 The Bazel Authors. All rights reserved.
 *
 * 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 com.google.idea.blaze.scala.run.producers;

import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.idea.blaze.base.async.executor.ProgressiveTaskWithProgressIndicator;
import com.google.idea.blaze.base.async.process.ExternalTask;
import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
import com.google.idea.blaze.base.command.BlazeCommand;
import com.google.idea.blaze.base.command.BlazeCommandName;
import com.google.idea.blaze.base.command.BlazeFlags;
import com.google.idea.blaze.base.command.BlazeInvocationContext;
import com.google.idea.blaze.base.command.buildresult.BlazeArtifact;
import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
import com.google.idea.blaze.base.command.buildresult.BuildResultHelper.GetArtifactsException;
import com.google.idea.blaze.base.command.buildresult.BuildResultHelperProvider;
import com.google.idea.blaze.base.console.BlazeConsoleLineProcessorProvider;
import com.google.idea.blaze.base.issueparser.IssueOutputFilter;
import com.google.idea.blaze.base.model.primitives.Label;
import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
import com.google.idea.blaze.base.projectview.ProjectViewManager;
import com.google.idea.blaze.base.projectview.ProjectViewSet;
import com.google.idea.blaze.base.run.ExecutorType;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.google.idea.blaze.base.scope.ScopedTask;
import com.google.idea.blaze.base.scope.output.StatusOutput;
import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
import com.google.idea.blaze.base.scope.scopes.ProblemsViewScope;
import com.google.idea.blaze.base.settings.Blaze;
import com.google.idea.blaze.base.settings.BlazeUserSettings;
import com.google.idea.blaze.base.sync.aspects.BuildResult;
import com.google.idea.blaze.base.util.SaveUtil;
import com.intellij.execution.BeforeRunTask;
import com.intellij.execution.BeforeRunTaskProvider;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.RunCanceledByUserException;
import com.intellij.execution.application.ApplicationConfiguration;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ExecutionUtil;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.LocalFileSystem;
import icons.BlazeIcons;
import java.io.File;
import java.util.List;
import java.util.concurrent.CancellationException;
import javax.annotation.Nullable;
import javax.swing.Icon;

class GenerateDeployableJarTaskProvider
    extends BeforeRunTaskProvider<GenerateDeployableJarTaskProvider.Task> {
  private static final Key<GenerateDeployableJarTaskProvider.Task> ID =
      Key.create("GenerateDeployableJarTarget");

  static class Task extends BeforeRunTask<Task> {
    Task() {
      super(ID);
      setEnabled(true);
    }
  }

  private final Project project;

  GenerateDeployableJarTaskProvider(Project project) {
    this.project = project;
  }

  @Override
  public Key<Task> getId() {
    return ID;
  }

  @Override
  public Icon getIcon() {
    return BlazeIcons.Logo;
  }

  @Override
  public Icon getTaskIcon(GenerateDeployableJarTaskProvider.Task task) {
    return BlazeIcons.Logo;
  }

  @Override
  public String getName() {
    return "Generate executable deployable JAR for custom target";
  }

  @Override
  public final boolean canExecuteTask(
      RunConfiguration configuration, GenerateDeployableJarTaskProvider.Task task) {
    return isValidConfiguration(configuration);
  }

  private boolean isValidConfiguration(RunConfiguration config) {
    return Blaze.isBlazeProject(project) && getTarget(config) != null;
  }

  @Nullable
  @Override
  public Task createTask(RunConfiguration config) {
    if (isValidConfiguration(config)) {
      return new Task();
    }
    return null;
  }

  @Nullable
  private static Label getTarget(RunConfiguration config) {
    return config instanceof ApplicationConfiguration
        ? ((ApplicationConfiguration) config)
            .getUserData(DeployableJarRunConfigurationProducer.TARGET_LABEL)
        : null;
  }

  @Override
  public boolean executeTask(
      DataContext context, RunConfiguration configuration, ExecutionEnvironment env, Task task) {
    Label target = getTarget(configuration);
    if (target == null) {
      return false;
    }

    try {
      File outputJar = getDeployableJar(configuration, env, target);
      LocalFileSystem.getInstance().refreshIoFiles(ImmutableList.of(outputJar));
      ((ApplicationConfiguration) configuration).setVMParameters("-cp " + outputJar.getPath());
      return true;
    } catch (ExecutionException e) {
      ExecutionUtil.handleExecutionError(
          env.getProject(), env.getExecutor().getToolWindowId(), env.getRunProfile(), e);
    }
    return false;
  }

  /**
   * Builds a deployable jar for the given target, and returns the corresponding output artifact.
   *
   * @throws ExecutionException if the build failed, or the output artifact cannot be found.
   */
  private static File getDeployableJar(
      RunConfiguration configuration, ExecutionEnvironment env, Label target)
      throws ExecutionException {
    Project project = env.getProject();
    try (BuildResultHelper buildResultHelper = BuildResultHelperProvider.create(project)) {

      SaveUtil.saveAllFiles();

      ListenableFuture<BuildResult> buildOperation =
          runBazelBuild(
              project,
              target,
              buildResultHelper,
              BlazeInvocationContext.runConfigContext(
                  ExecutorType.fromExecutor(env.getExecutor()), configuration.getType(), true));

      try {
        BuildResult result = buildOperation.get();
        if (result.status != BuildResult.Status.SUCCESS) {
          throw new ExecutionException("Bazel failure building deployable jar");
        }
      } catch (InterruptedException | CancellationException e) {
        buildOperation.cancel(true);
        throw new RunCanceledByUserException();
      } catch (java.util.concurrent.ExecutionException e) {
        throw new ExecutionException(e);
      }

      List<File> outputs =
          BlazeArtifact.getLocalFiles(
              buildResultHelper.getBuildArtifactsForTarget(
                  target.withTargetName(target.targetName() + "_deploy.jar"), file -> true));
      if (outputs.isEmpty()) {
        throw new ExecutionException(
            String.format("Failed to find deployable jar when building %s", target));
      }
      return outputs.get(0);
    } catch (GetArtifactsException e) {
      throw new ExecutionException(
          String.format(
              "Failed to find deployable jar when building %s: %s", target, e.getMessage()));
    }
  }

  /** Kicks off the bazel build task, returning a corresponding {@link ListenableFuture}. */
  private static ListenableFuture<BuildResult> runBazelBuild(
      Project project,
      Label target,
      BuildResultHelper buildResultHelper,
      BlazeInvocationContext invocationContext) {

    String binaryPath = Blaze.getBuildSystemProvider(project).getBinaryPath(project);
    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);

    String title = "Building deployable jar for " + target;
    return ProgressiveTaskWithProgressIndicator.builder(project, title)
        .submitTaskWithResult(
            new ScopedTask<BuildResult>() {
              @Override
              protected BuildResult execute(BlazeContext context) {
                context
                    .push(
                        new ProblemsViewScope(
                            project, BlazeUserSettings.getInstance().getShowProblemsViewOnRun()))
                    .push(
                        new BlazeConsoleScope.Builder(project)
                            .setPopupBehavior(
                                BlazeUserSettings.getInstance().getShowBlazeConsoleOnRun())
                            .addConsoleFilters(
                                new IssueOutputFilter(
                                    project, workspaceRoot, invocationContext.type(), true))
                            .build());

                context.output(new StatusOutput(title));
                BlazeCommand command =
                    BlazeCommand.builder(binaryPath, BlazeCommandName.BUILD)
                        .addTargets(target.withTargetName(target.targetName() + "_deploy.jar"))
                        .addBlazeFlags(
                            BlazeFlags.blazeFlags(
                                project, projectViewSet, BlazeCommandName.BUILD, invocationContext))
                        .addBlazeFlags(buildResultHelper.getBuildFlags())
                        .build();
                int exitCode =
                    ExternalTask.builder(workspaceRoot)
                        .addBlazeCommand(command)
                        .context(context)
                        .stderr(
                            LineProcessingOutputStream.of(
                                BlazeConsoleLineProcessorProvider.getAllStderrLineProcessors(
                                    context)))
                        .build()
                        .run();
                return BuildResult.fromExitCode(exitCode);
              }
            });
  }
}