// Copyright 2023 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;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.Collections.reverse;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.analysis.AspectCollection.AspectCycleOnPathException;
import com.google.devtools.build.lib.packages.Aspect;
import com.google.devtools.build.lib.packages.AspectClass;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
import com.google.devtools.build.lib.skyframe.toolchains.UnloadedToolchainContext;
import java.util.ArrayList;
import javax.annotation.Nullable;

/** Helpers for aspect resolution. */
public final class AspectResolutionHelpers {
  private AspectResolutionHelpers() {}

  /**
   * Computes the set of aspects that could be applied to a dependency.
   *
   * <p>This is composed of two parts:
   *
   * <ol>
   *   <li>The aspects that are visible to this aspect being evaluated, if any. If another aspect is
   *       visible on the configured target, it should also be visible on the dependencies for
   *       consistency. This is the argument {@code aspectsPath}.
   *   <li>The aspects propagated by the attributes of this configured target / aspect.
   * </ol>
   *
   * <p>The presence of an aspect here does not necessarily mean that it will be available on a
   * dependency: it can still be filtered out because it requires a provider that the configured
   * target it should be attached to it doesn't advertise. This is taken into account in {@link
   * #computeAspectCollection}.
   */
  public static ImmutableList<Aspect> computePropagatingAspects(
      DependencyKind kind,
      ImmutableList<Aspect> aspectsPath,
      Rule rule,
      @Nullable ToolchainCollection<UnloadedToolchainContext> baseTargetToolchainContext) {
    if (DependencyKind.isBaseTargetToolchain(kind)) {
      return computePropagatingAspectsToToolchainDep(
          (DependencyKind.BaseTargetToolchainDependencyKind) kind,
          aspectsPath,
          baseTargetToolchainContext);
    }

    Attribute attribute = kind.getAttribute();
    if (attribute == null) {
      return ImmutableList.of();
    }
    var aspectsBuilder = new ImmutableList.Builder<Aspect>().addAll(attribute.getAspects(rule));
    collectPropagatingAspects(
        aspectsPath, attribute.getName(), kind.getOwningAspect(), aspectsBuilder);
    return aspectsBuilder.build();
  }

  /**
   * Compute the set of aspects propagating to the given {@link BaseTargetToolchainDependencyKind}
   * based on the {@code toolchains_aspects} of each aspect in the {@code aspectsPath}.
   */
  private static ImmutableList<Aspect> computePropagatingAspectsToToolchainDep(
      DependencyKind.BaseTargetToolchainDependencyKind kind,
      ImmutableList<Aspect> aspectsPath,
      @Nullable ToolchainCollection<UnloadedToolchainContext> baseTargetToolchainContext) {
    var toolchainContext = baseTargetToolchainContext.getToolchainContext(kind.getExecGroupName());
    var toolchainType =
        toolchainContext.requestedLabelToToolchainType().get(kind.getToolchainType());

    // Since the label of the toolchain type can be an alias, we need to get all the labels that
    // point to the same toolchain type to compare them against the toolchain types that the aspects
    // can propagate.
    var allToolchainTypelabels =
        toolchainContext.requestedLabelToToolchainType().asMultimap().inverse().get(toolchainType);

    var filteredAspectPath = new ArrayList<Aspect>();

    int aspectsCount = aspectsPath.size();
    for (int i = aspectsCount - 1; i >= 0; i--) {
      Aspect aspect = aspectsPath.get(i);
      if (allToolchainTypelabels.stream()
              .anyMatch(label -> aspect.getDefinition().canPropagateToToolchainType(label))
          || isAspectRequired(aspect, filteredAspectPath)) {
        // Adds the aspect if it propagates to the toolchain type or it is
        // required by an aspect already in the {@code filteredAspectPath}.
        filteredAspectPath.add(aspect);
      }
    }
    reverse(filteredAspectPath);

    return ImmutableList.copyOf(filteredAspectPath);
  }

