// Copyright 2015 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.rules.cpp;

import static com.google.devtools.build.lib.rules.cpp.CcToolchainVariables.getSequenceValue;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Interner;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.InputMetadataProvider;
import com.google.devtools.build.lib.actions.PathMapper;
import com.google.devtools.build.lib.concurrent.BlazeInterners;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.rules.cpp.CcToolchainVariables.Expandable;
import com.google.devtools.build.lib.rules.cpp.CcToolchainVariables.SingleVariables;
import com.google.devtools.build.lib.rules.cpp.CcToolchainVariables.StringChunk;
import com.google.devtools.build.lib.skyframe.serialization.VisibleForSerialization;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletionException;
import java.util.function.Function;
import javax.annotation.Nullable;
import net.starlark.java.annot.Param;
import net.starlark.java.annot.StarlarkMethod;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Sequence;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkList;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.StarlarkValue;

/**
 * Provides access to features supported by a specific toolchain.
 *
 * <p>This class can be generated from the CToolchain protocol buffer.
 *
 * <p>TODO(bazel-team): Implement support for specifying the toolchain configuration directly from
 * the BUILD file.
 *
 * <p>TODO(bazel-team): Find a place to put the public-facing documentation and link to it from
 * here.
 *
 * <p>TODO(bazel-team): Split out Feature as CcToolchainFeature, which will modularize the crosstool
 * configuration into one part that is about handling a set of features (including feature
 * selection) and one part that is about how to apply a single feature (parsing flags and expanding
 * them from build variables).
 */
@Immutable
public class CcToolchainFeatures implements StarlarkValue {

  /**
   * Thrown when a flag value cannot be expanded under a set of build variables.
   *
   * <p>This happens for example when a flag references a variable that is not provided by the
   * action, or when a flag group implicitly references multiple variables of sequence type.
   */
  public static class ExpansionException extends EvalException {
    ExpansionException(String message) {
      super(message);
    }

    ExpansionException(String message, @Nullable Throwable cause) {
      super(message, cause);
    }
  }

  /** Thrown when multiple features provide the same string symbol. */
  public static class CollidingProvidesException extends Exception {
    CollidingProvidesException(String message) {
      super(message);
    }
  }

  /** Error message thrown when a toolchain enables two features that provide the same string. */
  public static final String COLLIDING_PROVIDES_ERROR =
      "Symbol %s is provided by all of the following features: %s";

  /** A single flag to be expanded under a set of variables. */
  @Immutable
  @AutoCodec
  record Flag(ImmutableList<StringChunk> chunks) implements Expandable {
    /** Expand this flag into a single new entry in {@code commandLine}. */
    @Override
    public void expand(
        CcToolchainVariables variables,
        @Nullable InputMetadataProvider inputMetadataProvider,
        PathMapper pathMapper,
        List<String> commandLine)
        throws ExpansionException {
      StringBuilder flag = new StringBuilder();
      for (StringChunk chunk : chunks) {
        flag.append(chunk.expand(variables, pathMapper));
      }
      commandLine.add(flag.toString().intern());
    }

    /** A single environment key/value pair to be expanded under a set of variables. */
    static Expandable create(ImmutableList<StringChunk> chunks) {
      if (chunks.size() == 1) {
        return new SingleChunkFlag(chunks.get(0));
      }
      return new Flag(chunks);
    }

    /** Optimization for single-chunk case */
    @Immutable
    @AutoCodec
    record SingleChunkFlag(StringChunk chunk) implements Expandable {

      @Override
      public void expand(
          CcToolchainVariables variables,
          @Nullable InputMetadataProvider inputMetadataProvider,
          PathMapper pathMapper,
          List<String> commandLine)
          throws ExpansionException {
        commandLine.add(chunk.expand(variables, pathMapper));
      }
    }
  }

  /** A single environment key/value pair to be expanded under a set of variables. */
  @Immutable
  @AutoCodec
  public record EnvEntry(
      String key,
      ImmutableList<StringChunk> valueChunks,
      ImmutableSet<String> expandIfAllAvailable) {

    private boolean canBeExpanded(CcToolchainVariables variables) {
      for (String variable : expandIfAllAvailable) {
        if (!variables.isAvailable(variable)) {
          return false;
        }
      }
      return true;
    }

    /**
     * Adds the key/value pair this object represents to the given map of environment variables. The
     * value of the entry is expanded with the given {@code variables}.
     */
    public void addEnvEntry(
        CcToolchainVariables variables,
        ImmutableMap.Builder<String, String> envBuilder,
        PathMapper pathMapper)
        throws ExpansionException {
      if (!canBeExpanded(variables)) {
        return;
      }
      StringBuilder value = new StringBuilder();
      for (StringChunk chunk : valueChunks) {
        value.append(chunk.expand(variables, pathMapper));
      }
      envBuilder.put(key, value.toString());
    }
  }

  /** Used for equality check between a variable and a specific value. */
  @Immutable
  @AutoCodec
  record VariableWithValue(String variable, String value) {}

