// Copyright 2014 Google Inc. 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.analysis;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
import com.google.devtools.build.lib.packages.AbstractAttributeMapper;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Label;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * {@link AttributeMap} implementation that binds a rule's attribute as follows:
 *
 * <ol>
 *   <li>If the attribute is selectable (i.e. its BUILD declaration is of the form
 *   "attr = { config1: "value1", "config2: "value2", ... }", returns the subset of values
 *   chosen by the current configuration in accordance with Bazel's documented policy on
 *   configurable attribute selection.
 *   <li>If the attribute is not selectable (i.e. its value is static), returns that value with
 *   no additional processing.
 * </ol>
 *
 * <p>Example usage:
 * <pre>
 *   Label fooLabel = ConfiguredAttributeMapper.of(ruleConfiguredTarget).get("foo", Type.LABEL);
 * </pre>
 */
public class ConfiguredAttributeMapper extends AbstractAttributeMapper {

  private final Map<Label, ConfigMatchingProvider> configConditions;
  private Rule rule;

  private ConfiguredAttributeMapper(Rule rule, Set<ConfigMatchingProvider> configConditions) {
    super(Preconditions.checkNotNull(rule).getPackage(), rule.getRuleClassObject(), rule.getLabel(),
        rule.getAttributeContainer());
    ImmutableMap.Builder<Label, ConfigMatchingProvider> builder = ImmutableMap.builder();
    for (ConfigMatchingProvider configCondition : configConditions) {
      builder.put(configCondition.label(), configCondition);
    }
    this.configConditions = builder.build();
    this.rule = rule;
  }

  /**
   * "Do-it-all" constructor that just needs a {@link RuleConfiguredTarget}.
   */
  public static ConfiguredAttributeMapper of(RuleConfiguredTarget ct) {
    return new ConfiguredAttributeMapper(ct.getTarget(), ct.getConfigConditions());
  }

  /**
   * "Manual" constructor that requires the caller to pass the set of configurability conditions
   * that trigger this rule's configurable attributes.
   *
   * <p>If you don't know how to do this, you really want to use one of the "do-it-all"
   * constructors.
   */
  @VisibleForTesting
  public static ConfiguredAttributeMapper of(
      Rule rule, Set<ConfigMatchingProvider> configConditions) {
    return new ConfiguredAttributeMapper(rule, configConditions);
  }

  /**
   * Checks that all attributes can be mapped to their configured values. This is
   * useful for checking that the configuration space in a configured attribute doesn't
   * contain unresolvable contradictions.
   *
   * @throws EvalException if any attribute's value can't be resolved under this mapper
   */
  public void validateAttributes() throws EvalException {
    for (String attrName : getAttributeNames()) {
      getAndValidate(attrName, getAttributeType(attrName));
    }
  }

  /**
   * Variation of {@link #get} that throws an informative exception if the attribute
   * can't be resolved due to intrinsic contradictions in the configuration.
   */
  private <T> T getAndValidate(String attributeName, Type<T> type) throws EvalException  {
    Type.SelectorList<T> selectorList = getSelectorList(attributeName, type);
    if (selectorList == null) {
      // This is a normal attribute.
      return super.get(attributeName, type);
    }

    List<T> resolvedList = new ArrayList<>();
    for (Type.Selector<T> selector : selectorList.getSelectors()) {
      resolvedList.add(resolveSelector(attributeName, selector));
    }
    return resolvedList.size() == 1 ? resolvedList.get(0) : type.concat(resolvedList);
  }

  private <T> T resolveSelector(String attributeName, Type.Selector<T> selector)
      throws EvalException {
    ConfigMatchingProvider matchingCondition = null;
    T matchingValue = null;

    // Find the matching condition and record its value (checking for duplicates).
    for (Map.Entry<Label, T> entry : selector.getEntries().entrySet()) {
      Label selectorKey = entry.getKey();
      if (Type.Selector.isReservedLabel(selectorKey)) {
        continue;
      }

      ConfigMatchingProvider curCondition = Verify.verifyNotNull(configConditions.get(selectorKey));

      if (curCondition.matches()) {
        if (matchingCondition == null || curCondition.refines(matchingCondition)) {
          // A match is valid if either this is the *only* condition that matches or this is a
          // more "precise" specification of another matching condition (in which case we choose
          // the most precise one).
          matchingCondition = curCondition;
          matchingValue = entry.getValue();
        } else if (matchingCondition.refines(curCondition)) {
          // The originally matching conditions is more precise, so keep that one.
        } else {
          throw new EvalException(rule.getAttributeLocation(attributeName),
              "Both " + matchingCondition.label() + " and " + curCondition.label()
              + " match configurable attribute \"" + attributeName + "\" in " + getLabel()
              + ". Multiple matches are not allowed unless one is a specialization of the other");
        }
      }
    }

    // If nothing matched, choose the default condition.
    if (matchingCondition == null) {
      if (!selector.hasDefault()) {
        throw new EvalException(rule.getAttributeLocation(attributeName),
            "Configurable attribute \"" + attributeName + "\" doesn't match this "
            + "configuration (would a default condition help?)");
      }
      matchingValue = selector.getDefault();
    }

    return matchingValue;
  }

  @Override
  public <T> T get(String attributeName, Type<T> type) {
    try {
      return getAndValidate(attributeName, type);
    } catch (EvalException e) {
      // Callers that reach this branch should explicitly validate the attribute through an
      // appropriate call and handle the exception directly. This method assumes
      // pre-validated attributes.
      throw new IllegalStateException(
          "lookup failed on attribute " + attributeName + ": " + e.getMessage());
    }
  }
}

@@ -706,16 +706,54 @@
 esting;%0A
+import com.google.common.base.Joiner;%0A
 import c
@@ -1321,24 +1321,56 @@
 .ArrayList;%0A
+import java.util.LinkedHashSet;%0A
 import java.
@@ -4932,16 +4932,72 @@
 = null;%0A
+    Set%3CLabel%3E conditionLabels = new LinkedHashSet%3C%3E();%0A
     T ma
@@ -5398,16 +5398,65 @@
 orKey));
+%0A      conditionLabels.add(curCondition.label());
 %0A%0A      
@@ -6770,17 +6770,95 @@
 n help?)
-%22
+.%5CnConditions checked:%5Cn %22%0A            + Joiner.on(%22%5Cn %22).join(conditionLabels)
 );%0A     

When builds fail because select statements don't match, include in the error which conditions were checked.