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

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.SubscriberExceptionHandler;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.AnalysisResult;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
import com.google.devtools.build.lib.analysis.ConfiguredAspect;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.ServerDirectories;
import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.test.CoverageReportActionFactory;
import com.google.devtools.build.lib.buildtool.BuildRequest;
import com.google.devtools.build.lib.clock.Clock;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.exec.ExecutorBuilder;
import com.google.devtools.build.lib.exec.ModuleActionContextRegistry;
import com.google.devtools.build.lib.exec.SpawnStrategyRegistry;
import com.google.devtools.build.lib.packages.Package.Builder.PackageSettings;
import com.google.devtools.build.lib.packages.PackageLoadingListener;
import com.google.devtools.build.lib.packages.PackageOverheadEstimator;
import com.google.devtools.build.lib.packages.PackageValidator;
import com.google.devtools.build.lib.skyframe.PrecomputedValue;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.OutputService;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParsingResult;
import com.google.devtools.common.options.OptionsProvider;
import java.util.Optional;
import java.util.UUID;
import javax.annotation.Nullable;

/**
 * Provides the ability to augment the functionality of the Logical Component (LC).
 *
 * <p>The augmentation is done by implementing one or more of the methods in this class, which are
 * called at well-defined points during the server's lifecycle.
 *
 * <p>The set of modules is passed into {@link BlazeRuntime#main} and is fixed for the lifetime of
 * the server. A module can be obtained by calling {@link BlazeRuntime#getBlazeModule}.
 *
 * <p>The constructors of individual Bazel modules must take no arguments and be empty. All work
 * should be done in the methods (e.g. {@link #blazeStartup}).
 */
public abstract class BlazeModule implements OptionsSupplier {

  @Override
  public Iterable<Class<? extends OptionsBase>> getStartupOptions() {
    return ImmutableList.of();
  }

  /**
   * Called at the beginning of Bazel startup, before {@link #getFileSystem} and {@link
   * #blazeStartup}.
   *
   * @param startupOptions the server's startup options
   * @param blazeServices the available services
   * @throws AbruptExitException to shut down the server immediately
   */
  public void globalInit(OptionsParsingResult startupOptions, Iterable<BlazeService> blazeServices)
      throws AbruptExitException {}

  /**
   * Returns the file system implementation used by Bazel.
   *
   * <p>Exactly one module must return a non-null value from this method, or an error will occur.
   *
   * <p>This method will be called at the beginning of Bazel startup (in-between {@link #globalInit}
   * and {@link #blazeStartup}).
   *
   * @param startupOptions the server's startup options
   */
  @Nullable
  public ModuleFileSystem getFileSystem(OptionsParsingResult startupOptions)
      throws AbruptExitException {
    return null;
  }

  /**
   * Returns the file system implementation used by Bazel to read or write build artifacts.
   *
   * <p>At most one module may return a non-null value from this method, or an error will occur. If
   * no module returns a non-null value, the file system returned by {@link #getFileSystem} from
   * this or another module will be used.
   *
   * <p>This method will be called at the beginning of Bazel startup (in-between {@link #globalInit}
   * and {@link #blazeStartup}).
   *
   * @param fileSystem the file system returned by {@link #getFileSystem} from this or another
   *     module
   */
  @Nullable
  public FileSystem getFileSystemForBuildArtifacts(FileSystem fileSystem) {
    return null;
  }

  /** Tuple returned by {@link #getFileSystem}. */
  @AutoValue
  public abstract static class ModuleFileSystem {
    public abstract FileSystem fileSystem();

    /**
     * Present if this filesystem virtualizes the source root. See {@link
     * ServerDirectories#getVirtualSourceRoot}.
     */
    abstract Optional<Root> virtualSourceRoot();

    public static ModuleFileSystem createWithVirtualization(
        FileSystem fileSystem, PathFragment virtualSourceRoot) {
      return new AutoValue_BlazeModule_ModuleFileSystem(
          fileSystem, Optional.of(Root.fromPath(fileSystem.getPath(virtualSourceRoot))));
    }