  /**
   * A group of flags. When iterateOverVariable is specified, we assume the variable is a sequence
   * and the flag_group will be expanded repeatedly for every value in the sequence.
   */
  @Immutable
  @AutoCodec
  record FlagGroup(
      ImmutableList<Expandable> expandables,
      String iterateOverVariable,
      ImmutableSet<String> expandIfAllAvailable,
      ImmutableSet<String> expandIfNoneAvailable,
      String expandIfTrue,
      String expandIfFalse,
      VariableWithValue expandIfEqual)
      implements Expandable {

    @Override
    public void expand(
        CcToolchainVariables variables,
        @Nullable InputMetadataProvider inputMetadataProvider,
        PathMapper pathMapper,
        final List<String> commandLine)
        throws ExpansionException {
      if (!canBeExpanded(variables, inputMetadataProvider, pathMapper)) {
        return;
      }
      if (iterateOverVariable != null) {
        for (CcToolchainVariables.VariableValue variableValue :
            getSequenceValue(
                iterateOverVariable,
                variables.getVariable(iterateOverVariable, inputMetadataProvider, pathMapper))) {
          CcToolchainVariables nestedVariables =
              new SingleVariables(variables, iterateOverVariable, variableValue);
          for (Expandable expandable : expandables) {
            expandable.expand(nestedVariables, inputMetadataProvider, pathMapper, commandLine);
          }
        }
      } else {
        for (Expandable expandable : expandables) {
          expandable.expand(variables, inputMetadataProvider, pathMapper, commandLine);
        }
      }
    }

    private boolean canBeExpanded(
        CcToolchainVariables variables,
        @Nullable InputMetadataProvider inputMetadataProvider,
        PathMapper pathMapper)
        throws ExpansionException {
      for (String variable : expandIfAllAvailable) {
        if (!variables.isAvailable(variable, inputMetadataProvider)) {
          return false;
        }
      }
      for (String variable : expandIfNoneAvailable) {
        if (variables.isAvailable(variable, inputMetadataProvider)) {
          return false;
        }
      }
      if (expandIfTrue != null
          && (!variables.isAvailable(expandIfTrue, inputMetadataProvider)
              || !variables.getVariable(expandIfTrue, pathMapper).isTruthy())) {
        return false;
      }
      if (expandIfFalse != null
          && (!variables.isAvailable(expandIfFalse, inputMetadataProvider)
              || variables.getVariable(expandIfFalse, pathMapper).isTruthy())) {
        return false;
      }
      if (expandIfEqual != null
          && (!variables.isAvailable(expandIfEqual.variable, inputMetadataProvider)
              || !variables
                  .getVariable(expandIfEqual.variable, pathMapper)
                  .getStringValue(expandIfEqual.variable, pathMapper)
                  .equals(expandIfEqual.value))) {
        return false;
      }
      return true;
    }

    /**
     * Expands all flags in this group and adds them to {@code commandLine}.
     *
     * <p>The flags of the group will be expanded either:
     *
     * <ul>
     *   <li>once, if there is no variable of sequence type in any of the group's flags, or
     *   <li>for each element in the sequence, if there is 'iterate_over' variable specified
     *       (preferred, explicit way), or
     *   <li>for each element in the sequence, if there is only one sequence variable used in the
     *       body of the flag_group (deprecated, implicit way). Having more than a single variable
     *       of sequence type in a single flag group with implicit iteration is not supported. Use
     *       explicit 'iterate_over' instead.
     * </ul>
     */
    private void expandCommandLine(
        CcToolchainVariables variables,
        @Nullable InputMetadataProvider inputMetadataProvider,
        PathMapper pathMapper,
        final List<String> commandLine)
        throws ExpansionException {
      expand(variables, inputMetadataProvider, pathMapper, commandLine);
    }
  }

  private static boolean isWithFeaturesSatisfied(
      Collection<WithFeatureSet> withFeatureSets, Set<String> enabledFeatureNames) {
    if (withFeatureSets.isEmpty()) {
      return true;
    }
    for (WithFeatureSet featureSet : withFeatureSets) {
      if (enabledFeatureNames.containsAll(featureSet.features())
          && featureSet.notFeatures().stream().noneMatch(enabledFeatureNames::contains)) {
        return true;
      }
    }
    return false;
  }

  /** Groups a set of flags to apply for certain actions. */
  @Immutable
  @AutoCodec
  public record FlagSet(
      ImmutableSet<String> actions,
      ImmutableSet<String> expandIfAllAvailable,
      ImmutableSet<WithFeatureSet> withFeatureSets,
      ImmutableList<FlagGroup> flagGroups) {

    /** Adds the flags that apply to the given {@code action} to {@code commandLine}. */
    private void expandCommandLine(
        String action,
        CcToolchainVariables variables,
        Set<String> enabledFeatureNames,
        @Nullable InputMetadataProvider inputMetadataProvider,
        PathMapper pathMapper,
        List<String> commandLine)
        throws ExpansionException {
      for (String variable : expandIfAllAvailable) {
        if (!variables.isAvailable(variable, inputMetadataProvider)) {
          return;
        }
      }
      if (!isWithFeaturesSatisfied(withFeatureSets, enabledFeatureNames)) {
        return;
      }
      if (!actions.contains(action)) {
        return;
      }
      for (FlagGroup flagGroup : flagGroups) {
        flagGroup.expandCommandLine(variables, inputMetadataProvider, pathMapper, commandLine);
      }
    }
  }

  /**
   * A set of positive and negative features. This stanza will evaluate to true when every 'feature'
   * is enabled, and every 'not_feature' is not enabled.
   */
  @Immutable
  @AutoCodec
  public record WithFeatureSet(ImmutableSet<String> features, ImmutableSet<String> notFeatures) {}

  /** Groups a set of environment variables to apply for certain actions. */
  @Immutable
  @AutoCodec
  public record EnvSet(
      ImmutableSet<String> actions,
      ImmutableList<EnvEntry> envEntries,
      ImmutableSet<WithFeatureSet> withFeatureSets) {

    /**
     * Adds the environment key/value pairs that apply to the given {@code action} to {@code
     * envBuilder}.
     */
    private void expandEnvironment(
        String action,
        CcToolchainVariables variables,
        PathMapper pathMapper,
        Set<String> enabledFeatureNames,
        ImmutableMap.Builder<String, String> envBuilder)
        throws ExpansionException {
      if (!actions.contains(action)) {
        return;
      }
      if (!isWithFeaturesSatisfied(withFeatureSets, enabledFeatureNames)) {
        return;
      }
      for (EnvEntry envEntry : envEntries) {
        envEntry.addEnvEntry(variables, envBuilder, pathMapper);
      }
    }
  }

