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

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.Pair;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * Utility functions over Targets that don't really belong in the base {@link
 * Target} interface.
 */
public final class TargetUtils {

  // *_test / test_suite attribute that used to specify constraint keywords.
  private static final String CONSTRAINTS_ATTR = "tags";

  private TargetUtils() {} // Uninstantiable.

  public static boolean isTestRuleName(String name) {
    return name.endsWith("_test");
  }

  public static boolean isTestSuiteRuleName(String name) {
    return name.equals("test_suite");
  }

  /**
   * Returns true iff {@code target} is a {@code *_test} rule; excludes {@code
   * test_suite}.
   */
  public static boolean isTestRule(Target target) {
    return (target instanceof Rule) && isTestRuleName(((Rule) target).getRuleClass());
  }

  /**
   * Returns true iff {@code target} is a {@code test_suite} rule.
   */
  public static boolean isTestSuiteRule(Target target) {
    return target instanceof Rule &&
        isTestSuiteRuleName(((Rule) target).getRuleClass());
  }

  /**
   * Returns true iff {@code target} is a {@code *_test} or {@code test_suite}.
   */
  public static boolean isTestOrTestSuiteRule(Target target) {
    return isTestRule (target) || isTestSuiteRule(target);
  }

  /**
   * Returns true if {@code target} has "manual" in the tags attribute and thus should be ignored by
   * command-line wildcards or by test_suite $implicit_tests attribute.
   */
  public static boolean hasManualTag(Target target) {
    return (target instanceof Rule) && hasConstraint((Rule) target, "manual");
  }

  /**
   * Returns true if test marked as "exclusive" by the appropriate keyword
   * in the tags attribute.
   *
   * Method assumes that passed target is a test rule, so usually it should be
   * used only after isTestRule() or isTestOrTestSuiteRule(). Behavior is
   * undefined otherwise.
   */
  public static boolean isExclusiveTestRule(Rule rule) {
    return hasConstraint(rule, "exclusive");
  }

  /**
   * Returns true if test marked as "local" by the appropriate keyword
   * in the tags attribute.
   *
   * Method assumes that passed target is a test rule, so usually it should be
   * used only after isTestRule() or isTestOrTestSuiteRule(). Behavior is
   * undefined otherwise.
   */
  public static boolean isLocalTestRule(Rule rule) {
    return hasConstraint(rule, "local")
        || NonconfigurableAttributeMapper.of(rule).get("local", Type.BOOLEAN);
  }

  /**
   * Returns true if the rule is a test or test suite and is local or exclusive.
   * Wraps the above calls into one generic check safely applicable to any rule.
   */
  public static boolean isTestRuleAndRunsLocally(Rule rule) {
    return isTestOrTestSuiteRule(rule) &&
        (isLocalTestRule(rule) || isExclusiveTestRule(rule));
  }

  /**
   * Returns true if test marked as "external" by the appropriate keyword
   * in the tags attribute.
   *
   * Method assumes that passed target is a test rule, so usually it should be
   * used only after isTestRule() or isTestOrTestSuiteRule(). Behavior is
   * undefined otherwise.
   */
  public static boolean isExternalTestRule(Rule rule) {
    return hasConstraint(rule, "external");
  }


  /**
   * If the given target is a rule, returns its <code>deprecation<code/> value, or null if unset.
   */
  @Nullable
  public static String getDeprecation(Target target) {
    if (!(target instanceof Rule)) {
      return null;
    }
    Rule rule = (Rule) target;
    return (rule.isAttrDefined("deprecation", Type.STRING))
        ? NonconfigurableAttributeMapper.of(rule).get("deprecation", Type.STRING)
        : null;
  }

  /**
   * Checks whether specified constraint keyword is present in the
   * tags attribute of the test or test suite rule.
   *
   * Method assumes that provided rule is a test or a test suite. Behavior is
   * undefined otherwise.
   */
  private static boolean hasConstraint(Rule rule, String keyword) {
    return NonconfigurableAttributeMapper.of(rule).get(CONSTRAINTS_ATTR, Type.STRING_LIST)
        .contains(keyword);
  }

  /**
   * Returns the execution info. These include execution requirement
   * tags ('requires-*' as well as "local") as keys with empty values.
   */
  public static Map<String, String> getExecutionInfo(Rule rule) {
    // tags may contain duplicate values.
    Map<String, String> map = new HashMap<>();
    for (String tag :
        NonconfigurableAttributeMapper.of(rule).get(CONSTRAINTS_ATTR, Type.STRING_LIST)) {
      if (tag.startsWith("block-") || tag.startsWith("requires-") || tag.equals("local")) {
        map.put(tag, "");
      }
    }
    return ImmutableMap.copyOf(map);
  }

