/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.gallery3d.ui;

import com.android.gallery3d.common.Utils;
import com.android.gallery3d.ui.PositionRepository.Position;

import android.content.Context;
import android.graphics.RectF;
import android.os.SystemClock;

class PositionController {
    private static final String TAG = "PositionController";
    private static final boolean DEBUG = false;

    private long mAnimationStartTime = NO_ANIMATION;
    private static final long NO_ANIMATION = -1;
    private static final long LAST_ANIMATION = -2;

    private int mAnimationKind;
    private float mAnimationDuration;
    private final static int ANIM_KIND_SCROLL = 0;
    private final static int ANIM_KIND_SCALE = 1;
    private final static int ANIM_KIND_SNAPBACK = 2;
    private final static int ANIM_KIND_SLIDE = 3;
    private final static int ANIM_KIND_ZOOM = 4;
    private final static int ANIM_KIND_FLING = 5;

    // Animation time in milliseconds. The order must match ANIM_KIND_* above.
    private final static int ANIM_TIME[] = {
        0,    // ANIM_KIND_SCROLL
        50,   // ANIM_KIND_SCALE
        600,  // ANIM_KIND_SNAPBACK
        400,  // ANIM_KIND_SLIDE
        300,  // ANIM_KIND_ZOOM
        0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
    };

    // We try to scale up the image to fill the screen. But in order not to
    // scale too much for small icons, we limit the max up-scaling factor here.
    private static final float SCALE_LIMIT = 4;

    private PhotoView mViewer;
    private EdgeView mEdgeView;
    private int mImageW, mImageH;
    private int mViewW, mViewH;

    // The X, Y are the coordinate on bitmap which shows on the center of
    // the view. We always keep the mCurrent{X,Y,Scale} sync with the actual
    // values used currently.
    private int mCurrentX, mFromX, mToX;
    private int mCurrentY, mFromY, mToY;
    private float mCurrentScale, mFromScale, mToScale;

    // The focus point of the scaling gesture (in bitmap coordinates).
    private int mFocusBitmapX;
    private int mFocusBitmapY;
    private boolean mInScale;

    // The minimum and maximum scale we allow.
    private float mScaleMin, mScaleMax = SCALE_LIMIT;

    // This is used by the fling animation
    private FlingScroller mScroller;

    // The bound of the stable region, see the comments above
    // calculateStableBound() for details.
    private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;

    // Assume the image size is the same as view size before we know the actual
    // size of image.
    private boolean mUseViewSize = true;

    private RectF mTempRect = new RectF();
    private float[] mTempPoints = new float[8];

    public PositionController(PhotoView viewer, Context context,
            EdgeView edgeView) {
        mViewer = viewer;
        mEdgeView = edgeView;
        mScroller = new FlingScroller();
    }

