// Copyright 2024 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.packages;

import static java.util.Objects.requireNonNull;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.packages.BuildType.SelectorList;
import com.google.devtools.build.lib.packages.Type.ConversionException;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.syntax.Location;

/**
 * Represents a use of a symbolic macro in a package.
 *
 * <p>There is one {@code MacroInstance} for each call to a {@link
 * StarlarkRuleClassFunctions#MacroFunction} that is executed during a package's evaluation. Just as
 * a {@link MacroClass} is analogous to a {@link RuleClass}, {@code MacroInstance} is analogous to a
 * {@link Rule} (i.e. a rule target).
 *
 * <p>Macro instance names are not guaranteed to be unique within a package; see {@link #getId}.
 */
public final class MacroInstance {

  // TODO: #19922 - If we want to save the cost of a field here, we can merge pkg and parent into a
  // single field of type Object, and walk up the parent hierarchy to answer getPackage() queries.
  private final Package pkg;

  @Nullable private final MacroInstance parent;

  private final Location buildFileLocation;

  private final CallStack.Node parentCallStack;

  private final MacroClass macroClass;

  private final int sameNameDepth;

  // Order isn't guaranteed, sort before dumping. You can use the schema map
  // MacroClass#getAttributes for a guaranteed order.
  private final ImmutableMap<String, Object> attrValues;

  // TODO(#19922): Consider switching to more optimized, indexed representation for attributes, as
  // in Rule.

  /**
   * Instantiates the given macro class with the given attribute values.
   *
   * <p>{@code attrValues} must have already been normalized based on the types of the attributes;
   * see {@link MacroClass#instantiateMacro}. Values for the {@code "name"} and {@code "visibility"}
   * attributes must exist with the correct types, and the {@code "visibility"} value must satisfy
   * {@link RuleVisibility#validate}.
   *
   * <p>{@code sameNameDepth} is the number of macro instances that this one is inside of that share
   * its name. For most instances it is 1, but for the main submacro of a parent macro it is one
   * more than the parent's depth.
   *
   * @throws EvalException if there's a problem with the attribute values (currently, only thrown if
   *     the {@code visibility} value is invalid)
   */
  // TODO: #19922 - Better encapsulate the invariant around attrValues, by either transitioning to
  // storing internal-typed values (the way Rules do) instead of Starlark-typed values, or else just
  // making the constructor private and moving instantiateMacro() to this class.
  public MacroInstance(
      Package pkg,
      @Nullable MacroInstance parent,
      Location buildFileLocation,
      CallStack.Node parentCallStack,
      MacroClass macroClass,
      Map<String, Object> attrValues,
      int sameNameDepth)
      throws EvalException {
    this.pkg = pkg;
    this.parent = parent;
    this.buildFileLocation = buildFileLocation;
    this.parentCallStack = parentCallStack;
    this.macroClass = macroClass;
    this.attrValues = ImmutableMap.copyOf(attrValues);
    Preconditions.checkArgument(sameNameDepth > 0);
    this.sameNameDepth = sameNameDepth;
    Preconditions.checkArgument(macroClass.getAttributes().keySet().equals(attrValues.keySet()));
  }

  /** Returns the package this instance was created in. */
  public Package getPackage() {
    return pkg;
  }

  /**
   * Returns the macro instance that instantiated this one, or null if this was created directly
   * during BUILD evaluation.
   */
  @Nullable
  public MacroInstance getParent() {
    return parent;
  }

  /**
   * Returns the location in the BUILD file at which this macro was created or its outermost
   * enclosing symbolic or legacy macro was called.
   */
  @VisibleForTesting
  public Location getBuildFileLocation() {
    return buildFileLocation;
  }

  /**
   * Returns the call stack of the Starlark thread that created this macro instance.
   *
   * <p>If this macro was instantiated in a BUILD file thread (as contrasted with a symbolic macro
   * thread), the call stack does not include the frame for the BUILD file top level, since it's
   * redundant with {@link #getBuildFileLocation}.
   */
  CallStack.Node getParentCallStack() {
    return parentCallStack;
  }