  /**
   * Computes the way aspects should be computed for the direct dependencies.
   *
   * <p>This is done by filtering the aspects that can be propagated on any attribute according to
   * the providers advertised by direct dependencies and by creating the {@link AspectCollection}
   * that tells how to compute the final set of providers based on the interdependencies between the
   * propagating aspects.
   */
  public static AspectCollection computeAspectCollection(
      ConfiguredTargetAndData prerequisite, ImmutableList<Aspect> propagatingAspects)
      throws InconsistentAspectOrderException {
    var aspects = propagatingAspects;
    if (prerequisite.isTargetOutputFile()) {
      // If `applyToGeneratingRules` holds, the aspect has no required providers so some of the
      // filtering logic below may be skipped, but it doesn't seem to be worth added complexity.
      aspects =
          aspects.stream()
              .filter(aspect -> aspect.getDefinition().applyToGeneratingRules())
              .collect(toImmutableList());
    }

    if (aspects.isEmpty() || (!prerequisite.isTargetOutputFile() && !prerequisite.isTargetRule())) {
      return AspectCollection.EMPTY;
    }

    var filteredAspectPath = new ArrayList<Aspect>();

    int aspectsCount = aspects.size();
    for (int i = aspectsCount - 1; i >= 0; i--) {
      Aspect aspect = aspects.get(i);
      if (prerequisite.satisfies(aspect.getDefinition().getRequiredProviders())
          || isAspectRequired(aspect, filteredAspectPath)) {
        // Adds the aspect if the configured target satisfies its required providers or it is
        // required by an aspect already in the {@code filteredAspectPath}.
        filteredAspectPath.add(aspect);
      }
    }
    reverse(filteredAspectPath);
    try {
      return AspectCollection.create(filteredAspectPath);
    } catch (AspectCycleOnPathException e) {
      throw new InconsistentAspectOrderException(
          prerequisite.getTargetLabel(), prerequisite.getLocation(), e);
    }
  }

  /**
   * Collects the aspects from {@code aspectsPath} that need to be propagated along the attribute
   * {@code attributeName}.
   *
   * <p>It can happen that some of the aspects cannot be propagated if the dependency doesn't have a
   * provider that's required by them. These will be filtered out after the rule class of the
   * dependency is known.
   */
  private static void collectPropagatingAspects(
      ImmutableList<Aspect> aspectsPath,
      String attributeName,
      @Nullable AspectClass aspectOwningAttribute,
      ImmutableList.Builder<Aspect> allFilteredAspects) {
    int aspectsNum = aspectsPath.size();
    var filteredAspectsPath = new ArrayList<Aspect>();

    // `aspectsPath` is ordered bottom up. Iterating backwards traverses top-down so the following
    // loop captures aspects that propagate along the given attribute and all their transitive
    // requirements.
    for (int i = aspectsNum - 1; i >= 0; i--) {
      Aspect aspect = aspectsPath.get(i);
      if (aspect.getAspectClass().equals(aspectOwningAttribute)) {
        // Do not propagate over the aspect's own attributes.
        continue;
      }

      if (aspect.getDefinition().propagateAlong(attributeName)
          || isAspectRequired(aspect, filteredAspectsPath)) {
        // Add the aspect if it can propagate over this {@code attributeName} based on its
        // attr_aspects or it is required by an aspect already in the {@code filteredAspectsPath}.
        filteredAspectsPath.add(aspect);
      }
    }
    // Reverse filteredAspectsPath to return it to the same order as the input aspectsPath.
    reverse(filteredAspectsPath);
    allFilteredAspects.addAll(filteredAspectsPath);
  }

  /** Checks if {@code aspect} is required by any {@link Aspect} in {@code aspectsPath}. */
  private static boolean isAspectRequired(Aspect aspect, Iterable<Aspect> aspectsPath) {
    for (Aspect existingAspect : aspectsPath) {
      if (existingAspect.getDefinition().requires(aspect)) {
        return true;
      }
    }
    return false;
  }
}