  /**
   * An interface for classes representing crosstool messages that can activate each other using
   * 'requires' and 'implies' semantics.
   *
   * <p>Currently there are two types of CrosstoolActivatable: Feature and ActionConfig.
   */
  interface CrosstoolSelectable {

    /** Returns the name of this selectable. */
    String getName();
  }

  /** Contains flags for a specific feature. */
  @Immutable
  @AutoCodec
  @VisibleForSerialization
  public static class Feature implements CrosstoolSelectable {
    private static final Interner<Feature> FEATURE_INTERNER = BlazeInterners.newWeakInterner();

    private final String name;
    private final ImmutableList<FlagSet> flagSets;
    private final ImmutableList<EnvSet> envSets;
    private final boolean enabled;
    private final ImmutableList<ImmutableSet<String>> requires;
    private final ImmutableList<String> implies;
    private final ImmutableList<String> provides;

    public Feature(
        String name,
        ImmutableList<FlagSet> flagSets,
        ImmutableList<EnvSet> envSets,
        boolean enabled,
        ImmutableList<ImmutableSet<String>> requires,
        ImmutableList<String> implies,
        ImmutableList<String> provides) {
      this.name = name;
      this.flagSets = flagSets;
      this.envSets = envSets;
      this.enabled = enabled;
      this.requires = requires;
      this.implies = implies;
      this.provides = provides;
    }

    @VisibleForSerialization
    @AutoCodec.Interner
    static Feature intern(Feature feature) {
      return FEATURE_INTERNER.intern(feature);
    }

    @Override
    public String getName() {
      return name;
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this).add("name", name).add("enabled", enabled).toString();
    }

    /** Adds environment variables for the given action to the provided builder. */
    private void expandEnvironment(
        String action,
        CcToolchainVariables variables,
        PathMapper pathMapper,
        Set<String> enabledFeatureNames,
        ImmutableMap.Builder<String, String> envBuilder)
        throws ExpansionException {
      for (EnvSet envSet : envSets) {
        envSet.expandEnvironment(action, variables, pathMapper, enabledFeatureNames, envBuilder);
      }
    }

    /** Adds the flags that apply to the given {@code action} to {@code commandLine}. */
    private void expandCommandLine(
        String action,
        CcToolchainVariables variables,
        Set<String> enabledFeatureNames,
        @Nullable InputMetadataProvider inputMetadataProvider,
        PathMapper pathMapper,
        List<String> commandLine)
        throws ExpansionException {
      for (FlagSet flagSet : flagSets) {
        flagSet.expandCommandLine(
            action, variables, enabledFeatureNames, inputMetadataProvider, pathMapper, commandLine);
      }
    }

    ImmutableList<FlagSet> getFlagSets() {
      return flagSets;
    }

    ImmutableList<EnvSet> getEnvSets() {
      return envSets;
    }

    @Override
    public boolean equals(@Nullable Object object) {
      if (this == object) {
        return true;
      }
      if (object instanceof Feature that) {
        return name.equals(that.name)
            && Iterables.elementsEqual(flagSets, that.flagSets)
            && Iterables.elementsEqual(envSets, that.envSets)
            && Iterables.elementsEqual(requires, that.requires)
            && Iterables.elementsEqual(implies, that.implies)
            && Iterables.elementsEqual(provides, that.provides)
            && enabled == that.enabled;
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hash(name, flagSets, envSets, requires, implies, provides, enabled);
    }

    boolean isEnabled() {
      return enabled;
    }

    public ImmutableList<ImmutableSet<String>> getRequires() {
      return requires;
    }

    public ImmutableList<String> getImplies() {
      return implies;
    }

    public ImmutableList<String> getProvides() {
      return provides;
    }
  }

  /**
   * An executable to be invoked by a blaze action. Can carry information on its platform
   * restrictions.
   */
  @Immutable
  public static class Tool {
    enum PathOrigin {
      CROSSTOOL_PACKAGE,
      FILESYSTEM_ROOT,
      WORKSPACE_ROOT
    }

    private final PathFragment toolPathFragment;
    private final PathOrigin toolPathOrigin;
    private final ImmutableSet<String> executionRequirements;
    private final ImmutableSet<WithFeatureSet> withFeatureSetSets;

    // Caching tool path string.
    @Nullable private String toolPathString = null;

    @VisibleForTesting
    public Tool(
        PathFragment toolPathFragment,
        PathOrigin toolPathOrigin,
        ImmutableSet<String> executionRequirements,
        ImmutableSet<WithFeatureSet> withFeatureSetSets)
        throws EvalException {
      checkToolPath(toolPathFragment, toolPathOrigin);
      this.toolPathFragment = toolPathFragment;
      this.toolPathOrigin = toolPathOrigin;
      this.executionRequirements = executionRequirements;
      this.withFeatureSetSets = withFeatureSetSets;
    }

    @Deprecated
    @VisibleForTesting
    public Tool(
        PathFragment toolPathFragment,
        ImmutableSet<String> executionRequirements,
        ImmutableSet<WithFeatureSet> withFeatureSetSets)
        throws EvalException {
      this(
          toolPathFragment,
          PathOrigin.CROSSTOOL_PACKAGE,
          executionRequirements,
          withFeatureSetSets);
    }

