// 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.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import com.google.devtools.build.lib.analysis.AnalysisOptions;
import com.google.devtools.build.lib.analysis.OutputGroupInfo;
import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
import com.google.devtools.build.lib.exec.ExecutionOptions;
import com.google.devtools.build.lib.packages.SkylarkSemanticsOptions;
import com.google.devtools.build.lib.pkgcache.LoadingOptions;
import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
import com.google.devtools.build.lib.runtime.BlazeCommandEventHandler;
import com.google.devtools.build.lib.runtime.KeepGoingOption;
import com.google.devtools.build.lib.runtime.LoadingPhaseThreadsOption;
import com.google.devtools.build.lib.util.OptionsUtils;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParsingResult;
import com.google.devtools.common.options.OptionsProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;

/**
 * A BuildRequest represents a single invocation of the build tool by a user.
 * A request specifies a list of targets to be built for a single
 * configuration, a pair of output/error streams, and additional options such
 * as --keep_going, --jobs, etc.
 */
public class BuildRequest implements OptionsProvider {
  private final UUID id;
  private final LoadingCache<Class<? extends OptionsBase>, Optional<OptionsBase>> optionsCache;
  private final Map<String, Object> skylarkOptions;

  /** A human-readable description of all the non-default option settings. */
  private final String optionsDescription;

  /**
   * The name of the Blaze command that the user invoked.
   * Used for --announce.
   */
  private final String commandName;

  private final OutErr outErr;
  private final List<String> targets;

  private long startTimeMillis = 0; // milliseconds since UNIX epoch.

  private boolean needsInstrumentationFilter;
  private boolean runningInEmacs;
  private boolean runTests;

  private static final ImmutableList<Class<? extends OptionsBase>> MANDATORY_OPTIONS =
      ImmutableList.of(
          BuildRequestOptions.class,
          PackageCacheOptions.class,
          SkylarkSemanticsOptions.class,
          LoadingOptions.class,
          AnalysisOptions.class,
          ExecutionOptions.class,
          KeepGoingOption.class,
          LoadingPhaseThreadsOption.class);

  private BuildRequest(String commandName,
                       final OptionsParsingResult options,
                       final OptionsParsingResult startupOptions,
                       List<String> targets,
                       OutErr outErr,
                       UUID id,
                       long startTimeMillis) {
    this.commandName = commandName;
    this.optionsDescription = OptionsUtils.asShellEscapedString(options);
    this.outErr = outErr;
    this.targets = targets;
    this.id = id;
    this.startTimeMillis = startTimeMillis;
    this.optionsCache = CacheBuilder.newBuilder()
        .build(new CacheLoader<Class<? extends OptionsBase>, Optional<OptionsBase>>() {
          @Override
          public Optional<OptionsBase> load(Class<? extends OptionsBase> key) throws Exception {
            OptionsBase result = options.getOptions(key);
            if (result == null && startupOptions != null) {
              result = startupOptions.getOptions(key);
            }

            return Optional.fromNullable(result);
          }
        });
    this.skylarkOptions = options.getSkylarkOptions();

    for (Class<? extends OptionsBase> optionsClass : MANDATORY_OPTIONS) {
      Preconditions.checkNotNull(getOptions(optionsClass));
    }
  }


  @Override
  public Map<String, Object> getSkylarkOptions() {
    return skylarkOptions;
  }

  /**
   * Returns a unique identifier that universally identifies this build.
   */
  public UUID getId() {
    return id;
  }

  /**
   * Returns the name of the Blaze command that the user invoked.
   */
  public String getCommandName() {
    return commandName;
  }

  /**
   * Set to true if this build request was initiated by Emacs.
   * (Certain output formatting may be necessary.)
   */
  public void setRunningInEmacs() {
    runningInEmacs = true;
  }

  boolean isRunningInEmacs() {
    return runningInEmacs;
  }

  /**
   * Enables test execution for this build request.
   */
  public void setRunTests() {
    runTests = true;
  }

  /**
   * Returns true if tests should be run by the build tool.
   */
  public boolean shouldRunTests() {
    return runTests;
  }

  /**
   * Returns the (immutable) list of targets to build in commandline
   * form.
   */
  public List<String> getTargets() {
    return targets;
  }

  /**
   * Returns the output/error streams to which errors and progress messages
   * should be sent during the fulfillment of this request.
   */
  public OutErr getOutErr() {
    return outErr;
  }

