/*
 The MIT License (MIT)

Copyright (c) 2014 Wireless Designs, LLC
Modifications Copyright(C) 2016 Fred Grott(GrottWorkShop)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

 */
package com.github.shareme.blackandroids.greenandroid.rvextensions;

import android.content.Context;
import android.graphics.PointF;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;

import java.util.HashSet;
import java.util.List;

import timber.log.Timber;

/**
 * A {@link RecyclerView.LayoutManager} implementation
 * that places children in a two-dimensional grid, sized to a fixed column count
 * value. User scrolling is possible in both horizontal and vertical directions
 * to view the data set.
 *
 * <p>The column count is controllable via {@link #setTotalColumnCount(int)}. The layout manager
 * will generate the number of rows necessary to accommodate the data set based on
 * the fixed column count.
 *
 * <p>This manager does make some assumptions to simplify the implementation:
 * <ul>
 *     <li>All child views are assumed to be the same size</li>
 *     <li>The window of visible views is a constant</li>
 * </ul>
 *
 * Created by fgrott on 9/15/2016.
 */
@SuppressWarnings("unused")
public class FixedGridLayoutManager extends RecyclerView.LayoutManager {

  private static final String TAG = FixedGridLayoutManager.class.getSimpleName();

  private static final int DEFAULT_COUNT = 1;

  /* View Removal Constants */
  private static final int REMOVE_VISIBLE = 0;
  private static final int REMOVE_INVISIBLE = 1;

  /* Fill Direction Constants */
  private static final int DIRECTION_NONE = -1;
  private static final int DIRECTION_START = 0;
  private static final int DIRECTION_END = 1;
  private static final int DIRECTION_UP = 2;
  private static final int DIRECTION_DOWN = 3;

  /* First (top-left) position visible at any point */
  private int mFirstVisiblePosition;
  /* Consistent size applied to all child views */
  private int mDecoratedChildWidth;
  private int mDecoratedChildHeight;
  /* Number of columns that exist in the grid */
  private int mTotalColumnCount = DEFAULT_COUNT;
  /* Metrics for the visible window of our data */
  private int mVisibleColumnCount;
  private int mVisibleRowCount;

  /* Used for tracking off-screen change events */
  private int mFirstChangedPosition;
  private int mChangedPositionCount;

  /**
   * Set the number of columns the layout manager will use. This will
   * trigger a layout update.
   * @param count Number of columns.
   */
  public void setTotalColumnCount(int count) {
    mTotalColumnCount = count;
    requestLayout();
  }

  /*
   * You must return true from this method if you want your
   * LayoutManager to support anything beyond "simple" item
   * animations. Enabling this causes onLayoutChildren() to
   * be called twice on each animated change; once for a
   * pre-layout, and again for the real layout.
   */
  @Override
  public boolean supportsPredictiveItemAnimations() {
    return true;
  }

