// Copyright 2014 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.devtools.build.lib.buildtool;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import com.google.devtools.build.lib.actions.ActionCacheChecker;
import com.google.devtools.build.lib.actions.ActionExecutionException;
import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
import com.google.devtools.build.lib.actions.ActionInputFileCache;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.BuildFailedException;
import com.google.devtools.build.lib.actions.Executor;
import com.google.devtools.build.lib.actions.MissingInputFileException;
import com.google.devtools.build.lib.actions.ResourceManager;
import com.google.devtools.build.lib.actions.TestExecException;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
import com.google.devtools.build.lib.buildtool.buildevent.ExecutionProgressReceiverAvailableEvent;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
import com.google.devtools.build.lib.rules.test.TestProvider;
import com.google.devtools.build.lib.skyframe.ActionExecutionInactivityWatchdog;
import com.google.devtools.build.lib.skyframe.AspectValue;
import com.google.devtools.build.lib.skyframe.Builder;
import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.LoggingUtil;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.ModifiedFileSet;
import com.google.devtools.build.skyframe.CycleInfo;
import com.google.devtools.build.skyframe.ErrorInfo;
import com.google.devtools.build.skyframe.EvaluationResult;
import com.google.devtools.build.skyframe.SkyKey;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import javax.annotation.Nullable;

/**
 * A {@link Builder} implementation driven by Skyframe.
 */
@VisibleForTesting
public class SkyframeBuilder implements Builder {

  private final SkyframeExecutor skyframeExecutor;
  private final boolean keepGoing;
  private final int numJobs;
  private final boolean finalizeActionsToOutputService;
  private final ModifiedFileSet modifiedOutputFiles;
  private final ActionInputFileCache fileCache;
  private final ActionCacheChecker actionCacheChecker;
  private final int progressReportInterval;

  @VisibleForTesting
  public SkyframeBuilder(SkyframeExecutor skyframeExecutor, ActionCacheChecker actionCacheChecker,
      boolean keepGoing, int numJobs, ModifiedFileSet modifiedOutputFiles,
      boolean finalizeActionsToOutputService, ActionInputFileCache fileCache,
      int progressReportInterval) {
    this.skyframeExecutor = skyframeExecutor;
    this.actionCacheChecker = actionCacheChecker;
    this.keepGoing = keepGoing;
    this.numJobs = numJobs;
    this.finalizeActionsToOutputService = finalizeActionsToOutputService;
    this.modifiedOutputFiles = modifiedOutputFiles;
    this.fileCache = fileCache;
    this.progressReportInterval = progressReportInterval;
  }