    private static void checkToolPath(PathFragment toolPath, PathOrigin origin)
        throws EvalException {
      switch (origin) {
        case CROSSTOOL_PACKAGE:
          // For legacy reasons, we allow absolute and relative paths here.
          return;

        case FILESYSTEM_ROOT:
          if (!toolPath.isAbsolute()) {
            throw Starlark.errorf(
                "Tool-path with origin FILESYSTEM_ROOT must be absolute, got '%s'.",
                toolPath.getPathString());
          }
          return;

        case WORKSPACE_ROOT:
          if (toolPath.isAbsolute()) {
            throw Starlark.errorf(
                "Tool-path with origin WORKSPACE_ROOT must be relative, got '%s'.",
                toolPath.getPathString());
          }
          return;
      }

      // Unreached.
      throw new IllegalStateException();
    }

    /** Returns the path to this action's tool relative to the provided crosstool path. */
    String getToolPathString(PathFragment ccToolchainPath) {
      return switch (toolPathOrigin) {
        case CROSSTOOL_PACKAGE -> {
          // Legacy behavior.
          if (toolPathString == null) {
            toolPathString = ccToolchainPath.getRelative(toolPathFragment).getSafePathString();
          }
          yield toolPathString;
        }
        case FILESYSTEM_ROOT, WORKSPACE_ROOT -> toolPathFragment.getSafePathString();
      };
    }

    /** Returns a list of requirement hints that apply to the execution of this tool. */
    ImmutableSet<String> getExecutionRequirements() {
      return executionRequirements;
    }

    /**
     * Returns a set of {@link WithFeatureSet} instances used to decide whether to use this tool
     * given a set of enabled features.
     */
    ImmutableSet<WithFeatureSet> getWithFeatureSetSets() {
      return withFeatureSetSets;
    }
  }

  /**
   * A container for information on a particular blaze action.
   *
   * <p>An ActionConfig can select a tool for its blaze action based on the set of active features.
   * Internally, an ActionConfig maintains an ordered list (the order being that of the list of
   * tools in the crosstool action_config message) of such tools and the feature sets for which they
   * are valid. For a given feature configuration, the ActionConfig will consider the first tool in
   * that list with a feature set that matches the configuration to be the tool for its blaze
   * action.
   *
   * <p>ActionConfigs can be activated by features. That is, a particular feature can cause an
   * ActionConfig to be applied in its "implies" field. Blaze may include certain actions in the
   * action graph only if a corresponding ActionConfig is activated in the toolchain - this provides
   * the crosstool with a mechanism for adding certain actions to the action graph based on feature
   * configuration.
   *
   * <p>It is invalid for a toolchain to contain two action configs for the same blaze action. In
   * that case, blaze will throw an error when it consumes the crosstool.
   */
  @Immutable
  @AutoCodec
  public static class ActionConfig implements CrosstoolSelectable {
    static final String FLAG_SET_WITH_ACTION_ERROR =
        "action_config %s specifies actions.  An action_config's flag sets automatically apply "
            + "to the configured action.  Thus, you must not specify action lists in an "
            + "action_config's flag set.";

    private static final Interner<ActionConfig> ACTION_CONFIG_INTERNER =
        BlazeInterners.newWeakInterner();

    private final String configName;
    private final String actionName;
    private final ImmutableList<Tool> tools;
    private final ImmutableList<FlagSet> flagSets;
    private final boolean enabled;
    private final ImmutableList<String> implies;

    public ActionConfig(
        String configName,
        String actionName,
        ImmutableList<Tool> tools,
        ImmutableList<FlagSet> flagSets,
        boolean enabled,
        ImmutableList<String> implies) {
      this.configName = configName;
      this.actionName = actionName;
      this.tools = tools;
      this.flagSets = flagSets;
      this.enabled = enabled;
      this.implies = implies;
    }

    @VisibleForSerialization
    @AutoCodec.Interner
    static ActionConfig intern(ActionConfig actionConfig) {
      return ACTION_CONFIG_INTERNER.intern(actionConfig);
    }

    @Override
    public String getName() {
      return configName;
    }

    /** Returns the name of the blaze action this action config applies to. */
    String getActionName() {
      return actionName;
    }

    /**
     * Returns the path to this action's tool relative to the provided crosstool path given a set of
     * enabled features.
     */
    private Tool getTool(final Set<String> enabledFeatureNames) {
      return tools.stream()
          .filter(t -> isWithFeaturesSatisfied(t.getWithFeatureSetSets(), enabledFeatureNames))
          .findFirst()
          .orElseThrow(
              () ->
                  new IllegalArgumentException(
                      "Matching tool for action %s not found for given feature configuration"
                          .formatted(getActionName())));
    }

    /** Adds the flags that apply to this action to {@code commandLine}. */
    private void expandCommandLine(
        CcToolchainVariables variables,
        Set<String> enabledFeatureNames,
        @Nullable InputMetadataProvider inputMetadataProvider,
        PathMapper pathMapper,
        List<String> commandLine)
        throws ExpansionException {
      for (FlagSet flagSet : flagSets) {
        flagSet.expandCommandLine(
            actionName,
            variables,
            enabledFeatureNames,
            inputMetadataProvider,
            pathMapper,
            commandLine);
      }
    }

    boolean isEnabled() {
      return enabled;
    }

    public ImmutableList<String> getImplies() {
      return implies;
    }

    @Override
    public boolean equals(Object other) {
      if (other == this) {
        return true;
      }
      if (!(other instanceof ActionConfig that)) {
        return false;
      }

      return Objects.equals(configName, that.configName)
          && Objects.equals(actionName, that.actionName)
          && enabled == that.enabled
          && Iterables.elementsEqual(tools, that.tools)
          && Iterables.elementsEqual(flagSets, that.flagSets)
          && Iterables.elementsEqual(implies, that.implies);
    }

    @Override
    public int hashCode() {
      return Objects.hash(configName, actionName, enabled, tools, flagSets, implies);
    }

    ImmutableList<Tool> getTools() {
      return tools;
    }

    ImmutableList<FlagSet> getFlagSets() {
      return flagSets;
    }
  }

