// 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.analysis.extra;

import static com.google.devtools.build.lib.analysis.constraints.ConstraintConstants.getOsFromConstraintsOrHost;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionKeyContext;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.CommandLine;
import com.google.devtools.build.lib.analysis.BashCommandConstructor;
import com.google.devtools.build.lib.analysis.CommandHelper;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.ShToolchain;
import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.packages.AspectDescriptor;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

/** The specification for a particular extra action type. */
@Immutable
public final class ExtraActionSpec implements TransitiveInfoProvider {
  private final NestedSet<Artifact> resolvedTools;
  private final ImmutableList<Artifact> resolvedData;
  private final ImmutableList<String> outputTemplates;
  private final ImmutableMap<String, String> executionInfo;
  private final String command;
  private final boolean requiresActionOutput;
  private final Label label;

  public ExtraActionSpec(
      NestedSet<Artifact> resolvedTools,
      List<Artifact> resolvedData,
      List<String> outputTemplates,
      String command,
      Label label,
      Map<String, String> executionInfo,
      boolean requiresActionOutput) {
    this.resolvedTools = resolvedTools;
    this.resolvedData = ImmutableList.copyOf(resolvedData);
    this.outputTemplates = ImmutableList.copyOf(outputTemplates);
    this.command = command;
    this.label = label;
    this.executionInfo = ImmutableMap.copyOf(executionInfo);
    this.requiresActionOutput = requiresActionOutput;
  }

  public Label getLabel() {
    return label;
  }

  /** Adds an extra_action to the action graph based on the action to shadow. */
  public Collection<Artifact.DerivedArtifact> addExtraAction(
      RuleContext owningRule, Action actionToShadow) throws InterruptedException {
    Collection<Artifact.DerivedArtifact> extraActionOutputs = new LinkedHashSet<>();
    Collection<Artifact.DerivedArtifact> protoOutputs = new ArrayList<>();
    NestedSetBuilder<Artifact> extraActionInputs = NestedSetBuilder.stableOrder();

    Label ownerLabel = owningRule.getLabel();
    if (requiresActionOutput) {
      extraActionInputs.addAll(actionToShadow.getOutputs());
    }

    extraActionInputs.addTransitive(resolvedTools);
    extraActionInputs.addAll(resolvedData);

    boolean createDummyOutput = false;

    for (String outputTemplate : outputTemplates) {
      // We create output for the extra_action based on the 'out_template' attribute.
      // See {link #getExtraActionOutputArtifact} for supported variables.
      extraActionOutputs.add(
          getExtraActionOutputArtifact(owningRule, actionToShadow, outputTemplate));
    }
    // extra_action has no output, we need to create some dummy output to keep the build up-to-date.
    if (extraActionOutputs.isEmpty()) {
      createDummyOutput = true;
      extraActionOutputs.add(
          getExtraActionOutputArtifact(owningRule, actionToShadow, "$(ACTION_ID).dummy"));
    }

    // We generate a file containing a protocol buffer describing the action that is being shadowed.
    // It is up to each action being shadowed to decide what contents to store here.
    Artifact.DerivedArtifact extraActionInfoFile =
        getExtraActionOutputArtifact(owningRule, actionToShadow, "$(ACTION_ID).xa");
    owningRule.registerAction(
        new ExtraActionInfoFileWriteAction(
            actionToShadow.getOwner(), extraActionInfoFile, actionToShadow));
    extraActionInputs.add(extraActionInfoFile);
    protoOutputs.add(extraActionInfoFile);

    // Expand extra_action specific variables from the provided command-line.
    // See {@link #createExpandedCommand} for list of supported variables.
    String command = createExpandedCommand(owningRule, actionToShadow, extraActionInfoFile);

    CommandHelper commandHelper = CommandHelper.builder(owningRule).build();

    // Multiple actions in the same configured target need to have different names for the artifact
    // that might be created here, so we append something that should be unique for each action.
    String actionUniquifier =
        actionToShadow.getPrimaryOutput().getExecPath().getBaseName()
            + "."
            + actionToShadow.getKey(
                owningRule.getActionKeyContext(), /* inputMetadataProvider= */ null);

    PathFragment shExecutable =
        ShToolchain.getPathForPlatform(
            owningRule.getConfiguration(), owningRule.getExecutionPlatform());
    BashCommandConstructor constructor =
        CommandHelper.buildBashCommandConstructor(
            executionInfo, shExecutable, "." + actionUniquifier + ".extra_action_script.sh");
    ImmutableList<String> argv =
        commandHelper.buildCommandLine(
            command,
            extraActionInputs,
            constructor,
            getOsFromConstraintsOrHost(owningRule.getExecutionPlatform()));

    String commandMessage = String.format("Executing extra_action %s on %s", label, ownerLabel);
    owningRule.registerAction(
        new ExtraAction(
            owningRule.getActionOwner(),
            extraActionInputs.build(),
            extraActionOutputs,
            actionToShadow,
            createDummyOutput,
            CommandLine.of(argv),
            owningRule.getConfiguration().getActionEnvironment(),
            owningRule.getConfiguration().modifiedExecutionInfo(executionInfo, label.getName()),
            commandMessage,
            label.getName()));

    return ImmutableSet.<Artifact.DerivedArtifact>builder()
        .addAll(extraActionOutputs)
        .addAll(protoOutputs)
        .build();
  }

