package com.thoughtbot.expandablerecyclerview;

import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.ViewGroup;
import com.thoughtbot.expandablerecyclerview.listeners.ExpandCollapseListener;
import com.thoughtbot.expandablerecyclerview.listeners.GroupExpandCollapseListener;
import com.thoughtbot.expandablerecyclerview.listeners.OnGroupClickListener;
import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup;
import com.thoughtbot.expandablerecyclerview.models.ExpandableList;
import com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition;
import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder;
import com.thoughtbot.expandablerecyclerview.viewholders.GroupViewHolder;
import java.util.List;

public abstract class ExpandableRecyclerViewAdapter<GVH extends GroupViewHolder, CVH extends ChildViewHolder>
    extends RecyclerView.Adapter implements ExpandCollapseListener, OnGroupClickListener {

  private static final String EXPAND_STATE_MAP = "expandable_recyclerview_adapter_expand_state_map";

  protected ExpandableList expandableList;
  private ExpandCollapseController expandCollapseController;

  private OnGroupClickListener groupClickListener;
  private GroupExpandCollapseListener expandCollapseListener;

  public ExpandableRecyclerViewAdapter(List<? extends ExpandableGroup> groups) {
    this.expandableList = new ExpandableList(groups);
    this.expandCollapseController = new ExpandCollapseController(expandableList, this);
  }

  /**
   * Implementation of Adapter.onCreateViewHolder(ViewGroup, int)
   * that determines if the list item is a group or a child and calls through
   * to the appropriate implementation of either {@link #onCreateGroupViewHolder(ViewGroup, int)}
   * or {@link #onCreateChildViewHolder(ViewGroup, int)}}.
   *
   * @param parent The {@link ViewGroup} into which the new {@link android.view.View}
   * will be added after it is bound to an adapter position.
   * @param viewType The view type of the new {@code android.view.View}.
   * @return Either a new {@link GroupViewHolder} or a new {@link ChildViewHolder}
   * that holds a {@code android.view.View} of the given view type.
   */
  @Override
  public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    switch (viewType) {
      case ExpandableListPosition.GROUP:
        GVH gvh = onCreateGroupViewHolder(parent, viewType);
        gvh.setOnGroupClickListener(this);
        return gvh;
      case ExpandableListPosition.CHILD:
        CVH cvh = onCreateChildViewHolder(parent, viewType);
        return cvh;
      default:
        throw new IllegalArgumentException("viewType is not valid");
    }
  }

  /**
   * Implementation of Adapter.onBindViewHolder(RecyclerView.ViewHolder, int)
   * that determines if the list item is a group or a child and calls through
   * to the appropriate implementation of either {@link #onBindGroupViewHolder(GroupViewHolder,
   * int,
   * ExpandableGroup)}
   * or {@link #onBindChildViewHolder(ChildViewHolder, int, ExpandableGroup, int)}.
   *
   * @param holder Either the GroupViewHolder or the ChildViewHolder to bind data to
   * @param position The flat position (or index in the list of {@link
   * ExpandableList#getVisibleItemCount()} in the list at which to bind
   */
  @Override
  public void onBindViewHolder(ViewHolder holder, int position) {
    ExpandableListPosition listPos = expandableList.getUnflattenedPosition(position);
    ExpandableGroup group = expandableList.getExpandableGroup(listPos);
    switch (listPos.type) {
      case ExpandableListPosition.GROUP:
        onBindGroupViewHolder((GVH) holder, position, group);

        if (isGroupExpanded(group)) {
          ((GVH) holder).expand();
        } else {
          ((GVH) holder).collapse();
        }
        break;
      case ExpandableListPosition.CHILD:
        onBindChildViewHolder((CVH) holder, position, group, listPos.childPos);
        break;
    }
  }

  /**
   * @return the number of group and child objects currently expanded
   * @see ExpandableList#getVisibleItemCount()
   */
  @Override
  public int getItemCount() {
    return expandableList.getVisibleItemCount();
  }

  /**
   * Gets the view type of the item at the given position.
   *
   * @param position The flat position in the list to get the view type of
   * @return {@value ExpandableListPosition#CHILD} or {@value ExpandableListPosition#GROUP}
   * @throws RuntimeException if the item at the given position in the list is not found
   */
  @Override
  public int getItemViewType(int position) {
    return expandableList.getUnflattenedPosition(position).type;
  }

  /**
   * Called when a group is expanded
   *
   * @param positionStart the flat position of the first child in the {@link ExpandableGroup}
   * @param itemCount the total number of children in the {@link ExpandableGroup}
   */
  @Override
  public void onGroupExpanded(int positionStart, int itemCount) {
    //update header
    int headerPosition = positionStart - 1;
    notifyItemChanged(headerPosition);

    // only insert if there items to insert
    if (itemCount > 0) {
      notifyItemRangeInserted(positionStart, itemCount);
      if (expandCollapseListener != null) {
        int groupIndex = expandableList.getUnflattenedPosition(positionStart).groupPos;
        expandCollapseListener.onGroupExpanded(getGroups().get(groupIndex));
      }
    }
  }

  /**
   * Called when a group is collapsed
   *
   * @param positionStart the flat position of the first child in the {@link ExpandableGroup}
   * @param itemCount the total number of children in the {@link ExpandableGroup}
   */
  @Override
  public void onGroupCollapsed(int positionStart, int itemCount) {
    //update header
    int headerPosition = positionStart - 1;
    notifyItemChanged(headerPosition);

    // only remote if there items to remove
    if (itemCount > 0) {
      notifyItemRangeRemoved(positionStart, itemCount);
      if (expandCollapseListener != null) {
        //minus one to return the position of the header, not first child
        int groupIndex = expandableList.getUnflattenedPosition(positionStart - 1).groupPos;
        expandCollapseListener.onGroupCollapsed(getGroups().get(groupIndex));
      }
    }
  }

  /**
   * Triggered by a click on a {@link GroupViewHolder}
   *
   * @param flatPos the flat position of the {@link GroupViewHolder} that was clicked
   * @return false if click expanded group, true if click collapsed group
   */
  @Override
  public boolean onGroupClick(int flatPos) {
    if (groupClickListener != null) {
      groupClickListener.onGroupClick(flatPos);
    }
    return expandCollapseController.toggleGroup(flatPos);
  }

  /**
   * @param flatPos The flat list position of the group
   * @return true if the group is expanded, *after* the toggle, false if the group is now collapsed
   */
  public boolean toggleGroup(int flatPos) {
    return expandCollapseController.toggleGroup(flatPos);
  }

  /**
   * @param group the {@link ExpandableGroup} being toggled
   * @return true if the group is expanded, *after* the toggle, false if the group is now collapsed
   */
  public boolean toggleGroup(ExpandableGroup group) {
    return expandCollapseController.toggleGroup(group);
  }

  /**
   * @param flatPos the flattened position of an item in the list
   * @return true if {@code group} is expanded, false if it is collapsed
   */
  public boolean isGroupExpanded(int flatPos) {
    return expandCollapseController.isGroupExpanded(flatPos);
  }

  /**
   * @param group the {@link ExpandableGroup} being checked for its collapsed state
   * @return true if {@code group} is expanded, false if it is collapsed
   */
  public boolean isGroupExpanded(ExpandableGroup group) {
    return expandCollapseController.isGroupExpanded(group);
  }

  /**
   * Stores the expanded state map across state loss.
   * <p>
   * Should be called from whatever {@link Activity} that hosts the RecyclerView that {@link
   * ExpandableRecyclerViewAdapter} is attached to.
   * <p>
   * This will make sure to add the expanded state map as an extra to the
   * instance state bundle to be used in {@link #onRestoreInstanceState(Bundle)}.
   *
   * @param savedInstanceState The {@code Bundle} into which to store the
   * expanded state map
   */
  public void onSaveInstanceState(Bundle savedInstanceState) {
    savedInstanceState.putBooleanArray(EXPAND_STATE_MAP, expandableList.expandedGroupIndexes);
  }

  /**
   * Fetches the expandable state map from the saved instance state {@link Bundle}
   * and restores the expanded states of all of the list items.
   * <p>
   * Should be called from {@link Activity#onRestoreInstanceState(Bundle)}  in
   * the {@link Activity} that hosts the RecyclerView that this
   * {@link ExpandableRecyclerViewAdapter} is attached to.
   * <p>
   *
   * @param savedInstanceState The {@code Bundle} from which the expanded
   * state map is loaded
   */
  public void onRestoreInstanceState(Bundle savedInstanceState) {
    if (savedInstanceState == null || !savedInstanceState.containsKey(EXPAND_STATE_MAP)) {
      return;
    }
    expandableList.expandedGroupIndexes = savedInstanceState.getBooleanArray(EXPAND_STATE_MAP);
    notifyDataSetChanged();
  }

  public void setOnGroupClickListener(OnGroupClickListener listener) {
    groupClickListener = listener;
  }

  public void setOnGroupExpandCollapseListener(GroupExpandCollapseListener listener) {
    expandCollapseListener = listener;
  }

  /**
   * The full list of {@link ExpandableGroup} backing this RecyclerView
   *
   * @return the list of {@link ExpandableGroup} that this object was instantiated with
   */
  public List<? extends ExpandableGroup> getGroups() {
    return expandableList.groups;
  }

  /**
   * Called from {@link #onCreateViewHolder(ViewGroup, int)} when  the list item created is a group
   *
   * @param viewType an int returned by {@link ExpandableRecyclerViewAdapter#getItemViewType(int)}
   * @param parent the {@link ViewGroup} in the list for which a {@link GVH}  is being created
   * @return A {@link GVH} corresponding to the group list item with the  {@code ViewGroup} parent
   */
  public abstract GVH onCreateGroupViewHolder(ViewGroup parent, int viewType);

  /**
   * Called from {@link #onCreateViewHolder(ViewGroup, int)} when the list item created is a child
   *
   * @param viewType an int returned by {@link ExpandableRecyclerViewAdapter#getItemViewType(int)}
   * @param parent the {@link ViewGroup} in the list for which a {@link CVH}  is being created
   * @return A {@link CVH} corresponding to child list item with the  {@code ViewGroup} parent
   */
  public abstract CVH onCreateChildViewHolder(ViewGroup parent, int viewType);

  /**
   * Called from onBindViewHolder(RecyclerView.ViewHolder, int) when the list item
   * bound to is a  child.
   * <p>
   * Bind data to the {@link CVH} here.
   *
   * @param holder The {@code CVH} to bind data to
   * @param flatPosition the flat position (raw index) in the list at which to bind the child
   * @param group The {@link ExpandableGroup} that the the child list item belongs to
   * @param childIndex the index of this child within it's {@link ExpandableGroup}
   */
  public abstract void onBindChildViewHolder(CVH holder, int flatPosition, ExpandableGroup group,
      int childIndex);

  /**
   * Called from onBindViewHolder(RecyclerView.ViewHolder, int) when the list item bound to is a
   * group
   * <p>
   * Bind data to the {@link GVH} here.
   *
   * @param holder The {@code GVH} to bind data to
   * @param flatPosition the flat position (raw index) in the list at which to bind the group
   * @param group The {@link ExpandableGroup} to be used to bind data to this {@link GVH}
   */
  public abstract void onBindGroupViewHolder(GVH holder, int flatPosition, ExpandableGroup group);
}