  /*
   * Called by RecyclerView when a view removal is triggered. This is called
   * before onLayoutChildren() in pre-layout if the views removed are not visible. We
   * use it in this case to inform pre-layout that a removal took place.
   *
   * This method is still called if the views removed were visible, but it will
   * happen AFTER pre-layout.
   */
  @Override
  public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
    mFirstChangedPosition = positionStart;
    mChangedPositionCount = itemCount;
  }

  /*
   * This method is your initial call from the framework. You will receive it when you
   * need to start laying out the initial set of views. This method will not be called
   * repeatedly, so don't rely on it to continually process changes during user
   * interaction.
   *
   * This method will be called when the data set in the adapter changes, so it can be
   * used to update a layout based on a new item count.
   *
   * If predictive animations are enabled, you will see this called twice. First, with
   * state.isPreLayout() returning true to lay out children in their initial conditions.
   * Then again to lay out children in their final locations.
   */
  @Override
  public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    //We have nothing to show for an empty data set but clear any existing views
    if (getItemCount() == 0) {
      detachAndScrapAttachedViews(recycler);
      return;
    }
    if (getChildCount() == 0 && state.isPreLayout()) {
      //Nothing to do during prelayout when empty
      return;
    }

    //Clear change tracking state when a real layout occurs
    if (!state.isPreLayout()) {
      mFirstChangedPosition = mChangedPositionCount = 0;
    }

    if (getChildCount() == 0) { //First or empty layout
      //Scrap measure one child
      View scrap = recycler.getViewForPosition(0);
      addView(scrap);
      measureChildWithMargins(scrap, 0, 0);

            /*
             * We make some assumptions in this code based on every child
             * view being the same size (i.e. a uniform grid). This allows
             * us to compute the following values up front because they
             * won't change.
             */
      mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
      mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);

      detachAndScrapView(scrap, recycler);
    }

    //Always update the visible row/column counts
    updateWindowSizing();

    SparseIntArray removedCache = null;
        /*
         * During pre-layout, we need to take note of any views that are
         * being removed in order to handle predictive animations
         */
    if (state.isPreLayout()) {
      removedCache = new SparseIntArray(getChildCount());
      for (int i=0; i < getChildCount(); i++) {
        final View view = getChildAt(i);
        LayoutParams lp = (LayoutParams) view.getLayoutParams();

        if (lp.isItemRemoved()) {
          //Track these view removals as visible
          removedCache.put(lp.getViewLayoutPosition(), REMOVE_VISIBLE);
        }
      }

      //Track view removals that happened out of bounds (i.e. off-screen)
      if (removedCache.size() == 0 && mChangedPositionCount > 0) {
        for (int i = mFirstChangedPosition; i < (mFirstChangedPosition + mChangedPositionCount); i++) {
          removedCache.put(i, REMOVE_INVISIBLE);
        }
      }
    }


    int childLeft;
    int childTop;
    if (getChildCount() == 0) { //First or empty layout
      //Reset the visible and scroll positions
      mFirstVisiblePosition = 0;
      childLeft = getPaddingLeft();
      childTop = getPaddingTop();
    } else if (!state.isPreLayout()
            && getVisibleChildCount() >= state.getItemCount()) {
      //Data set is too small to scroll fully, just reset position
      mFirstVisiblePosition = 0;
      childLeft = getPaddingLeft();
      childTop = getPaddingTop();
    } else { //Adapter data set changes
            /*
             * Keep the existing initial position, and save off
             * the current scrolled offset.
             */
      final View topChild = getChildAt(0);
      childLeft = getDecoratedLeft(topChild);
      childTop = getDecoratedTop(topChild);

            /*
             * When data set is too small to scroll vertically, adjust vertical offset
             * and shift position to the first row, preserving current column
             */
      if (!state.isPreLayout() && getVerticalSpace() > (getTotalRowCount() * mDecoratedChildHeight)) {
        mFirstVisiblePosition = mFirstVisiblePosition % getTotalColumnCount();
        childTop = getPaddingTop();

        //If the shift overscrolls the column max, back it off
        if ((mFirstVisiblePosition + mVisibleColumnCount) > state.getItemCount()) {
          mFirstVisiblePosition = Math.max(state.getItemCount() - mVisibleColumnCount, 0);
          childLeft = getPaddingLeft();
        }
      }

            /*
             * Adjust the visible position if out of bounds in the
             * new layout. This occurs when the new item count in an adapter
             * is much smaller than it was before, and you are scrolled to
             * a location where no items would exist.
             */
      int maxFirstRow = getTotalRowCount() - (mVisibleRowCount-1);
      int maxFirstCol = getTotalColumnCount() - (mVisibleColumnCount-1);
      boolean isOutOfRowBounds = getFirstVisibleRow() > maxFirstRow;
      boolean isOutOfColBounds =  getFirstVisibleColumn() > maxFirstCol;
      if (isOutOfRowBounds || isOutOfColBounds) {
        int firstRow;
        if (isOutOfRowBounds) {
          firstRow = maxFirstRow;
        } else {
          firstRow = getFirstVisibleRow();
        }
        int firstCol;
        if (isOutOfColBounds) {
          firstCol = maxFirstCol;
        } else {
          firstCol = getFirstVisibleColumn();
        }
        mFirstVisiblePosition = firstRow * getTotalColumnCount() + firstCol;

        childLeft = getHorizontalSpace() - (mDecoratedChildWidth * mVisibleColumnCount);
        childTop = getVerticalSpace() - (mDecoratedChildHeight * mVisibleRowCount);

        //Correct cases where shifting to the bottom-right overscrolls the top-left
        // This happens on data sets too small to scroll in a direction.
        if (getFirstVisibleRow() == 0) {
          childTop = Math.min(childTop, getPaddingTop());
        }
        if (getFirstVisibleColumn() == 0) {
          childLeft = Math.min(childLeft, getPaddingLeft());
        }
      }
    }

    //Clear all attached views into the recycle bin
    detachAndScrapAttachedViews(recycler);

    //Fill the grid for the initial layout of views
    fillGrid(DIRECTION_NONE, childLeft, childTop, recycler, state, removedCache);

    //Evaluate any disappearing views that may exist
    if (!state.isPreLayout() && !recycler.getScrapList().isEmpty()) {
      final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
      final HashSet<View> disappearingViews = new HashSet<View>(scrapList.size());

      for (RecyclerView.ViewHolder holder : scrapList) {
        final View child = holder.itemView;
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.isItemRemoved()) {
          disappearingViews.add(child);
        }
      }

      for (View child : disappearingViews) {
        layoutDisappearingView(child);
      }
    }
  }

  @Override
  public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
    //Completely scrap the existing layout
    removeAllViews();
  }

  /*
   * Rather than continuously checking how many views we can fit
   * based on scroll offsets, we simplify the math by computing the
   * visible grid as what will initially fit on screen, plus one.
   */
  private void updateWindowSizing() {
    mVisibleColumnCount = (getHorizontalSpace() / mDecoratedChildWidth) + 1;
    if (getHorizontalSpace() % mDecoratedChildWidth > 0) {
      mVisibleColumnCount++;
    }

    //Allow minimum value for small data sets
    if (mVisibleColumnCount > getTotalColumnCount()) {
      mVisibleColumnCount = getTotalColumnCount();
    }


    mVisibleRowCount = (getVerticalSpace()/ mDecoratedChildHeight) + 1;
    if (getVerticalSpace() % mDecoratedChildHeight > 0) {
      mVisibleRowCount++;
    }

    if (mVisibleRowCount > getTotalRowCount()) {
      mVisibleRowCount = getTotalRowCount();
    }
  }

  private void fillGrid(int direction, RecyclerView.Recycler recycler, RecyclerView.State state) {
    fillGrid(direction, 0, 0, recycler, state, null);
  }

  private void fillGrid(int direction, int emptyLeft, int emptyTop,
                        RecyclerView.Recycler recycler,
                        RecyclerView.State state,
                        SparseIntArray removedPositions) {
    if (mFirstVisiblePosition < 0) mFirstVisiblePosition = 0;
    if (mFirstVisiblePosition >= getItemCount()) mFirstVisiblePosition = (getItemCount() - 1);

        /*
         * First, we will detach all existing views from the layout.
         * detachView() is a lightweight operation that we can use to
         * quickly reorder views without a full add/remove.
         */
    SparseArray<View> viewCache = new SparseArray<View>(getChildCount());
    int startLeftOffset = emptyLeft;
    int startTopOffset = emptyTop;
    if (getChildCount() != 0) {
      final View topView = getChildAt(0);
      startLeftOffset = getDecoratedLeft(topView);
      startTopOffset = getDecoratedTop(topView);
      switch (direction) {
        case DIRECTION_START:
          startLeftOffset -= mDecoratedChildWidth;
          break;
        case DIRECTION_END:
          startLeftOffset += mDecoratedChildWidth;
          break;
        case DIRECTION_UP:
          startTopOffset -= mDecoratedChildHeight;
          break;
        case DIRECTION_DOWN:
          startTopOffset += mDecoratedChildHeight;
          break;
      }

      //Cache all views by their existing position, before updating counts
      for (int i=0; i < getChildCount(); i++) {
        int position = positionOfIndex(i);
        final View child = getChildAt(i);
        viewCache.put(position, child);
      }

      //Temporarily detach all views.
      // Views we still need will be added back at the proper index.
      for (int i=0; i < viewCache.size(); i++) {
        detachView(viewCache.valueAt(i));
      }
    }

        /*
         * Next, we advance the visible position based on the fill direction.
         * DIRECTION_NONE doesn't advance the position in any direction.
         */
    switch (direction) {
      case DIRECTION_START:
        mFirstVisiblePosition--;
        break;
      case DIRECTION_END:
        mFirstVisiblePosition++;
        break;
      case DIRECTION_UP:
        mFirstVisiblePosition -= getTotalColumnCount();
        break;
      case DIRECTION_DOWN:
        mFirstVisiblePosition += getTotalColumnCount();
        break;
    }

        /*
         * Next, we supply the grid of items that are deemed visible.
         * If these items were previously there, they will simply be
         * re-attached. New views that must be created are obtained
         * from the Recycler and added.
         */
    int leftOffset = startLeftOffset;
    int topOffset = startTopOffset;

    for (int i = 0; i < getVisibleChildCount(); i++) {
      int nextPosition = positionOfIndex(i);

            /*
             * When a removal happens out of bounds, the pre-layout positions of items
             * after the removal are shifted to their final positions ahead of schedule.
             * We have to track off-screen removals and shift those positions back
             * so we can properly lay out all current (and appearing) views in their
             * initial locations.
             */
      int offsetPositionDelta = 0;
      if (state.isPreLayout()) {
        int offsetPosition = nextPosition;

        for (int offset = 0; offset < removedPositions.size(); offset++) {
          //Look for off-screen removals that are less-than this
          if (removedPositions.valueAt(offset) == REMOVE_INVISIBLE
                  && removedPositions.keyAt(offset) < nextPosition) {
            //Offset position to match
            offsetPosition--;
          }
        }
        offsetPositionDelta = nextPosition - offsetPosition;
        nextPosition = offsetPosition;
      }

      if (nextPosition < 0 || nextPosition >= state.getItemCount()) {
        //Item space beyond the data set, don't attempt to add a view
        continue;
      }

      //Layout this position
      View view = viewCache.get(nextPosition);
      if (view == null) {
                /*
                 * The Recycler will give us either a newly constructed view,
                 * or a recycled view it has on-hand. In either case, the
                 * view will already be fully bound to the data by the
                 * adapter for us.
                 */
        view = recycler.getViewForPosition(nextPosition);
        addView(view);

                /*
                 * Update the new view's metadata, but only when this is a real
                 * layout pass.
                 */
        if (!state.isPreLayout()) {
          LayoutParams lp = (LayoutParams) view.getLayoutParams();
          lp.row = getGlobalRowOfPosition(nextPosition);
          lp.column = getGlobalColumnOfPosition(nextPosition);
        }

                /*
                 * It is prudent to measure/layout each new view we
                 * receive from the Recycler. We don't have to do
                 * this for views we are just re-arranging.
                 */
        measureChildWithMargins(view, 0, 0);
        layoutDecorated(view, leftOffset, topOffset,
                leftOffset + mDecoratedChildWidth,
                topOffset + mDecoratedChildHeight);

      } else {
        //Re-attach the cached view at its new index
        attachView(view);
        viewCache.remove(nextPosition);
      }

      if (i % mVisibleColumnCount == (mVisibleColumnCount - 1)) {
        leftOffset = startLeftOffset;
        topOffset += mDecoratedChildHeight;

        //During pre-layout, on each column end, apply any additional appearing views
        if (state.isPreLayout()) {
          layoutAppearingViews(recycler, view, nextPosition, removedPositions.size(), offsetPositionDelta);
        }
      } else {
        leftOffset += mDecoratedChildWidth;
      }
    }

        /*
         * Finally, we ask the Recycler to scrap and store any views
         * that we did not re-attach. These are views that are not currently
         * necessary because they are no longer visible.
         */
    for (int i=0; i < viewCache.size(); i++) {
      final View removingView = viewCache.valueAt(i);
      recycler.recycleView(removingView);
    }
  }

  /*
   * You must override this method if you would like to support external calls
   * to shift the view to a given adapter position. In our implementation, this
   * is the same as doing a fresh layout with the given position as the top-left
   * (or first visible), so we simply set that value and trigger onLayoutChildren()
   */
  @Override
  public void scrollToPosition(int position) {
    if (position >= getItemCount()) {
      Timber.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
      return;
    }

    //Set requested position as first visible
    mFirstVisiblePosition = position;
    //Toss all existing views away
    removeAllViews();
    //Trigger a new view layout
    requestLayout();
  }

  /*
   * You must override this method if you would like to support external calls
   * to animate a change to a new adapter position. The framework provides a
   * helper scroller implementation (LinearSmoothScroller), which we leverage
   * to do the animation calculations.
   */
  @Override
  public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, final int position) {
    if (position >= getItemCount()) {
      Timber.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
      return;
    }

        /*
         * LinearSmoothScroller's default behavior is to scroll the contents until
         * the child is fully visible. It will snap to the top-left or bottom-right
         * of the parent depending on whether the direction of travel was positive
         * or negative.
         */
    LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
      /*
       * LinearSmoothScroller, at a minimum, just need to know the vector
       * (x/y distance) to travel in order to get from the current positioning
       * to the target.
       */
      @Override
      public PointF computeScrollVectorForPosition(int targetPosition) {
        final int rowOffset = getGlobalRowOfPosition(targetPosition)
                - getGlobalRowOfPosition(mFirstVisiblePosition);
        final int columnOffset = getGlobalColumnOfPosition(targetPosition)
                - getGlobalColumnOfPosition(mFirstVisiblePosition);

        return new PointF(columnOffset * mDecoratedChildWidth, rowOffset * mDecoratedChildHeight);
      }
    };
    scroller.setTargetPosition(position);
    startSmoothScroll(scroller);
  }

  /*
   * Use this method to tell the RecyclerView if scrolling is even possible
   * in the horizontal direction.
   */
  @Override
  public boolean canScrollHorizontally() {
    //We do allow scrolling
    return true;
  }

  /*
   * This method describes how far RecyclerView thinks the contents should scroll horizontally.
   * You are responsible for verifying edge boundaries, and determining if this scroll
   * event somehow requires that new views be added or old views get recycled.
   */
  @Override
  public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() == 0) {
      return 0;
    }

    //Take leftmost measurements from the top-left child
    final View topView = getChildAt(0);
    //Take rightmost measurements from the top-right child
    final View bottomView = getChildAt(mVisibleColumnCount-1);

    //Optimize the case where the entire data set is too small to scroll
    int viewSpan = getDecoratedRight(bottomView) - getDecoratedLeft(topView);
    if (viewSpan < getHorizontalSpace()) {
      //We cannot scroll in either direction
      return 0;
    }

    int delta;
    boolean leftBoundReached = getFirstVisibleColumn() == 0;
    boolean rightBoundReached = getLastVisibleColumn() >= getTotalColumnCount();
    if (dx > 0) { // Contents are scrolling left
      //Check right bound
      if (rightBoundReached) {
        //If we've reached the last column, enforce limits
        int rightOffset = getHorizontalSpace() - getDecoratedRight(bottomView) + getPaddingRight();
        delta = Math.max(-dx, rightOffset);
      } else {
        //No limits while the last column isn't visible
        delta = -dx;
      }
    } else { // Contents are scrolling right
      //Check left bound
      if (leftBoundReached) {
        int leftOffset = -getDecoratedLeft(topView) + getPaddingLeft();
        delta = Math.min(-dx, leftOffset);
      } else {
        delta = -dx;
      }
    }

    offsetChildrenHorizontal(delta);

    if (dx > 0) {
      if (getDecoratedRight(topView) < 0 && !rightBoundReached) {
        fillGrid(DIRECTION_END, recycler, state);
      } else if (!rightBoundReached) {
        fillGrid(DIRECTION_NONE, recycler, state);
      }
    } else {
      if (getDecoratedLeft(topView) > 0 && !leftBoundReached) {
        fillGrid(DIRECTION_START, recycler, state);
      } else if (!leftBoundReached) {
        fillGrid(DIRECTION_NONE, recycler, state);
      }
    }

        /*
         * Return value determines if a boundary has been reached
         * (for edge effects and flings). If returned value does not
         * match original delta (passed in), RecyclerView will draw
         * an edge effect.
         */
    return -delta;
  }

  /*
   * Use this method to tell the RecyclerView if scrolling is even possible
   * in the vertical direction.
   */
  @Override
  public boolean canScrollVertically() {
    //We do allow scrolling
    return true;
  }

  /*
   * This method describes how far RecyclerView thinks the contents should scroll vertically.
   * You are responsible for verifying edge boundaries, and determining if this scroll
   * event somehow requires that new views be added or old views get recycled.
   */
  @Override
  public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() == 0) {
      return 0;
    }

    //Take top measurements from the top-left child
    final View topView = getChildAt(0);
    //Take bottom measurements from the bottom-right child.
    final View bottomView = getChildAt(getChildCount()-1);

    //Optimize the case where the entire data set is too small to scroll
    int viewSpan = getDecoratedBottom(bottomView) - getDecoratedTop(topView);
    if (viewSpan < getVerticalSpace()) {
      //We cannot scroll in either direction
      return 0;
    }

    int delta;
    int maxRowCount = getTotalRowCount();
    boolean topBoundReached = getFirstVisibleRow() == 0;
    boolean bottomBoundReached = getLastVisibleRow() >= maxRowCount;
    if (dy > 0) { // Contents are scrolling up
      //Check against bottom bound
      if (bottomBoundReached) {
        //If we've reached the last row, enforce limits
        int bottomOffset;
        if (rowOfIndex(getChildCount() - 1) >= (maxRowCount - 1)) {
          //We are truly at the bottom, determine how far
          bottomOffset = getVerticalSpace() - getDecoratedBottom(bottomView)
                  + getPaddingBottom();
        } else {
                    /*
                     * Extra space added to account for allowing bottom space in the grid.
                     * This occurs when the overlap in the last row is not large enough to
                     * ensure that at least one element in that row isn't fully recycled.
                     */
          bottomOffset = getVerticalSpace() - (getDecoratedBottom(bottomView)
                  + mDecoratedChildHeight) + getPaddingBottom();
        }

        delta = Math.max(-dy, bottomOffset);
      } else {
        //No limits while the last row isn't visible
        delta = -dy;
      }
    } else { // Contents are scrolling down
      //Check against top bound
      if (topBoundReached) {
        int topOffset = -getDecoratedTop(topView) + getPaddingTop();

        delta = Math.min(-dy, topOffset);
      } else {
        delta = -dy;
      }
    }

    offsetChildrenVertical(delta);

    if (dy > 0) {
      if (getDecoratedBottom(topView) < 0 && !bottomBoundReached) {
        fillGrid(DIRECTION_DOWN, recycler, state);
      } else if (!bottomBoundReached) {
        fillGrid(DIRECTION_NONE, recycler, state);
      }
    } else {
      if (getDecoratedTop(topView) > 0 && !topBoundReached) {
        fillGrid(DIRECTION_UP, recycler, state);
      } else if (!topBoundReached) {
        fillGrid(DIRECTION_NONE, recycler, state);
      }
    }

        /*
         * Return value determines if a boundary has been reached
         * (for edge effects and flings). If returned value does not
         * match original delta (passed in), RecyclerView will draw
         * an edge effect.
         */
    return -delta;
  }

  /*
   * This is a helper method used by RecyclerView to determine
   * if a specific child view can be returned.
   */
  @Override
  public View findViewByPosition(int position) {
    for (int i=0; i < getChildCount(); i++) {
      if (positionOfIndex(i) == position) {
        return getChildAt(i);
      }
    }

    return null;
  }

  /** Boilerplate to extend LayoutParams for tracking row/column of attached views */

    /*
     * Even without extending LayoutParams, we must override this method
     * to provide the default layout parameters that each child view
     * will receive when added.
     */
  @Override
  public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT);
  }
  @Override
  public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
    return new LayoutParams(c, attrs);
  }
  @Override
  public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
    if (lp instanceof ViewGroup.MarginLayoutParams) {
      return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
    } else {
      return new LayoutParams(lp);
    }
  }
  @Override
  public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
    return lp instanceof LayoutParams;
  }

  public static class LayoutParams extends RecyclerView.LayoutParams {

    //Current row in the grid
    public int row;
    //Current column in the grid
    public int column;

    public LayoutParams(Context c, AttributeSet attrs) {
      super(c, attrs);
    }
    public LayoutParams(int width, int height) {
      super(width, height);
    }
    public LayoutParams(ViewGroup.MarginLayoutParams source) {
      super(source);
    }
    public LayoutParams(ViewGroup.LayoutParams source) {
      super(source);
    }
    public LayoutParams(RecyclerView.LayoutParams source) {
      super(source);
    }
  }

  /** Animation Layout Helpers */

    /* Helper to obtain and place extra appearing views */
  private void layoutAppearingViews(RecyclerView.Recycler recycler, View referenceView, int referencePosition, int extraCount, int offset) {
    //Nothing to do...
    if (extraCount < 1) return;

    //FIXME: This code currently causes double layout of views that are still visibleâ€¦
    for (int extra = 1; extra <= extraCount; extra++) {
      //Grab the next position after the reference
      final int extraPosition = referencePosition + extra;
      if (extraPosition < 0 || extraPosition >= getItemCount()) {
        //Can't do anything with this
        continue;
      }

            /*
             * Obtain additional position views that we expect to appear
             * as part of the animation.
             */
      View appearing = recycler.getViewForPosition(extraPosition);
      addView(appearing);

      //Find layout delta from reference position
      final int newRow = getGlobalRowOfPosition(extraPosition + offset);
      final int rowDelta = newRow - getGlobalRowOfPosition(referencePosition + offset);
      final int newCol = getGlobalColumnOfPosition(extraPosition + offset);
      final int colDelta = newCol - getGlobalColumnOfPosition(referencePosition + offset);

      layoutTempChildView(appearing, rowDelta, colDelta, referenceView);
    }
  }

  /* Helper to place a disappearing view */
  private void layoutDisappearingView(View disappearingChild) {
        /*
         * LayoutManager has a special method for attaching views that
         * will only be around long enough to animate.
         */
    addDisappearingView(disappearingChild);

    //Adjust each disappearing view to its proper place
    final LayoutParams lp = (LayoutParams) disappearingChild.getLayoutParams();

    final int newRow = getGlobalRowOfPosition(lp.getViewAdapterPosition());
    final int rowDelta = newRow - lp.row;
    final int newCol = getGlobalColumnOfPosition(lp.getViewAdapterPosition());
    final int colDelta = newCol - lp.column;

    layoutTempChildView(disappearingChild, rowDelta, colDelta, disappearingChild);
  }


  /* Helper to lay out appearing/disappearing children */
  private void layoutTempChildView(View child, int rowDelta, int colDelta, View referenceView) {
    //Set the layout position to the global row/column difference from the reference view
    int layoutTop = getDecoratedTop(referenceView) + rowDelta * mDecoratedChildHeight;
    int layoutLeft = getDecoratedLeft(referenceView) + colDelta * mDecoratedChildWidth;

    measureChildWithMargins(child, 0, 0);
    layoutDecorated(child, layoutLeft, layoutTop,
            layoutLeft + mDecoratedChildWidth,
            layoutTop + mDecoratedChildHeight);
  }

  /** Private Helpers and Metrics Accessors */

    /* Return the overall column index of this position in the global layout */
  private int getGlobalColumnOfPosition(int position) {
    return position % mTotalColumnCount;
  }
  /* Return the overall row index of this position in the global layout */
  private int getGlobalRowOfPosition(int position) {
    return position / mTotalColumnCount;
  }

  /*
   * Mapping between child view indices and adapter data
   * positions helps fill the proper views during scrolling.
   */
  private int positionOfIndex(int childIndex) {
    int row = childIndex / mVisibleColumnCount;
    int column = childIndex % mVisibleColumnCount;

    return mFirstVisiblePosition + (row * getTotalColumnCount()) + column;
  }

  private int rowOfIndex(int childIndex) {
    int position = positionOfIndex(childIndex);

    return position / getTotalColumnCount();
  }

  private int getFirstVisibleColumn() {
    return (mFirstVisiblePosition % getTotalColumnCount());
  }

  private int getLastVisibleColumn() {
    return getFirstVisibleColumn() + mVisibleColumnCount;
  }

  private int getFirstVisibleRow() {
    return (mFirstVisiblePosition / getTotalColumnCount());
  }

  private int getLastVisibleRow() {
    return getFirstVisibleRow() + mVisibleRowCount;
  }

  private int getVisibleChildCount() {
    return mVisibleColumnCount * mVisibleRowCount;
  }

  private int getTotalColumnCount() {
    if (getItemCount() < mTotalColumnCount) {
      return getItemCount();
    }

    return mTotalColumnCount;
  }

  private int getTotalRowCount() {
    if (getItemCount() == 0 || mTotalColumnCount == 0) {
      return 0;
    }
    int maxRow = getItemCount() / mTotalColumnCount;
    //Bump the row count if it's not exactly even
    if (getItemCount() % mTotalColumnCount != 0) {
      maxRow++;
    }

    return maxRow;
  }

  private int getHorizontalSpace() {
    return getWidth() - getPaddingRight() - getPaddingLeft();
  }

  private int getVerticalSpace() {
    return getHeight() - getPaddingBottom() - getPaddingTop();
  }
}
