/**
   * Post merge clean up.
   *
   * <p>- Remove the removed items. - Clear the state of all the items (this allow newly overridden
   * items to lose their WRITTEN state) - Set the items that are part of the new merge to be WRITTEN
   * to allow the next merge to be incremental.
   */
  private void postMergeCleanUp() {
    ListMultimap<String, I> itemMap = ArrayListMultimap.create();

    // remove all removed items, and copy the rest in the full map while resetting their state.
    for (S dataSet : mDataSets) {
      ListMultimap<String, I> map = dataSet.getDataMap();

      List<String> keys = Lists.newArrayList(map.keySet());
      for (String key : keys) {
        List<I> list = map.get(key);
        for (int i = 0; i < list.size(); ) {
          I item = list.get(i);
          if (item.isRemoved()) {
            list.remove(i);
          } else {
            //noinspection unchecked
            itemMap.put(key, (I) item.resetStatus());
            i++;
          }
        }
      }
    }

    // for the last items (the one that have been written into the consumer), set their
    // state to WRITTEN
    for (String key : itemMap.keySet()) {
      List<I> itemList = itemMap.get(key);
      itemList.get(itemList.size() - 1).resetStatusToWritten();
    }
  }
  /**
   * Returns the number of items.
   *
   * @return the number of items.
   * @see DataMap
   */
  @Override
  public int size() {
    // put all the resource keys in a set.
    Set<String> keys = Sets.newHashSet();

    for (S resourceSet : mDataSets) {
      ListMultimap<String, I> map = resourceSet.getDataMap();
      keys.addAll(map.keySet());
    }

    return keys.size();
  }
  /**
   * Returns a map of the data items.
   *
   * @return a map of items.
   * @see DataMap
   */
  @NonNull
  @Override
  public ListMultimap<String, I> getDataMap() {
    // put all the sets in a multimap. The result is that for each key,
    // there is a sorted list of items from all the layers, including removed ones.
    ListMultimap<String, I> fullItemMultimap = ArrayListMultimap.create();

    for (S resourceSet : mDataSets) {
      ListMultimap<String, I> map = resourceSet.getDataMap();
      for (Map.Entry<String, Collection<I>> entry : map.asMap().entrySet()) {
        fullItemMultimap.putAll(entry.getKey(), entry.getValue());
      }
    }

    return fullItemMultimap;
  }
  /**
   * Sets the post blob load state to TOUCHED.
   *
   * <p>After a load from the blob file, all items have their state set to nothing. If the load mode
   * is not set to incrementalState then we want the items that are in the current merge result to
   * have their state be TOUCHED.
   *
   * <p>This will allow the first use of {@link #mergeData(MergeConsumer, boolean)} to add these to
   * the consumer as if they were new items.
   *
   * @see #loadFromBlob(java.io.File, boolean)
   * @see DataItem#isTouched()
   */
  private void setPostBlobLoadStateToTouched() {
    ListMultimap<String, I> itemMap = ArrayListMultimap.create();

    // put all the sets into list per keys. The order is important as the lower sets are
    // overridden by the higher sets.
    for (S dataSet : mDataSets) {
      ListMultimap<String, I> map = dataSet.getDataMap();
      for (Map.Entry<String, Collection<I>> entry : map.asMap().entrySet()) {
        itemMap.putAll(entry.getKey(), entry.getValue());
      }
    }

    // the items that represent the current state is the last item in the list for each key.
    for (String key : itemMap.keySet()) {
      List<I> itemList = itemMap.get(key);
      itemList.get(itemList.size() - 1).resetStatusToTouched();
    }
  }
  /**
   * Merges the data into a given consumer.
   *
   * @param consumer the consumer of the merge.
   * @param doCleanUp clean up the state to be able to do further incremental merges. If this is a
   *     one-shot merge, this can be false to improve performance.
   * @throws MergingException such as a DuplicateDataException or a MergeConsumer.ConsumerException
   *     if something goes wrong
   */
  public void mergeData(@NonNull MergeConsumer<I> consumer, boolean doCleanUp)
      throws MergingException {

    consumer.start(mFactory);

    try {
      // get all the items keys.
      Set<String> dataItemKeys = Sets.newHashSet();

      for (S dataSet : mDataSets) {
        // quick check on duplicates in the resource set.
        dataSet.checkItems();
        ListMultimap<String, I> map = dataSet.getDataMap();
        dataItemKeys.addAll(map.keySet());
      }

      // loop on all the data items.
      for (String dataItemKey : dataItemKeys) {
        if (requiresMerge(dataItemKey)) {
          // get all the available items, from the lower priority, to the higher
          // priority
          List<I> items = Lists.newArrayListWithExpectedSize(mDataSets.size());
          for (S dataSet : mDataSets) {

            // look for the resource key in the set
            ListMultimap<String, I> itemMap = dataSet.getDataMap();

            List<I> setItems = itemMap.get(dataItemKey);
            items.addAll(setItems);
          }

          mergeItems(dataItemKey, items, consumer);
          continue;
        }

        // for each items, look in the data sets, starting from the end of the list.

        I previouslyWritten = null;
        I toWrite = null;

        /*
         * We are looking for what to write/delete: the last non deleted item, and the
         * previously written one.
         */

        boolean foundIgnoredItem = false;

        setLoop:
        for (int i = mDataSets.size() - 1; i >= 0; i--) {
          S dataSet = mDataSets.get(i);

          // look for the resource key in the set
          ListMultimap<String, I> itemMap = dataSet.getDataMap();

          List<I> items = itemMap.get(dataItemKey);
          if (items.isEmpty()) {
            continue;
          }

          // The list can contain at max 2 items. One touched and one deleted.
          // More than one deleted means there was more than one which isn't possible
          // More than one touched means there is more than one and this isn't possible.
          for (int ii = items.size() - 1; ii >= 0; ii--) {
            I item = items.get(ii);

            if (consumer.ignoreItemInMerge(item)) {
              foundIgnoredItem = true;
              continue;
            }

            if (item.isWritten()) {
              assert previouslyWritten == null;
              previouslyWritten = item;
            }

            if (toWrite == null && !item.isRemoved()) {
              toWrite = item;
            }

            if (toWrite != null && previouslyWritten != null) {
              break setLoop;
            }
          }
        }

        // done searching, we should at least have something, unless we only
        // found items that are not meant to be written (attr inside declare styleable)
        assert foundIgnoredItem || previouslyWritten != null || toWrite != null;

        //noinspection ConstantConditions
        if (previouslyWritten == null && toWrite == null) {
          continue;
        }

        // now need to handle, the type of each (single res file, multi res file), whether
        // they are the same object or not, whether the previously written object was deleted.

        if (toWrite == null) {
          // nothing to write? delete only then.
          assert previouslyWritten.isRemoved();

          consumer.removeItem(previouslyWritten, null /*replacedBy*/);

        } else if (previouslyWritten == null || previouslyWritten == toWrite) {
          // easy one: new or updated res
          consumer.addItem(toWrite);
        } else {
          // replacement of a resource by another.

          // force write the new value
          toWrite.setTouched();
          consumer.addItem(toWrite);
          // and remove the old one
          consumer.removeItem(previouslyWritten, toWrite);
        }
      }
    } finally {
      consumer.end();
    }

    if (doCleanUp) {
      // reset all states. We can't just reset the toWrite and previouslyWritten objects
      // since overlayed items might have been touched as well.
      // Should also clean (remove) objects that are removed.
      postMergeCleanUp();
    }
  }