  @Override
  public void buildArtifacts(
      Reporter reporter,
      Set<Artifact> artifacts,
      Set<ConfiguredTarget> parallelTests,
      Set<ConfiguredTarget> exclusiveTests,
      Collection<ConfiguredTarget> targetsToBuild,
      Collection<AspectValue> aspects,
      Executor executor,
      Set<ConfiguredTarget> builtTargets,
      boolean explain,
      @Nullable Range<Long> lastExecutionTimeRange,
      TopLevelArtifactContext topLevelArtifactContext)
      throws BuildFailedException, AbruptExitException, TestExecException, InterruptedException {
    skyframeExecutor.prepareExecution(modifiedOutputFiles, lastExecutionTimeRange);
    skyframeExecutor.setFileCache(fileCache);
    // Note that executionProgressReceiver accesses builtTargets concurrently (after wrapping in a
    // synchronized collection), so unsynchronized access to this variable is unsafe while it runs.
    ExecutionProgressReceiver executionProgressReceiver =
        new ExecutionProgressReceiver(
            Preconditions.checkNotNull(builtTargets),
            countTestActions(exclusiveTests),
            ImmutableSet.<ConfiguredTarget>builder()
                .addAll(parallelTests)
                .addAll(exclusiveTests)
                .build(),
            skyframeExecutor.getEventBus());
    skyframeExecutor
        .getEventBus()
        .post(new ExecutionProgressReceiverAvailableEvent(executionProgressReceiver));
    ResourceManager.instance().setEventBus(skyframeExecutor.getEventBus());

    List<ExitCode> exitCodes = new LinkedList<>();
    EvaluationResult<?> result;

    ActionExecutionStatusReporter statusReporter = ActionExecutionStatusReporter.create(
        reporter, executor, skyframeExecutor.getEventBus());

    AtomicBoolean isBuildingExclusiveArtifacts = new AtomicBoolean(false);
    ActionExecutionInactivityWatchdog watchdog = new ActionExecutionInactivityWatchdog(
        executionProgressReceiver.createInactivityMonitor(statusReporter),
        executionProgressReceiver.createInactivityReporter(statusReporter,
            isBuildingExclusiveArtifacts), progressReportInterval);

    skyframeExecutor.setActionExecutionProgressReportingObjects(executionProgressReceiver,
        executionProgressReceiver, statusReporter);
    watchdog.start();

    try {
      result =
          skyframeExecutor.buildArtifacts(
              reporter,
              executor,
              artifacts,
              targetsToBuild,
              aspects,
              parallelTests,
              /*exclusiveTesting=*/ false,
              keepGoing,
              explain,
              finalizeActionsToOutputService,
              numJobs,
              actionCacheChecker,
              executionProgressReceiver,
              topLevelArtifactContext);
      // progressReceiver is finished, so unsynchronized access to builtTargets is now safe.
      Optional<ExitCode> exitCode = processResult(reporter, result, keepGoing, skyframeExecutor);

      Preconditions.checkState(
          exitCode != null
              || result.keyNames().size()
                  == (artifacts.size()
                      + targetsToBuild.size()
                      + aspects.size()
                      + parallelTests.size()),
          "Build reported as successful but not all artifacts and targets built: %s, %s",
          result,
          artifacts);

      if (exitCode != null) {
        exitCodes.add(exitCode.orNull());
      }

      // Run exclusive tests: either tagged as "exclusive" or is run in an invocation with
      // --test_output=streamed.
      isBuildingExclusiveArtifacts.set(true);
      for (ConfiguredTarget exclusiveTest : exclusiveTests) {
        // Since only one artifact is being built at a time, we don't worry about an artifact being
        // built and then the build being interrupted.
        result =
            skyframeExecutor.buildArtifacts(
                reporter,
                executor,
                ImmutableSet.<Artifact>of(),
                targetsToBuild,
                aspects,
                ImmutableSet.of(exclusiveTest), /*exclusiveTesting=*/
                true,
                keepGoing,
                explain,
                finalizeActionsToOutputService,
                numJobs,
                actionCacheChecker,
                null,
                topLevelArtifactContext);
        exitCode = processResult(reporter, result, keepGoing, skyframeExecutor);
        Preconditions.checkState(
            exitCode != null || !result.keyNames().isEmpty(),
            "Build reported as successful but test %s not executed: %s",
            exclusiveTest,
            result);

        if (exitCode != null) {
          exitCodes.add(exitCode.orNull());
        }
      }
    } finally {
      watchdog.stop();
      ResourceManager.instance().unsetEventBus();
      skyframeExecutor.setActionExecutionProgressReportingObjects(null, null, null);
      statusReporter.unregisterFromEventBus();
    }

    if (!exitCodes.isEmpty()) {
      if (keepGoing) {
        // Use the exit code with the highest priority.
        throw new BuildFailedException(
            null, Collections.max(exitCodes, ExitCodeComparator.INSTANCE));
      } else {
        throw new BuildFailedException();
      }
    }
  }