  /** A description of how artifacts of a certain type are named. */
  @Immutable
  @AutoCodec
  record ArtifactNamePattern(String prefix, String extension) {

    /** Returns the artifact name that this pattern selects. */
    private String getArtifactName(String baseName) {
      return prefix + baseName + extension;
    }
  }

  static final class ArtifactNamePatternMapper {
    private static final ImmutableMap<ArtifactCategory, ArtifactNamePattern> DEFAULT_PATTERNS =
        Arrays.stream(ArtifactCategory.values())
            .collect(
                ImmutableMap.toImmutableMap(
                    Function.identity(),
                    c -> new ArtifactNamePattern(c.getDefaultPrefix(), c.getDefaultExtension())));

    private final ImmutableMap<ArtifactCategory, ArtifactNamePattern> prefixExtensionOverrides;

    private ArtifactNamePatternMapper(
        ImmutableMap<ArtifactCategory, ArtifactNamePattern> prefixExtensionOverrides) {
      this.prefixExtensionOverrides = prefixExtensionOverrides;
    }

    public ArtifactNamePattern get(ArtifactCategory category) {
      ArtifactNamePattern result = prefixExtensionOverrides.get(category);
      return result != null ? result : DEFAULT_PATTERNS.get(category);
    }

    static class Builder {
      private final ImmutableMap.Builder<ArtifactCategory, ArtifactNamePattern> overrides =
          ImmutableMap.builder();

      @CanIgnoreReturnValue
      Builder addOverride(ArtifactCategory category, String prefix, String extension) {
        if (!category.getDefaultPrefix().equals(prefix)
            || !category.getDefaultExtension().equals(extension)) {
          overrides.put(category, new ArtifactNamePattern(prefix, extension));
        }
        return this;
      }

      ArtifactNamePatternMapper build() {
        return new ArtifactNamePatternMapper(overrides.buildOrThrow());
      }
    }
  }

  /** Captures the set of enabled features and action configs for a rule. */
  @Immutable
  @AutoCodec
  @SuppressWarnings("InconsistentHashCode") // enabledFeatureNames, see definition of equals().
  public static class FeatureConfiguration {
    private static final Interner<FeatureConfiguration> FEATURE_CONFIGURATION_INTERNER =
        BlazeInterners.newWeakInterner();

    private final ImmutableSet<String> requestedFeatures;
    private final ImmutableSet<String> enabledFeatureNames;
    private final ImmutableList<Feature> enabledFeatures;
    private final ImmutableSet<String> enabledActionConfigActionNames;

    private final ImmutableMap<String, ActionConfig> actionConfigByActionName;

    private final PathFragment ccToolchainPath;

    /**
     * {@link FeatureConfiguration} instance that doesn't produce any command lines. This is to be
     * used when creation of the real {@link FeatureConfiguration} failed, the rule error was
     * reported, but the analysis continues to collect more rule errors.
     */
    @SerializationConstant
    public static final FeatureConfiguration EMPTY =
        FEATURE_CONFIGURATION_INTERNER.intern(new FeatureConfiguration());

    protected FeatureConfiguration() {
      this(
          /* requestedFeatures= */ ImmutableSet.of(),
          /* enabledFeatures= */ ImmutableList.of(),
          /* enabledActionConfigActionNames= */ ImmutableSet.of(),
          /* actionConfigByActionName= */ ImmutableMap.of(),
          /* ccToolchainPath= */ PathFragment.EMPTY_FRAGMENT);
    }

    FeatureConfiguration(
        ImmutableSet<String> requestedFeatures,
        ImmutableList<Feature> enabledFeatures,
        ImmutableSet<String> enabledActionConfigActionNames,
        ImmutableMap<String, ActionConfig> actionConfigByActionName,
        PathFragment ccToolchainPath) {
      // The order of elements in requestFeatures does not matter for equality of any behavior, but
      // coupled with interning, it makes serialization non-deterministic because it'd depend on
      // the order in which two objects that are equal but have the different order are first
      // encountered. Sorting prevents this issue.
      this.requestedFeatures = ImmutableSet.copyOf(ImmutableList.sortedCopyOf(requestedFeatures));
      this.enabledFeatures = enabledFeatures;

      this.actionConfigByActionName = ImmutableSortedMap.copyOf(actionConfigByActionName);
      ImmutableSet.Builder<String> featureBuilder = ImmutableSet.builder();
      for (Feature feature : enabledFeatures) {
        featureBuilder.add(feature.getName());
      }
      this.enabledFeatureNames = featureBuilder.build();
      this.enabledActionConfigActionNames = enabledActionConfigActionNames;
      this.ccToolchainPath = ccToolchainPath;
    }

    @VisibleForSerialization
    @AutoCodec.Instantiator
    static FeatureConfiguration createForSerialization(
        ImmutableSet<String> requestedFeatures,
        ImmutableList<Feature> enabledFeatures,
        ImmutableSet<String> enabledActionConfigActionNames,
        ImmutableMap<String, ActionConfig> actionConfigByActionName,
        PathFragment ccToolchainPath) {
      return intern(
          new FeatureConfiguration(
              requestedFeatures,
              enabledFeatures,
              enabledActionConfigActionNames,
              actionConfigByActionName,
              ccToolchainPath));
    }

    @VisibleForTesting
    static FeatureConfiguration intern(FeatureConfiguration featureConfiguration) {
      return FEATURE_CONFIGURATION_INTERNER.intern(featureConfiguration);
    }

    /**
     * @return whether the given {@code feature} is enabled.
     */
    public boolean isEnabled(String feature) {
      return enabledFeatureNames.contains(feature);
    }

    /** The list of requested features, even if they do not exist in CROSSTOOLs. */
    public ImmutableSet<String> getRequestedFeatures() {
      return requestedFeatures;
    }