  /**
   * Returns the call stack of the Starlark thread that created this macro instance.
   *
   * <p>Requires reconstructing the call stack from a compact representation, so should only be
   * called when the full call stack is needed.
   */
  @VisibleForTesting
  public ImmutableList<StarlarkThread.CallStackEntry> reconstructParentCallStack() {
    ImmutableList.Builder<StarlarkThread.CallStackEntry> stack = ImmutableList.builder();
    if (parent == null) {
      stack.add(StarlarkThread.callStackEntry(StarlarkThread.TOP_LEVEL, buildFileLocation));
    }
    for (CallStack.Node node = parentCallStack; node != null; node = node.next()) {
      stack.add(node.toCallStackEntry());
    }
    return stack.build();
  }

  /** Returns the {@link MacroClass} (i.e. schema info) that this instance parameterizes. */
  public MacroClass getMacroClass() {
    return macroClass;
  }

  /**
   * The depth of this macro instance in a chain of nested macros having the same name.
   *
   * <p>1 for any macro that is not declared in a macro of the same name.
   *
   * <p>Used by {@link #getId}.
   */
  public int getSameNameDepth() {
    return sameNameDepth;
  }

  /**
   * Returns the id of this macro instance. The id is the name, concatenated with {@code ":n"} where
   * n is an integer distinguishing this from other macro instances of the same name in the package.
   *
   * <p>Within a package, two macro instances are not allowed to share the same name except when one
   * of them is the main submacro of the other. More generally, there may be a contiguous chain of
   * nested main submacros that all share the same name, but these may not share with any other
   * macro outside the chain. We allow this exception so that the build does not break if the rule
   * of a main target is refactored into a macro. The tradeoff of this design is that the name alone
   * is not enough to disambiguate between macros in the chain.
   *
   * <p>The number n is simply the depth of the macro in the chain of same-named macros, starting at
   * 1. For example, if we have a chain of macro expansions foo -> foo_bar -> foo_bar -> foo_bar ->
   * foo_bar_baz, then the ids of these macros are respectively "foo:1", "foo_bar:1", "foo_bar:2",
   * "foo_bar:3", "foo_bar_baz:1".
   *
   * <p>Note that ids only serve to canonically identify macro instances, and play no role in naming
   * or name conflict detection.
   */
  public String getId() {
    return getName() + ":" + sameNameDepth;
  }

  /**
   * Returns the name of this instance, as given in the {@code name = ...} attribute in the calling
   * BUILD file or macro.
   */
  public String getName() {
    // Type and existence enforced by RuleClass.NAME_ATTRIBUTE.
    return (String) Preconditions.checkNotNull(attrValues.get("name"));
  }

  /**
   * Returns the visibility of this macro instance, analogous to {@link Target#getActualVisibility}.
   *
   * <p>This value will be observed as the {@code visibility} parameter of the implementation
   * function. It is not necessarily the same as the {@code visibility} value passed in when
   * instantiating the macro, since the latter needs processing to add the call site's location and
   * possibly apply the package's default visibility.
   *
   * <p>It can be assumed that the returned list satisfies {@link RuleVisibility#validate}.
   */
  public ImmutableList<Label> getActualVisibility() {
    @SuppressWarnings("unchecked")
    List<Label> visibility = (List<Label>) Preconditions.checkNotNull(attrValues.get("visibility"));
    return ImmutableList.copyOf(visibility);
  }

  /**
   * Dictionary of attributes for this instance.
   *
   * <p>Contains all attributes, as seen after processing by {@link
   * MacroClass#instantiateAndAddMacro}.
   */
  public ImmutableMap<String, Object> getAttrValues() {
    return attrValues;
  }

