// Copyright 2022 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.constraints;

import static com.google.common.base.Predicates.notNull;
import static com.google.common.collect.ImmutableList.toImmutableList;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.ConfiguredTargetValue;
import com.google.devtools.build.lib.analysis.Dependency;
import com.google.devtools.build.lib.analysis.DependencyKind;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.IncompatiblePlatformProvider;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
import com.google.devtools.build.lib.analysis.TransitiveInfoProviderMapBuilder;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.ConfigConditions;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
import com.google.devtools.build.lib.analysis.platform.ConstraintValueInfo;
import com.google.devtools.build.lib.analysis.platform.PlatformInfo;
import com.google.devtools.build.lib.analysis.platform.PlatformProviderUtils;
import com.google.devtools.build.lib.analysis.test.TestActionBuilder;
import com.google.devtools.build.lib.analysis.test.TestConfiguration;
import com.google.devtools.build.lib.analysis.test.TestProvider;
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.collect.nestedset.Order;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.ConfiguredAttributeMapper;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.PackageSpecification;
import com.google.devtools.build.lib.packages.PackageSpecification.PackageGroupContents;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
import com.google.devtools.build.lib.skyframe.RuleConfiguredTargetValue;
import com.google.devtools.build.lib.util.OrderedSetMultimap;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import com.google.devtools.build.skyframe.SkyframeLookupResult;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;

/**
 * Helpers for creating configured targets that are incompatible.
 *
 * <p>A target is considered incompatible if any of the following applies:
 *
 * <ol>
 *   <li>The target's <code>target_compatible_with</code> attribute specifies a constraint that is
 *       not present in the target platform. The target is said to be "directly incompatible".
 *   <li>One or more of the target's dependencies is incompatible. The target is said to be
 *       "indirectly incompatible."
 * </ol>
 *
 * The intent of these helpers is that they get called as early in the analysis phase as possible.
 * That's why there are two helpers instead of just one. The first helper determines direct
 * incompatibility very early in the analysis phase. If a target is not directly incompatible, the
 * dependencies need to be analysed and then we can check for indirect incompatibility. Doing these
 * checks as early as possible allows us to skip analysing unused dependencies and ignore unused
 * toolchains.
 *
 * <p>See https://bazel.build/docs/platforms#skipping-incompatible-targets for more information on
 * incompatible target skipping.
 */
public class IncompatibleTargetChecker {
  /**
   * Creates an incompatible configured target if it is "directly incompatible".
   *
   * <p>In other words, this function checks if a target is incompatible because of its
   * "target_compatible_with" attribute.
   *
   * <p>This function returns a nullable {@code Optional} of a {@link RuleConfiguredTargetValue}.
   * This provides three states of return values:
   *
   * <ul>
   *   <li>{@code null}: A skyframe restart is required.
   *   <li>{@code Optional.empty()}: The target is not directly incompatible. Analysis can continue.
   *   <li>{@code !Optional.empty()}: The target is directly incompatible. Analysis should not
   *       continue.
   * </ul>
   */
  @Nullable
  public static Optional<RuleConfiguredTargetValue> createDirectlyIncompatibleTarget(
      TargetAndConfiguration targetAndConfiguration,
      ConfigConditions configConditions,
      Environment env,
      @Nullable PlatformInfo platformInfo,
      NestedSetBuilder<Package> transitivePackages)
      throws ConfiguredAttributeMapper.ValidationException, InterruptedException {
    Target target = targetAndConfiguration.getTarget();
    Rule rule = target.getAssociatedRule();

    if (rule == null || rule.getRuleClass().equals("toolchain") || platformInfo == null) {
      return Optional.empty();
    }

    // Retrieve the label list for the target_compatible_with attribute.
    BuildConfigurationValue configuration = targetAndConfiguration.getConfiguration();
    ConfiguredAttributeMapper attrs =
        ConfiguredAttributeMapper.of(rule, configConditions.asProviders(), configuration);
    if (!attrs.has("target_compatible_with", BuildType.LABEL_LIST)) {
      return Optional.empty();
    }

    // Resolve the constraint labels.
    List<Label> labels = attrs.getAndValidate("target_compatible_with", BuildType.LABEL_LIST);
    ImmutableList<ConfiguredTargetKey> constraintKeys =
        labels.stream()
            .map(
                label ->
                    Dependency.builder()
                        .setLabel(label)
                        .setConfiguration(configuration)
                        .build()
                        .getConfiguredTargetKey())
            .collect(toImmutableList());
    SkyframeLookupResult constraintValues = env.getValuesAndExceptions(constraintKeys);
    if (env.valuesMissing()) {
      return null;
    }

    // Find the constraints that don't satisfy the target platform.
    ImmutableList<ConstraintValueInfo> invalidConstraintValues =
        constraintKeys.stream()
            .map(key -> (ConfiguredTargetValue) constraintValues.get(key))
            .filter(notNull())
            .map(ctv -> PlatformProviderUtils.constraintValue(ctv.getConfiguredTarget()))
            .filter(notNull())
            .filter(cv -> !platformInfo.constraints().hasConstraintValue(cv))
            .collect(toImmutableList());
    if (invalidConstraintValues.isEmpty()) {
      return Optional.empty();
    }

    return Optional.of(
        createIncompatibleRuleConfiguredTarget(
            target,
            configuration,
            configConditions,
            IncompatiblePlatformProvider.incompatibleDueToConstraints(
                platformInfo.label(), invalidConstraintValues),
            rule.getRuleClass(),
            transitivePackages));
  }

