// Copyright 2014 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.cmdline;

import com.google.common.base.Preconditions;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Interner;
import com.google.devtools.build.lib.actions.CommandLineItem;
import com.google.devtools.build.lib.cmdline.LabelValidator.BadLabelException;
import com.google.devtools.build.lib.concurrent.BlazeInterners;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.skylarkinterface.Param;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
import com.google.devtools.build.lib.util.StringUtilities;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.skyframe.SkyFunctionName;
import com.google.devtools.build.skyframe.SkyKey;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Arrays;
import javax.annotation.Nullable;

/**
 * A class to identify a BUILD target. All targets belong to exactly one package. The name of a
 * target is called its label. A typical label looks like this: //dir1/dir2:target_name where
 * 'dir1/dir2' identifies the package containing a BUILD file, and 'target_name' identifies the
 * target within the package.
 *
 * <p>Parsing is robust against bad input, for example, from the command line.
 */
@SkylarkModule(
  name = "Label",
  category = SkylarkModuleCategory.BUILTIN,
  doc = "A BUILD target identifier."
)
@AutoCodec
@Immutable
@ThreadSafe
public final class Label
    implements Comparable<Label>, Serializable, SkylarkValue, SkyKey, CommandLineItem {
  public static final PathFragment EXTERNAL_PACKAGE_NAME = PathFragment.create("external");
  public static final PathFragment WORKSPACE_FILE_NAME = PathFragment.create("WORKSPACE");
  public static final String DEFAULT_REPOSITORY_DIRECTORY = "__main__";

  /**
   * Package names that aren't made relative to the current repository because they mean special
   * things to Bazel.
   */
  public static final ImmutableSet<PathFragment> ABSOLUTE_PACKAGE_NAMES =
      ImmutableSet.of(
          // Used for select
          PathFragment.create("conditions"),
          // dependencies that are a function of the configuration
          PathFragment.create("tools/defaults"),
          // Visibility is labels aren't actually targets
          PathFragment.create("visibility"),
          // There is only one //external package
          Label.EXTERNAL_PACKAGE_NAME);

  public static final PackageIdentifier EXTERNAL_PACKAGE_IDENTIFIER =
      PackageIdentifier.createInMainRepo(EXTERNAL_PACKAGE_NAME);

  public static final PathFragment EXTERNAL_PATH_PREFIX = PathFragment.create("external");
  public static final SkyFunctionName TRANSITIVE_TRAVERSAL =
      SkyFunctionName.createHermetic("TRANSITIVE_TRAVERSAL");

  private static final Interner<Label> LABEL_INTERNER = BlazeInterners.newWeakInterner();

  /**
   * Factory for Labels from absolute string form. e.g.
   *
   * <pre>
   * //foo/bar
   * //foo/bar:quux
   * {@literal @}foo
   * {@literal @}foo//bar
   * {@literal @}foo//bar:baz
   * </pre>
   *
   * <p>Treats labels in the default repository as being in the main repository instead.
   *
   * <p>Labels that begin with a repository name may have the repository name remapped to a
   * different name if it appears in {@code repositoryMapping}. This happens if the current
   * repository being evaluated is external to the main repository and the main repository set the
   * {@code repo_mapping} attribute when declaring this repository.
   *
   * @param absName label-like string to be parsed
   * @param repositoryMapping map of repository names from the local name found in the current
   *     repository to the global name declared in the main repository
   */
  public static Label parseAbsolute(
      String absName, ImmutableMap<RepositoryName, RepositoryName> repositoryMapping)
      throws LabelSyntaxException {
    return parseAbsolute(
        absName, /* defaultToMain= */ true, repositoryMapping);
  }

  /**
   * Factory for Labels from absolute string form. e.g.
   *
   * <pre>
   * //foo/bar
   * //foo/bar:quux
   * {@literal @}foo
   * {@literal @}foo//bar
   * {@literal @}foo//bar:baz
   * </pre>
   *
   * <p>Labels that begin with a repository name may have the repository name remapped to a
   * different name if it appears in {@code repositoryMapping}. This happens if the current
   * repository being evaluated is external to the main repository and the main repository set the
   * {@code repo_mapping} attribute when declaring this repository.
   *
   * @param absName label-like string to be parsed
   * @param defaultToMain Treat labels in the default repository as being in the main one instead.
   * @param repositoryMapping map of repository names from the local name found in the current
   *     repository to the global name declared in the main repository
   */
  public static Label parseAbsolute(
      String absName,
      boolean defaultToMain,
      ImmutableMap<RepositoryName, RepositoryName> repositoryMapping)
      throws LabelSyntaxException {
    Preconditions.checkNotNull(repositoryMapping);
    String repo = defaultToMain ? "@" : RepositoryName.DEFAULT_REPOSITORY;
    int packageStartPos = absName.indexOf("//");
    if (packageStartPos > 0) {
      repo = absName.substring(0, packageStartPos);
      absName = absName.substring(packageStartPos);
    } else if (absName.startsWith("@")) {
      repo = absName;
      absName = "//:" + absName.substring(1);
    }
    String error = RepositoryName.validate(repo);
    if (error != null) {
      throw new LabelSyntaxException(
          "invalid repository name '" + StringUtilities.sanitizeControlChars(repo) + "': " + error);
    }
    try {
      LabelValidator.PackageAndTarget labelParts = LabelValidator.parseAbsoluteLabel(absName);
      PackageIdentifier pkgId =
          validatePackageName(
              labelParts.getPackageName(), labelParts.getTargetName(), repo, repositoryMapping);
      PathFragment packageFragment = pkgId.getPackageFragment();
      if (repo.isEmpty() && ABSOLUTE_PACKAGE_NAMES.contains(packageFragment)) {
        pkgId =
            PackageIdentifier.create(getGlobalRepoName("@", repositoryMapping), packageFragment);
      }
      return create(pkgId, labelParts.getTargetName());
    } catch (BadLabelException e) {
      throw new LabelSyntaxException(e.getMessage());
    }
  }

  private static RepositoryName getGlobalRepoName(
      String repo, ImmutableMap<RepositoryName, RepositoryName> repositoryMapping)
      throws LabelSyntaxException {
    Preconditions.checkNotNull(repositoryMapping);
    RepositoryName repoName = RepositoryName.create(repo);
    return repositoryMapping.getOrDefault(repoName, repoName);
  }

  /**
   * Alternate factory method for Labels from absolute strings. This is a convenience method for
   * cases when a Label needs to be initialized statically, so the declared exception is
   * inconvenient.
   *
   * <p>Do not use this when the argument is not hard-wired.
   */
  @Deprecated
  // TODO(b/110698008): create parseAbsoluteUnchecked that passes repositoryMapping
  public static Label parseAbsoluteUnchecked(String absName, boolean defaultToMain) {
    try {
      return parseAbsolute(absName, defaultToMain, /* repositoryMapping= */ ImmutableMap.of());
    } catch (LabelSyntaxException e) {
      throw new IllegalArgumentException(e);
    }
  }

  public static Label parseAbsoluteUnchecked(String absName) {
    return parseAbsoluteUnchecked(absName, true);
  }

  /**
   * Factory for Labels from separate components.
   *
   * @param packageName The name of the package. The package name does <b>not</b> include {@code
   *     //}. Must be valid according to {@link LabelValidator#validatePackageName}.
   * @param targetName The name of the target within the package. Must be valid according to {@link
   *     LabelValidator#validateTargetName}.
   * @throws LabelSyntaxException if either of the arguments was invalid.
   */
  public static Label create(String packageName, String targetName) throws LabelSyntaxException {
    return create(validatePackageName(packageName, targetName), targetName);
  }

  /**
   * Similar factory to above, but takes a package identifier to allow external repository labels to
   * be created.
   */
  public static Label create(PackageIdentifier packageId, String targetName)
      throws LabelSyntaxException {
    return createUnvalidated(packageId, validateTargetName(packageId, targetName));
  }

  /**
   * Similar factory to above, but does not perform target name validation.
   *
   * <p>Only call this method if you know what you're doing; in particular, don't call it on
   * arbitrary {@code name} inputs
   */
  @AutoCodec.Instantiator
  public static Label createUnvalidated(PackageIdentifier packageIdentifier, String name) {
    return LABEL_INTERNER.intern(new Label(packageIdentifier, name));
  }

  /**
   * Resolves a relative label using a workspace-relative path to the current working directory. The
   * method handles these cases:
   *
   * <ul>
   *   <li>The label is absolute.
   *   <li>The label starts with a colon.
   *   <li>The label consists of a relative path, a colon, and a local part.
   *   <li>The label consists only of a local part.
   * </ul>
   *
   * <p>Note that this method does not support any of the special syntactic constructs otherwise
   * supported on the command line, like ":all", "/...", and so on.
   *
   * <p>It would be cleaner to use the TargetPatternEvaluator for this resolution, but that is not
   * possible, because it is sometimes necessary to resolve a relative label before the package path
   * is setup; in particular, before the tools/defaults package is created.
   *
   * @throws LabelSyntaxException if the resulting label is not valid
   */
  public static Label parseCommandLineLabel(String label, PathFragment workspaceRelativePath)
      throws LabelSyntaxException {
    Preconditions.checkArgument(!workspaceRelativePath.isAbsolute());
    if (LabelValidator.isAbsolute(label)) {
      return parseAbsolute(label, ImmutableMap.of());
    }
    int index = label.indexOf(':');
    if (index < 0) {
      index = 0;
      label = ":" + label;
    }
    PathFragment path = workspaceRelativePath.getRelative(label.substring(0, index));
    // Use the String, String constructor, to make sure that the package name goes through the
    // validity check.
    return create(path.getPathString(), label.substring(index + 1));
  }

  /**
   * Validates the given target name and returns a normalized name if it is valid. Otherwise it
   * throws a SyntaxException.
   */
  private static String validateTargetName(PackageIdentifier packageIdentifier, String name)
      throws LabelSyntaxException {
    String error = LabelValidator.validateTargetName(name);
    if (error != null) {
      error = "invalid target name '" + StringUtilities.sanitizeControlChars(name) + "': " + error;
      if (packageIdentifier.getPackageFragment().getPathString().endsWith("/" + name)) {
        error += " (perhaps you meant \":" + name + "\"?)";
      }
      throw new LabelSyntaxException(error);
    }

    // TODO(bazel-team): This should be an error, but we can't make it one for legacy reasons.
    if (name.endsWith("/.")) {
      name = name.substring(0, name.length() - 2);
    }
    return name;
  }

  private static PackageIdentifier validatePackageName(String packageIdentifier, String name)
      throws LabelSyntaxException {
    return validatePackageName(
        packageIdentifier, name, /* repo= */ null, /* repositoryMapping= */ null);
  }

  /**
   * Validates the given package name and returns a canonical {@link PackageIdentifier} instance if
   * it is valid. Otherwise it throws a SyntaxException.
   */
  private static PackageIdentifier validatePackageName(
      String packageIdentifier,
      String name,
      String repo,
      ImmutableMap<RepositoryName, RepositoryName> repositoryMapping)
      throws LabelSyntaxException {
    String error = null;
    try {
      return PackageIdentifier.parse(packageIdentifier, repo, repositoryMapping);
    } catch (LabelSyntaxException e) {
      error = e.getMessage();
      error = "invalid package name '" + packageIdentifier + "': " + error;
      // This check is just for a more helpful error message
      // i.e. valid target name, invalid package name, colon-free label form
      // used => probably they meant "//foo:bar.c" not "//foo/bar.c".
      if (packageIdentifier.endsWith("/" + name)) {
        error += " (perhaps you meant \":" + name + "\"?)";
      }
      throw new LabelSyntaxException(error);
    }
  }

  /** The name and repository of the package. */
  private final PackageIdentifier packageIdentifier;

  /** The name of the target within the package. Canonical. */
  private final String name;

  private Label(PackageIdentifier packageIdentifier, String name) {
    Preconditions.checkNotNull(packageIdentifier);
    Preconditions.checkNotNull(name);

    this.packageIdentifier = packageIdentifier;
    this.name = name;
  }

  private Object writeReplace() {
    return new LabelSerializationProxy(getUnambiguousCanonicalForm());
  }

  private void readObject(ObjectInputStream unusedStream) throws InvalidObjectException {
    throw new InvalidObjectException("Serialization is allowed only by proxy");
  }

  public PackageIdentifier getPackageIdentifier() {
    return packageIdentifier;
  }

  /**
   * Returns the name of the package in which this rule was declared (e.g. {@code
   * //file/base:fileutils_test} returns {@code file/base}).
   */
  @SkylarkCallable(
    name = "package",
    structField = true,
    doc =
        "The package part of this label. "
            + "For instance:<br>"
            + "<pre class=language-python>Label(\"//pkg/foo:abc\").package == \"pkg/foo\"</pre>"
  )
  public String getPackageName() {
    return packageIdentifier.getPackageFragment().getPathString();
  }

  /**
   * Returns the execution root for the workspace, relative to the execroot (e.g., for label
   * {@code @repo//pkg:b}, it will returns {@code external/repo/pkg} and for label {@code //pkg:a},
   * it will returns an empty string.
   */
  @SkylarkCallable(
    name = "workspace_root",
    structField = true,
    doc =
        "Returns the execution root for the workspace of this label, relative to the execroot. "
            + "For instance:<br>"
            + "<pre class=language-python>Label(\"@repo//pkg/foo:abc\").workspace_root =="
            + " \"external/repo\"</pre>"
  )
  public String getWorkspaceRoot() {
    return packageIdentifier.getRepository().getSourceRoot().toString();
  }

  /**
   * Returns the path fragment of the package in which this rule was declared (e.g. {@code
   * //file/base:fileutils_test} returns {@code file/base}).
   *
   * <p>This is <b>not</b> suitable for inferring a path under which files related to a rule with
   * this label will be under the exec root, in particular, it won't work for rules in external
   * repositories.
   */
  public PathFragment getPackageFragment() {
    return packageIdentifier.getPackageFragment();
  }

  /**
   * Returns the label as a path fragment, using the package and the label name.
   *
   * <p>Make sure that the label refers to a file. Non-file labels do not necessarily have
   * PathFragment representations.
   */
  public PathFragment toPathFragment() {
    // PathFragments are normalized, so if we do this on a non-file target named '.'
    // then the package would be returned. Detect this and throw.
    // A target named '.' can never refer to a file.
    Preconditions.checkArgument(!name.equals("."));
    return packageIdentifier.getPackageFragment().getRelative(name);
  }

  /**
   * Returns the name by which this rule was declared (e.g. {@code //foo/bar:baz} returns {@code
   * baz}).
   */
  @SkylarkCallable(
    name = "name",
    structField = true,
    doc =
        "The name of this label within the package. "
            + "For instance:<br>"
            + "<pre class=language-python>Label(\"//pkg/foo:abc\").name == \"abc\"</pre>"
  )
  public String getName() {
    return name;
  }

  /**
   * Renders this label in canonical form.
   *
   * <p>invariant: {@code parseAbsolute(x.toString(), false).equals(x)}
   */
  @Override
  public String toString() {
    return getCanonicalForm();
  }

  /**
   * Renders this label in canonical form.
   *
   * <p>invariant: {@code parseAbsolute(x.getCanonicalForm(), false).equals(x)}
   */
  public String getCanonicalForm() {
    return getDefaultCanonicalForm();
  }

  public String getUnambiguousCanonicalForm() {
    return packageIdentifier.getRepository()
        + "//"
        + packageIdentifier.getPackageFragment()
        + ":"
        + name;
  }

  /**
   * Renders this label in canonical form, except with labels in the main and default repositories
   * conflated.
   */
  public String getDefaultCanonicalForm() {
    String repository;
    if (packageIdentifier.getRepository().isMain()) {
      repository = "";
    } else {
      repository = packageIdentifier.getRepository().getName();
    }
    return repository + "//" + packageIdentifier.getPackageFragment() + ":" + name;
  }

  /**
   * Renders this label in shorthand form.
   *
   * <p>Labels with canonical form {@code //foo/bar:bar} have the shorthand form {@code //foo/bar}.
   * All other labels have identical shorthand and canonical forms.
   */
  public String toShorthandString() {
    if (!getPackageFragment().getBaseName().equals(name)) {
      return toString();
    }
    String repository;
    if (packageIdentifier.getRepository().isMain()) {
      repository = "";
    } else {
      repository = packageIdentifier.getRepository().getName();
    }
    return repository + "//" + getPackageFragment();
  }

  /**
   * Returns a label in the same package as this label with the given target name.
   *
   * @throws LabelSyntaxException if {@code targetName} is not a valid target name
   */
  public Label getLocalTargetLabel(String targetName) throws LabelSyntaxException {
    return create(packageIdentifier, targetName);
  }

  /**
   * Resolves a relative or absolute label name. If given name is absolute, then this method calls
   * {@link #parseAbsolute}. Otherwise, it calls {@link #getLocalTargetLabel}.
   *
   * <p>For example: {@code :quux} relative to {@code //foo/bar:baz} is {@code //foo/bar:quux};
   * {@code //wiz:quux} relative to {@code //foo/bar:baz} is {@code //wiz:quux}.
   *
   * @param relName the relative label name; must be non-empty.
   */
  @SkylarkCallable(
    name = "relative",
    doc =
        "Resolves a label that is either absolute (starts with <code>//</code>) or relative to the"
            + " current package. If this label is in a remote repository, the argument will be "
            + " resolved relative to that repository. If the argument contains a repository, it"
            + " will be returned as-is. Reserved labels will also be returned as-is.<br>"
            + "For example:<br>"
            + "<pre class=language-python>\n"
            + "Label(\"//foo/bar:baz\").relative(\":quux\") == Label(\"//foo/bar:quux\")\n"
            + "Label(\"//foo/bar:baz\").relative(\"//wiz:quux\") == Label(\"//wiz:quux\")\n"
            + "Label(\"@repo//foo/bar:baz\").relative(\"//wiz:quux\") == "
            + "Label(\"@repo//wiz:quux\")\n"
            + "Label(\"@repo//foo/bar:baz\").relative(\"//visibility:public\") == "
            + "Label(\"//visibility:public\")\n"
            + "Label(\"@repo//foo/bar:baz\").relative(\"@other//wiz:quux\") == "
            + "Label(\"@other//wiz:quux\")\n"
            + "</pre>",
    parameters = {
      @Param(
        name = "relName",
        type = String.class,
        doc = "The label that will be resolved relative to this one."
      )
    }
  )
  public Label getRelative(String relName) throws LabelSyntaxException {
    return getRelativeWithRemapping(relName, /* repositoryMapping= */ ImmutableMap.of());
  }

  /**
   * Resolves a relative or absolute label name. If given name is absolute, then this method calls
   * {@link #parseAbsolute}. Otherwise, it calls {@link #getLocalTargetLabel}.
   *
   * <p>For example: {@code :quux} relative to {@code //foo/bar:baz} is {@code //foo/bar:quux};
   * {@code //wiz:quux} relative to {@code //foo/bar:baz} is {@code //wiz:quux};
   * {@code @repo//foo:bar} relative to anything will be {@code @repo//foo:bar} if {@code @repo} is
   * not in {@code repositoryMapping} but will be {@code @other_repo//foo:bar} if there is an entry
   * {@code @repo -> @other_repo} in {@code repositoryMapping}
   *
   * @param relName the relative label name; must be non-empty
   * @param repositoryMapping the map of local repository names in external repository to global
   *     repository names in main repo; can be empty, but not null
   */
  public Label getRelativeWithRemapping(
      String relName, ImmutableMap<RepositoryName, RepositoryName> repositoryMapping)
      throws LabelSyntaxException {
    Preconditions.checkNotNull(repositoryMapping);
    if (relName.length() == 0) {
      throw new LabelSyntaxException("empty package-relative label");
    }

    if (LabelValidator.isAbsolute(relName)) {
      return resolveRepositoryRelative(parseAbsolute(relName, false, repositoryMapping));
    } else if (relName.equals(":")) {
      throw new LabelSyntaxException("':' is not a valid package-relative label");
    } else if (relName.charAt(0) == ':') {
      return getLocalTargetLabel(relName.substring(1));
    } else {
      return getLocalTargetLabel(relName);
    }
  }

  /**
   * Resolves the repository of a label in the context of another label.
   *
   * <p>This is necessary so that dependency edges in remote repositories do not need to explicitly
   * mention their repository name. Otherwise, referring to e.g. <code>//a:b</code> in a remote
   * repository would point back to the main repository, which is usually not what is intended.
   *
   * <p>The return value will not be in the default repository.
   */
  public Label resolveRepositoryRelative(Label relative) {
    if (packageIdentifier.getRepository().isDefault()
        || !relative.packageIdentifier.getRepository().isDefault()) {
      return relative;
    } else {
      try {
        return create(
            PackageIdentifier.create(
                packageIdentifier.getRepository(), relative.getPackageFragment()),
            relative.getName());
      } catch (LabelSyntaxException e) {
        // We are creating the new label from an existing one which is guaranteed to be valid, so
        // this can't happen
        throw new IllegalStateException(e);
      }
    }
  }

  @Override
  public SkyFunctionName functionName() {
    return TRANSITIVE_TRAVERSAL;
  }

  @Override
  public int hashCode() {
    return hashCode(name, packageIdentifier);
  }

  /** Two labels are equal iff both their name and their package name are equal. */
  @Override
  public boolean equals(Object other) {
    if (!(other instanceof Label)) {
      return false;
    }
    Label otherLabel = (Label) other;
    // Package identifiers are interned so we compare them first.
    return packageIdentifier.equals(otherLabel.packageIdentifier) && name.equals(otherLabel.name);
  }

  /**
   * Defines the order between labels.
   *
   * <p>Labels are ordered primarily by package name and secondarily by target name. Both components
   * are ordered lexicographically. Thus {@code //a:b/c} comes before {@code //a/b:a}, i.e. the
   * position of the colon is significant to the order.
   */
  @Override
  public int compareTo(Label other) {
    return ComparisonChain.start()
        .compare(packageIdentifier, other.packageIdentifier)
        .compare(name, other.name)
        .result();
  }

  /**
   * Returns a suitable string for the user-friendly representation of the Label. Works even if the
   * argument is null.
   */
  public static String print(@Nullable Label label) {
    return label == null ? "(unknown)" : label.toString();
  }

  @Override
  public boolean isImmutable() {
    return true;
  }

  @Override
  public void repr(SkylarkPrinter printer) {
    printer.append("Label(");
    printer.repr(getCanonicalForm());
    printer.append(")");
  }

  @Override
  public void str(SkylarkPrinter printer) {
    printer.append(getCanonicalForm());
  }

  @Override
  public String expandToCommandLine() {
    return getCanonicalForm();
  }

  /**
   * Specialization of {@link Arrays#hashCode()} that does not require constructing a 2-element
   * array.
   */
  private static final int hashCode(Object obj1, Object obj2) {
    int result = 31 + (obj1 == null ? 0 : obj1.hashCode());
    return 31 * result + (obj2 == null ? 0 : obj2.hashCode());
  }
}
