// 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.skyframe;

import com.google.devtools.build.lib.actions.InconsistentFilesystemException;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.NoSuchTargetException;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import javax.annotation.Nullable;

/**
 * A SkyFunction for {@link TargetMarkerValue}s. Returns a {@link
 * TargetMarkerValue#TARGET_MARKER_INSTANCE} if the {@link Label} in the {@link SkyKey}
 * specifies a {@link Package} that exists and a {@link Target} that exists in that package. The
 * package may have errors.
 */
public final class TargetMarkerFunction implements SkyFunction {

  @Override
  public SkyValue compute(SkyKey key, Environment env)
      throws TargetMarkerFunctionException, InterruptedException {
    try {
      return computeTargetMarkerValue(key, env);
    } catch (NoSuchTargetException e) {
      throw new TargetMarkerFunctionException(e);
    } catch (NoSuchPackageException e) {
      // Re-throw this exception with our key because root causes should be targets, not packages.
      throw new TargetMarkerFunctionException(e);
    }
  }

  @Nullable
  static TargetMarkerValue computeTargetMarkerValue(SkyKey key, Environment env)
      throws NoSuchTargetException, NoSuchPackageException, InterruptedException {
    Label label = (Label) key.argument();
    PathFragment pkgForLabel = label.getPackageFragment();

    if (label.getName().contains("/")) {
      // This target is in a subdirectory, therefore it could potentially be invalidated by
      // a new BUILD file appearing in the hierarchy.
      PathFragment containingDirectory = getContainingDirectory(label);
      ContainingPackageLookupValue containingPackageLookupValue;
      try {
        PackageIdentifier newPkgId = PackageIdentifier.create(
            label.getPackageIdentifier().getRepository(), containingDirectory);
        containingPackageLookupValue = (ContainingPackageLookupValue) env.getValueOrThrow(
            ContainingPackageLookupValue.key(newPkgId),
            BuildFileNotFoundException.class, InconsistentFilesystemException.class);
      } catch (InconsistentFilesystemException e) {
        throw new NoSuchTargetException(label, e.getMessage());
      }
      if (containingPackageLookupValue == null) {
        return null;
      }

      if (!containingPackageLookupValue.hasContainingPackage()) {
        // This means the label's package doesn't exist. E.g. there is no package 'a' and we are
        // trying to build the target for label 'a:b/foo'.
        throw new BuildFileNotFoundException(
            label.getPackageIdentifier(),
            "BUILD file not found on package path for '" + pkgForLabel.getPathString() + "'");
      }
      if (!containingPackageLookupValue.getContainingPackageName().equals(
              label.getPackageIdentifier())) {
        throw new NoSuchTargetException(
            label,
            String.format(
                "Label '%s' crosses boundary of subpackage '%s'",
                label,
                containingPackageLookupValue.getContainingPackageName()));
      }
    }

    SkyKey pkgSkyKey = PackageValue.key(label.getPackageIdentifier());
    PackageValue value =
        (PackageValue) env.getValueOrThrow(pkgSkyKey, NoSuchPackageException.class);
    if (value == null) {
      return null;
    }

    Package pkg = value.getPackage();
    Target target = pkg.getTarget(label.getName());
    if (pkg.containsErrors()) {
      // There is a target, but its package is in error. We rethrow so that the root cause is the
      // target, not the package. Note that targets are only in error when their package is
      // "in error" (because a package is in error if there was an error evaluating the package, or
      // if one of its targets was in error).
      throw new NoSuchTargetException(target);
    }
    return TargetMarkerValue.TARGET_MARKER_INSTANCE;
  }

  private static PathFragment getContainingDirectory(Label label) {
    PathFragment pkg = label.getPackageFragment();
    String name = label.getName();
    return name.equals(".") ? pkg : pkg.getRelative(name).getParentDirectory();
  }

  @Override
  public String extractTag(SkyKey skyKey) {
    return Label.print((Label) skyKey.argument());
  }

  /**
   * Used to declare all the exception types that can be wrapped in the exception thrown by
   * {@link TargetMarkerFunction#compute}.
   */
  private static final class TargetMarkerFunctionException extends SkyFunctionException {
    public TargetMarkerFunctionException(NoSuchTargetException e) {
      super(e, Transience.PERSISTENT);
    }

    public TargetMarkerFunctionException(NoSuchPackageException e) {
      super(e, Transience.PERSISTENT);
    }
  }
}