  /**
   * Returns the package containing the .bzl file from which this macro instance's macro class was
   * exported.
   *
   * <p>This is considered to be the place where the macro's code lives, and is used as the place
   * where a target is instantiated for the purposes of Macro-Aware Visibility.
   */
  public PackageIdentifier getDefinitionPackage() {
    return macroClass.getDefiningBzlLabel().getPackageIdentifier();
  }

  /**
   * Visits all labels appearing in non-implicit attributes of {@link Type.LabelClass#DEPENDENCY}
   * label type, i.e. ignoring nodep labels.
   *
   * <p>This is useful for checking whether a given label was passed as an input to this macro by
   * the caller, which in turn is needed in order to decide whether the caller delegated a
   * visibility privilege to us.
   */
  public void visitExplicitAttributeLabels(Consumer<Label> consumer) {
    for (Attribute attribute : macroClass.getAttributes().values()) {
      String name = attribute.getName();
      Type<?> type = attribute.getType();
      if (name.startsWith("_")) {
        continue;
      }
      if (type.getLabelClass() != Type.LabelClass.DEPENDENCY) {
        continue;
      }
      Object value = attrValues.get(name);
      if (value == Starlark.NONE) {
        continue;
      }
      visitAttributeLabels(value, type, attribute, consumer);
    }
  }

  // Separate method needed to satisfy type system w.r.t. Type<T>.
  // `value` is either a T or SelectorList<T>.
  private static <T> void visitAttributeLabels(
      Object value, Type<T> type, Attribute attribute, Consumer<Label> consumer) {
    // The attribute value is stored as a Starlark value. Convert it to the internal type as would
    // be used in rules, so we can apply visitLabels() machinery to it. selectableConvert() will
    // yield either a T or a BuildType.SelectorList.
    Object convertedValue;
    try {
      convertedValue =
          BuildType.selectableConvert(
              type,
              value,
              "macro attribute (internal)",
              // No string -> Label conversion is being done here.
              /* context= */ null,
              // Macros always preserve selects as selects.
              /* simplifyUnconditionalSelects= */ false);
    } catch (ConversionException e) {
      // TODO: #19922 - The fact that we have to do this seems like a signal that we should
      // transition to storing macro attribute values as native-typed attributes in the future.
      throw new IllegalStateException("Could not convert macro attribute value internally", e);
    }

    // Unlike rules, null attribute values are disallowed here by construction (the attrValues
    // map won't tolerate them). It's unclear if the visitor can be passed null values like it can
    // for rules, so filter them out just in case.
    Type.LabelVisitor visitor =
        (label, unusedAttribute) -> {
          if (label != null) {
            consumer.accept(label);
          }
        };

    if (convertedValue instanceof SelectorList) {
      @SuppressWarnings("unchecked") // safe by precondition assumption
      SelectorList<T> selectorList = (SelectorList<T>) convertedValue;
      AggregatingAttributeMapper.visitLabelsInSelect(
          selectorList,
          attribute,
          type,
          visitor,
          /* rule= */ null, // safe because late-bound defaults aren't a thing for macros
          /* includeKeys= */ false,
          /* includeValues= */ true);
    } else {
      T castValue = type.cast(convertedValue);
      type.visitLabels(visitor, castValue, attribute);
    }
  }

  /** Returns a human-readable description of the macro suitable for debugging output. */
  public String getShortDescription() {
    return String.format(
        "%smacro %s:%s defined by %s%%%s",
        macroClass.isFinalizer() ? "finalizer " : "",
        pkg.getPackageIdentifier().getCanonicalForm(),
        getName(),
        macroClass.getDefiningBzlLabel().getCanonicalForm(),
        macroClass.getName());
  }

  /**
   * Logical tuple of the package and id within the package. Used to label the Starlark evaluation
   * environment.
   */
  record UniqueId(PackageIdentifier packageId, String id) {
    UniqueId {
      requireNonNull(packageId, "packageId");
      requireNonNull(id, "id");
    }

    static UniqueId create(PackageIdentifier packageId, String id) {
      return new UniqueId(packageId, id);
    }
  }
}