    public static ModuleFileSystem create(FileSystem fileSystem) {
      return new AutoValue_BlazeModule_ModuleFileSystem(
          fileSystem, /* virtualSourceRoot= */ Optional.empty());
    }
  }

  /**
   * Returns handler for {@link com.google.common.eventbus.EventBus} subscriber and async thread
   * exceptions. For async thread exceptions, {@link
   * SubscriberExceptionHandler#handleException(Throwable,
   * com.google.common.eventbus.SubscriberExceptionContext)} will be called with null {@link
   * com.google.common.eventbus.SubscriberExceptionContext}. If all modules return null, a handler
   * that crashes on all async exceptions and files bug reports for all EventBus subscriber
   * exceptions will be used.
   */
  public SubscriberExceptionHandler getEventBusAndAsyncExceptionHandler() {
    return null;
  }

  /**
   * Called when Bazel starts up after {@link #getStartupOptions}, {@link #globalInit} and {@link
   * #getFileSystem}.
   *
   * @param startupOptions the server's startup options
   * @param versionInfo the Bazel version currently running
   * @param instanceId the id of the current Bazel server
   * @param directories the install directory
   * @throws AbruptExitException to shut down the server immediately
   */
  public void blazeStartup(
      OptionsParsingResult startupOptions,
      BlazeVersionInfo versionInfo,
      UUID instanceId,
      FileSystem fileSystem,
      ServerDirectories directories,
      Clock clock)
      throws AbruptExitException {}

  /**
   * Called to initialize a new server ({@link BlazeRuntime}). Modules can override this method to
   * affect how the server is configured. This is called after the startup options have been
   * collected and parsed, and after the file system was setup.
   *
   * @param startupOptions the server startup options
   * @param builder builder class that collects the server configuration
   * @throws AbruptExitException to shut down the server immediately
   */
  public void serverInit(OptionsParsingResult startupOptions, ServerBuilder builder)
      throws AbruptExitException {}

  /**
   * Sets up the configured rule class provider, which contains the built-in rule classes, aspects,
   * configuration fragments, and other things; called during Blaze startup (after {@link
   * #blazeStartup}).
   *
   * <p>Bazel only creates one provider per server, so it is not possible to have different contents
   * for different workspaces.
   *
   * @param builder the configured rule class provider builder
   */
  public void initializeRuleClasses(ConfiguredRuleClassProvider.Builder builder) {}

  /**
   * Called when Bazel initializes a new workspace; this is only called after {@link #serverInit},
   * and only if the server initialization was successful. Modules can override this method to
   * affect how the workspace is configured.
   *
   * @param runtime the blaze runtime
   * @param directories the workspace directories
   * @param builder the workspace builder
   */
  public void workspaceInit(
      BlazeRuntime runtime, BlazeDirectories directories, WorkspaceBuilder builder) {}

  /**
   * Called to notify modules that the given command is about to be executed. This allows capturing
   * the {@link com.google.common.eventbus.EventBus}, {@link Command}, or {@link
   * OptionsParsingResult}.
   *
   * @param env the command
   * @throws AbruptExitException modules can throw this exception to abort the command
   */
  public void beforeCommand(CommandEnvironment env) throws AbruptExitException {}

  /**
   * Returns additional listeners to the console output stream. Called at the beginning of each
   * command (after #beforeCommand).
   */
  @Nullable
  public OutErr getOutputListener() {
    return null;
  }

  /**
   * Returns the {@link OutputService} to be used.
   *
   * <p>It is an error if more than one module returns a non-null output service. If all modules
   * return {@code null}, then {@link com.google.devtools.build.lib.vfs.LocalOutputService} will be
   * used.
   *
   * <p>This method is called at the beginning of each command (after {@link #beforeCommand}).
   */
  public OutputService getOutputService() throws AbruptExitException {
    return null;
  }

  @Override
  public Iterable<Class<? extends OptionsBase>> getCommandOptions(String commandName) {
    return ImmutableList.of();
  }