  /**
   * Process the Skyframe update, taking into account the keepGoing setting.
   *
   * <p> Returns optional {@link ExitCode} based on following conditions:
   *    1. null, if result had no errors.
   *    2. Optional.absent(), if result had errors but none of the errors specified an exit code.
   *    3. Optional.of(e), if result had errors and one of them specified exit code 'e'.
   * Throws on fail-fast failures.
   */
  @Nullable
  private static Optional<ExitCode> processResult(
      EventHandler eventHandler,
      EvaluationResult<?> result,
      boolean keepGoing,
      SkyframeExecutor skyframeExecutor)
      throws BuildFailedException, TestExecException {
    if (result.hasError()) {
      for (Map.Entry<SkyKey, ErrorInfo> entry : result.errorMap().entrySet()) {
        Iterable<CycleInfo> cycles = entry.getValue().getCycleInfo();
        skyframeExecutor.reportCycles(eventHandler, cycles, entry.getKey());
      }

      if (result.getCatastrophe() != null) {
        rethrow(result.getCatastrophe());
      }
      if (keepGoing) {
        // If build fails and keepGoing is true, an exit code is assigned using reported errors
        // in the following order:
        //   1. First infrastructure error with non-null exit code
        //   2. First non-infrastructure error with non-null exit code
        //   3. Null (later default to 1)
        ExitCode exitCode = null;
        for (Map.Entry<SkyKey, ErrorInfo> error : result.errorMap().entrySet()) {
          Throwable cause = error.getValue().getException();
          if (cause instanceof ActionExecutionException) {
            ActionExecutionException actionExecutionCause = (ActionExecutionException) cause;
            ExitCode code = actionExecutionCause.getExitCode();
            // Update global exit code when current exit code is not null and global exit code has
            // a lower 'reporting' priority.
            if (ExitCodeComparator.INSTANCE.compare(code, exitCode) > 0) {
              exitCode = code;
            }
          }
        }

        return Optional.fromNullable(exitCode);
      }
      ErrorInfo errorInfo = Preconditions.checkNotNull(result.getError(), result);
      Exception exception = errorInfo.getException();
      if (exception == null) {
        Preconditions.checkState(!Iterables.isEmpty(errorInfo.getCycleInfo()), errorInfo);
        // If a keepGoing=false build found a cycle, that means there were no other errors thrown
        // during evaluation (otherwise, it wouldn't have bothered to find a cycle). So the best
        // we can do is throw a generic build failure exception, since we've already reported the
        // cycles above.
        throw new BuildFailedException(null, /*hasCatastrophe=*/ false);
      } else {
        rethrow(exception);
      }
    }

    return null;
  }

  /** Figure out why an action's execution failed and rethrow the right kind of exception. */
  @VisibleForTesting
  public static void rethrow(Throwable cause) throws BuildFailedException, TestExecException {
    Throwable innerCause = cause.getCause();
    if (innerCause instanceof TestExecException) {
      throw (TestExecException) innerCause;
    }
    if (cause instanceof ActionExecutionException) {
      ActionExecutionException actionExecutionCause = (ActionExecutionException) cause;
      // Sometimes ActionExecutionExceptions are caused by Actions with no owner.
      String message =
          (actionExecutionCause.getLocation() != null)
              ? (actionExecutionCause.getLocation().print() + " " + cause.getMessage())
              : cause.getMessage();
      throw new BuildFailedException(
          message,
          actionExecutionCause.isCatastrophe(),
          actionExecutionCause.getAction(),
          actionExecutionCause.getRootCauses(),
          /*errorAlreadyShown=*/ !actionExecutionCause.showError(),
          actionExecutionCause.getExitCode());
    } else if (cause instanceof MissingInputFileException) {
      throw new BuildFailedException(cause.getMessage());
    } else if (cause instanceof BuildFileNotFoundException) {
      // Sadly, this can happen because we may load new packages during input discovery. Any
      // failures reading those packages shouldn't terminate the build, but in Skyframe they do.
      LoggingUtil.logToRemote(Level.WARNING, "undesirable loading exception", cause);
      throw new BuildFailedException(cause.getMessage());
    } else if (cause instanceof RuntimeException) {
      throw (RuntimeException) cause;
    } else if (cause instanceof Error) {
      throw (Error) cause;
    } else {
      // We encountered an exception we don't think we should have encountered. This can indicate
      // a bug in our code, such as lower level exceptions not being properly handled, or in our
      // expectations in this method.
      throw new IllegalArgumentException(
          "action terminated with " + "unexpected exception: " + cause.getMessage(), cause);
    }
  }

  private static int countTestActions(Iterable<ConfiguredTarget> testTargets) {
    int count = 0;
    for (ConfiguredTarget testTarget : testTargets) {
      count += TestProvider.getTestStatusArtifacts(testTarget).size();
    }
    return count;
  }

  /**
   * A comparator to determine the reporting priority of {@link ExitCode}.
   *
   * <p> Priority: infrastructure exit codes > non-infrastructure exit codes > null exit codes.
   */
  private static class ExitCodeComparator implements Comparator<ExitCode> {
    private static final ExitCodeComparator INSTANCE = new ExitCodeComparator();

    @Override
    public int compare(ExitCode c1, ExitCode c2) {
      // returns POSITIVE result when the priority of c1 is HIGHER than the priority of c2
      return getPriority(c1) - getPriority(c2);
    }

    private int getPriority(ExitCode code) {
      if (code == null) {
        return 0;
      } else {
        return code.isInfrastructureFailure() ? 2 : 1;
      }
    }
  }
}
