package com.tumblr.graywater;

import android.annotation.SuppressLint;
import android.support.annotation.AnyRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.util.ArrayMap;
import android.support.v4.util.Pair;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import javax.inject.Provider;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

/**
 * Maps models to multiple view holders.
 * <p>
 * Created by ericleong on 3/11/16.
 *
 * @param <T>
 * 		the model type.
 * @param <VH>
 * 		the viewholder type.
 * @param <B>
 * 		the binder type.
 * @param <MT>
 * 		the type of the model type ({@code Class<T>} for example)
 */
public abstract class GraywaterAdapter<
		T,
		VH extends RecyclerView.ViewHolder,
		B extends GraywaterAdapter.Binder<? extends T, VH, ? extends VH>,
		MT>
		extends RecyclerView.Adapter<VH> {

	private static final int NO_PREVIOUS_BOUND_VIEWHOLDER = -1;

	/**
	 * The T list for adapter items.
	 */
	@NonNull
	protected final List<T> mItems = new ArrayList<>();

	/**
	 * Map of viewtypes to ViewHolderCreators.
	 */
	@NonNull
	private final Map<Integer, ViewHolderCreator> mViewHolderCreatorMap = new ArrayMap<>();

	/**
	 * Map of viewtypes to the class of the viewholders they correspond to. This is just informational.
	 */
	@NonNull
	private final Map<Integer, Class<? extends VH>> mViewTypeToViewHolderClassMap = new ArrayMap<>();

	/**
	 * Map of viewtypes to the class of the viewholders they correspond to. This is just informational.
	 */
	@NonNull
	private final Map<Class<? extends VH>, Integer> mViewHolderClassToViewTypeMap = new ArrayMap<>();

	/**
	 * Map from model to a list of binders. A model may have multiple binders.
	 */
	@NonNull
	protected final Map<MT, ItemBinder<? extends T, ? extends VH, ? extends B>> mItemBinderMap = new ArrayMap<>();

	/**
	 * Map from model to an action listener. A model may have multiple action listeners if there are multiple events.
	 */
	@NonNull
	private final Map<MT, ActionListener<? extends T, VH, ? extends VH>> mActionListenerMap = new ArrayMap<>();

	/**
	 * Index of the last model that was bound.
	 */
	private int mPreviousBoundViewHolderPosition = NO_PREVIOUS_BOUND_VIEWHOLDER;

	private final List<List<Provider<Binder<? super T, VH, ? extends VH>>>> mBinderListCache = new ArrayList<>();
	private final List<Integer> mViewHolderToItemPositionCache = new ArrayList<>();
	private final List<Integer> mItemPositionToFirstViewHolderPositionCache = new ArrayList<>();
	/**
	 * The viewholders that have had {@link #prepare(int, Binder, Object, List, int)} called on them.
	 * <p>
	 * {@link #add(Object)}, {@link #remove(int)}, {@link #onViewRecycled(RecyclerView.ViewHolder)}
	 * will all cause this to be cleared.
	 */
	private final Set<Integer> mViewHolderPreparedCache = new HashSet<>();

	/**
	 * @param viewHolderCreator
	 * 		the view holder creator to register.
	 * @param viewHolderClass
	 * 		the class of the viewholder (useful when debugging).
	 */
	protected void register(final ViewHolderCreator viewHolderCreator, final Class<? extends VH> viewHolderClass) {
		final int viewType = viewHolderCreator.getViewType();
		mViewHolderCreatorMap.put(viewType, viewHolderCreator);
		mViewTypeToViewHolderClassMap.put(viewType, viewHolderClass);
		mViewHolderClassToViewTypeMap.put(viewHolderClass, viewType);
	}

	/**
	 * @param modelType
	 * 		the model type.
	 * @param parts
	 * 		the binders to use to display this model.
	 * @param listener
	 * 		the listener to associate with the model.
	 */
	protected void register(@NonNull final MT modelType,
	                        @NonNull final ItemBinder<? extends T, ? extends VH, ? extends B> parts,
	                        @Nullable final ActionListener<? extends T, VH, ? extends VH> listener) {
		mItemBinderMap.put(modelType, parts);
		mActionListenerMap.put(modelType, listener);
	}

	/**
	 * @param model
	 * 		the model to get the type of.
	 * @return the appropriate type (for example, {@link Class<T>}).
	 */
	@NonNull
	protected abstract MT getModelType(T model);

	/**
	 * @param model
	 * 		the model to get the {@link ItemBinder} for.
	 * @return the {@link ItemBinder} for the given model.
	 */
	@Nullable
	protected ItemBinder<? extends T, ? extends VH, ? extends B> getItemBinder(final T model) {
		return mItemBinderMap.get(getModelType(model));
	}

	/**
	 * @param model
	 * 		the model to get the {@link ItemBinder} for.
	 * @return the {@link ActionListener} for the given model.
	 */
	@Nullable
	protected ActionListener<? extends T, VH, ? extends VH> getActionListener(final T model) {
		return mActionListenerMap.get(getModelType(model));
	}

	/**
	 * @param model
	 * 		the model to get parts for.
	 * @param position
	 * 		the position of the model.
	 * @return the list of binders to use.
	 */
	@Nullable
	protected List<Provider<Binder<? super T, VH, ? extends VH>>> getParts(final T model, final int position) {
		final List<Provider<Binder<? super T, VH, ? extends VH>>> list;

		final ItemBinder itemBinder = getItemBinder(model);

		if (itemBinder != null) {
			list = itemBinder.getBinderList(model, position);
		} else {
			list = null;
		}

		return list;
	}

	/**
	 * Computes the position of the item and the position of the binder within the item's binder list.
	 * Note that this is an <i>O(n)</i> operation.
	 *
	 * @param viewHolderPosition
	 * 		the position of the view holder in the adapter.
	 * @return the item position and the position of the binder in the item's binder list.
	 */
	@VisibleForTesting
	BinderResult computeItemAndBinderIndex(final int viewHolderPosition) {
		// subtract off the length of each list until we get to the desired item

		final int itemIndex = mViewHolderToItemPositionCache.get(viewHolderPosition);
		final T item = mItems.get(itemIndex);
		final List<Provider<Binder<? super T, VH, ? extends VH>>> binders = mBinderListCache.get(itemIndex);
		// index of the first item in the set of viewholders for the current item.
		final int firstVHPosForItem = mItemPositionToFirstViewHolderPositionCache.get(itemIndex);

		return new BinderResult(item, itemIndex, binders, viewHolderPosition - firstVHPosForItem);
	}

	@Override
	public int getItemViewType(final int position) {
		return internalGetItemViewType(position);
	}

	/**
	 * Return the view type of the item at <code>position</code> for the purposes
	 * of view recycling.
	 *
	 * @param viewHolderPosition
	 * 		position to query
	 * @return integer value identifying the type of the view needed to represent the item at
	 * <code>position</code>. Type codes need not be contiguous.
	 */
	protected int internalGetItemViewType(final int viewHolderPosition) {
		final BinderResult result = computeItemAndBinderIndex(viewHolderPosition);

		final Provider<Binder<? super T, VH, ? extends VH>> binder = result.getBinder();
		final int viewType;

		if (binder != null) {
			viewType = getViewHolderCreatorMap().get(binder.get().getViewType(result.item)).getViewType();
		} else {
			viewType = -1;
		}

		return viewType;
	}

	/**
	 * @param viewType
	 * 		the internal viewtype.
	 * @return the viewholder class.
	 */
	protected Class<? extends VH> getViewHolderClass(final int viewType) {
		return mViewTypeToViewHolderClassMap.get(viewType);
	}

	@NonNull
	protected Map<Integer, ViewHolderCreator> getViewHolderCreatorMap() {
		return mViewHolderCreatorMap;
	}

	@Override
	public VH onCreateViewHolder(final ViewGroup parent, final int viewType) {
		return (VH) getViewHolderCreatorMap().get(viewType).create(parent);
	}

	@Override
	@SuppressLint("RecyclerView")
	public void onBindViewHolder(final VH holder, final int viewHolderPosition) {

		final BinderResult result = computeItemAndBinderIndex(viewHolderPosition);
		final Binder binder = result.getBinder().get();

		if (binder != null && result.item != null) {

			if (mPreviousBoundViewHolderPosition == NO_PREVIOUS_BOUND_VIEWHOLDER) {
				prepare(viewHolderPosition, binder, result.item, result.binderList, result.binderIndex);
			}

			binder.bind(result.item, holder, result.binderList, result.binderIndex, getActionListener(result.item));

			prepareInternal(viewHolderPosition);
			mPreviousBoundViewHolderPosition = viewHolderPosition;
		}
	}

	private void prepareInternal(final int viewHolderPosition) {
		prepare(viewHolderPosition, Integer.signum(viewHolderPosition - mPreviousBoundViewHolderPosition));
	}

	/**
	 * Calls {@link #prepare(int, Binder, Object, List, int)}.
	 *
	 * @param lastBoundViewHolderPosition
	 * 		the position of the last viewholder that was bound.
	 * @param direction
	 * 		the direction the list is moving.
	 */
	protected void prepare(final int lastBoundViewHolderPosition, final int direction) {
		for (int i = 1; i <= numViewHoldersToPrepare(); i++) {
			final int viewHolderPosition = lastBoundViewHolderPosition + direction * i;
			if (isViewHolderPositionWithinBounds(viewHolderPosition)) {
				final BinderResult result = computeItemAndBinderIndex(viewHolderPosition);
				final Binder binder = result.getBinder().get();

				if (binder != null && result.item != null) {
					prepare(viewHolderPosition, binder, result.item, result.binderList, result.binderIndex);
				}
			}
		}
	}

	/**
	 * Calls {@link Binder#prepare(Object, List, int)}.
	 *
	 * @param viewHolderPosition
	 * 		the position of the viewholder.
	 * @param binder
	 * 		the binder to call.
	 * @param model
	 * 		the model being prepared.
	 * @param binderList
	 * 		the list of binders
	 * @param binderIndex
	 * 		the index in the list of viewholders associated with this model
	 */
	protected void prepare(final int viewHolderPosition,
	                       final Binder<T, VH, ? extends VH> binder,
	                       final T model,
	                       final List<Provider<Binder<? super T, VH, ? extends VH>>> binderList,
	                       final int binderIndex) {
		if (!mViewHolderPreparedCache.contains(viewHolderPosition)) {
			binder.prepare(model, binderList, binderIndex);
			mViewHolderPreparedCache.add(viewHolderPosition);
		}
	}

	/**
	 * @return Number of viewholders to prepare ahead.
	 */
	@SuppressWarnings("checkstyle:magicnumber")
	protected int numViewHoldersToPrepare() {
		return 3;
	}

	/**
	 * Checks if the timeline position is within the bounds of the underlying List
	 *
	 * @param itemPosition
	 * 		timeline item position.
	 * @return true if within list bounds. False otherwise.
	 */
	protected boolean isItemPositionWithinBounds(final int itemPosition) {
		return itemPosition >= 0 && itemPosition < mItems.size();
	}

	/**
	 * Checks if the viewholder is within the bounds of underlying list.
	 *
	 * @param viewHolderPosition
	 * 		viewholder position
	 * @return true of within list bound. False otherwise.
	 */
	protected boolean isViewHolderPositionWithinBounds(final int viewHolderPosition) {
		return viewHolderPosition >= 0 && viewHolderPosition < mViewHolderToItemPositionCache.size();
	}

	/**
	 * Note that this is an <i>O(n)</i> operation, but it does not query for the list of binders.
	 *
	 * @param itemPosition
	 * 		the position in the list of items.
	 * @return the number of viewholders before the given item position.
	 */
	@VisibleForTesting
	public int getViewHolderCount(final int itemPosition) {
		if (itemPosition >= 0 && !mItemPositionToFirstViewHolderPositionCache.isEmpty()) {
			if (itemPosition >= mItemPositionToFirstViewHolderPositionCache.size()) {
				return mViewHolderToItemPositionCache.size();
			} else {
				return mItemPositionToFirstViewHolderPositionCache.get(itemPosition);
			}
		} else {
			return 0;
		}
	}

	@Override
	public int getItemCount() {
		return mViewHolderToItemPositionCache.size();
	}

	/**
	 * Note that this does not notify.
	 *
	 * @param item
	 * 		the item to add to the adapter.
	 */
	public void add(@NonNull final T item) {
		add(mItems.size(), item, true);
	}

	/**
	 * @param item
	 * 		the item to add to the adapter.
	 * @param notify
	 * 		whether or not to notify the adapter.
	 */
	public void add(@NonNull final T item, final boolean notify) {
		add(mItems.size(), item, notify);
	}

	/**
	 * This is an <i>O(1)</i> operation since it is cached.
	 *
	 * @param viewHolderPosition
	 * 		the position in the view holder.
	 * @return the position of the item in the list of items.
	 */
	public int getItemPosition(final int viewHolderPosition) {
		if (isViewHolderPositionWithinBounds(viewHolderPosition)) {
			return mViewHolderToItemPositionCache.get(viewHolderPosition);
		} else {
			return -1;
		}
	}

	/**
	 * @param itemIndex
	 * 		the current view holders binder position.
	 * @param viewHolderPosition
	 * 		the view holder position.
	 * @return the binder index associated with view holder.
	 */
	public int getBinderPosition(final int itemIndex, final int viewHolderPosition) {
		return viewHolderPosition - mItemPositionToFirstViewHolderPositionCache.get(itemIndex);
	}

	/**
	 * Note that this is an <i>O(n)</i> operation, since the cache needs to be updated.
	 *
	 * @param position
	 * 		the position to insert into the list.
	 * @param item
	 * 		the item to add. Note that if it is <code>null</code>, there is no way to determine which binder to use.
	 * @param notify
	 * 		whether or not to notify the adapter.
	 */
	public void add(final int position, @NonNull final T item, final boolean notify) {
		final int numViewHolders = getViewHolderCount(position);

		final List<Provider<Binder<? super T, VH, ? extends VH>>> binders = getParts(item, position);

		mItems.add(position, item);
		mBinderListCache.add(position, binders);

		if (binders != null) {
			if (notify) {
				notifyItemRangeInserted(numViewHolders, binders.size());
			}

			final List<Integer> itemPositions = new ArrayList<>();
			for (int i = 0; i < binders.size(); i++) {
				itemPositions.add(position);
			}

			mViewHolderToItemPositionCache.addAll(numViewHolders, itemPositions);
			for (int viewHolderIndex = numViewHolders + binders.size(); viewHolderIndex < mViewHolderToItemPositionCache.size();
			     viewHolderIndex++) {
				mViewHolderToItemPositionCache.set(viewHolderIndex, mViewHolderToItemPositionCache.get(viewHolderIndex) + 1);
				mViewHolderPreparedCache.remove(viewHolderIndex);
			}

			mItemPositionToFirstViewHolderPositionCache.add(position, numViewHolders);
			for (int itemIndex = position + 1; itemIndex < mItemPositionToFirstViewHolderPositionCache.size(); itemIndex++) {
				mItemPositionToFirstViewHolderPositionCache.set(itemIndex,
						mItemPositionToFirstViewHolderPositionCache.get(itemIndex) + binders.size());
			}
		}
	}

	/**
	 * Note that this is an <i>O(n)</i> operation, since the cache needs to be updated.
	 *
	 * @param itemPosition
	 * 		removes the item at the position from the adapter.
	 * @return the removed item, or <code>null</code> if the position was out of bounds.
	 */
	@Nullable
	public T remove(final int itemPosition) {
		return remove(itemPosition, true);
	}

	/**
	 * Note that this is an <i>O(n)</i> operation, since the cache needs to be updated.
	 *
	 * @param itemPosition
	 * 		removes the item at the position from the adapter.
	 * @param notify
	 * 		whether or not to call {@link #notifyItemRangeRemoved(int, int)}
	 * @return the removed item, or <code>null</code> if the position was out of bounds.
	 */
	@Nullable
	public T remove(final int itemPosition, final boolean notify) {

		final T item;

		if (isItemPositionWithinBounds(itemPosition)) {
			final int numViewHolders = getViewHolderCount(itemPosition);

			item = mItems.get(itemPosition);

			final List<Provider<Binder<? super T, VH, ? extends VH>>> binders = mBinderListCache.get(itemPosition);

			mItems.remove(itemPosition);

			for (final ListIterator<Integer> iter = mViewHolderToItemPositionCache.listIterator(); iter.hasNext(); ) {
				if (iter.next() == itemPosition) {
					iter.remove();
				}
			}

			for (int viewHolderIndex = numViewHolders; viewHolderIndex < mViewHolderToItemPositionCache.size();
			     viewHolderIndex++) {
				mViewHolderToItemPositionCache.set(viewHolderIndex, mViewHolderToItemPositionCache.get(viewHolderIndex) - 1);
				mViewHolderPreparedCache.remove(viewHolderIndex);
			}

			mItemPositionToFirstViewHolderPositionCache.remove(itemPosition);

			if (binders != null) {
				for (int itemIndex = itemPosition; itemIndex < mItemPositionToFirstViewHolderPositionCache.size(); itemIndex++) {
					mItemPositionToFirstViewHolderPositionCache.set(itemIndex,
							mItemPositionToFirstViewHolderPositionCache.get(itemIndex) - binders.size());
				}
			}

			mBinderListCache.remove(itemPosition);

			if (binders != null && notify) {
				notifyItemRangeRemoved(numViewHolders, binders.size());
			}
		} else {
			item = null;
		}

		return item;
	}

	/**
	 * Finds the adapter data position for the first instance of a particular view holder used in an item.
	 *
	 * @param itemPosition
	 * 		the position for the item that uses the view holder.
	 * @param viewHolderClass
	 * 		the view holder type to look for.
	 * @return the adapter data position for the view holder, or -1 if not found.
	 */
	public int getFirstViewHolderPosition(final int itemPosition, @NonNull final Class<? extends VH> viewHolderClass) {
		if (isItemPositionWithinBounds(itemPosition) && mViewHolderClassToViewTypeMap.containsKey(viewHolderClass)) {
			final int itemStartPos = getViewHolderCount(itemPosition);
			int viewHolderIndex = 0;
			final List<Provider<Binder<? super T, VH, ? extends VH>>> binders = mBinderListCache.get(itemPosition);
			final int viewType = mViewHolderClassToViewTypeMap.get(viewHolderClass);
			final T item = mItems.get(itemPosition);
			for (Provider<Binder<? super T, VH, ? extends VH>> binder : binders) {
				if (binder.get().getViewType(item) == viewType) {
					return itemStartPos + viewHolderIndex;
				}
				viewHolderIndex++;
			}
		}
		return -1;
	}

	/**
	 * Clears the adapter.
	 */
	public void clear() {
		mItems.clear();
		mBinderListCache.clear();
		mViewHolderToItemPositionCache.clear();
		mItemPositionToFirstViewHolderPositionCache.clear();
		mViewHolderPreparedCache.clear();
		mPreviousBoundViewHolderPosition = NO_PREVIOUS_BOUND_VIEWHOLDER;
	}

	/**
	 * This is very similar to {@link View#inflate(android.content.Context, int, ViewGroup)}
	 * but does not attach the inflated view.
	 *
	 * @param parent
	 * 		the parent viewgroup
	 * @param layoutRes
	 * 		the layout to inflate
	 * @return the inflated and unattached view.
	 */
	public static View inflate(final ViewGroup parent, @LayoutRes final int layoutRes) {
		return LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false);
	}

	@Override
	public void onViewRecycled(final VH holder) {
		super.onViewRecycled(holder);

		onViewRecycled(holder, holder.getAdapterPosition());
	}

	/**
	 * @param holder
	 * 		the viewholder to recycle.
	 * @param viewHolderPosition
	 * 		the position of the viewholder.
	 */
	protected void onViewRecycled(final VH holder, final int viewHolderPosition) {
		if (isViewHolderPositionWithinBounds(viewHolderPosition)) {
			final BinderResult result = computeItemAndBinderIndex(viewHolderPosition);
			final Binder binder = result.getBinder().get();

			if (binder != null) {
				mViewHolderPreparedCache.remove(viewHolderPosition);

				if (holder.getItemViewType() == binder.getViewType(result.item)) {
					binder.unbind(holder);
				}
			}
		}
	}

	@NonNull
	public List<T> getItems() {
		return mItems;
	}

	/**
	 * @param itemPosition
	 * 		the item position.
	 * @return the binders that belong to the item at the given position.
	 */
	@Nullable
	public List<Provider<Binder<? super T, VH, ? extends VH>>> getBindersForPosition(final int itemPosition) {
		final List<Provider<Binder<? super T, VH, ? extends VH>>> binders;

		if (isItemPositionWithinBounds(itemPosition)) {
			binders = mBinderListCache.get(itemPosition);
		} else {
			binders = null;
		}

		return binders;
	}

	/**
	 * @param itemPosition
	 * 		the item position.
	 * @return the range of viewholders that represent the item. The first is the offset, the second is the count.
	 */
	@Nullable
	public Pair<Integer, Integer> getViewHolderRange(final int itemPosition) {
		final Pair<Integer, Integer> range;

		if (isItemPositionWithinBounds(itemPosition)) {
			final int numViewHolders = getViewHolderCount(itemPosition);

			final List<Provider<Binder<? super T, VH, ? extends VH>>> binders = mBinderListCache.get(itemPosition);

			range = new Pair<>(numViewHolders, binders.size());
		} else {
			range = null;
		}

		return range;
	}

	/**
	 * Binds a model of type {@code U} to a viewholder of type {@code V}.
	 *
	 * @param <U>
	 * 		the model.
	 * @param <V>
	 * 		the viewholder type of the adapter.
	 * @param <W>
	 * 		the viewholder type to be bound.
	 */
	public interface Binder<U, V extends RecyclerView.ViewHolder, W extends V> {

		/**
		 * @param model
		 * 		the model that will be bound.
		 * @return the type of the viewholder.
		 */
		@AnyRes
		int getViewType(U model);

		/**
		 * Called to notify this binder that it may be called soon in the future. It may be called multiple times
		 * before the view is actually ready to be bound. It is useful for preloading images.
		 *
		 * @param model
		 * 		the model that will be bound.
		 * @param binderList
		 * 		the list of binders.
		 * @param binderIndex
		 * 		the index of the binder in the list of binders.
		 */
		void prepare(@NonNull U model, List<Provider<Binder<? super U, V, ? extends V>>> binderList, int binderIndex);

		/**
		 * Called when {@link android.support.v7.widget.RecyclerView.Adapter#onBindViewHolder(RecyclerView.ViewHolder,
		 * int)}
		 * is called.
		 *
		 * @param model
		 * 		the model to bind to the viewholder
		 * @param holder
		 * 		the viewholder to update
		 * @param binderList
		 * 		the list of binders
		 * @param binderIndex
		 * 		the index in the list of viewholders associated with this model
		 * @param actionListener
		 * 		the action listener to use
		 */
		void bind(@NonNull U model, @NonNull W holder, @NonNull List<Provider<Binder<? super U, V, ? extends V>>> binderList,
		          int binderIndex, @Nullable ActionListener<U, V, W> actionListener);

		/**
		 * Called when {@link android.support.v7.widget.RecyclerView.Adapter#onViewRecycled(RecyclerView.ViewHolder)}
		 * is called.
		 *
		 * @param holder
		 * 		the view holder that was recycled.
		 */
		void unbind(@NonNull W holder);
	}

	/**
	 * Creates a viewholder.
	 */
	public interface ViewHolderCreator {
		/**
		 * Called when {@link android.support.v7.widget.RecyclerView.Adapter#onCreateViewHolder(ViewGroup, int)}
		 * is called.
		 *
		 * @param parent
		 * 		the parent view.
		 * @return the inflated view.
		 */
		RecyclerView.ViewHolder create(ViewGroup parent);

		/**
		 * In nearly all cases, this should simply return the layout id.
		 *
		 * @return the view type to associate with this {@link android.support.v7.widget.RecyclerView.ViewHolder}.
		 */
		int getViewType();
	}

	/**
	 * Gets the list of binders associated with an item.
	 *
	 * @param <U>
	 * 		the model type.
	 * @param <V>
	 * 		the viewholder type.
	 * @param <B>
	 * 		the binder type.
	 */
	public interface ItemBinder<U, V extends RecyclerView.ViewHolder, B extends GraywaterAdapter.Binder<U, V, ? extends V>> {
		/**
		 * @param model
		 * 		the model that will be bound.
		 * @param position
		 * 		the position of the model in the list.
		 * @return the list of binders to use.
		 */
		@NonNull
		List<Provider<? extends B>> getBinderList(@NonNull U model, int position);
	}

	/**
	 * @param <U>
	 * 		the model type.
	 * @param <V>
	 * 		the viewholder type from the adapter.
	 * @param <W>
	 * 		the viewholder type.
	 */
	public interface ActionListener<U, V extends RecyclerView.ViewHolder, W extends V> {
		/**
		 * @param model
		 * 		the model associated with the view that was modified.
		 * @param holder
		 * 		the viewholder associated with the view that was touched.
		 * @param v
		 * 		the view that was touched.
		 * @param binderList
		 * 		the list of binders associated with the model.
		 * @param binderIndex
		 * 		the index of the binder that was modified.
		 * @param obj
		 * 		an extra object for message passing.
		 */
		void act(@NonNull U model, @NonNull W holder, @NonNull View v,
		         @NonNull List<Provider<Binder<? super U, V, ? extends V>>> binderList,
		         int binderIndex, @Nullable Object obj);
	}

	/**
	 * A helper {@link android.view.View.OnClickListener} that can be used to hold references to objects that are
	 * passed in during a {@link Binder#bind(Object, RecyclerView.ViewHolder, List, int, ActionListener)}.
	 * <p>
	 * Note that it uses strong references.
	 *
	 * @param <U>
	 * 		the model type.
	 * @param <V>
	 * 		the viewholder type of the adapter.
	 * @param <W>
	 * 		the viewholder type to be bound.
	 */
	public static class ActionListenerDelegate<U, V extends RecyclerView.ViewHolder, W extends V>
			implements View.OnClickListener {
		/**
		 * The model.
		 */
		public U model;
		/**
		 * The viewholder.
		 */
		public W holder;
		/**
		 * The list of binders.
		 */
		public List<Provider<Binder<? super U, V, ? extends V>>> binders;
		/**
		 * The index into the list of binders.
		 */
		public int binderIndex;
		/**
		 * A spare object to pass around.
		 */
		@Nullable
		public Object obj;
		/**
		 * The listener to call on click.
		 */
		public ActionListener<U, V, W> actionListener;

		/**
		 * @param actionListener
		 * 		the listener to call on click.
		 * @param model
		 * 		the model that is being clicked.
		 * @param holder
		 * 		the view holder that is being clicked.
		 * @param binders
		 * 		the list of binders associated with the model.
		 * @param binderIndex
		 * 		the index into the list of binders of the view holder that is being clicked.
		 * @param obj
		 * 		an extra object that can be used to pass around extra data.
		 */
		public void update(final ActionListener<U, V, W> actionListener,
		                   @NonNull final U model, @NonNull final W holder,
		                   @NonNull final List<Provider<Binder<? super U, V, ? extends V>>> binders, final int binderIndex,
		                   @Nullable final Object obj) {
			this.model = model;
			this.holder = holder;
			this.binders = binders;
			this.binderIndex = binderIndex;
			this.obj = obj;
			this.actionListener = actionListener;
		}

		@Override
		public void onClick(final View v) {
			actionListener.act(model, holder, v, binders, binderIndex, obj);
		}
	}

	/**
	 * Internal class that holds the item and binder associated with a viewholder.
	 */
	@VisibleForTesting
	final class BinderResult {
		/**
		 * The item associated with the viewholder.
		 */
		@Nullable
		public final T item;
		/**
		 * The position of th item in the list of items.
		 */
		@VisibleForTesting
		public final int itemPosition;
		/**
		 * The list of binders associated with the item.
		 */
		@Nullable
		public final List<Provider<Binder<? super T, VH, ? extends VH>>> binderList;
		/**
		 * The index of the binder to use in the list of binders.
		 */
		public final int binderIndex;

		/**
		 * @param item
		 * 		the model.
		 * @param itemPosition
		 * 		the position of the model in the list of models.
		 * @param binderList
		 * 		the list of binders associated with the item.
		 * @param binderIndex
		 * 		the index of the specific binder to use in the {@code binderList}.
		 */
		BinderResult(@Nullable final T item,
		             final int itemPosition,
		             @Nullable final List<Provider<Binder<? super T, VH, ? extends VH>>> binderList,
		             final int binderIndex) {
			this.item = item;
			this.itemPosition = itemPosition;
			this.binderList = binderList;
			this.binderIndex = binderIndex;
		}

		/**
		 * @return the binder to use.
		 */
		@Nullable
		public Provider<Binder<? super T, VH, ? extends VH>> getBinder() {
			return binderList != null && binderIndex >= 0 && binderIndex < binderList.size()
					? binderList.get(binderIndex) : null;
		}
	}
}