/*
 * Copyright 2016 Hippo Seven
 *
 * 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.hippo.glgallery;

import android.content.Context;
import android.graphics.Rect;
import android.util.Log;

import androidx.annotation.NonNull;

import com.hippo.glview.anim.Animation;
import com.hippo.glview.view.GLView;
import com.hippo.glview.widget.GLProgressView;
import com.hippo.glview.widget.GLTextureView;
import com.hippo.yorozuya.AnimationUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.MathUtils;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

class ScrollLayoutManager extends GalleryView.LayoutManager {
    private static final String TAG = ScrollLayoutManager.class.getSimpleName();

    private static final float RESERVATION = 0.5f;
    private static final float DEFAULT_SCALE = 1.0f;
    private static final float MAX_SCALE = 2.0f;
    private static final float MIN_SCALE = 0.3f;
    private static final float SCALE_ERROR = 0.01f;

    private static final int INVALID_TOP = Integer.MAX_VALUE;
    private final LinkedList<GalleryPageView> mPages = new LinkedList<>();
    private final LinkedList<GalleryPageView> mTempPages = new LinkedList<>();
    private final PageFling mPageFling;
    private final SmoothScaler mSmoothScaler;
    private GalleryView.Adapter mAdapter;
    private GLProgressView mProgress;
    private String mErrorStr;
    private GLTextureView mErrorView;
    private float mScale = DEFAULT_SCALE;
    private int mOffsetX;
    private int mOffsetY;
    private int mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;
    private int mKeepTop = INVALID_TOP;
    private int mFirstShownPageIndex = GalleryPageView.INVALID_INDEX;
    private boolean mScrollUp;
    private boolean mFlingUp;
    private boolean mStopAnimationFinger;
    private int mInterval;
    // Current index
    private int mIndex;

    private int mBottomStateBottom;
    private boolean mBottomStateHasNext;

    public ScrollLayoutManager(Context context, @NonNull GalleryView galleryView, int interval) {
        super(galleryView);

        mInterval = interval;
        mPageFling = new PageFling(context);
        mSmoothScaler = new SmoothScaler();
    }

    public void setInterval(int interval) {
        if (mInterval == interval) {
            return;
        }

        if (mAdapter != null) {
            int index = getInternalCurrentIndex();
            GalleryView.Adapter adapter = onDetach();
            mInterval = interval;
            onAttach(adapter);
            setCurrentIndex(index);
            mGalleryView.requestFill();
        } else {
            mInterval = interval;
        }
    }

    private void resetParameters() {
        mScale = DEFAULT_SCALE;
        mOffsetX = 0;
        mOffsetY = 0;
        mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;
        mKeepTop = INVALID_TOP;
        mFirstShownPageIndex = GalleryPageView.INVALID_INDEX;
        mScrollUp = false;
        mFlingUp = false;
        mStopAnimationFinger = false;
    }

    // Return true for animations are running
    private boolean cancelAllAnimations() {
        boolean running = mPageFling.isRunning() ||
                mSmoothScaler.isRunning();
        mPageFling.cancel();
        mSmoothScaler.cancel();
        return running;
    }

    @Override
    public void onAttach(GalleryView.Adapter adapter) {
        AssertUtils.assertNull("The ScrollLayoutManager is attached", mAdapter);
        AssertUtils.assertNotNull("The iterator is null", adapter);
        mAdapter = adapter;
        // Reset parameters
        resetParameters();
    }

    private void removeProgress() {
        if (mProgress != null) {
            mGalleryView.removeComponent(mProgress);
            mProgress = null;
        }
    }

    private void removeErrorView() {
        if (mErrorView != null) {
            mGalleryView.removeComponent(mErrorView);
            mGalleryView.releaseErrorView(mErrorView);
            mErrorView = null;
            mErrorStr = null;
        }
    }

    private void removePage(@NonNull GalleryPageView page) {
        mGalleryView.removeComponent(page);
        mAdapter.unbind(page);
        mGalleryView.releasePage(page);
    }

    private void removeAllPages() {
        for (GalleryPageView page : mPages) {
            removePage(page);
        }
        mPages.clear();
    }

    @Override
    public GalleryView.Adapter onDetach() {
        AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter);

        // Cancel all animations
        cancelAllAnimations();

        // Remove all view
        removeProgress();
        removeErrorView();
        removeAllPages();

        // Clear iterator
        GalleryView.Adapter iterator = mAdapter;
        mAdapter = null;

        return iterator;
    }

    private GalleryPageView obtainPage() {
        GalleryPageView page = mGalleryView.obtainPage();
        page.getImageView().setScaleOffset(ImageView.SCALE_FIT, ImageView.START_POSITION_TOP_RIGHT, 1.0f);
        return page;
    }

    private GalleryPageView getPageForIndex(List<GalleryPageView> pages, int index, boolean remove) {
        for (Iterator<GalleryPageView> iterator = pages.iterator(); iterator.hasNext(); ) {
            GalleryPageView page = iterator.next();
            if (index == page.getIndex()) {
                if (remove) {
                    iterator.remove();
                }
                return page;
            }
        }
        return null;
    }

    private boolean isInScreen(GalleryPageView page, boolean includeFirst) {
        int height = mGalleryView.getHeight();
        Rect bound = page.bounds();
        int pageTop = bound.top;
        int pageBottom = bound.bottom;
        return (includeFirst && pageTop >= 0 && pageTop < height) ||
                (pageBottom > 0 && pageBottom <= height) ||
                (pageTop < 0 && pageBottom > height);
    }

    private float getReservation() {
        return Math.max(RESERVATION, (((1 + 2 * RESERVATION) * mScale) - 1) / 2);
    }

    private void fillPages(int startIndex, int startOffset) {
        final GalleryView.Adapter adapter = mAdapter;
        final GalleryView galleryView = mGalleryView;
        final LinkedList<GalleryPageView> pages = mPages;
        final LinkedList<GalleryPageView> tempPages = mTempPages;
        final int width = galleryView.getWidth();
        final int height = galleryView.getHeight();
        final int pageWidth = (int) (width * mScale);
        final int size = adapter.size();
        final int interval = mInterval;
        final float reservation = getReservation();
        final int minY = (int) (-height * reservation);
        final int maxY = (int) (height * (1 + reservation));
        final int widthSpec = GLView.MeasureSpec.makeMeasureSpec(pageWidth, GLView.MeasureSpec.EXACTLY);
        final int heightSpec = GLView.MeasureSpec.makeMeasureSpec(height, GLView.MeasureSpec.UNSPECIFIED);

        // Fix start index and start offset
        if (startIndex < 0) {
            startIndex = 0;
            startOffset = 0;
        } else if (startIndex >= size) {
            startIndex = size - 1;
            startOffset = 0;
        } else if (startOffset < minY) {
            while (true) {
                GalleryPageView page = getPageForIndex(pages, startIndex, false);
                if (null == page) {
                    startOffset = minY;
                    break;
                } else {
                    page.measure(widthSpec, heightSpec);
                    if (startOffset + page.getHeight() > minY) {
                        break;
                    } else if (size - 1 == startIndex) {
                        startOffset = 0;
                        break;
                    } else {
                        ++startIndex;
                        startOffset += page.getHeight() + interval;
                        if (startOffset >= minY) {
                            break;
                        }
                    }
                }
            }
        } else if (startOffset >= maxY) {
            if (0 == startIndex) {
                startOffset = 0;
            } else {
                --startIndex;
                int startBottomOffset = startOffset - interval;
                while (true) {
                    GalleryPageView page = getPageForIndex(pages, startIndex, false);
                    if (null == page) {
                        startOffset = maxY - 1;
                        break;
                    } else {
                        page.measure(widthSpec, heightSpec);
                        startOffset = startBottomOffset - page.getHeight();
                        if (startOffset < maxY) {
                            break;
                        } else if (0 == startIndex) {
                            startOffset = 0;
                            break;
                        } else {
                            --startIndex;
                            startBottomOffset = startOffset - interval;
                        }
                    }
                }
            }
        }

        // Put page to temp list
        tempPages.addAll(pages);
        pages.clear();

        // Sanitize offsetX
        int margin = pageWidth - width;
        if (margin >= 0) {
            mOffsetX = MathUtils.clamp(mOffsetX, -margin, 0);
        } else {
            mOffsetX = -margin / 2;
        }

        // Layout start page
        GalleryPageView page = getPageForIndex(tempPages, startIndex, true);
        if (null == page) {
            page = obtainPage();
            galleryView.addComponent(page);
            adapter.bind(page, startIndex);
        }
        pages.add(page);
        page.measure(widthSpec, heightSpec);
        page.layout(mOffsetX, startOffset, mOffsetX + pageWidth, startOffset + page.getMeasuredHeight());

        // Prepare for check up and down
        int bottomOffset = startOffset - interval;
        int topOffset = startOffset + page.getMeasuredHeight() + interval;

        // Check up
        int index = startIndex - 1;
        while (bottomOffset > minY && index >= 0) {
            page = getPageForIndex(tempPages, index, true);
            if (null == page) {
                page = obtainPage();
                galleryView.addComponent(page);
                adapter.bind(page, index);
            }
            pages.addFirst(page);
            page.measure(widthSpec, heightSpec);
            page.layout(mOffsetX, bottomOffset - page.getMeasuredHeight(), mOffsetX + pageWidth, bottomOffset);
            // Update
            bottomOffset -= page.getMeasuredHeight() + interval;
            index--;
        }

        // Avoid space in top
        page = pages.getFirst();
        if (0 == page.getIndex() && page.bounds().top > 0) {
            int offset = -page.bounds().top;
            for (GalleryPageView p : pages) {
                p.offsetTopAndBottom(offset);
            }
            topOffset += offset;
        }

        // Check down
        index = startIndex + 1;
        while (topOffset < maxY && index < size) {
            page = getPageForIndex(tempPages, index, true);
            if (null == page) {
                page = obtainPage();
                galleryView.addComponent(page);
                adapter.bind(page, index);
            }
            pages.addLast(page);
            page.measure(widthSpec, heightSpec);
            page.layout(mOffsetX, topOffset, mOffsetX + pageWidth, topOffset + page.getMeasuredHeight());
            // Update
            topOffset += page.getMeasuredHeight() + interval;
            index++;
        }

        // Avoid space in bottom
        if (size - 1 == pages.getLast().getIndex()) {
            while (true) {
                page = pages.getLast();
                int pagesBottom = page.bounds().bottom;
                if (pagesBottom >= height) {
                    break;
                }
                page = pages.getFirst();
                index = page.getIndex();
                if (0 == index) {
                    break;
                }
                --index;
                int pagesTop = page.bounds().top;

                page = getPageForIndex(tempPages, index, true);
                if (null == page) {
                    page = obtainPage();
                    galleryView.addComponent(page);
                    adapter.bind(page, index);
                }
                pages.addFirst(page);
                page.measure(widthSpec, heightSpec);

                int offset = Math.min(height - pagesBottom, page.getMeasuredHeight());
                for (GalleryPageView p : pages) {
                    p.offsetTopAndBottom(offset);
                }
                int bottom = pagesTop - interval + offset;
                page.layout(mOffsetX, bottom - page.getMeasuredHeight(), mOffsetX + pageWidth, bottom);
            }
        }

        // Remove remain page
        for (GalleryPageView p : tempPages) {
            removePage(p);
        }
        tempPages.clear();

        // Update state
        if (!pages.isEmpty()) {
            page = pages.getFirst();
            mIndex = page.getIndex();
            mOffsetY = page.bounds().top;
        }
    }

    @Override
    public void onFill() {
        GalleryView.Adapter adapter = mAdapter;
        GalleryView galleryView = mGalleryView;
        AssertUtils.assertNotNull("The PagerLayoutManager is not attached", adapter);

        int size = adapter.size();

        if (size == 0) { // Get empty, show error text
            String errorStr = galleryView.getEmptyStr();

            // Remove progress and all pages
            removeProgress();
            removeAllPages();

            // Ensure error view
            if (mErrorView == null) {
                mErrorView = galleryView.obtainErrorView();
                galleryView.addComponent(mErrorView);
            }

            // Update error string
            if (!errorStr.equals(mErrorStr)) {
                mErrorStr = errorStr;
                galleryView.bindErrorView(mErrorView, errorStr);
            }

            // Place error view center
            placeCenter(mErrorView);
        } else {
            // Remove progress and error view
            removeProgress();
            removeErrorView();

            // Ensure index in range
            int index = mIndex;
            if (index < 0) {
                Log.e(TAG, "index < 0, index = " + index);
                index = 0;
                mIndex = index;
                removeAllPages();
            } else if (index >= size) {
                Log.e(TAG, "index >= size, index = " + index + ", size = " + size);
                index = size - 1;
                mIndex = index;
                removeAllPages();
            }

            // Find keep index and keep top
            int keepTop = INVALID_TOP;
            int keepTopIndex;
            if (GalleryPageView.INVALID_INDEX != mKeepTopPageIndex) {
                keepTopIndex = mKeepTopPageIndex;
                keepTop = mKeepTop;
                mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;
            } else if (GalleryPageView.INVALID_INDEX != mFirstShownPageIndex) {
                keepTopIndex = mFirstShownPageIndex;
            } else {
                keepTopIndex = GalleryPageView.INVALID_INDEX;
            }
            if (GalleryPageView.INVALID_INDEX != keepTopIndex && INVALID_TOP == keepTop) {
                keepTop = mOffsetY;
                for (GalleryPageView page : mPages) {
                    // Check keep page
                    if (keepTopIndex == page.getIndex()) {
                        break;
                    }
                    keepTop += page.getHeight() + mInterval;
                }
            }

            int startIndex;
            int startOffset;
            if (GalleryPageView.INVALID_INDEX != keepTopIndex) {
                startIndex = keepTopIndex;
                startOffset = keepTop;
            } else {
                startIndex = mIndex;
                startOffset = mOffsetY;
            }
            fillPages(startIndex, startOffset);

            // Get first shown image
            mFirstShownPageIndex = GalleryPageView.INVALID_INDEX;
            for (GalleryPageView page : mPages) {
                // Check first shown loaded page
                if ((mScrollUp || mFlingUp) && !page.isLoaded()) {
                    continue;
                }

                if (isInScreen(page, true)) {
                    mFirstShownPageIndex = page.getIndex();
                    break;
                }
            }
        }
    }

    @Override
    public void onDown() {
        mScrollUp = false;
        mStopAnimationFinger = cancelAllAnimations();
    }

    @Override
    public void onUp() {
        mScrollUp = false;
    }

    @Override
    public void onDoubleTapConfirmed(float x, float y) {
        if (mPages.isEmpty()) {
            return;
        }

        float startScale = mScale;
        float endScale;
        if (startScale < DEFAULT_SCALE - SCALE_ERROR) {
            endScale = DEFAULT_SCALE;
        } else if (startScale < MAX_SCALE - SCALE_ERROR) {
            endScale = MAX_SCALE;
        } else {
            endScale = DEFAULT_SCALE;
        }

        mSmoothScaler.startSmoothScaler(x, y, startScale, endScale, 300);
    }

    private void getBottomState() {
        List<GalleryPageView> pages = mPages;
        int bottom = mOffsetY;
        int i = 0;
        for (GalleryPageView page : pages) {
            if (i != 0) {
                bottom += mInterval;
            }
            bottom += page.getHeight();
            i++;
        }
        boolean hasNext = mIndex + pages.size() < mAdapter.size();

        mBottomStateBottom = bottom;
        mBottomStateHasNext = hasNext;
    }

    // True for get top or bottom
    private boolean scrollInternal(float dx, float dy) {
        if (mPages.isEmpty()) {
            return false;
        }

        GalleryView galleryView = mGalleryView;
        int width = galleryView.getWidth();
        int height = galleryView.getHeight();
        int pageWidth = (int) (width * mScale);
        final float reservation = getReservation();
        boolean requestFill = false;
        boolean result = false;

        int margin = pageWidth - width;
        int dxInt = (int) dx;
        if (margin > 0 && 0 != dxInt) {
            int oldOffsetX = mOffsetX;
            int exceptOffsetX = oldOffsetX - dxInt;
            mOffsetX = MathUtils.clamp(exceptOffsetX, -margin, 0);
            if (mOffsetX != oldOffsetX) {
                requestFill = true;
            }
        }

        int remainY = (int) dy;
        while (remainY != 0) {
            if (remainY < 0) { // Try to show top
                int limit;
                if (mIndex > 0) {
                    limit = (int) (-height * reservation) + mInterval;
                } else {
                    limit = 0;
                }

                if (mOffsetY - remainY <= limit) {
                    mOffsetY -= remainY;
                    remainY = 0;
                    requestFill = true;
                } else {
                    if (mIndex > 0) {
                        mOffsetY = limit;
                        // Offset one pixel to avoid infinite loop
                        ++mOffsetY;
                        ++remainY;
                        galleryView.forceFill();
                        requestFill = false;
                    } else {
                        if (mOffsetY != limit) {
                            mOffsetY = limit;
                            requestFill = true;
                        }
                        remainY = 0;
                        result = true;
                    }
                }
            } else { // Try to show bottom
                getBottomState();
                int bottom = mBottomStateBottom;
                boolean hasNext = mBottomStateHasNext;

                int limit;
                if (hasNext) {
                    limit = (int) (height * (1 + reservation)) - mInterval;
                } else {
                    limit = height;
                }
                // Fix limit for page not fill screen
                limit = Math.min(bottom, limit);

                if (bottom - remainY >= limit) {
                    mOffsetY -= remainY;
                    remainY = 0;
                    requestFill = true;
                } else {
                    if (hasNext) {
                        mOffsetY -= bottom - limit;
                        remainY = remainY + limit - bottom;
                        // Offset one pixel to avoid infinite loop
                        --mOffsetY;
                        --remainY;
                        galleryView.forceFill();
                        requestFill = false;
                    } else {
                        if (mOffsetY != limit) {
                            mOffsetY -= bottom - limit;
                            requestFill = true;
                        }
                        remainY = 0;
                        result = true;
                    }
                }
            }
        }

        if (requestFill) {
            mGalleryView.requestFill();
        }

        return result;
    }

    @Override
    public void onScroll(float dx, float dy, float totalX, float totalY, float x, float y) {
        mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;
        mKeepTop = INVALID_TOP;
        mScrollUp = dy < 0;
        scrollInternal(dx, dy);
    }

    @Override
    public void onFling(float velocityX, float velocityY) {
        if (mPages.isEmpty()) {
            return;
        }

        mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;
        mKeepTop = INVALID_TOP;
        mFlingUp = velocityY > 0;

        int maxX;
        int minX;
        int width = mGalleryView.getWidth();
        int pageWidth = (int) (width * mScale);
        int margin = pageWidth - width;
        if (margin > 0) {
            maxX = -mOffsetX;
            minX = -margin + mOffsetX;
        } else {
            maxX = 0;
            minX = 0;
        }

        int maxY;
        if (mIndex > 0) {
            maxY = Integer.MAX_VALUE;
        } else {
            maxY = -mOffsetY;
        }

        getBottomState();
        int bottom = mBottomStateBottom;
        boolean hasNext = mBottomStateHasNext;
        int minY;
        if (hasNext) {
            minY = Integer.MIN_VALUE;
        } else {
            minY = mGalleryView.getHeight() - bottom;
        }

        mPageFling.startFling((int) velocityX, minX, maxX,
                (int) velocityY, minY, maxY);
    }

    @Override
    public boolean canScale() {
        return !mPages.isEmpty();
    }

    @Override
    public void onScale(float focusX, float focusY, float scale) {
        if (mPages.isEmpty()) {
            return;
        }

        float oldScale = mScale;
        mScale = MathUtils.clamp(oldScale * scale, MIN_SCALE, MAX_SCALE);
        scale = mScale / oldScale;

        if (oldScale != mScale) {
            GalleryPageView page = null;
            // Keep scale page origin position
            for (GalleryPageView p : mPages) {
                if (p.bounds().top < focusY) {
                    page = p;
                } else {
                    break;
                }
            }

            if (null != page) {
                mKeepTopPageIndex = page.getIndex();
                mKeepTop = page.bounds().top;

                mGalleryView.forceFill();
                int oldKeepTop = mKeepTop;
                mKeepTop = INVALID_TOP;

                // Apply scroll
                int newOffsetX = (int) (focusX - ((focusX - mOffsetX) * scale));
                int newKeepTop;
                if (page.isLoaded()) {
                    newKeepTop = (int) (focusY - ((focusY - oldKeepTop) * scale));
                } else {
                    newKeepTop = oldKeepTop;
                }
                scrollInternal(mOffsetX - newOffsetX, oldKeepTop - newKeepTop);
            } else {
                Log.e(TAG, "Can't find target page");
                mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;
                mKeepTop = INVALID_TOP;
                mGalleryView.forceFill();
            }
        }
    }

    @Override
    public boolean onUpdateAnimation(long time) {
        boolean invalidate = mPageFling.calculate(time);
        invalidate |= mSmoothScaler.calculate(time);
        return invalidate;
    }

    @Override
    public void onDataChanged() {
        AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter);

        // Cancel all animations
        cancelAllAnimations();
        // Remove all views
        removeProgress();
        removeErrorView();
        removeAllPages();
        // Reset parameters
        resetParameters();
        mGalleryView.requestFill();
    }

    @Override
    public void onPageLeft() {
        if (mAdapter.size() <= 0 || mPages.isEmpty()) {
            return;
        }

        ///////
        // UP
        ///////
        GalleryView galleryView = mGalleryView;
        if (mIndex > 0 || mOffsetY < 0) {
            // Cancel all animations
            cancelAllAnimations();

            // Get first shown page
            GalleryPageView previousPage = null;
            GalleryPageView firstShownPage = null;
            for (GalleryPageView p : mPages) {
                if (isInScreen(p, true)) {
                    firstShownPage = p;
                    break;
                }
                previousPage = p;
            }

            int height = galleryView.getHeight();
            int maxOffset = height - mInterval;
            if (null == firstShownPage) {
                Log.e(TAG, "Can't find first shown page when paging left");
                mOffsetY += height / 2;
            } else {
                int firstShownTop = firstShownPage.bounds().top;
                if (firstShownTop >= 0) {
                    if (null == previousPage) {
                        Log.e(TAG, "Can't find previous page when paging left and offsetY == 0");
                        mOffsetY += height / 2;
                    } else {
                        mOffsetY += Math.min(maxOffset, -previousPage.bounds().top);
                    }
                } else {
                    mOffsetY += Math.min(maxOffset, -firstShownTop);
                }
            }

            // Request fill
            mGalleryView.requestFill();
        }
    }

    @Override
    public void onPageRight() {
        if (mAdapter.size() <= 0 || mPages.isEmpty()) {
            return;
        }

        /////////
        // DOWN
        /////////
        GalleryView galleryView = mGalleryView;
        getBottomState();
        int bottom = mBottomStateBottom;
        boolean hasNext = mBottomStateHasNext;
        if (hasNext || bottom > galleryView.getHeight()) {
            // Cancel all animations
            cancelAllAnimations();

            // Get first shown page
            GalleryPageView lastShownPage = null;
            GalleryPageView nextPage = null;
            for (GalleryPageView p : mPages) {
                if (isInScreen(p, true)) {
                    lastShownPage = p;
                } else if (null != lastShownPage) {
                    nextPage = p;
                    break;
                }
            }

            int height = galleryView.getHeight();
            int maxOffset = height - mInterval;
            if (null == lastShownPage) {
                Log.e(TAG, "Can't find last shown page when paging left");
                mOffsetY -= height / 2;
            } else {
                int lastShownBottom = lastShownPage.bounds().bottom;
                if (lastShownBottom <= height) {
                    if (null == nextPage) {
                        Log.e(TAG, "Can't find previous page when paging left and offsetY == 0");
                        mOffsetY -= height / 2;
                    } else {
                        mOffsetY -= Math.min(maxOffset, nextPage.bounds().bottom - height);
                    }
                } else {
                    mOffsetY -= Math.min(maxOffset, lastShownBottom - height);
                }
            }

            // Request fill
            mGalleryView.requestFill();
        }
    }

    @Override
    public boolean isTapOrPressDisable() {
        return mStopAnimationFinger;
    }

    @Override
    public GalleryPageView findPageByIndex(int index) {
        for (GalleryPageView page : mPages) {
            if (page.getIndex() == index) {
                return page;
            }
        }
        return null;
    }

    @Override
    public int getCurrentIndex() {
        int index = GalleryPageView.INVALID_INDEX;
        for (GalleryPageView page : mPages) {
            if (isInScreen(page, false)) {
                index = page.getIndex();
            }
        }
        return index;
    }

    @Override
    public void setCurrentIndex(int index) {
        int size = mAdapter.size();
        if (size <= 0) {
            // Can't get size now, assume size is MAX
            size = Integer.MAX_VALUE;
        }
        if (index < 0 || index >= size) {
            return;
        }

        mKeepTopPageIndex = index;
        mKeepTop = INVALID_TOP;

        if (mPages.isEmpty()) {
            mIndex = index;
        } else {
            // Fix the index page
            GalleryPageView targetPage = null;
            for (GalleryPageView page : mPages) {
                if (page.getIndex() == index) {
                    targetPage = page;
                    break;
                }
            }

            if (targetPage != null) {
                // Cancel all animations
                cancelAllAnimations();
                mOffsetY -= targetPage.bounds().top;
                // Request fill
                mGalleryView.requestFill();
            } else {
                mIndex = index;
                mOffsetY = 0;
                // Cancel all animations
                cancelAllAnimations();
                // Remove all view
                removeProgress();
                removeErrorView();
                removeAllPages();
                // Request fill
                mGalleryView.requestFill();
            }
        }
    }

    @Override
    public int getIndexUnder(float x, float y) {
        if (!mPages.isEmpty()) {
            int intX = (int) x;
            int intY = (int) y;
            for (GalleryPageView page : mPages) {
                if (page.bounds().contains(intX, intY)) {
                    return page.getIndex();
                }
            }
        }
        return GalleryPageView.INVALID_INDEX;
    }

    @Override
    int getInternalCurrentIndex() {
        int currentIndex = getCurrentIndex();
        if (currentIndex == GalleryPageView.INVALID_INDEX) {
            currentIndex = mIndex;
        }
        return currentIndex;
    }

    private class PageFling extends Fling {
        private int mDx;
        private int mDy;
        private int mLastX;
        private int mLastY;

        public PageFling(Context context) {
            super(context);
        }

        public void startFling(int velocityX, int minX, int maxX,
                               int velocityY, int minY, int maxY) {
            mDx = (int) (getSplineFlingDistance(velocityX) * Math.signum(velocityX));
            mDy = (int) (getSplineFlingDistance(velocityY) * Math.signum(velocityY));
            mLastX = 0;
            mLastY = 0;
            int durationX = getSplineFlingDuration(velocityX);
            int durationY = getSplineFlingDuration(velocityY);

            if (mDx < minX) {
                durationX = adjustDuration(mDx, minX, durationX);
                mDx = minX;
            }
            if (mDx > maxX) {
                durationX = adjustDuration(mDx, maxX, durationX);
                mDx = maxX;
            }
            if (mDy < minY) {
                durationY = adjustDuration(mDy, minY, durationY);
                mDy = minY;
            }
            if (mDy > maxY) {
                durationY = adjustDuration(mDy, maxY, durationY);
                mDy = maxY;
            }

            if (mDx == 0 && mDy == 0) {
                return;
            }

            setDuration(Math.max(durationX, durationY));
            start();
            mGalleryView.invalidate();
        }

        @Override
        protected void onCalculate(float progress) {
            int x = (int) (mDx * progress);
            int y = (int) (mDy * progress);
            int offsetX = x - mLastX;
            int offsetY = y - mLastY;
            if (scrollInternal(-offsetX, -offsetY)) {
                cancel();
                onFinish();
            }
            mLastX = x;
            mLastY = y;
        }

        @Override
        protected void onFinish() {
            mFlingUp = false;
            getBottomState();
        }
    }

    private class SmoothScaler extends Animation {
        private float mFocusX;
        private float mFocusY;
        private float mStartScale;
        private float mEndScale;
        private float mLastScale;

        public SmoothScaler() {
            setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);
        }

        public void startSmoothScaler(float focusX, float focusY,
                                      float startScale, float endScale, int duration) {
            mFocusX = focusX;
            mFocusY = focusY;
            mStartScale = startScale;
            mEndScale = endScale;
            mLastScale = startScale;
            setDuration(duration);
            start();
            mGalleryView.invalidate();
        }

        @Override
        protected void onCalculate(float progress) {
            if (mPages.isEmpty()) {
                return;
            }

            float scale = MathUtils.lerp(mStartScale, mEndScale, progress);
            onScale(mFocusX, mFocusY, scale / mLastScale);
            mLastScale = scale;
        }
    }
}