  @Override
  @SuppressWarnings("unchecked")
  public <T extends OptionsBase> T getOptions(Class<T> clazz) {
    try {
      return (T) optionsCache.get(clazz).orNull();
    } catch (ExecutionException e) {
      throw new IllegalStateException(e);
    }
  }


  /**
   * Returns the set of command-line options specified for this request.
   */
  public BuildRequestOptions getBuildOptions() {
    return getOptions(BuildRequestOptions.class);
  }

  /**
   * Returns the set of options related to the loading phase.
   */
  public PackageCacheOptions getPackageCacheOptions() {
    return getOptions(PackageCacheOptions.class);
  }

  /**
   * Returns the set of options related to the loading phase.
   */
  public LoadingOptions getLoadingOptions() {
    return getOptions(LoadingOptions.class);
  }

  /**
   * Returns the set of command-line options related to the view specified for
   * this request.
   */
  public AnalysisOptions getViewOptions() {
    return getOptions(AnalysisOptions.class);
  }

  /** Returns the value of the --keep_going option. */
  boolean getKeepGoing() {
    return getOptions(KeepGoingOption.class).keepGoing;
  }

  /** Returns the value of the --loading_phase_threads option. */
  int getLoadingPhaseThreadCount() {
    return getOptions(LoadingPhaseThreadsOption.class).threads;
  }
  /**
   * Returns the set of execution options specified for this request.
   */
  public ExecutionOptions getExecutionOptions() {
    return getOptions(ExecutionOptions.class);
  }

  /**
   * Returns the human-readable description of the non-default options
   * for this build request.
   */
  public String getOptionsDescription() {
    return optionsDescription;
  }

  /**
   * Return the time (according to System.currentTimeMillis()) at which the
   * service of this request was started.
   */
  public long getStartTime() {
    return startTimeMillis;
  }

  public void setNeedsInstrumentationFilter(boolean needInstrumentationFilter) {
    this.needsInstrumentationFilter = needInstrumentationFilter;
  }

  public boolean needsInstrumentationFilter() {
    return needsInstrumentationFilter;
  }

  /**
   * Validates the options for this BuildRequest.
   *
   * <p>Issues warnings or throws {@code InvalidConfigurationException} for option settings that
   * conflict.
   *
   * @return list of warnings
   */
  public List<String> validateOptions() throws InvalidConfigurationException {
    List<String> warnings = new ArrayList<>();

    int localTestJobs = getExecutionOptions().localTestJobs;
    if (localTestJobs < 0) {
      throw new InvalidConfigurationException(String.format(
          "Invalid parameter for --local_test_jobs: %d. Only values 0 or greater are "
              + "allowed.", localTestJobs));
    }
    int jobs = getBuildOptions().jobs;
    if (localTestJobs > jobs) {
      warnings.add(
          String.format("High value for --local_test_jobs: %d. This exceeds the value for --jobs: "
              + "%d. Only up to %d local tests will run concurrently.", localTestJobs, jobs, jobs));
    }

    // Validate other BuildRequest options.
    if (getBuildOptions().verboseExplanations && getBuildOptions().explanationPath == null) {
      warnings.add("--verbose_explanations has no effect when --explain=<file> is not enabled");
    }

    return warnings;
  }

  /** Creates a new TopLevelArtifactContext from this build request. */
  public TopLevelArtifactContext getTopLevelArtifactContext() {
    return new TopLevelArtifactContext(
        getOptions(ExecutionOptions.class).testStrategy.equals("exclusive"),
        OutputGroupInfo.determineOutputGroups(getBuildOptions().outputGroups));
  }

  public ImmutableSortedSet<String> getMultiCpus() {
    return ImmutableSortedSet.copyOf(getBuildOptions().multiCpus);
  }

  public ImmutableList<String> getAspects() {
    return ImmutableList.copyOf(getBuildOptions().aspects);
  }

  public static BuildRequest create(String commandName, OptionsParsingResult options,
      OptionsParsingResult startupOptions,
      List<String> targets, OutErr outErr, UUID commandId, long commandStartTime) {

    BuildRequest request = new BuildRequest(commandName, options, startupOptions, targets, outErr,
        commandId, commandStartTime);

    // All this, just to pass a global boolean from the client to the server. :(
    if (options.getOptions(BlazeCommandEventHandler.Options.class).runningInEmacs) {
      request.setRunningInEmacs();
    }

    return request;
  }

}