  /**
   * Creates an incompatible target if it is "indirectly incompatible".
   *
   * <p>In other words, this function checks if a target is incompatible because of one of its
   * dependencies. If a dependency is incompatible, then this target is also incompatible.
   *
   * <p>This function returns an {@code Optional} of a {@link RuleConfiguredTargetValue}. This
   * provides two states of return values:
   *
   * <ul>
   *   <li>{@code Optional.empty()}: The target is not indirectly incompatible. Analysis can
   *       continue.
   *   <li>{@code !Optional.empty()}: The target is indirectly incompatible. Analysis should not
   *       continue.
   * </ul>
   */
  public static Optional<RuleConfiguredTargetValue> createIndirectlyIncompatibleTarget(
      TargetAndConfiguration targetAndConfiguration,
      OrderedSetMultimap<DependencyKind, ConfiguredTargetAndData> depValueMap,
      ConfigConditions configConditions,
      @Nullable PlatformInfo platformInfo,
      NestedSetBuilder<Package> transitivePackages) {
    Target target = targetAndConfiguration.getTarget();
    Rule rule = target.getAssociatedRule();

    if (rule == null || rule.getRuleClass().equals("toolchain")) {
      return Optional.empty();
    }

    // Find all the incompatible dependencies.
    ImmutableList<ConfiguredTarget> incompatibleDeps =
        depValueMap.values().stream()
            .map(ConfiguredTargetAndData::getConfiguredTarget)
            .filter(
                dep -> RuleContextConstraintSemantics.checkForIncompatibility(dep).isIncompatible())
            .collect(toImmutableList());
    if (incompatibleDeps.isEmpty()) {
      return Optional.empty();
    }

    BuildConfigurationValue configuration = targetAndConfiguration.getConfiguration();
    Label platformLabel = platformInfo != null ? platformInfo.label() : null;
    return Optional.of(
        createIncompatibleRuleConfiguredTarget(
            target,
            configuration,
            configConditions,
            IncompatiblePlatformProvider.incompatibleDueToTargets(platformLabel, incompatibleDeps),
            rule.getRuleClass(),
            transitivePackages));
  }

  /** Creates an incompatible target. */
  private static RuleConfiguredTargetValue createIncompatibleRuleConfiguredTarget(
      Target target,
      BuildConfigurationValue configuration,
      ConfigConditions configConditions,
      IncompatiblePlatformProvider incompatiblePlatformProvider,
      String ruleClassString,
      NestedSetBuilder<Package> transitivePackages) {
    // Create dummy instances of the necessary data for a configured target. None of this data will
    // actually be used because actions associated with incompatible targets must not be evaluated.
    NestedSet<Artifact> filesToBuild = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
    FileProvider fileProvider = new FileProvider(filesToBuild);
    FilesToRunProvider filesToRunProvider = new FilesToRunProvider(filesToBuild, null, null);

    TransitiveInfoProviderMapBuilder providerBuilder =
        new TransitiveInfoProviderMapBuilder()
            .put(incompatiblePlatformProvider)
            .add(RunfilesProvider.simple(Runfiles.EMPTY))
            .add(fileProvider)
            .add(filesToRunProvider)
            .add(
                new SupportedEnvironments(
                    EnvironmentCollection.EMPTY, EnvironmentCollection.EMPTY, ImmutableMap.of()));
    if (configuration.hasFragment(TestConfiguration.class)) {
      // Create a dummy TestProvider instance so that other parts of the code base stay happy. Even
      // though this test will never execute, some code still expects the provider.
      TestProvider.TestParams testParams = TestActionBuilder.createEmptyTestParams();
      providerBuilder.put(TestProvider.class, new TestProvider(testParams));
    }

    RuleConfiguredTarget configuredTarget =
        new RuleConfiguredTarget(
            target.getLabel(),
            configuration.getKey(),
            convertVisibility(),
            providerBuilder.build(),
            configConditions.asProviders(),
            ruleClassString);
    return new RuleConfiguredTargetValue(
        configuredTarget, transitivePackages == null ? null : transitivePackages.build());
  }

  /**
   * Generates visibility for an incompatible target.
   *
   * <p>The intent is for this function is to match ConfiguredTargetFactory.convertVisibility().
   * Since visibility is currently validated after incompatibility is evaluated, however, it doesn't
   * matter what visibility we set here. To keep it simple, we pretend that all incompatible targets
   * are public.
   *
   * <p>TODO(#16044): Set up properly validated visibility here.
   */
  private static NestedSet<PackageGroupContents> convertVisibility() {
    return NestedSetBuilder.create(
        Order.STABLE_ORDER,
        PackageGroupContents.create(ImmutableList.of(PackageSpecification.everything())));
  }
}
