content: /*
   * Copyright (C) 2015 The Android Open Source Project
   *
   * 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.android.setupwizardlib.items;
  
  import android.content.Context;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.util.SparseIntArray;
  import java.util.ArrayList;
  import java.util.List;
  
  public class ItemGroup extends AbstractItemHierarchy
      implements ItemInflater.ItemParent, ItemHierarchy.Observer {
  
    /* static section */
  
    private static final String TAG = "ItemGroup";
  
    /**
     * Binary search for the closest value that's smaller than or equal to {@code value}, and return
     * the corresponding key.
     */
    private static int binarySearch(SparseIntArray array, int value) {
      final int size = array.size();
      int lo = 0;
      int hi = size - 1;
  
      while (lo <= hi) {
        final int mid = (lo + hi) >>> 1;
        final int midVal = array.valueAt(mid);
  
        if (midVal < value) {
          lo = mid + 1;
        } else if (midVal > value) {
          hi = mid - 1;
        } else {
          return array.keyAt(mid); // value found
        }
      }
      // Value not found. Return the last item before our search range, which is the closest
      // value smaller than the value we are looking for.
      return array.keyAt(lo - 1);
    }
  
    /**
     * Same as {@link List#indexOf(Object)}, but using identity comparison rather than {@link
     * Object#equals(Object)}.
     */
    private static <T> int identityIndexOf(List<T> list, T object) {
      final int count = list.size();
      for (int i = 0; i < count; i++) {
        if (list.get(i) == object) {
          return i;
        }
      }
      return -1;
    }
  
    /* non-static section */
  
    private final List<ItemHierarchy> children = new ArrayList<>();
  
    /**
     * A mapping from the index of an item hierarchy in children, to the first position in which the
     * corresponding child hierarchy represents. For example:
     *
     * <p>ItemHierarchy Item Item Position Index
     *
     * <p>0 [ Wi-Fi AP 1 ] 0 | Wi-Fi AP 2 | 1 | Wi-Fi AP 3 | 2 | Wi-Fi AP 4 | 3 [ Wi-Fi AP 5 ] 4
     *
     * <p>1 [ <Empty Item Hierarchy> ]
     *
     * <p>2 [ Use cellular data ] 5
     *
     * <p>3 [ Don't connect ] 6
     *
     * <p>For this example of Wi-Fi screen, the following mapping will be produced: [ 0 -> 0 | 2 -> 5
     * | 3 -> 6 ]
     *
     * <p>Also note how ItemHierarchy index 1 is not present in the map, because it is empty.
     *
     * <p>ItemGroup uses this map to look for which ItemHierarchy an item at a given position belongs
     * to.
     */
    private final SparseIntArray hierarchyStart = new SparseIntArray();
  
    private int count = 0;
    private boolean dirty = false;
  
    public ItemGroup() {
      super();
    }
  
    public ItemGroup(Context context, AttributeSet attrs) {
      // Constructor for XML inflation
      super(context, attrs);
    }
  
    /** Add a child hierarchy to this item group. */
    @Override
    public void addChild(ItemHierarchy child) {
      dirty = true;
      children.add(child);
      child.registerObserver(this);
  
      final int count = child.getCount();
      if (count > 0) {
        notifyItemRangeInserted(getChildPosition(child), count);
      }
    }
  
    /**
     * Remove a previously added child from this item group.
     *
     * @return True if there is a match for the child and it is removed. False if the child could not
     *     be found in our list of child hierarchies.
     */
    public boolean removeChild(ItemHierarchy child) {
      final int childIndex = identityIndexOf(children, child);
      final int childPosition = getChildPosition(childIndex);
      dirty = true;
      if (childIndex != -1) {
        final int childCount = child.getCount();
        children.remove(childIndex);
        child.unregisterObserver(this);
        if (childCount > 0) {
          notifyItemRangeRemoved(childPosition, childCount);
        }
        return true;
      }
      return false;
    }
  
    /** Remove all children from this hierarchy. */
    public void clear() {
      if (children.isEmpty()) {
        return;
      }
  
      final int numRemoved = getCount();
  
      for (ItemHierarchy item : children) {
        item.unregisterObserver(this);
      }
      dirty = true;
      children.clear();
      notifyItemRangeRemoved(0, numRemoved);
    }
  
    @Override
    public int getCount() {
      updateDataIfNeeded();
      return count;
    }
  
    @Override
    public IItem getItemAt(int position) {
      int itemIndex = getItemIndex(position);
      ItemHierarchy item = children.get(itemIndex);
      int subpos = position - hierarchyStart.get(itemIndex);
      return item.getItemAt(subpos);
    }
  
    @Override
    public void onChanged(ItemHierarchy hierarchy) {
      // Need to set dirty, because our children may have gotten more items.
      dirty = true;
      notifyChanged();
    }
  
    /**
     * @return The "Item Position" of the given child, or -1 if the child is not found. If the given
     *     child is empty, position of the next visible item is returned.
     */
    private int getChildPosition(ItemHierarchy child) {
      // Check the identity of the child rather than using .equals(), because here we want
      // to find the index of the instance itself rather than something that equals to it.
      return getChildPosition(identityIndexOf(children, child));
    }
  
    private int getChildPosition(int childIndex) {
      updateDataIfNeeded();
      if (childIndex != -1) {
        int childPos = -1;
        int childCount = children.size();
        for (int i = childIndex; childPos < 0 && i < childCount; i++) {
          // Find the position of the first visible child after childIndex. This is required
          // when removing the last item from a nested ItemGroup.
          childPos = hierarchyStart.get(i, -1);
        }
        if (childPos < 0) {
          // If the last item in a group is being removed, there will be no visible item.
          // In that case return the count instead, since that is where the item would have
          // been if the child is not empty.
          childPos = getCount();
        }
        return childPos;
      }
      return -1;
    }
  
    @Override
    public void onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
      // No need to set dirty because onItemRangeChanged does not include any structural changes.
      final int childPosition = getChildPosition(itemHierarchy);
      if (childPosition >= 0) {
        notifyItemRangeChanged(childPosition + positionStart, itemCount);
      } else {
        Log.e(TAG, "Unexpected child change " + itemHierarchy);
      }
    }
  
    @Override
    public void onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
      dirty = true;
      final int childPosition = getChildPosition(itemHierarchy);
      if (childPosition >= 0) {
        notifyItemRangeInserted(childPosition + positionStart, itemCount);
      } else {
        Log.e(TAG, "Unexpected child insert " + itemHierarchy);
      }
    }
  
    @Override
    public void onItemRangeMoved(
        ItemHierarchy itemHierarchy, int fromPosition, int toPosition, int itemCount) {
      dirty = true;
      final int childPosition = getChildPosition(itemHierarchy);
      if (childPosition >= 0) {
        notifyItemRangeMoved(childPosition + fromPosition, childPosition + toPosition, itemCount);
      } else {
        Log.e(TAG, "Unexpected child move " + itemHierarchy);
      }
    }
  
    @Override
    public void onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
      dirty = true;
      final int childPosition = getChildPosition(itemHierarchy);
      if (childPosition >= 0) {
        notifyItemRangeRemoved(childPosition + positionStart, itemCount);
      } else {
        Log.e(TAG, "Unexpected child remove " + itemHierarchy);
      }
    }
  
    @Override
    public ItemHierarchy findItemById(int id) {
      if (id == getId()) {
        return this;
      }
      for (ItemHierarchy child : children) {
        ItemHierarchy childFindItem = child.findItemById(id);
        if (childFindItem != null) {
          return childFindItem;
        }
      }
      return null;
    }
  
    /** If dirty, this method will recalculate the number of items and hierarchyStart. */
    private void updateDataIfNeeded() {
      if (dirty) {
        count = 0;
        hierarchyStart.clear();
        for (int itemIndex = 0; itemIndex < children.size(); itemIndex++) {
          ItemHierarchy item = children.get(itemIndex);
          if (item.getCount() > 0) {
            hierarchyStart.put(itemIndex, count);
          }
          count += item.getCount();
        }
        dirty = false;
      }
    }
  
    /**
     * Use binary search to locate the item hierarchy a position is contained in.
     *
     * @return Index of the item hierarchy which is responsible for the item at {@code position}.
     */
    private int getItemIndex(int position) {
      updateDataIfNeeded();
      if (position < 0 || position >= count) {
        throw new IndexOutOfBoundsException("size=" + count + "; index=" + position);
      }
      int result = binarySearch(hierarchyStart, position);
      if (result < 0) {
        throw new IllegalStateException("Cannot have item start index < 0");
      }
      return result;
    }
  }
  