  /**
   * Expand extra_action specific variables: $(EXTRA_ACTION_FILE): expands to a path of the file
   * containing a protocol buffer describing the action being shadowed. $(output <out_template>):
   * expands the output template to the execPath of the file. e.g. $(output $(ACTION_ID).out) ->
   * <build_path>/extra_actions/bar/baz/devtools/build/test_A41234.out
   */
  private String createExpandedCommand(
      RuleContext owningRule, Action action, Artifact extraActionInfoFile)
      throws InterruptedException {
    String realCommand =
        command.replace("$(EXTRA_ACTION_FILE)", extraActionInfoFile.getExecPathString());

    for (String outputTemplate : outputTemplates) {
      String outFile =
          getExtraActionOutputArtifact(owningRule, action, outputTemplate).getExecPathString();
      realCommand = realCommand.replace("$(output " + outputTemplate + ")", outFile);
    }
    return realCommand;
  }

  /**
   * Creates an output artifact for the extra_action based on the output_template. The path will be
   * in the following form: <output
   * dir>/<target-configuration-specific-path>/extra_actions/<extra_action_label>/ +
   * <configured_target_label>/<expanded_template>
   *
   * <p>The template can use the following variables: $(ACTION_ID): a unique id for the
   * extra_action.
   *
   * <p>Sample: extra_action: foo/bar:extra template: $(ACTION_ID).analysis target: foo/bar:main
   * expands to: output/configuration/extra_actions/\
   * foo/bar/extra/foo/bar/4683026f7ac1dd1a873ccc8c3d764132.analysis
   */
  private Artifact.DerivedArtifact getExtraActionOutputArtifact(
      RuleContext ruleContext, Action action, String template) throws InterruptedException {
    String actionId =
        getActionId(ruleContext.getActionKeyContext(), ruleContext.getActionOwner(), action);

    template = template.replace("$(ACTION_ID)", actionId);
    template = template.replace("$(OWNER_LABEL_DIGEST)", getOwnerDigest(ruleContext));

    return getRootRelativePath(template, ruleContext);
  }

  private Artifact.DerivedArtifact getRootRelativePath(String template, RuleContext ruleContext) {
    PathFragment extraActionPackageFragment = label.getPackageIdentifier().getSourceRoot();
    PathFragment extraActionPrefix = extraActionPackageFragment.getRelative(label.getName());
    PathFragment rootRelativePath =
        PathFragment.create("extra_actions")
            .getRelative(extraActionPrefix)
            .getRelative(ruleContext.getPackageDirectory())
            .getRelative(template);
    // We need to use getDerivedArtifact here because extra actions are at
    // <EXTRA ACTION LABEL> / <RULE LABEL> instead of <RULE LABEL> / <EXTRA ACTION LABEL>. Bummer.
    return ruleContext
        .getAnalysisEnvironment()
        .getDerivedArtifact(
            rootRelativePath,
            ruleContext
                .getConfiguration()
                .getOutputDirectory(ruleContext.getRule().getRepository()));
  }

  /**
   * Calculates a digest representing the rule context. We use the digest instead of the original
   * value as the original value might lead to a filename that is too long. By using a digest, tools
   * can deterministically find all extra_action outputs for a given target, without having to open
   * every file in the package.
   */
  private static String getOwnerDigest(RuleContext ruleContext) {
    Fingerprint f = new Fingerprint();
    f.addString(ruleContext.getLabel().toString());
    return f.hexDigestAndReset();
  }

  /**
   * Creates a unique id for the action shadowed by this extra_action.
   *
   * <p>We need to have a unique id for the extra_action to use. We build this from the owner's
   * label and the shadowed action id (which is only guaranteed to be unique per target). Together
   * with the subfolder matching the original target's package name, we believe this is enough of a
   * uniqueness guarantee.
   */
  @VisibleForTesting
  public static String getActionId(
      ActionKeyContext actionKeyContext, ActionOwner owner, Action action)
      throws InterruptedException {
    Fingerprint f = new Fingerprint();
    f.addString(owner.getLabel().toString());
    ImmutableList<AspectDescriptor> aspectDescriptors = owner.getAspectDescriptors();
    f.addInt(aspectDescriptors.size());
    for (AspectDescriptor aspectDescriptor : aspectDescriptors) {
      f.addString(aspectDescriptor.getDescription());
    }
    f.addString(action.getKey(actionKeyContext, /* inputMetadataProvider= */ null));
    return f.hexDigestAndReset();
  }
}