  @Override
  public Iterable<Class<? extends OptionsBase>> getCommonCommandOptions() {
    return ImmutableList.of();
  }

  /**
   * Called after Bazel analyzes the build's top-level targets. This is called once per build if
   * --analyze is enabled. Modules can override this to perform extra checks on analysis results.
   *
   * @param env the command environment
   * @param request the build request
   * @param buildOptions the build's top-level options
   * @param analysisResult the build's analysis result
   */
  public void afterAnalysis(
      CommandEnvironment env,
      BuildRequest request,
      BuildOptions buildOptions,
      AnalysisResult analysisResult)
      throws InterruptedException, ViewCreationFailedException {}

  /**
   * Called after Bazel analyzes a single top-level target.
   *
   * @param env the command environment
   * @param request the build request
   * @param buildOptions the build's top-level options
   * @param configuredTarget the analyzed top-level target
   */
  public void afterTopLevelTargetAnalysis(
      CommandEnvironment env,
      BuildRequest request,
      BuildOptions buildOptions,
      ConfiguredTarget configuredTarget)
      throws InterruptedException, ViewCreationFailedException {}

  public void afterSingleAspectAnalysis(BuildRequest request, ConfiguredAspect configuredTarget) {}

  public void afterSingleTestAnalysis(BuildRequest request, ConfiguredTarget configuredTarget) {}

  public void coverageArtifactsKnown(ImmutableSet<Artifact> coverageArtifacts) {}

  /**
   * Called when Bazel initializes the action execution subsystem. This is called once per build if
   * action execution is enabled. Modules can override this method to affect how execution is
   * performed.
   *
   * @param env the command environment
   * @param request the build request
   * @param builder the builder to add action context providers and consumers to
   */
  public void executorInit(CommandEnvironment env, BuildRequest request, ExecutorBuilder builder)
      throws AbruptExitException {}

  /**
   * Registers any action contexts this module provides with the execution phase. They will be
   * available for {@linkplain
   * com.google.devtools.build.lib.actions.ActionContext.ActionContextRegistry#getContext querying}
   * to actions and other action contexts.
   *
   * <p>This method is invoked before actions are executed but after {@link #executorInit}.
   *
   * @param registryBuilder builder with which to register action contexts
   * @param env environment for the current command
   * @param buildRequest the current build request
   * @throws AbruptExitException if there are fatal issues creating or registering action contexts
   */
  public void registerActionContexts(
      ModuleActionContextRegistry.Builder registryBuilder,
      CommandEnvironment env,
      BuildRequest buildRequest)
      throws AbruptExitException {}

  /**
   * Registers any spawn strategies this module provides with the execution phase.
   *
   * <p>This method is invoked before actions are executed but after {@link #executorInit}.
   *
   * @param registryBuilder builder with which to register strategies
   * @param env environment for the current command
   * @throws AbruptExitException if there are fatal issues creating or registering strategies
   */
  public void registerSpawnStrategies(
      SpawnStrategyRegistry.Builder registryBuilder, CommandEnvironment env)
      throws AbruptExitException, InterruptedException {}

  /**
   * Called after each command.
   *
   * @throws AbruptExitException modules can throw this exception to modify the command exit code
   */
  public void afterCommand() throws AbruptExitException {}

  /**
   * Called after {@link #afterCommand()}. This method can be used to close and cleanup resources
   * specific to the command.
   *
   * <p>This method must not throw any exceptions, report any errors or generate any stdout/stderr.
   * Any of the above will make Bazel crash occasionally. Please use {@link #afterCommand()}
   * instead.
   */
  public void commandComplete() {}

  /**
   * Called when Blaze shuts down.
   *
   * <p>If you are also implementing {@link #blazeShutdownOnCrash}, consider putting the common
   * shutdown code in the latter and calling that other hook from here.
   *
   * <p>This is also called after each test case in {@link
   * com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase} and can be used to avoid
   * leaking resources when this module instance is thrown away between tests.
   */
  public void blazeShutdown() {}