    public void setImageSize(int width, int height) {
        if (DEBUG) Log.i(TAG, "setImageSize() scale=" + mCurrentScale + " maxScale=" + mScaleMax);

        // If no image available, use view size.
        if (width == 0 || height == 0) {
            mUseViewSize = true;
            mImageW = mViewW;
            mImageH = mViewH;
            mCurrentX = mImageW / 2;
            mCurrentY = mImageH / 2;
            mCurrentScale = 1;
            mScaleMin = 1;
            mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
            return;
        }

        mUseViewSize = false;

        float ratio = Math.min(
                (float) mImageW / width, (float) mImageH / height);

        // See the comment above translate() for details.
        mCurrentX = translate(mCurrentX, mImageW, width, ratio);
        mCurrentY = translate(mCurrentY, mImageH, height, ratio);
        mCurrentScale = mCurrentScale * ratio;

        mFromX = translate(mFromX, mImageW, width, ratio);
        mFromY = translate(mFromY, mImageH, height, ratio);
        mFromScale = mFromScale * ratio;

        mToX = translate(mToX, mImageW, width, ratio);
        mToY = translate(mToY, mImageH, height, ratio);
        mToScale = mToScale * ratio;

        mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio);
        mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio);

        mImageW = width;
        mImageH = height;

        mScaleMin = getMinimalScale(mImageW, mImageH);

        // Start animation from the saved position if we have one.
        Position position = mViewer.retrieveSavedPosition();
        if (position != null) {
            // The animation starts from 240 pixels and centers at the image
            // at the saved position.
            float scale = 240f / Math.min(width, height);
            mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2;
            mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2;
            mCurrentScale = scale;
            mViewer.openAnimationStarted();
            startSnapback();
        } else if (mAnimationStartTime == NO_ANIMATION) {
            mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
        }
        mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
    }

    public void zoomIn(float tapX, float tapY, float targetScale) {
        if (DEBUG) Log.i(TAG, "zoomIn() scale=" + mCurrentScale + " maxScale=" + mScaleMax);

        if (targetScale > mScaleMax) targetScale = mScaleMax;

        // Convert the tap position to image coordinate
        int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX);
        int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY);

        calculateStableBound(targetScale);
        int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight);
        int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom);

        startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
    }

    public float getScaleMax() {
        return mScaleMax;
    }

    public void resetToFullView() {
        startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM);
    }

    public float getMinimalScale(int w, int h) {
        return Math.min(SCALE_LIMIT,
                Math.min((float) mViewW / w, (float) mViewH / h));
    }

    // Translate a coordinate on bitmap if the bitmap size changes.
    // If the aspect ratio doesn't change, it's easy:
    //
    //         r  = w / w' (= h / h')
    //         x' = x / r
    //         y' = y / r
    //
    // However the aspect ratio may change. That happens when the user slides
    // a image before it's loaded, we don't know the actual aspect ratio, so
    // we will assume one. When we receive the actual bitmap size, we need to
    // translate the coordinate from the old bitmap into the new bitmap.
    //
    // What we want to do is center the bitmap at the original position.
    //
    //         ...+--+...
    //         .  |  |  .
    //         .  |  |  .
    //         ...+--+...
    //
    // First we scale down the new bitmap by a factor r = min(w/w', h/h').
    // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps
    // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of
    // the old bitmap maps to (x', y') in the new bitmap, where
    //         x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r
    //         y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r
    private static int translate(int value, int size, int newSize, float ratio) {
        return Math.round(newSize / 2f + (value - size / 2f) / ratio);
    }

    public void setViewSize(int viewW, int viewH) {
        boolean needLayout = mViewW == 0 || mViewH == 0;

        mViewW = viewW;
        mViewH = viewH;

        if (mUseViewSize) {
            mImageW = viewW;
            mImageH = viewH;
            mCurrentX = mImageW / 2;
            mCurrentY = mImageH / 2;
            mCurrentScale = 1;
            mScaleMin = 1;
            mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
            return;
        }

        // In most cases we want to keep the scaling factor intact when the
        // view size changes. The cases we want to reset the scaling factor
        // (to fit the view if possible) are (1) the scaling factor is too
        // small for the new view size (2) the scaling factor has not been
        // changed by the user.
        boolean wasMinScale = (mCurrentScale == mScaleMin);
        mScaleMin = getMinimalScale(mImageW, mImageH);

        if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
            mCurrentX = mImageW / 2;
            mCurrentY = mImageH / 2;
            mCurrentScale = mScaleMin;
            mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
        }
    }

    public void stopAnimation() {
        mAnimationStartTime = NO_ANIMATION;
    }

    public void skipAnimation() {
        if (mAnimationStartTime == NO_ANIMATION) return;
        mAnimationStartTime = NO_ANIMATION;
        mCurrentX = mToX;
        mCurrentY = mToY;
        mCurrentScale = mToScale;
    }

    public void beginScale(float focusX, float focusY) {
        mInScale = true;
        mFocusBitmapX = Math.round(mCurrentX +
                (focusX - mViewW / 2f) / mCurrentScale);
        mFocusBitmapY = Math.round(mCurrentY +
                (focusY - mViewH / 2f) / mCurrentScale);
    }

    private static final float MIN_SCALE_FOR_EVENT = 0.001f; // 0.005f;
    private static final float MIN_SCALE_DELTA = 0.001f; //0.005f;
    private static final float MIN_SCALE_TO_GROW = 1.0f + MIN_SCALE_DELTA;
    private static final float MAX_SCALE_TO_SHRINK = 1.0f - MIN_SCALE_DELTA;
    private static final float SCALE_GROW_FACTOR = 1.05f;
    private static final float SCALE_SHRINK_FACTOR = 0.95f;

    public void scaleBy(float s, float focusX, float focusY) {
        /*
        // We want to keep the focus point (on the bitmap) the same as when
        // we begin the scale guesture, that is,
        //
        // mCurrentX' + (focusX - mViewW / 2f) / scale = mFocusBitmapX
        //
        float inDeltaScale = s;
        float inScale = getTargetScale();
        float outDeltaScale;
        if (inDeltaScale > MIN_SCALE_TO_GROW)
            outDeltaScale = inDeltaScale * SCALE_GROW_FACTOR;
        else if (inDeltaScale < MAX_SCALE_TO_SHRINK)
            outDeltaScale = inDeltaScale * SCALE_SHRINK_FACTOR;
        else
            outDeltaScale = inDeltaScale;
        float outScale = inScale * outDeltaScale;
        if (DEBUG) Log.i(TAG, "scaleBy() in=" + inScale + " inDelta=" + inDeltaScale
                + " outDelta=" + outDeltaScale + " out=" + outScale
                + " focusX=" + focusX + " focusY=" + focusY);
        if (inDeltaScale == outDeltaScale)
            return;
        int x = Math.round(mFocusBitmapX - (focusX - mViewW / 2f) / outScale);
        int y = Math.round(mFocusBitmapY - (focusY - mViewH / 2f) / outScale);
        startAnimation(x, y, outScale, ANIM_KIND_SCALE);
        */

        int targetX = Math.round(mFocusBitmapX - (focusX - mViewW / 2f) / s);
        int targetY = Math.round(mFocusBitmapY - (focusY - mViewH / 2f) / s);
        float scale = s;

        //if (targetX == mCurrentX && targetY == mCurrentY
        //        && scale == mCurrentScale) return;
        /*
        mFromX = mCurrentX;
        mFromY = mCurrentY;
        mFromScale = mCurrentScale;

        mToX = targetX;
        mToY = targetY;
        mToScale = s;
        */
        mCurrentX = targetX;
        mCurrentY = targetY;
        mCurrentScale = s;
        if (DEBUG) Log.v(TAG, "Exception: scaleBy s=" + s + " (" + targetX + ", " + targetY + ")");
        //mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax);
        //mCurrentScale = mToScale;

        if (mViewer != null) {
            mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
            mViewer.invalidate();
        }

        //startAnimation(targetX, targetY, s, ANIM_KIND_SCALE);
    }

    public void endScale() {
        mInScale = false;
        startSnapbackIfNeeded();
    }

    public float getCurrentScale() {
        return mCurrentScale;
    }

    public boolean isAtMinimalScale() {
        return isAlmostEquals(mCurrentScale, mScaleMin);
    }

    private static boolean isAlmostEquals(float a, float b) {
        float diff = a - b;
        return (diff < 0 ? -diff : diff) < 0.02f;
    }

    public void up() {
        startSnapback();
    }

    //             |<--| (1/2) * mImageW
    // +-------+-------+-------+
    // |       |       |       |
    // |       |   o   |       |
    // |       |       |       |
    // +-------+-------+-------+
    // |<----------| (3/2) * mImageW
    // Slide in the image from left or right.
    // Precondition: mCurrentScale = 1 (mView{W|H} == mImage{W|H}).
    // Sliding from left:  mCurrentX = (1/2) * mImageW
    //              right: mCurrentX = (3/2) * mImageW
    public void startSlideInAnimation(int direction) {
        int fromX = (direction == PhotoView.TRANS_SLIDE_IN_LEFT) ?
                mImageW / 2 : 3 * mImageW / 2;
        mFromX = Math.round(fromX);
        mFromY = Math.round(mImageH / 2f);
        mCurrentX = mFromX;
        mCurrentY = mFromY;
        startAnimation(
                mImageW / 2, mImageH / 2, mCurrentScale, ANIM_KIND_SLIDE);
    }

    public void startHorizontalSlide(int distance) {
        scrollBy(distance, 0, ANIM_KIND_SLIDE);
    }

    private void scrollBy(float dx, float dy, int type) {
        startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
                getTargetY() + Math.round(dy / mCurrentScale),
                mCurrentScale, type);
    }

    public void startScroll(float dx, float dy, boolean hasNext,
            boolean hasPrev) {
        int x = getTargetX() + Math.round(dx / mCurrentScale);
        int y = getTargetY() + Math.round(dy / mCurrentScale);

        calculateStableBound(mCurrentScale);

        // Vertical direction: If we have space to move in the vertical
        // direction, we show the edge effect when scrolling reaches the edge.
        if (mBoundTop != mBoundBottom) {
            if (y < mBoundTop) {
                mEdgeView.onPull(mBoundTop - y, EdgeView.TOP);
            } else if (y > mBoundBottom) {
                mEdgeView.onPull(y - mBoundBottom, EdgeView.BOTTOM);
            }
        }

        y = Utils.clamp(y, mBoundTop, mBoundBottom);

        // Horizontal direction: we show the edge effect when the scrolling
        // tries to go left of the first image or go right of the last image.
        boolean zoomedOut = getCurrentScale() <= 1.0f || isAtMinimalScale();
        if (!zoomedOut) {
            if (x < mBoundLeft) {
                int pixels = Math.round((mBoundLeft - x) * mCurrentScale);
                mEdgeView.onPull(pixels, EdgeView.LEFT);
                x = mBoundLeft;
            } else if (x > mBoundRight) {
                int pixels = Math.round((x - mBoundRight) * mCurrentScale);
                mEdgeView.onPull(pixels, EdgeView.RIGHT);
                x = mBoundRight;
            }
        }
        else {
            if (!hasPrev && x < mBoundLeft) {
                int pixels = Math.round((mBoundLeft - x) * mCurrentScale);
                mEdgeView.onPull(pixels, EdgeView.LEFT);
                x = mBoundLeft;
            } else if (!hasNext && x > mBoundRight) {
                int pixels = Math.round((x - mBoundRight) * mCurrentScale);
                mEdgeView.onPull(pixels, EdgeView.RIGHT);
                x = mBoundRight;
            }
        }

        startAnimation(x, y, mCurrentScale, ANIM_KIND_SCROLL);
    }

    public boolean fling(float velocityX, float velocityY) {
        // We only want to do fling when the picture is zoomed-in.
        if (mImageW * mCurrentScale <= mViewW &&
            mImageH * mCurrentScale <= mViewH) {
            return false;
        }

        calculateStableBound(mCurrentScale);
        mScroller.fling(mCurrentX, mCurrentY,
                Math.round(-velocityX / mCurrentScale),
                Math.round(-velocityY / mCurrentScale),
                mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
        int targetX = mScroller.getFinalX();
        int targetY = mScroller.getFinalY();
        mAnimationDuration = mScroller.getDuration();
        startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING);
        return true;
    }

    private void startAnimation(
            int targetX, int targetY, float scale, int kind) {
        if (targetX == mCurrentX && targetY == mCurrentY
                && scale == mCurrentScale) return;

        mFromX = mCurrentX;
        mFromY = mCurrentY;
        mFromScale = mCurrentScale;

        mToX = targetX;
        mToY = targetY;
        mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax);

        // If the scaled height is smaller than the view height,
        // force it to be in the center.
        // (We do for height only, not width, because the user may
        // want to scroll to the previous/next image.)
        if (Math.floor(mImageH * mToScale) <= mViewH) {
            mToY = mImageH / 2;
        }

        mAnimationStartTime = SystemClock.uptimeMillis();
        mAnimationKind = kind;
        if (mAnimationKind != ANIM_KIND_FLING) {
            mAnimationDuration = ANIM_TIME[mAnimationKind];
        }
        if (advanceAnimation()) mViewer.invalidate();
    }

    // Returns true if redraw is needed.
    public boolean advanceAnimation() {
        if (mAnimationStartTime == NO_ANIMATION) {
            return false;
        } else if (mAnimationStartTime == LAST_ANIMATION) {
            mAnimationStartTime = NO_ANIMATION;
            if (mViewer.isInTransition()) {
                mViewer.notifyTransitionComplete();
                return false;
            } else {
                return startSnapbackIfNeeded();
            }
        }

        long now = SystemClock.uptimeMillis();
        float progress;
        if (mAnimationDuration == 0) {
            progress = 1;
        } else {
            progress = (now - mAnimationStartTime) / mAnimationDuration;
        }

        if (progress >= 1) {
            progress = 1;
            mCurrentX = mToX;
            mCurrentY = mToY;
            mCurrentScale = mToScale;
            mAnimationStartTime = LAST_ANIMATION;
        } else {
            float f = 1 - progress;
            switch (mAnimationKind) {
                case ANIM_KIND_SCROLL:
                case ANIM_KIND_FLING:
                    progress = 1 - f;  // linear
                    break;
                case ANIM_KIND_SCALE:
                    progress = 1 - f * f;  // quadratic
                    break;
                case ANIM_KIND_SNAPBACK:
                case ANIM_KIND_ZOOM:
                case ANIM_KIND_SLIDE:
                    progress = 1 - f * f * f * f * f; // x^5
                    break;
            }
            if (mAnimationKind == ANIM_KIND_FLING) {
                flingInterpolate(progress);
            } else {
                linearInterpolate(progress);
            }
        }
        mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
        return true;
    }

    private void flingInterpolate(float progress) {
        mScroller.computeScrollOffset(progress);
        int oldX = mCurrentX;
        int oldY = mCurrentY;
        mCurrentX = mScroller.getCurrX();
        mCurrentY = mScroller.getCurrY();

        // Check if we hit the edges; show edge effects if we do.
        if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
            int v = Math.round(-mScroller.getCurrVelocityX() * mCurrentScale);
            mEdgeView.onAbsorb(v, EdgeView.LEFT);
        } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
            int v = Math.round(mScroller.getCurrVelocityX() * mCurrentScale);
            mEdgeView.onAbsorb(v, EdgeView.RIGHT);
        }

        if (oldY > mBoundTop && mCurrentY == mBoundTop) {
            int v = Math.round(-mScroller.getCurrVelocityY() * mCurrentScale);
            mEdgeView.onAbsorb(v, EdgeView.TOP);
        } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
            int v = Math.round(mScroller.getCurrVelocityY() * mCurrentScale);
            mEdgeView.onAbsorb(v, EdgeView.BOTTOM);
        }
    }

    // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1].
    private void linearInterpolate(float progress) {
        // To linearly interpolate the position on view coordinates, we do the
        // following steps:
        // (1) convert a bitmap position (x, y) to view coordinates:
        //     from: (x - mFromX) * mFromScale + mViewW / 2
        //     to: (x - mToX) * mToScale + mViewW / 2
        // (2) interpolate between the "from" and "to" coordinates:
        //     (x - mFromX) * mFromScale * (1 - p) + (x - mToX) * mToScale * p
        //     + mViewW / 2
        //     should be equal to
        //     (x - mCurrentX) * mCurrentScale + mViewW / 2
        // (3) The x-related terms in the above equation can be removed because
        //     mFromScale * (1 - p) + ToScale * p = mCurrentScale
        // (4) Solve for mCurrentX, we have mCurrentX =
        // (mFromX * mFromScale * (1 - p) + mToX * mToScale * p) / mCurrentScale
        float fromX = mFromX * mFromScale;
        float toX = mToX * mToScale;
        float currentX = fromX + progress * (toX - fromX);

        float fromY = mFromY * mFromScale;
        float toY = mToY * mToScale;
        float currentY = fromY + progress * (toY - fromY);

        mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
        mCurrentX = Math.round(currentX / mCurrentScale);
        mCurrentY = Math.round(currentY / mCurrentScale);
    }

    // Returns true if redraw is needed.
    private boolean startSnapbackIfNeeded() {
        if (mAnimationStartTime != NO_ANIMATION) return false;
        if (mInScale) return false;
        if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) {
            return false;
        }
        return startSnapback();
    }

    public boolean startSnapback() {
        boolean needAnimation = false;
        float scale = mCurrentScale;

        if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) {
            needAnimation = true;
            scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
        }

        calculateStableBound(scale);
        int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight);
        int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);

        if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) {
            needAnimation = true;
        }

        if (needAnimation) {
            startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
        }

        return needAnimation;
    }

    // Calculates the stable region of mCurrent{X/Y}, where "stable" means
    //
    // (1) If the dimension of scaled image >= view dimension, we will not
    // see black region outside the image (at that dimension).
    // (2) If the dimension of scaled image < view dimension, we will center
    // the scaled image.
    //
    // We might temporarily go out of this stable during user interaction,
    // but will "snap back" after user stops interaction.
    //
    // The results are stored in mBound{Left/Right/Top/Bottom}.
    //
    private void calculateStableBound(float scale) {
        // The number of pixels between the center of the view
        // and the edge when the edge is aligned.
        mBoundLeft = (int) Math.ceil(mViewW / (2 * scale));
        mBoundRight = mImageW - mBoundLeft;
        mBoundTop = (int) Math.ceil(mViewH / (2 * scale));
        mBoundBottom = mImageH - mBoundTop;

        // If the scaled height is smaller than the view height,
        // force it to be in the center.
        if (Math.floor(mImageH * scale) <= mViewH) {
            mBoundTop = mBoundBottom = mImageH / 2;
        }

        // Same for width
        if (Math.floor(mImageW * scale) <= mViewW) {
            mBoundLeft = mBoundRight = mImageW / 2;
        }
    }

    private boolean useCurrentValueAsTarget() {
        return mAnimationStartTime == NO_ANIMATION ||
                mAnimationKind == ANIM_KIND_SNAPBACK ||
                mAnimationKind == ANIM_KIND_FLING;
    }

    private float getTargetScale() {
        return useCurrentValueAsTarget() ? mCurrentScale : mToScale;
    }

    private int getTargetX() {
        return useCurrentValueAsTarget() ? mCurrentX : mToX;
    }

    private int getTargetY() {
        return useCurrentValueAsTarget() ? mCurrentY : mToY;
    }

    public RectF getImageBounds() {
        float points[] = mTempPoints;

        /*
         * (p0,p1)----------(p2,p3)
         *   |                  |
         *   |                  |
         * (p4,p5)----------(p6,p7)
         */
        points[0] = points[4] = -mCurrentX;
        points[1] = points[3] = -mCurrentY;
        points[2] = points[6] = mImageW - mCurrentX;
        points[5] = points[7] = mImageH - mCurrentY;

        RectF rect = mTempRect;
        rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
                Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);

        float scale = mCurrentScale;
        float offsetX = mViewW / 2;
        float offsetY = mViewH / 2;
        for (int i = 0; i < 4; ++i) {
            float x = points[i + i] * scale + offsetX;
            float y = points[i + i + 1] * scale + offsetY;
            if (x < rect.left) rect.left = x;
            if (x > rect.right) rect.right = x;
            if (y < rect.top) rect.top = y;
            if (y > rect.bottom) rect.bottom = y;
        }
        return rect;
    }

    public int getImageWidth() {
        return mImageW;
    }

    public int getImageHeight() {
        return mImageH;
    }

    public boolean isAtLeftEdge() {
        calculateStableBound(mCurrentScale);
        return mCurrentX <= mBoundLeft;
    }

    public boolean isAtRightEdge() {
        calculateStableBound(mCurrentScale);
        return mCurrentX >= mBoundRight;
    }
}