    /**
     * @return whether an action config for the blaze action with the given name is enabled.
     */
    boolean actionIsConfigured(String actionName) {
      return enabledActionConfigActionNames.contains(actionName);
    }

    /**
     * @return the command line for the given {@code action}.
     */
    public List<String> getCommandLine(String action, CcToolchainVariables variables)
        throws ExpansionException {
      return getCommandLine(action, variables, /* inputMetadataProvider= */ null, PathMapper.NOOP);
    }

    public List<String> getCommandLine(
        String action,
        CcToolchainVariables variables,
        @Nullable InputMetadataProvider inputMetadataProvider,
        PathMapper pathMapper)
        throws ExpansionException {
      List<String> commandLine = new ArrayList<>();
      if (actionIsConfigured(action)) {
        actionConfigByActionName
            .get(action)
            .expandCommandLine(
                variables, enabledFeatureNames, inputMetadataProvider, pathMapper, commandLine);
      }

      for (Feature feature : enabledFeatures) {
        feature.expandCommandLine(
            action, variables, enabledFeatureNames, inputMetadataProvider, pathMapper, commandLine);
      }

      return commandLine;
    }

    /**
     * @return the flags expanded for the given {@code action} in per-feature buckets.
     */
    public ImmutableList<Pair<String, List<String>>> getPerFeatureExpansions(
        String action, CcToolchainVariables variables, PathMapper pathMapper)
        throws ExpansionException {
      return getPerFeatureExpansions(action, variables, null, pathMapper);
    }

    public ImmutableList<Pair<String, List<String>>> getPerFeatureExpansions(
        String action,
        CcToolchainVariables variables,
        @Nullable InputMetadataProvider inputMetadataProvider,
        PathMapper pathMapper)
        throws ExpansionException {
      ImmutableList.Builder<Pair<String, List<String>>> perFeatureExpansions =
          ImmutableList.builder();
      if (actionIsConfigured(action)) {
        List<String> commandLine = new ArrayList<>();
        ActionConfig actionConfig = actionConfigByActionName.get(action);
        actionConfig.expandCommandLine(
            variables, enabledFeatureNames, inputMetadataProvider, pathMapper, commandLine);
        perFeatureExpansions.add(Pair.of(actionConfig.getName(), commandLine));
      }

      for (Feature feature : enabledFeatures) {
        List<String> commandLine = new ArrayList<>();
        feature.expandCommandLine(
            action, variables, enabledFeatureNames, inputMetadataProvider, pathMapper, commandLine);
        perFeatureExpansions.add(Pair.of(feature.getName(), commandLine));
      }

      return perFeatureExpansions.build();
    }

    /**
     * @return the environment variables (key/value pairs) for the given {@code action}.
     */
    public ImmutableMap<String, String> getEnvironmentVariables(
        String action, CcToolchainVariables variables, PathMapper pathMapper)
        throws ExpansionException {
      ImmutableMap.Builder<String, String> envBuilder = ImmutableMap.builder();
      for (Feature feature : enabledFeatures) {
        feature.expandEnvironment(action, variables, pathMapper, enabledFeatureNames, envBuilder);
      }
      return envBuilder.buildOrThrow();
    }

    public String getToolPathForAction(String actionName) {
      Preconditions.checkArgument(
          actionConfigByActionName.containsKey(actionName),
          "Action %s does not have an enabled configuration in the toolchain.",
          actionName);
      ActionConfig actionConfig = actionConfigByActionName.get(actionName);
      return actionConfig.getTool(enabledFeatureNames).getToolPathString(ccToolchainPath);
    }

    ImmutableSet<String> getToolRequirementsForAction(String actionName) {
      Preconditions.checkArgument(
          actionConfigByActionName.containsKey(actionName),
          "Action %s does not have an enabled configuration in the toolchain.",
          actionName);
      ActionConfig actionConfig = actionConfigByActionName.get(actionName);
      return actionConfig.getTool(enabledFeatureNames).getExecutionRequirements();
    }

    @Override
    public boolean equals(Object object) {
      if (object == this) {
        return true;
      }
      if (object instanceof FeatureConfiguration that) {
        // Only compare actionConfigByActionName, enabledActionConfigActionnames and enabledFeatures
        // because enabledFeatureNames is based on the list of Features.
        return Objects.equals(actionConfigByActionName, that.actionConfigByActionName)
            && Iterables.elementsEqual(
                enabledActionConfigActionNames, that.enabledActionConfigActionNames)
            && Iterables.elementsEqual(enabledFeatures, that.enabledFeatures);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hash(
          actionConfigByActionName,
          enabledActionConfigActionNames,
          enabledFeatureNames,
          enabledFeatures);
    }

    ImmutableSet<String> getEnabledFeatureNames() {
      return enabledFeatureNames;
    }
  }

  private final ArtifactNamePatternMapper artifactNamePatterns;

  /**
   * All features and action configs in the order in which they were specified in the configuration.
   *
   * <p>We guarantee the command line to be in the order in which the flags were specified in the
   * configuration.
   */
  private final ImmutableList<CrosstoolSelectable> selectables;

  /** Maps the selectables's name to the selectable. */
  private final ImmutableMap<String, CrosstoolSelectable> selectablesByName;

  /** Maps an action's name to the ActionConfig. */
  private final ImmutableMap<String, ActionConfig> actionConfigsByActionName;

  /** Maps from a selectable to a set of all the selectables it has a direct 'implies' edge to. */
  private final ImmutableMultimap<CrosstoolSelectable, CrosstoolSelectable> implies;

  /**
   * Maps from a selectable to all features that have an direct 'implies' edge to this selectable.
   */
  private final ImmutableMultimap<CrosstoolSelectable, CrosstoolSelectable> impliedBy;