  /**
   * Returns the language part of the rule name (e.g. "foo" for foo_test or foo_binary).
   *
   * <p>In practice this is the part before the "_", if any, otherwise the entire rule class name.
   *
   * <p>Precondition: isTestRule(target) || isRunnableNonTestRule(target).
   */
  public static String getRuleLanguage(Target target) {
    return getRuleLanguage(((Rule) target).getRuleClass());
  }

  /**
   * Returns the language part of the rule name (e.g. "foo" for foo_test or foo_binary).
   *
   * <p>In practice this is the part before the "_", if any, otherwise the entire rule class name.
   */
  public static String getRuleLanguage(String ruleClass) {
    int index = ruleClass.lastIndexOf('_');
    // Chop off "_binary" or "_test".
    return index != -1 ? ruleClass.substring(0, index) : ruleClass;
  }

  private static boolean isExplicitDependency(Rule rule, Label label) throws InterruptedException {
    if (rule.getVisibility().getDependencyLabels().contains(label)) {
      return true;
    }

    ExplicitEdgeVisitor visitor = new ExplicitEdgeVisitor(rule, label);
    AggregatingAttributeMapper.of(rule).visitLabels(visitor);
    return visitor.isExplicit();
  }

  /**
   * Returns a predicate to be used for test tag filtering, i.e., that only accepts tests that match
   * all of the required tags and none of the excluded tags.
   */
  public static Predicate<Target> tagFilter(List<String> tagFilterList) {
    Pair<Collection<String>, Collection<String>> tagLists =
        TestTargetUtils.sortTagsBySense(tagFilterList);
    final Collection<String> requiredTags = tagLists.first;
    final Collection<String> excludedTags = tagLists.second;
    return new Predicate<Target>() {
      @Override
      public boolean apply(Target input) {
        if (requiredTags.isEmpty() && excludedTags.isEmpty()) {
          return true;
        }

        if (!(input instanceof Rule)) {
          return false;
        }
        // Note that test_tags are those originating from the XX_test rule,
        // whereas the requiredTags and excludedTags originate from the command
        // line or test_suite rule.
        return TestTargetUtils.testMatchesFilters(((Rule) input).getRuleTags(),
            requiredTags, excludedTags, false);
      }
    };
  }

  private static class ExplicitEdgeVisitor implements AttributeMap.AcceptsLabelAttribute {
    private final Label expectedLabel;
    private final Rule rule;
    private boolean isExplicit = false;

    public ExplicitEdgeVisitor(Rule rule, Label expected) {
      this.rule = rule;
      this.expectedLabel = expected;
    }

    @Override
    public void acceptLabelAttribute(Label label, Attribute attr) {
      if (isExplicit || !rule.isAttributeValueExplicitlySpecified(attr)) {
        // Nothing to do here.
      } else if (expectedLabel.equals(label)) {
        isExplicit = true;
      }
    }

    public boolean isExplicit() {
      return isExplicit;
    }
  }

  /**
   * Return {@link Location} for {@link Target} target, if it should not be null.
   */
  public static Location getLocationMaybe(Target target) {
    return (target instanceof Rule) || (target instanceof InputFile) ? target.getLocation() : null;
  }

  /**
   * Return nicely formatted error message that {@link Label} label that was pointed to by {@link
   * Target} target did not exist, due to {@link NoSuchThingException} e.
   */
  public static String formatMissingEdge(
      @Nullable Target target, Label label, NoSuchThingException e) throws InterruptedException {
    // instanceof returns false if target is null (which is exploited here)
    if (target instanceof Rule) {
      Rule rule = (Rule) target;
      if (isExplicitDependency(rule, label)) {
        return String.format("%s and referenced by '%s'", e.getMessage(), target.getLabel());
      } else {
        // N.B. If you see this error message in one of our integration tests during development of
        // a change that adds a new implicit dependency when running Blaze, maybe you forgot to add
        // a new mock target to the integration test's setup.
        return String.format("every rule of type %s implicitly depends upon the target '%s', but "
            + "this target could not be found because of: %s", rule.getRuleClass(), label,
            e.getMessage());
      }
    } else if (target instanceof InputFile) {
      return e.getMessage() + " (this is usually caused by a missing package group in the"
          + " package-level visibility declaration)";
    } else {
      if (target != null) {
        return String.format("in target '%s', no such label '%s': %s", target.getLabel(), label,
            e.getMessage());
      }
      return e.getMessage();
    }
  }
}