  /**
   * Called when Blaze shuts down due to a crash.
   *
   * <p>Modules may use this to flush pending state, but they must be careful to only do a minimal
   * number of things. Keep in mind that we are crashing so who knows what state we are in. Modules
   * rarely need to implement this.
   */
  public void blazeShutdownOnCrash(DetailedExitCode exitCode) {}

  /**
   * Returns true if the module will arrange for a {@code BuildMetricsEvent} to be posted after the
   * build completes.
   *
   * <p>The Blaze runtime ensures that it has exactly one module for which this method returns true,
   * substituting its own module if none is supplied explicitly.
   *
   * <p>It is an error if multiple modules return true.
   */
  public boolean postsBuildMetricsEvent() {
    return false;
  }

  /**
   * Returns a {@link QueryRuntimeHelper.Factory} that will be used by the query, cquery, and aquery
   * commands.
   *
   * <p>It is an error if multiple modules return non-null values.
   */
  public QueryRuntimeHelper.Factory getQueryRuntimeHelperFactory() {
    return null;
  }

  /**
   * Returns {@link PackageSettings} for creating packages.
   *
   * <p>Called once during server startup some time after {@link #serverInit}.
   *
   * <p>Note that only one helper per Bazel/Blaze runtime is allowed.
   */
  @Nullable
  public PackageSettings getPackageSettings() {
    return null;
  }

  /**
   * Returns a {@link PackageValidator} to be used to validate loaded packages, or null if the
   * module does not provide any validator.
   *
   * <p>Called once during server startup some time after {@link #serverInit}.
   *
   * <p>Note that only one instance per Bazel/Blaze runtime is allowed.
   */
  @Nullable
  public PackageValidator getPackageValidator() {
    return null;
  }

  /**
   * Returns a {@link PackageOverheadEstimator} to be used to estimate the cost of loaded packages,
   * or null if the module does not provide any such functionality.
   *
   * <p>Called once during server startup some time after {@link #serverInit}.
   *
   * <p>Note that only one instance per Bazel/Blaze runtime is allowed
   */
  @Nullable
  public PackageOverheadEstimator getPackageOverheadEstimator() {
    return null;
  }

  /**
   * Returns a {@link PackageLoadingListener} for observing successful package loading, or null if
   * the module does not provide any validator.
   *
   * <p>Called once during server startup some time after {@link #serverInit}.
   */
  @Nullable
  public PackageLoadingListener getPackageLoadingListener(
      PackageSettings packageSettings,
      ConfiguredRuleClassProvider ruleClassProvider,
      FileSystem fs) {
    return null;
  }

  @Nullable
  public String getSlowThreadInterruptMessageSuffix() {
    return null;
  }

  /**
   * Optionally returns a provider for project files that can be used to bundle targets and
   * command-line options.
   */
  @Nullable
  public ProjectFile.Provider createProjectFileProvider() {
    return null;
  }

  /**
   * Optionally returns a factory to create coverage report actions; this is called once per build,
   * such that it can be affected by command options.
   *
   * <p>It is an error if multiple modules return non-null values.
   *
   * @param commandOptions the options for the current command
   */
  @Nullable
  public CoverageReportActionFactory getCoverageReportFactory(OptionsProvider commandOptions) {
    return null;
  }

  /** Services provided for Blaze modules via BlazeRuntime. */
  public interface ModuleEnvironment {
    /**
     * Gets a file from the depot based on its label and returns the {@link Path} where it can be
     * found.
     *
     * <p>Returns null when the package designated by the label does not exist.
     */
    @Nullable
    Path getFileFromWorkspace(Label label);

    /** Exits Blaze as early as possible by sending an interrupt to the command's main thread. */
    void exit(AbruptExitException exception);
  }

  /**
   * Provides additional precomputed values to inject into the skyframe graph. Called on every
   * command execution.
   */
  public ImmutableList<PrecomputedValue.Injected> getPrecomputedValues() {
    return ImmutableList.of();
  }

  @Override
  public String toString() {
    return this.getClass().getSimpleName();
  }
}