  /**
   * Maps from a selectable to a set of selecatable sets, where:
   *
   * <ul>
   *   <li>a selectable set satisfies the 'requires' condition, if all selectables in the selectable
   *       set are enabled
   *   <li>the 'requires' condition is satisfied, if at least one of the selectable sets satisfies
   *       the 'requires' condition.
   * </ul>
   */
  private final ImmutableMultimap<CrosstoolSelectable, ImmutableSet<CrosstoolSelectable>> requires;

  /** Maps from a string to the set of selectables that 'provide' it. */
  private final ImmutableMultimap<String, CrosstoolSelectable> provides;

  /**
   * Maps from a selectable to all selectables that have a requirement referencing it.
   *
   * <p>This will be used to determine which selectables need to be re-checked after a selectable
   * was disabled.
   */
  private final ImmutableMultimap<CrosstoolSelectable, CrosstoolSelectable> requiredBy;

  private final ImmutableList<String> defaultSelectables;

  /**
   * A cache of feature selection results, so we do not recalculate the feature selection for all
   * actions. This may not be initialized on deserialization.
   */
  private transient LoadingCache<ImmutableSet<String>, FeatureConfiguration> configurationCache =
      buildConfigurationCache();

  private final PathFragment ccToolchainPath;

  /**
   * Constructs the feature configuration from a {@link CcToolchainConfigInfo}.
   *
   * @param ccToolchainConfigInfo the toolchain information as specified by the user.
   * @param ccToolchainPath location of the cc_toolchain.
   * @throws EvalException if the configuration has logical errors.
   */
  CcToolchainFeatures(CcToolchainConfigInfo ccToolchainConfigInfo, PathFragment ccToolchainPath)
      throws EvalException {
    // Build up the feature/action config graph.  We refer to features/action configs as
    // 'selectables'.
    // First, we build up the map of name -> selectables in one pass, so that earlier selectables
    // can reference later features in their configuration.
    ImmutableList.Builder<CrosstoolSelectable> selectablesBuilder = ImmutableList.builder();
    HashMap<String, CrosstoolSelectable> selectablesByName = new HashMap<>();

    // Also build a map from action -> action_config, for use in tool lookups
    ImmutableMap.Builder<String, ActionConfig> actionConfigsByActionName = ImmutableMap.builder();

    ImmutableList.Builder<String> defaultSelectablesBuilder = ImmutableList.builder();
    ImmutableList<Feature> features = ccToolchainConfigInfo.getFeatures();
    for (Feature feature : features) {
      selectablesBuilder.add(feature);
      selectablesByName.put(feature.getName(), feature);
      if (feature.isEnabled()) {
        defaultSelectablesBuilder.add(feature.getName());
      }
    }

    ImmutableList<ActionConfig> actionConfigs = ccToolchainConfigInfo.getActionConfigs();
    for (ActionConfig actionConfig : actionConfigs) {
      selectablesBuilder.add(actionConfig);
      selectablesByName.put(actionConfig.getName(), actionConfig);
      actionConfigsByActionName.put(actionConfig.getActionName(), actionConfig);
      if (actionConfig.isEnabled()) {
        defaultSelectablesBuilder.add(actionConfig.getName());
      }
    }
    this.defaultSelectables = defaultSelectablesBuilder.build();

    this.selectables = selectablesBuilder.build();
    this.selectablesByName = ImmutableSortedMap.copyOf(selectablesByName);

    checkForActionNameDups(actionConfigs);
    checkForActivatableDups(this.selectables);

    this.actionConfigsByActionName = actionConfigsByActionName.buildOrThrow();

    this.artifactNamePatterns = ccToolchainConfigInfo.getArtifactNamePatterns();

    // Next, we build up all forward references for 'implies', 'requires', and 'provides' edges.
    ImmutableMultimap.Builder<CrosstoolSelectable, CrosstoolSelectable> implies =
        ImmutableMultimap.builder();
    ImmutableMultimap.Builder<CrosstoolSelectable, ImmutableSet<CrosstoolSelectable>> requires =
        ImmutableMultimap.builder();
    ImmutableMultimap.Builder<CrosstoolSelectable, String> provides = ImmutableMultimap.builder();
    // We also store the reverse 'implied by' and 'required by' edges during this pass.
    ImmutableMultimap.Builder<CrosstoolSelectable, CrosstoolSelectable> impliedBy =
        ImmutableMultimap.builder();
    ImmutableMultimap.Builder<CrosstoolSelectable, CrosstoolSelectable> requiredBy =
        ImmutableMultimap.builder();

    for (Feature feature : features) {
      String name = feature.getName();
      CrosstoolSelectable selectable = selectablesByName.get(name);
      for (ImmutableSet<String> requiredFeatures : feature.getRequires()) {
        ImmutableSet.Builder<CrosstoolSelectable> allOf = ImmutableSet.builder();
        for (String requiredName : requiredFeatures) {
          CrosstoolSelectable required = getActivatableOrFail(requiredName, name);
          allOf.add(required);
          requiredBy.put(required, selectable);
        }
        requires.put(selectable, allOf.build());
      }
      for (String impliedName : feature.getImplies()) {
        CrosstoolSelectable implied = getActivatableOrFail(impliedName, name);
        impliedBy.put(implied, selectable);
        implies.put(selectable, implied);
      }
      for (String providesName : feature.getProvides()) {
        provides.put(selectable, providesName);
      }
    }

    for (ActionConfig actionConfig : actionConfigs) {
      String name = actionConfig.getName();
      CrosstoolSelectable selectable = selectablesByName.get(name);
      for (String impliedName : actionConfig.getImplies()) {
        CrosstoolSelectable implied = getActivatableOrFail(impliedName, name);
        impliedBy.put(implied, selectable);
        implies.put(selectable, implied);
      }
    }

    this.implies = implies.build();
    this.requires = requires.build();
    this.provides = provides.build().inverse();
    this.impliedBy = impliedBy.build();
    this.requiredBy = requiredBy.build();
    this.ccToolchainPath = ccToolchainPath;
  }

