/**
  * Using the criteria in {@code parentIdMap}, recursively adds all children under the partition ID
  * of {@code parentNode} to {@code parentNode}.
  *
  * @param parentNode required
  * @param parentIdMap the multimap from parent partition ID to list of child criteria
  */
 private static void addChildNodes(
     ProductPartitionNode parentNode, ListMultimap<Long, AdGroupCriterion> parentIdMap) {
   if (parentIdMap.containsKey(parentNode.getProductPartitionId())) {
     parentNode = parentNode.asSubdivision();
   }
   for (AdGroupCriterion adGroupCriterion : parentIdMap.get(parentNode.getProductPartitionId())) {
     ProductPartition partition = (ProductPartition) adGroupCriterion.getCriterion();
     ProductPartitionNode childNode = parentNode.addChild(partition.getCaseValue());
     childNode = childNode.setProductPartitionId(partition.getId());
     if (ProductPartitionType.SUBDIVISION.equals(partition.getPartitionType())) {
       childNode = childNode.asSubdivision();
     } else {
       if (adGroupCriterion instanceof BiddableAdGroupCriterion) {
         childNode = childNode.asBiddableUnit();
         Money cpcBidAmount = getBid((BiddableAdGroupCriterion) adGroupCriterion);
         if (cpcBidAmount != null) {
           childNode = childNode.setBid(cpcBidAmount.getMicroAmount());
         }
       } else {
         childNode = childNode.asExcludedUnit();
       }
     }
     addChildNodes(childNode, parentIdMap);
   }
 }
  /**
   * 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 instance of this class by retrieving the product partitions of the specified ad
   * group. All parameters are required.
   */
  public static ProductPartitionTree createAdGroupTree(
      AdWordsServices services, AdWordsSession session, Long adGroupId)
      throws ApiException, RemoteException {
    // Get the AdGroupCriterionService.
    AdGroupCriterionServiceInterface criterionService =
        services.get(session, AdGroupCriterionServiceInterface.class);

    SelectorBuilder selectorBuilder =
        new SelectorBuilder()
            .fields(
                REQUIRED_SELECTOR_FIELD_ENUMS.toArray(
                    new AdGroupCriterionField[REQUIRED_SELECTOR_FIELD_ENUMS.size()]))
            .equals(AdGroupCriterionField.AdGroupId, adGroupId.toString())
            .equals(AdGroupCriterionField.CriteriaType, "PRODUCT_PARTITION")
            .in(
                AdGroupCriterionField.Status,
                UserStatus.ENABLED.getValue(),
                UserStatus.PAUSED.getValue())
            .limit(PAGE_SIZE);

    AdGroupCriterionPage adGroupCriterionPage;

    // A multimap from each product partition ID to its direct children.
    ListMultimap<Long, AdGroupCriterion> parentIdMap = LinkedListMultimap.create();
    int offset = 0;
    do {
      // Get the next page of results.
      adGroupCriterionPage = criterionService.get(selectorBuilder.build());

      if (adGroupCriterionPage != null && adGroupCriterionPage.getEntries() != null) {
        for (AdGroupCriterion adGroupCriterion : adGroupCriterionPage.getEntries()) {
          ProductPartition partition = (ProductPartition) adGroupCriterion.getCriterion();
          parentIdMap.put(partition.getParentCriterionId(), adGroupCriterion);
        }
        offset += adGroupCriterionPage.getEntries().length;
        selectorBuilder.increaseOffsetBy(PAGE_SIZE);
      }
    } while (offset < adGroupCriterionPage.getTotalNumEntries());

    // Construct the ProductPartitionTree from the parentIdMap.
    if (!parentIdMap.containsKey(null)) {
      Preconditions.checkState(
          parentIdMap.isEmpty(), "No root criterion found in the tree but the tree is not empty");
      return createEmptyAdGroupTree(
          adGroupId, getAdGroupBiddingStrategyConfiguration(services, session, adGroupId));
    }

    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);
  }