// Copyright 2025 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 com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.RepositoryMapping;
import com.google.devtools.build.lib.cmdline.StarlarkThreadContext;
import com.google.devtools.build.lib.packages.Package.Builder.PackageLimits;
import com.google.devtools.build.lib.packages.Package.Builder.PackageSettings;
import com.google.devtools.build.lib.packages.Package.ConfigSettingVisibilityPolicy;
import com.google.devtools.build.lib.packages.Package.Declarations;
import com.google.devtools.build.lib.packages.Package.Metadata;
import com.google.devtools.build.lib.packages.TargetRecorder.MacroNamespaceViolationException;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Semaphore;
import javax.annotation.Nullable;
import net.starlark.java.eval.Module;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.SymbolGenerator;
import net.starlark.java.syntax.Location;

/**
 * A piece of a {@link Package}: either the subset obtained by evaluating a BUILD file and not
 * expanding any symbolic macros; or the subset obtained by evaluating exactly one symbolic macro
 * instance.
 *
 * <p>To obtain a {@link Package} from a {@link PackagePiece}, use a PackageProvider or skyframe
 * machinery.
 */
// TODO(https://github.com/bazelbuild/bazel/issues/23852): as a future optimization, consider adding
// another class of package piece obtained by evaluating a set of macros.
public abstract sealed class PackagePiece extends Packageoid
    permits PackagePiece.ForBuildFile, PackagePiece.ForMacro {
  /**
   * The collection of all symbolic macro instances defined in this package piece, indexed by their
   * name (not by {@link MacroInstance#getId id} - contrast with {@link Package#macros}). Null until
   * the package piece is fully initialized by {@link #setMacrosByName}, in turn called by this
   * package piece's builder's {@code finishBuild()}.
   */
  @Nullable private ImmutableSortedMap<String, MacroInstance> macrosByName;

  public abstract PackagePieceIdentifier getIdentifier();

  /**
   * Returns a (read-only, ordered) iterable of all the targets belonging to this package piece
   * which are instances of the specified class. Doesn't search in any other package pieces.
   */
  public <T extends Target> Iterable<T> getTargets(Class<T> targetClass) {
    return Iterables.filter(targets, targetClass);
  }

  @Override
  public Target getTarget(String targetName) throws NoSuchTargetException {
    Target target = getTargetOrNull(targetName);
    if (target != null) {
      return target;
    }

    throw noSuchTargetException(targetName);
  }

  /**
   * Returns the macro instance declared in this package piece having the provided name; or null if
   * no such macro instance exists.
   */
  @Nullable
  public MacroInstance getMacroByName(String name) {
    return macrosByName.get(name);
  }

  /** Returns a list of all the macro instances defined in this package piece, ordered by name. */
  public ImmutableList<MacroInstance> getMacros() {
    return ImmutableList.copyOf(macrosByName.values());
  }

  private NoSuchTargetException noSuchTargetException(String targetName) {
    Label label;
    try {
      label = Label.create(getPackageIdentifier(), targetName);
    } catch (LabelSyntaxException e) {
      throw new IllegalArgumentException(targetName, e);
    }

    if (getMetadata().succinctTargetNotFoundErrors()) {
      return new NoSuchTargetException(
          label,
          String.format("target '%s' not declared in %s", targetName, getShortDescription()));
    } else {
      String alternateTargetSuggestion =
          Package.getAlternateTargetSuggestion(getMetadata(), targetName, targets);
      return new NoSuchTargetException(
          label,
          String.format(
              "target '%s' not declared in %s%s",
              targetName, getShortDescription(), alternateTargetSuggestion));
    }
  }

  @Override
  public String toString() {
    return String.format(
        "PackagePiece(%s defined by %s)=%s",
        getIdentifier().getCanonicalFormName(),
        getCanonicalFormDefinedBy(),
        targets != null ? getTargets(Rule.class) : "initializing...");
  }

  /**
   * Returns the canonical form of the BUILD file label if this is a {@link
   * PackagePiece.ForBuildFile}, or the canonical form of the macro class's declaring .bzl label and
   * macro name, in {@code label%name} format, if this is a {@link PackagePiece.ForMacro}.
   */
  public abstract String getCanonicalFormDefinedBy();

  /**
   * Sets the macros map for this package piece. Intended only to be called by this package piece's
   * builder.
   *
   * @param macros a collection of macro instances, which must have unique names.
   */
  protected void setMacrosByName(Collection<MacroInstance> macros) {
    ImmutableSortedMap.Builder<String, MacroInstance> macrosByName =
        ImmutableSortedMap.naturalOrder();
    for (MacroInstance macro : macros) {
      macrosByName.put(macro.getName(), macro);
    }
    this.macrosByName = macrosByName.buildOrThrow();
  }

  protected PackagePiece(Metadata metadata, Declarations declarations) {
    super(metadata, declarations);
  }

  /**
   * A {@link PackagePiece} obtained by evaluating a BUILD file, without expanding any symbolic
   * macros.
   */
  public static final class ForBuildFile extends PackagePiece {
    private final PackagePieceIdentifier.ForBuildFile identifier;
    // Can be changed during BUILD file evaluation due to exports_files() modifying its visibility.
    // Cannot be in declarations because, since it's a Target, it holds a back reference to this
    // PackagePiece.ForBuildFile object.
    private InputFile buildFile;

    @Override
    public PackagePieceIdentifier.ForBuildFile getIdentifier() {
      return identifier;
    }

    @Override
    public String getCanonicalFormDefinedBy() {
      return getMetadata().buildFileLabel().getCanonicalForm();
    }

    @Override
    public String getShortDescription() {
      return String.format("top-level package piece defined by %s", getCanonicalFormDefinedBy());
    }

    /** Returns the InputFile target for this package's BUILD file. */
    public InputFile getBuildFile() {
      return buildFile;
    }

    @Override
    public void checkMacroNamespaceCompliance(Target target) {
      checkArgument(this.equals(target.getPackageoid()), "Target must belong to this packageoid");
      // No-op: no macros to violate.
    }

    private ForBuildFile(PackagePieceIdentifier.ForBuildFile identifier, Metadata metadata) {
      super(metadata, new Declarations.Builder());
      checkArgument(identifier.getPackageIdentifier().equals(metadata.packageIdentifier()));
      this.identifier = identifier;
    }

    /** Creates a new {@link PackagePiece.ForBuildFile.Builder}. */
    // TODO(bazel-team): when JEP 482 ("flexible constructors") is enabled, we can remove this
    // method and use the builder's constructor directly.
    public static Builder newBuilder(
        PackageSettings packageSettings,
        PackagePieceIdentifier.ForBuildFile identifier,
        RootedPath filename,
        String workspaceName,
        Optional<String> associatedModuleName,
        Optional<String> associatedModuleVersion,
        boolean noImplicitFileExport,
        boolean simplifyUnconditionalSelectsInRuleAttrs,
        boolean symbolicMacroStrictAttrs,
        RepositoryMapping repositoryMapping,
        RepositoryMapping mainRepositoryMapping,
        @Nullable Semaphore cpuBoundSemaphore,
        PackageOverheadEstimator packageOverheadEstimator,
        @Nullable ImmutableMap<Location, String> generatorMap,
        @Nullable ConfigSettingVisibilityPolicy configSettingVisibilityPolicy,
        @Nullable Globber globber,
        boolean enableNameConflictChecking,
        boolean trackFullMacroInformation,
        PackageValidator packageValidator,
        PackageLimits packageLimits) {
      Metadata metadata =
          Metadata.builder()
              .packageIdentifier(identifier.getPackageIdentifier())
              .buildFilename(filename)
              .workspaceName(workspaceName)
              .repositoryMapping(repositoryMapping)
              .associatedModuleName(associatedModuleName)
              .associatedModuleVersion(associatedModuleVersion)
              .configSettingVisibilityPolicy(configSettingVisibilityPolicy)
              .succinctTargetNotFoundErrors(packageSettings.succinctTargetNotFoundErrors())
              .build();
      ForBuildFile forBuildFile = new ForBuildFile(identifier, metadata);
      return new Builder(
          forBuildFile,
          packageSettings.precomputeTransitiveLoads(),
          noImplicitFileExport,
          simplifyUnconditionalSelectsInRuleAttrs,
          symbolicMacroStrictAttrs,
          mainRepositoryMapping,
          cpuBoundSemaphore,
          packageOverheadEstimator,
          generatorMap,
          globber,
          enableNameConflictChecking,
          trackFullMacroInformation,
          packageValidator,
          packageLimits);
    }

    /** A builder for {@link PackagePiece.ForBuildFile} objects. */
    public static class Builder extends Package.AbstractBuilder {

      /** Retrieves this object from a Starlark thread. Returns null if not present. */
      @Nullable
      public static Builder fromOrNull(StarlarkThread thread) {
        StarlarkThreadContext ctx = thread.getThreadLocal(StarlarkThreadContext.class);
        return ctx instanceof Builder builder ? builder : null;
      }

      public PackagePiece.ForBuildFile getPackagePiece() {
        return (PackagePiece.ForBuildFile) pkg;
      }

      @Override
      @CanIgnoreReturnValue
      public Builder setLoads(Iterable<Module> directLoads) {
        return (Builder) super.setLoads(directLoads);
      }

      @Override
      public boolean eagerlyExpandMacros() {
        return false;
      }

      @Override
      @CanIgnoreReturnValue
      public Builder buildPartial() throws NoSuchPackageException {
        return (Builder) super.buildPartial();
      }

      @Override
      protected void setBuildFile(InputFile buildFile) {
        ((ForBuildFile) pkg).buildFile = checkNotNull(buildFile);
      }

      @Override
      public ForBuildFile finishBuild() {
        return (ForBuildFile) super.finishBuild();
      }

      @Override
      protected void packageoidInitializationHook() {
        super.packageoidInitializationHook();
        getPackagePiece().computationSteps = getComputationSteps();
        getPackagePiece().setMacrosByName(recorder.getMacroMap().values());
      }

      private Builder(
          ForBuildFile forBuildFile,
          boolean precomputeTransitiveLoads,
          boolean noImplicitFileExport,
          boolean simplifyUnconditionalSelectsInRuleAttrs,
          boolean symbolicMacroStrictAttrs,
          RepositoryMapping mainRepositoryMapping,
          @Nullable Semaphore cpuBoundSemaphore,
          PackageOverheadEstimator packageOverheadEstimator,
          @Nullable ImmutableMap<Location, String> generatorMap,
          @Nullable Globber globber,
          boolean enableNameConflictChecking,
          boolean trackFullMacroInformation,
          PackageValidator packageValidator,
          PackageLimits packageLimits) {
        super(
            forBuildFile.getMetadata(),
            forBuildFile,
            SymbolGenerator.create(forBuildFile.getIdentifier()),
            precomputeTransitiveLoads,
            noImplicitFileExport,
            simplifyUnconditionalSelectsInRuleAttrs,
            symbolicMacroStrictAttrs,
            mainRepositoryMapping,
            cpuBoundSemaphore,
            packageOverheadEstimator,
            generatorMap,
            globber,
            enableNameConflictChecking,
            trackFullMacroInformation,
            /* enableTargetMapSnapshotting= */ false,
            packageValidator,
            packageLimits);
      }
    }
  }

  /** A {@link PackagePiece} obtained by evaluating a symbolic macro instance. */
  public static final class ForMacro extends PackagePiece {
    private final PackagePieceIdentifier.ForMacro identifier;
    private final MacroInstance evaluatedMacro;
    // Null until the package piece is fully initialized by its builder's {@code finishBuild()}.
    @Nullable private ImmutableSet<String> macroNamespaceViolations = null;

    @Override
    public PackagePieceIdentifier.ForMacro getIdentifier() {
      return identifier;
    }

    @Override
    public String getCanonicalFormDefinedBy() {
      MacroClass macroClass = evaluatedMacro.getMacroClass();
      return String.format(
          "%s%%%s", macroClass.getDefiningBzlLabel().getCanonicalForm(), macroClass.getName());
    }

    @Override
    public String getShortDescription() {
      return String.format(
          "package piece for %smacro %s defined by %s",
          getEvaluatedMacro().getMacroClass().isFinalizer() ? "finalizer " : "",
          getIdentifier().getCanonicalFormName(),
          getCanonicalFormDefinedBy());
    }

    public MacroInstance getEvaluatedMacro() {
      return evaluatedMacro;
    }

    /**
     * Returns the ID of the package of the .bzl file declaring the macro which was expanded to
     * produce this package piece; it is considered to be the location in which this package piece's
     * targets are declared for visibility purposes.
     */
    public PackageIdentifier getDeclaringPackage() {
      return evaluatedMacro.getMacroClass().getDefiningBzlLabel().getPackageIdentifier();
    }

    @Override
    public void checkMacroNamespaceCompliance(Target target)
        throws MacroNamespaceViolationException {
      checkArgument(this.equals(target.getPackageoid()), "Target must belong to this packageoid");
      checkNotNull(
          macroNamespaceViolations,
          "This method is only available after the package piece has been fully initialized.");
      if (macroNamespaceViolations.contains(target.getName())) {
        throw new MacroNamespaceViolationException(
            String.format(
                "Target %s declared in symbolic macro '%s' violates macro naming rules and cannot"
                    + " be built. %s",
                target.getLabel(), evaluatedMacro.getName(), TargetRecorder.MACRO_NAMING_RULES),
            target);
      }
    }

    private static void checkIdentifierMatchesMacro(
        PackagePieceIdentifier.ForMacro identifier, MacroInstance macro) {
      checkArgument(
          macro.getPackageMetadata().packageIdentifier().equals(identifier.getPackageIdentifier()));
      checkArgument(macro.getName().equals(identifier.getInstanceName()));
    }

    private ForMacro(
        Metadata metadata,
        Declarations declarations,
        MacroInstance evaluatedMacro,
        PackagePieceIdentifier parentIdentifier) {
      super(metadata, declarations.checkImmutable());
      checkArgument(
          metadata
              .packageIdentifier()
              .equals(evaluatedMacro.getPackageMetadata().packageIdentifier()));
      checkArgument(metadata.packageIdentifier().equals(parentIdentifier.getPackageIdentifier()));
      if (evaluatedMacro.getParent() != null) {
        checkIdentifierMatchesMacro(
            (PackagePieceIdentifier.ForMacro) parentIdentifier, evaluatedMacro.getParent());
      } else {
        checkArgument(parentIdentifier instanceof PackagePieceIdentifier.ForBuildFile);
      }
      this.identifier =
          new PackagePieceIdentifier.ForMacro(
              metadata.packageIdentifier(), parentIdentifier, evaluatedMacro.getName());
      this.evaluatedMacro = evaluatedMacro;
    }

    /** Creates a new {@link PackagePiece.ForMacro.Builder}. */
    // TODO(bazel-team): when JEP 482 ("flexible constructors") is enabled, we can remove this
    // method and use the builder's constructor directly.
    public static Builder newBuilder(
        Metadata metadata,
        Declarations declarations,
        MacroInstance evaluatedMacro,
        PackagePieceIdentifier parentIdentifier,
        boolean simplifyUnconditionalSelectsInRuleAttrs,
        boolean symbolicMacroStrictAttrs,
        RepositoryMapping mainRepositoryMapping,
        @Nullable Semaphore cpuBoundSemaphore,
        PackageOverheadEstimator packageOverheadEstimator,
        boolean enableNameConflictChecking,
        boolean trackFullMacroInformation,
        PackageLimits packageLimits,
        @Nullable ImmutableMap<String, Rule> existingRulesMapForFinalizer) {
      ForMacro forMacro = new ForMacro(metadata, declarations, evaluatedMacro, parentIdentifier);
      return new Builder(
          forMacro,
          simplifyUnconditionalSelectsInRuleAttrs,
          symbolicMacroStrictAttrs,
          mainRepositoryMapping,
          cpuBoundSemaphore,
          packageOverheadEstimator,
          enableNameConflictChecking,
          trackFullMacroInformation,
          packageLimits,
          existingRulesMapForFinalizer);
    }

    /** A builder for {@link PackagePieceForMacro} objects. */
    public static class Builder extends TargetDefinitionContext {
      // Non-null iff this is a builder for a finalizer package piece and the non-finalizer package
      // pieces that it depends upon are not in error. Used for native.existing_rules() and
      // native.existing_rule().
      @Nullable private final ImmutableMap<String, Rule> existingRulesMapForFinalizer;

      /** Retrieves this object from a Starlark thread. Returns null if not present. */
      @Nullable
      public static Builder fromOrNull(StarlarkThread thread) {
        StarlarkThreadContext ctx = thread.getThreadLocal(StarlarkThreadContext.class);
        return ctx instanceof Builder builder ? builder : null;
      }

      public PackagePiece.ForMacro getPackagePiece() {
        return (PackagePiece.ForMacro) pkg;
      }

      @Override
      public boolean eagerlyExpandMacros() {
        return false;
      }

      /** Can only be called for a finalizer package piece. */
      @Override
      Map<String, Rule> getRulesSnapshotView() {
        checkState(
            getPackagePiece().getEvaluatedMacro().getMacroClass().isFinalizer(),
            "%s is defined by a non-finalizer macro",
            getPackagePiece().getShortDescription());
        return checkNotNull(
            existingRulesMapForFinalizer,
            "native.existing_rules map was not set in builder for %s",
            getPackagePiece().getShortDescription());
      }

      /** Can only be called for a finalizer package piece. */
      @Nullable
      @Override
      Rule getNonFinalizerInstantiatedRule(String name) {
        return getRulesSnapshotView().get(name);
      }

      @Override
      @CanIgnoreReturnValue
      public Builder buildPartial() throws NoSuchPackageException {
        return (Builder) super.buildPartial();
      }

      @Override
      public ForMacro finishBuild() {
        return (ForMacro) super.finishBuild();
      }

      @Override
      protected void packageoidInitializationHook() {
        getPackagePiece().computationSteps = getComputationSteps();
        super.packageoidInitializationHook();
        ForMacro forMacro = getPackagePiece();
        forMacro.setMacrosByName(recorder.getMacroMap().values());
        forMacro.macroNamespaceViolations =
            ImmutableSet.copyOf(recorder.getMacroNamespaceViolatingTargets().keySet());
      }

      private Builder(
          ForMacro forMacro,
          boolean simplifyUnconditionalSelectsInRuleAttrs,
          boolean symbolicMacroStrictAttrs,
          RepositoryMapping mainRepositoryMapping,
          @Nullable Semaphore cpuBoundSemaphore,
          PackageOverheadEstimator packageOverheadEstimator,
          boolean enableNameConflictChecking,
          boolean trackFullMacroInformation,
          PackageLimits packageLimits,
          @Nullable ImmutableMap<String, Rule> existingRulesMapForFinalizer) {
        super(
            forMacro.getMetadata(),
            forMacro,
            SymbolGenerator.create(forMacro.getIdentifier()),
            simplifyUnconditionalSelectsInRuleAttrs,
            symbolicMacroStrictAttrs,
            mainRepositoryMapping,
            cpuBoundSemaphore,
            packageOverheadEstimator,
            /* generatorMap= */ null,
            /* globber= */ null,
            enableNameConflictChecking,
            trackFullMacroInformation,
            /* enableTargetMapSnapshotting= */ false,
            packageLimits);
        this.existingRulesMapForFinalizer = existingRulesMapForFinalizer;
      }
    }
  }
}
