// 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 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.actions.RunfilesSupplier;
import com.google.devtools.build.lib.analysis.CommandHelper;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
import com.google.devtools.build.lib.cmdline.Label;
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 PathFragment shExecutable;
  private final ImmutableList<Artifact> resolvedTools;
  private final RunfilesSupplier runfilesSupplier;
  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(
      PathFragment shExecutable,
      Iterable<Artifact> resolvedTools,
      RunfilesSupplier runfilesSupplier,
      Iterable<Artifact> resolvedData,
      Iterable<String> outputTemplates,
      String command,
      Label label,
      Map<String, String> executionInfo,
      boolean requiresActionOutput) {
    this.shExecutable = shExecutable;
    this.resolvedTools = ImmutableList.copyOf(resolvedTools);
    this.runfilesSupplier = runfilesSupplier;
    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> addExtraAction(RuleContext owningRule, Action actionToShadow) {
    Collection<Artifact> extraActionOutputs = new LinkedHashSet<>();
    Collection<Artifact> protoOutputs = new ArrayList<>();
    NestedSetBuilder<Artifact> extraActionInputs = NestedSetBuilder.stableOrder();

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

    extraActionInputs.addAll(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 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 =
        new CommandHelper(
            owningRule,
            ImmutableList.<TransitiveInfoCollection>of(),
            ImmutableMap.<Label, Iterable<Artifact>>of());

    // 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());
    List<String> argv =
        commandHelper.buildCommandLine(
            shExecutable,
            command,
            extraActionInputs,
            "." + actionUniquifier + ".extra_action_script.sh",
            executionInfo);

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

    return ImmutableSet.<Artifact>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) {
    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>
   *
   * The template can use the following variables:
   * $(ACTION_ID): a unique id for the extra_action.
   *
   *  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 getExtraActionOutputArtifact(
      RuleContext ruleContext, Action action, String template) {
    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 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) {
    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));
    return f.hexDigestAndReset();
  }
}
