/**
   * Returns a new instance of this class based on the collection of ad group criteria provided.
   *
   * <p>NOTE: If retrieving existing criteria for use with this method, you must include all of the
   * fields in {@link #REQUIRED_SELECTOR_FIELDS} in your {@link Selector}.
   *
   * @param adGroupId the ID of the ad group
   * @param biddingStrategyConfig the {@link BiddingStrategyConfiguration} for the ad group
   * @param adGroupCriteria the non-null (but possibly empty) list of ad group criteria
   * @throws NullPointerException if any argument is null, any element in {@code adGroupCriteria} is
   *     null, or any required field from {@link #REQUIRED_SELECTOR_FIELDS} is missing from an
   *     element in {@code adGroupCriteria}
   * @throws IllegalArgumentException if {@code adGroupCriteria} does not include the root criterion
   *     of the product partition tree
   */
  public static ProductPartitionTree createAdGroupTree(
      Long adGroupId,
      BiddingStrategyConfiguration biddingStrategyConfig,
      List<AdGroupCriterion> adGroupCriteria) {
    Preconditions.checkNotNull(adGroupId, "Null ad group ID");
    Preconditions.checkNotNull(biddingStrategyConfig, "Null bidding strategy configuration");
    Preconditions.checkNotNull(adGroupCriteria, "Null criteria list");
    if (adGroupCriteria.isEmpty()) {
      return createEmptyAdGroupTree(adGroupId, biddingStrategyConfig);
    }

    ListMultimap<Long, AdGroupCriterion> parentIdMap = LinkedListMultimap.create();
    for (AdGroupCriterion adGroupCriterion : adGroupCriteria) {
      Preconditions.checkNotNull(
          adGroupCriterion.getCriterion(), "AdGroupCriterion has a null criterion");
      if (adGroupCriterion instanceof BiddableAdGroupCriterion) {
        BiddableAdGroupCriterion biddableCriterion = (BiddableAdGroupCriterion) adGroupCriterion;
        Preconditions.checkNotNull(
            biddableCriterion.getUserStatus(),
            "User status is null for criterion ID %s",
            biddableCriterion.getCriterion().getId());
        if (UserStatus.REMOVED.equals(biddableCriterion.getUserStatus())) {
          // Skip REMOVED criteria.
          continue;
        }
      }
      if (adGroupCriterion.getCriterion() instanceof ProductPartition) {
        ProductPartition partition = (ProductPartition) adGroupCriterion.getCriterion();
        parentIdMap.put(partition.getParentCriterionId(), adGroupCriterion);
      }
    }

    return createNonEmptyAdGroupTree(adGroupId, parentIdMap);
  }
  /**
   * Returns a new tree based on a non-empty collection of ad group criteria. All parameters
   * required.
   *
   * @param adGroupId the ID of the ad group
   * @param parentIdMap the multimap from parent product partition ID to child criteria
   * @return a new ProductPartitionTree
   */
  private static ProductPartitionTree createNonEmptyAdGroupTree(
      Long adGroupId, ListMultimap<Long, AdGroupCriterion> parentIdMap) {
    Preconditions.checkNotNull(adGroupId, "Null ad group ID");
    Preconditions.checkArgument(
        !parentIdMap.isEmpty(), "parentIdMap passed for ad group ID %s is empty", adGroupId);
    Preconditions.checkArgument(
        parentIdMap.containsKey(null),
        "No root criterion found in the list of ad group criteria for ad group ID %s",
        adGroupId);

    AdGroupCriterion rootCriterion = Iterables.getOnlyElement(parentIdMap.get(null));

    Preconditions.checkState(
        rootCriterion instanceof BiddableAdGroupCriterion,
        "Root criterion for ad group ID %s is not a BiddableAdGroupCriterion",
        adGroupId);
    BiddableAdGroupCriterion biddableRootCriterion = (BiddableAdGroupCriterion) rootCriterion;

    BiddingStrategyConfiguration biddingStrategyConfig =
        biddableRootCriterion.getBiddingStrategyConfiguration();
    Preconditions.checkState(
        biddingStrategyConfig != null,
        "Null bidding strategy config on the root node of ad group ID %s",
        adGroupId);
    ProductPartitionNode rootNode =
        new ProductPartitionNode(
            null,
            (ProductDimension) null,
            rootCriterion.getCriterion().getId(),
            new ProductDimensionComparator());

    // Set the root's bid if a bid exists on the BiddableAdGroupCriterion.
    Money rootNodeBid = getBid(biddableRootCriterion);
    if (rootNodeBid != null) {
      rootNode = rootNode.asBiddableUnit().setBid(rootNodeBid.getMicroAmount());
    }

    addChildNodes(rootNode, parentIdMap);

    return new ProductPartitionTree(adGroupId, biddingStrategyConfig, rootNode);
  }
 /** Returns the criterion-level bid, or null if no such bid exists. */
 private static Money getBid(BiddableAdGroupCriterion biddableCriterion) {
   BiddingStrategyConfiguration biddingConfig =
       biddableCriterion.getBiddingStrategyConfiguration();
   Money cpcBidAmount = null;
   if (biddingConfig.getBids() != null) {
     for (Bids bid : biddingConfig.getBids()) {
       if (bid instanceof CpcBid) {
         CpcBid cpcBid = (CpcBid) bid;
         if (BidSource.CRITERION.equals(cpcBid.getCpcBidSource())) {
           cpcBidAmount = cpcBid.getBid();
           break;
         }
       }
     }
   }
   return cpcBidAmount;
 }