  private static void checkForActivatableDups(Iterable<CrosstoolSelectable> selectables)
      throws EvalException {
    Collection<String> names = new HashSet<>();
    for (CrosstoolSelectable selectable : selectables) {
      if (!names.add(selectable.getName())) {
        throw Starlark.errorf(
            "Invalid toolchain configuration: feature or action config '%s' was specified multiple"
                + " times.",
            selectable.getName());
      }
    }
  }

  private static void checkForActionNameDups(Iterable<ActionConfig> actionConfigs)
      throws EvalException {
    Collection<String> actionNames = new HashSet<>();
    for (ActionConfig actionConfig : actionConfigs) {
      if (!actionNames.add(actionConfig.getActionName())) {
        throw Starlark.errorf(
            "Invalid toolchain configuration: multiple action configs for action '%s'",
            actionConfig.getActionName());
      }
    }
  }

  /**
   * @return an empty {@code FeatureConfiguration} cache.
   */
  private LoadingCache<ImmutableSet<String>, FeatureConfiguration> buildConfigurationCache() {
    return Caffeine.newBuilder()
        // TODO(klimek): Benchmark and tweak once we support a larger configuration.
        .maximumSize(10000)
        .build(requestedFeatures -> computeFeatureConfiguration(requestedFeatures));
  }

  @StarlarkMethod(
      name = "configure_features",
      documented = false,
      parameters = {@Param(name = "requested_features", named = true)},
      useStarlarkThread = true)
  public FeatureConfigurationForStarlark configureFeatures(
      StarlarkList<?> requestedFeatures, StarlarkThread thread) throws EvalException {
    try {
      CcModule.checkPrivateStarlarkificationAllowlist(thread);
      return FeatureConfigurationForStarlark.from(
          getFeatureConfiguration(
              ImmutableSet.copyOf(
                  Sequence.cast(requestedFeatures, String.class, "requested_features"))));
    } catch (CollidingProvidesException ex) {
      throw new EvalException(ex);
    }
  }

  /**
   * Given a list of {@code requestedSelectables}, returns all features that are enabled by the
   * toolchain configuration.
   *
   * <p>A requested feature will not be enabled if the toolchain does not support it (which may
   * depend on other requested features).
   *
   * <p>Additional features will be enabled if the toolchain supports them and they are implied by
   * requested features.
   *
   * <p>If multiple threads call this method we may do additional work in initializing the cache.
   * This reinitialization is benign.
   */
  public FeatureConfiguration getFeatureConfiguration(ImmutableSet<String> requestedSelectables)
      throws CollidingProvidesException {
    try {
      if (configurationCache == null) {
        configurationCache = buildConfigurationCache();
      }
      return configurationCache.get(requestedSelectables);
    } catch (CompletionException e) {
      Throwables.throwIfInstanceOf(e.getCause(), CollidingProvidesException.class);
      throw e;
    }
  }

  /**
   * Given {@code featureSpecification}, returns a FeatureConfiguration with all requested features
   * enabled.
   *
   * <p>A requested feature will not be enabled if the toolchain does not support it (which may
   * depend on other requested features).
   *
   * <p>Additional features will be enabled if the toolchain supports them and they are implied by
   * requested features.
   */
  public FeatureConfiguration computeFeatureConfiguration(ImmutableSet<String> requestedSelectables)
      throws CollidingProvidesException {
    // Command line flags will be output in the order in which they are specified in the toolchain
    // configuration.
    return new FeatureSelection(
            requestedSelectables,
            selectablesByName,
            selectables,
            provides,
            implies,
            impliedBy,
            requires,
            requiredBy,
            actionConfigsByActionName,
            ccToolchainPath)
        .run();
  }

  @StarlarkMethod(
      name = "default_features_and_action_configs",
      documented = false,
      useStarlarkThread = true)
  public StarlarkList<String> getDefaultFeaturesAndActionConfigsForStarlark(StarlarkThread thread)
      throws EvalException {
    CcModule.checkPrivateStarlarkificationAllowlist(thread);
    return StarlarkList.immutableCopyOf(defaultSelectables);
  }

  public ImmutableList<String> getDefaultFeaturesAndActionConfigs() {
    return defaultSelectables;
  }

  /**
   * @return the selectable with the given {@code name}.s
   * @throws EvalException if no selectable with the given name was configured.
   */
  private CrosstoolSelectable getActivatableOrFail(String name, String reference)
      throws EvalException {
    if (!selectablesByName.containsKey(name)) {
      throw Starlark.errorf(
          "Invalid toolchain configuration: feature '%s', which is referenced from feature '%s',"
              + " is not defined.",
          name, reference);
    }
    return selectablesByName.get(name);
  }

  @VisibleForTesting
  Collection<String> getActivatableNames() {
    return selectablesByName.keySet();
  }

  /**
   * Returns the artifact selected by the toolchain for the given action type and action category.
   */
  String getArtifactNameForCategory(ArtifactCategory artifactCategory, String outputName) {
    PathFragment output = PathFragment.create(outputName);
    return output
        .getParentDirectory()
        .getChild(artifactNamePatterns.get(artifactCategory).getArtifactName(output.getBaseName()))
        .getPathString();
  }

  /**
   * Returns the artifact name extension selected by the toolchain for the given artifact category.
   */
  String getArtifactNameExtensionForCategory(ArtifactCategory artifactCategory) {
    return artifactNamePatterns.get(artifactCategory).extension();
  }
}
