/*******************************************************************************
 * Copyright  2015 Albin Mathew.
 *  <p/>
 * 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
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * 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 cn.joy.imageselector.crop;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Matrix.ScaleToFit;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.FloatMath;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLongClickListener;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;

import java.lang.ref.WeakReference;

import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;

class CropPhotoViewAttacher implements IPhotoView, View.OnTouchListener, OnGestureListener,
	   ViewTreeObserver.OnGlobalLayoutListener {

   static final Interpolator sInterpolator = new AccelerateDecelerateInterpolator();
   static final int EDGE_NONE = -1;
   static final int EDGE_LEFT = 0;
   static final int EDGE_RIGHT = 1;
   static final int EDGE_BOTH = 2;
   private static final String LOG_TAG = "PhotoViewAttacher";
   // let debug flag be dynamic, but still Proguard can be used to remove from
   // release builds
   private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
   // These are set so we don't keep allocating them on the heap
   private final Matrix mBaseMatrix = new Matrix();
   private final Matrix mDrawMatrix = new Matrix();
   private final Matrix mSuppMatrix = new Matrix();
   private final RectF mDisplayRect = new RectF();
   private final float[] mMatrixValues = new float[9];
   int ZOOM_DURATION = DEFAULT_ZOOM_DURATION;
   IGetImageBounds boundsListener;
   private float mMinScale = DEFAULT_MIN_SCALE;
   private float mMidScale = DEFAULT_MID_SCALE;
   private float mMaxScale = DEFAULT_MAX_SCALE;
   private boolean mAllowParentInterceptOnEdge = true;
   private WeakReference<ImageView> mImageView;
   // Gesture Detectors
   private android.view.GestureDetector mGestureDetector;
   private GestureDetector mScaleDragDetector;
   // Listeners
   private OnMatrixChangedListener mMatrixChangeListener;
   private OnPhotoTapListener mPhotoTapListener;
   private OnViewTapListener mViewTapListener;
   private OnLongClickListener mLongClickListener;
   private int mIvTop, mIvRight, mIvBottom, mIvLeft;
   private FlingRunnable mCurrentFlingRunnable;
   private int mScrollEdge = EDGE_BOTH;
   private boolean mZoomEnabled;
   private ScaleType mScaleType = ScaleType.FIT_CENTER;
   public CropPhotoViewAttacher(ImageView imageView) {
	   mImageView = new WeakReference<>(imageView);

	   imageView.setDrawingCacheEnabled(true);
	   imageView.setOnTouchListener(this);

	   ViewTreeObserver observer = imageView.getViewTreeObserver();
//        if (null != observer) observer.addOnGlobalLayoutListener(this);

	   // Make sure we using MATRIX Scale Type
	   setImageViewScaleTypeMatrix(imageView);

	   if (imageView.isInEditMode()) {
		   return;
	   }
	   // Create Gesture Detectors...
	   mScaleDragDetector = CropVersionedGestureDetector.newInstance(imageView.getContext(), this);

	   mGestureDetector = new android.view.GestureDetector(imageView.getContext(),
			   new android.view.GestureDetector.SimpleOnGestureListener() {

				   // forward long click listener
				   @Override
				   public void onLongPress(MotionEvent e) {
					   if (null != mLongClickListener) {
						   mLongClickListener.onLongClick(getImageView());
					   }
				   }
			   });

	   mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));

	   // Finally, update the UI so that we're zoomable
	   setZoomable(true);
   }

   private static void checkZoomLevels(float minZoom, float midZoom, float maxZoom) {

	   //TODO : Commenting this check for now. Loading image with different dimension causes this exception
//        if (minZoom >= midZoom) {
//            throw new IllegalArgumentException(
//                    "MinZoom has to be less than MidZoom");
//        } else if (midZoom >= maxZoom) {
//            throw new IllegalArgumentException(
//                    "MidZoom has to be less than MaxZoom");
//        }
   }

   /**
	* @return true if the ImageView exists, and it's Drawable existss
	*/
   private static boolean hasDrawable(ImageView imageView) {
	   return null != imageView && null != imageView.getDrawable();
   }

   /**
	* @return true if the ScaleType is supported.
	*/
   private static boolean isSupportedScaleType(final ScaleType scaleType) {
	   if (null == scaleType) {
		   return false;
	   }

	   switch (scaleType) {
		   case MATRIX:
			   throw new IllegalArgumentException(scaleType.name()
					   + " is not supported in PhotoView");

		   default:
			   return true;
	   }
   }

   /**
	* Set's the ImageView's ScaleType to Matrix.
	*/
   private static void setImageViewScaleTypeMatrix(ImageView imageView) {
	   /**
		* PhotoView sets it's own ScaleType to Matrix, then diverts all calls
		* setScaleType to this.setScaleType automatically.
		*/
	   if (null != imageView && !(imageView instanceof IPhotoView)) {
		   if (!ScaleType.MATRIX.equals(imageView.getScaleType())) {
			   imageView.setScaleType(ScaleType.MATRIX);
		   }
	   }
   }

   @Override
   public void setOnDoubleTapListener(android.view.GestureDetector.OnDoubleTapListener newOnDoubleTapListener) {
	   if (newOnDoubleTapListener != null)
		   this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener);
	   else
		   this.mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));
   }

   @Override
   public boolean canZoom() {
	   return mZoomEnabled;
   }

   /**
	* Clean-up the resources attached to this object. This needs to be called when the ImageView is
	* no longer used. A good example is from {@link View#onDetachedFromWindow()} or
	* from {@link android.app.Activity#onDestroy()}.
	*/
   @SuppressWarnings("deprecation")
   public void cleanup() {
	   if (null == mImageView) {
		   return; // cleanup already done
	   }

	   final ImageView imageView = mImageView.get();

	   if (null != imageView) {
		   // Remove this as a global layout listener
		   ViewTreeObserver observer = imageView.getViewTreeObserver();
		   if (null != observer && observer.isAlive()) {
			   observer.removeGlobalOnLayoutListener(this);
		   }

		   // Remove the ImageView's reference to this
		   imageView.setOnTouchListener(null);

		   // make sure a pending fling runnable won't be run
		   cancelFling();
	   }

	   if (null != mGestureDetector) {
		   mGestureDetector.setOnDoubleTapListener(null);
	   }

	   // Clear listeners too
	   mMatrixChangeListener = null;
	   mPhotoTapListener = null;
	   mViewTapListener = null;

	   // Finally, clear ImageView
	   mImageView = null;
   }

   @Override
   public RectF getDisplayRect() {
	   checkMatrixBounds();
	   return getDisplayRect(getDrawMatrix());
   }

   @Override
   public boolean setDisplayMatrix(Matrix finalMatrix) {
	   if (finalMatrix == null)
		   throw new IllegalArgumentException("Matrix cannot be null");

	   ImageView imageView = getImageView();
	   if (null == imageView)
		   return false;

	   if (null == imageView.getDrawable())
		   return false;

	   mSuppMatrix.set(finalMatrix);
	   setImageViewMatrix(getDrawMatrix());
	   checkMatrixBounds();

	   return true;
   }

   /**
	* @deprecated use {@link #setRotationTo(float)}
	*/
   @Override
   public void setPhotoViewRotation(float degrees) {
	   mSuppMatrix.setRotate(degrees % 360);
	   checkAndDisplayMatrix();
   }

   @Override
   public void setRotationTo(float degrees) {
	   mSuppMatrix.setRotate(degrees % 360);
	   checkAndDisplayMatrix();
   }

   @Override
   public void setRotationBy(float degrees) {
	   mSuppMatrix.postRotate(degrees % 360);
	   checkAndDisplayMatrix();
   }

   public ImageView getImageView() {
	   ImageView imageView = null;

	   if (null != mImageView) {
		   imageView = mImageView.get();
	   }

	   // If we don't have an ImageView, call cleanup()
	   if (null == imageView) {
		   cleanup();
		   Log.i(LOG_TAG, "ImageView no longer exists. You should not use this PhotoViewAttacher any more.");
	   }

	   return imageView;
   }

   @Override
   @Deprecated
   public float getMinScale() {
	   return getMinimumScale();
   }

   @Override
   @Deprecated
   public void setMinScale(float minScale) {
	   setMinimumScale(minScale);
   }

   @Override
   public float getMinimumScale() {
	   return mMinScale;
   }

   @Override
   public void setMinimumScale(float minimumScale) {
	   checkZoomLevels(minimumScale, mMidScale, mMaxScale);
	   mMinScale = minimumScale;
   }

   @Override
   @Deprecated
   public float getMidScale() {
	   return getMediumScale();
   }

   @Override
   @Deprecated
   public void setMidScale(float midScale) {
	   setMediumScale(midScale);
   }

   @Override
   public float getMediumScale() {
	   return mMidScale;
   }

   @Override
   public void setMediumScale(float mediumScale) {
	   checkZoomLevels(mMinScale, mediumScale, mMaxScale);
	   mMidScale = mediumScale;
   }

   @Override
   @Deprecated
   public float getMaxScale() {
	   return getMaximumScale();
   }

   @Override
   @Deprecated
   public void setMaxScale(float maxScale) {
	   setMaximumScale(maxScale);
   }

   @Override
   public float getMaximumScale() {
	   return mMaxScale;
   }

   @Override
   public void setMaximumScale(float maximumScale) {
	   checkZoomLevels(mMinScale, mMidScale, maximumScale);
	   mMaxScale = maximumScale;
   }

   @Override
   public float getScale() {
	   return FloatMath.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow(getValue(mSuppMatrix, Matrix.MSKEW_Y), 2));
   }

   @Override
   public void setScale(float scale) {
	   setScale(scale, false);
   }

   @Override
   public ScaleType getScaleType() {
	   return mScaleType;
   }

   @Override
   public void setScaleType(ScaleType scaleType) {
	   if (isSupportedScaleType(scaleType) && scaleType != mScaleType) {
		   mScaleType = scaleType;

		   // Finally update
		   update();
	   }
   }

   @Override
   public void onDrag(float dx, float dy) {
	   if (mScaleDragDetector.isScaling()) {
		   return; // Do not drag if we are already scaling
	   }

	   if (DEBUG) {
		   Log.d(LOG_TAG, String.format("onDrag: dx: %.2f. dy: %.2f", dx, dy));
	   }

	   ImageView imageView = getImageView();
	   mSuppMatrix.postTranslate(dx, dy);
	   checkAndDisplayMatrix();

	   /**
		* Here we decide whether to let the ImageView's parent to start taking
		* over the touch event.
		*
		* First we check whether this function is enabled. We never want the
		* parent to take over if we're scaling. We then check the edge we're
		* on, and the direction of the scroll (i.e. if we're pulling against
		* the edge, aka 'overscrolling', let the parent take over).
		*/
	   ViewParent parent = imageView.getParent();
	   if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling()) {
		   if (mScrollEdge == EDGE_BOTH
				   || (mScrollEdge == EDGE_LEFT && dx >= 1f)
				   || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) {
			   if (null != parent)
				   parent.requestDisallowInterceptTouchEvent(false);
		   }
	   } else {
		   if (null != parent) {
			   parent.requestDisallowInterceptTouchEvent(true);
		   }
	   }
   }

   @Override
   public void onFling(float startX, float startY, float velocityX, float velocityY) {
	   if (DEBUG) {
		   Log.d(LOG_TAG, "onFling. sX: " + startX + " sY: " + startY + " Vx: " + velocityX + " Vy: " + velocityY);
	   }
	   ImageView imageView = getImageView();
	   mCurrentFlingRunnable = new FlingRunnable(imageView.getContext());
	   mCurrentFlingRunnable.fling(getImageViewWidth(imageView), getImageViewHeight(imageView), (int) velocityX, (int) velocityY);
	   imageView.post(mCurrentFlingRunnable);
   }

   @Override
   public void onGlobalLayout() {
	   ImageView imageView = getImageView();

	   if (null != imageView) {
		   if (mZoomEnabled) {
			   final int top = imageView.getTop();
			   final int right = imageView.getRight();
			   final int bottom = imageView.getBottom();
			   final int left = imageView.getLeft();

			   /**
				* We need to check whether the ImageView's bounds have changed.
				* This would be easier if we targeted API 11+ as we could just use
				* View.OnLayoutChangeListener. Instead we have to replicate the
				* work, keeping track of the ImageView's bounds and then checking
				* if the values change.
				*/
			   if (top != mIvTop || bottom != mIvBottom || left != mIvLeft
					   || right != mIvRight) {
				   // Update our base matrix, as the bounds have changed
				   updateBaseMatrix(imageView.getDrawable());

				   // Update values as something has changed
				   mIvTop = top;
				   mIvRight = right;
				   mIvBottom = bottom;
				   mIvLeft = left;
			   }
		   } else {
			   updateBaseMatrix(imageView.getDrawable());
		   }
	   }
   }

   @Override
   public void onScale(float scaleFactor, float focusX, float focusY) {
	   if (DEBUG) {
		   Log.d(LOG_TAG, String.format("onScale: scale: %.2f. fX: %.2f. fY: %.2f", scaleFactor, focusX, focusY));
	   }

	   if (getScale() < mMaxScale || scaleFactor < 1f) {
		   mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
		   checkAndDisplayMatrix();
	   }
   }

   @Override
   public boolean onTouch(View v, MotionEvent ev) {
	   boolean handled = false;

	   if (mZoomEnabled && hasDrawable((ImageView) v)) {
		   ViewParent parent = v.getParent();
		   switch (ev.getAction()) {
			   case ACTION_DOWN:
				   // First, disable the Parent from intercepting the touch
				   // event
				   if (null != parent)
					   parent.requestDisallowInterceptTouchEvent(true);
				   else
					   Log.i(LOG_TAG, "onTouch getParent() returned null");

				   // If we're flinging, and the user presses down, cancel
				   // fling
				   cancelFling();
				   break;

			   case ACTION_CANCEL:
			   case ACTION_UP:
				   // If the user has zoomed less than min scale, zoom back
				   // to min scale
				   if (getScale() < mMinScale) {
					   RectF rect = getDisplayRect();
					   if (null != rect) {
						   v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
								   rect.centerX(), rect.centerY()));
						   handled = true;
					   }
				   }
				   break;
		   }

		   // Try the Scale/Drag detector
		   if (null != mScaleDragDetector && mScaleDragDetector.onTouchEvent(ev)) {
			   handled = true;
		   }

		   // Check to see if the user double tapped
		   if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
			   handled = true;
		   }
	   }

	   return handled;
   }

   @Override
   public void setAllowParentInterceptOnEdge(boolean allow) {
	   mAllowParentInterceptOnEdge = allow;
   }

   @Override
   public void setOnLongClickListener(OnLongClickListener listener) {
	   mLongClickListener = listener;
   }

   @Override
   public void setOnMatrixChangeListener(OnMatrixChangedListener listener) {
	   mMatrixChangeListener = listener;
   }

   @Override
   public OnPhotoTapListener getOnPhotoTapListener() {
	   return mPhotoTapListener;
   }

   @Override
   public void setOnPhotoTapListener(OnPhotoTapListener listener) {
	   mPhotoTapListener = listener;
   }

   @Override
   public OnViewTapListener getOnViewTapListener() {
	   return mViewTapListener;
   }

   @Override
   public void setOnViewTapListener(OnViewTapListener listener) {
	   mViewTapListener = listener;
   }

   @Override
   public void setScale(float scale, boolean animate) {
	   ImageView imageView = getImageView();

	   if (null != imageView) {
		   setScale(scale,
				   (imageView.getRight()) / 2,
				   (imageView.getBottom()) / 2,
				   animate);
	   }
   }

   @Override
   public void setScale(float scale, float focalX, float focalY,
						boolean animate) {
	   ImageView imageView = getImageView();

	   if (null != imageView) {
		   // Check to see if the scale is within bounds
		   if (scale < mMinScale || scale > mMaxScale) {
			   Log.i(LOG_TAG, "Scale must be within the range of minScale and maxScale");
			   return;
		   }

		   if (animate) {
			   imageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY));
		   } else {
			   mSuppMatrix.setScale(scale, scale, focalX, focalY);
			   checkAndDisplayMatrix();
		   }
	   }
   }

   @Override
   public void setZoomable(boolean zoomable) {
	   mZoomEnabled = zoomable;
	   update();
   }

   @Override
   public void update() {
	   ImageView imageView = getImageView();
	   if (null != imageView) {
		   if (mZoomEnabled) {

			   // Make sure we using MATRIX Scale Type
			   setImageViewScaleTypeMatrix(imageView);

			   // Update the base matrix using the current drawable
			   updateBaseMatrix(imageView.getDrawable());
		   } else {
			   // Reset the Matrix...
			   resetMatrix();
		   }
	   }
   }

   @Override
   public Matrix getDisplayMatrix() {
	   return new Matrix(getDrawMatrix());
   }

   public Matrix getDrawMatrix() {
	   mDrawMatrix.set(mBaseMatrix);
	   mDrawMatrix.postConcat(mSuppMatrix);
	   return mDrawMatrix;
   }

   private void cancelFling() {
	   if (null != mCurrentFlingRunnable) {
		   mCurrentFlingRunnable.cancelFling();
		   mCurrentFlingRunnable = null;
	   }
   }

   /**
	* Helper method that simply checks the Matrix, and then displays the result
	*/
   private void checkAndDisplayMatrix() {
	   if (checkMatrixBounds()) {
		   setImageViewMatrix(getDrawMatrix());
	   }
   }

   private void checkImageViewScaleType() {
	   ImageView imageView = getImageView();

	   /**
		* PhotoView's getScaleType() will just divert to this.getScaleType() so
		* only call if we're not attached to a PhotoView.
		*/
	   if (null != imageView && !(imageView instanceof IPhotoView)) {
		   if (!ScaleType.MATRIX.equals(imageView.getScaleType())) {
			   throw new IllegalStateException(
					   "The ImageView's ScaleType has been changed since attaching a PhotoViewAttacher");
		   }
	   }
   }

   public void addListener(IGetImageBounds listener) {
	   this.boundsListener = listener;
   }

   public Rect getImageBounds() {
	   if (getImageView() == null) {
		   return null;
	   } else if (boundsListener != null) {
		   return boundsListener.getImageBounds();
	   } else {
		   return new Rect(getImageViewWidth(getImageView()), 0, 0, getImageViewHeight(getImageView()));
	   }
//        int cropHeight = 500;
//        int cropWidth = 500;
//        DisplayMetrics displayMetrics = getImageView().getContext().getResources().getDisplayMetrics();
//        int h = displayMetrics.heightPixels;
//        int w = displayMetrics.widthPixels;
//        int edgeT = Math.round(h/2) - Math.round(cropHeight/2);
//        int edgeB = Math.round(h/2) + Math.round(cropHeight/2);
//        int edgeL = Math.round(w/2) - Math.round(cropWidth/2);
//        int edgeR = Math.round(w/2) + Math.round(cropWidth/2);
//        return new Rect(edgeL, edgeT, edgeR, edgeB);
   }

   private boolean checkMatrixBounds() {
	   final ImageView imageView = getImageView();
	   if (null == imageView) {
		   return false;
	   }

	   final RectF rect = getDisplayRect(getDrawMatrix());
	   if (null == rect) {
		   return false;
	   }

	   final float height = rect.height(), width = rect.width();

	   float deltaX = 0, deltaY = 0;

	   Rect overalyImageBounds = getImageBounds();
	   final int overlayViewHeight = overalyImageBounds.height();

	   final int viewHeight = getImageViewHeight(imageView);

	   if (height <= overlayViewHeight) {
		   switch (mScaleType) {
			   case FIT_START:
				   deltaY = -rect.top;
				   break;
			   case FIT_END:
				   deltaY = overlayViewHeight - height - rect.top;
				   break;
			   default:
				   // TODO : Need to fix this ..Need to do something to be more accurate...
				   deltaY = 0;//(overlayViewHeight - height) / 2 ;//- rect.top;
				   break;
		   }
	   } else if (rect.top > overalyImageBounds.top) {
		   deltaY = -(rect.top - overalyImageBounds.top);
	   } else if (rect.bottom < overalyImageBounds.bottom) {
		   //TODO: Need to do something to be accurate...
		   deltaY = (overalyImageBounds.bottom - rect.bottom);
	   }

	   final int overlayViewWidth = overalyImageBounds.width();
	   if (width <= overlayViewWidth) {
		   switch (mScaleType) {
			   case FIT_START:
				   deltaX = -rect.left;
				   break;
			   case FIT_END:
				   deltaX = overlayViewWidth - width - rect.left;
				   break;
			   default:
				   deltaX = (overlayViewWidth - width) / 2 - rect.left;
				   break;
		   }
		   mScrollEdge = EDGE_BOTH;
	   } else if (rect.left > overalyImageBounds.left) {
		   mScrollEdge = EDGE_LEFT;
		   deltaX = -(rect.left - overalyImageBounds.left); // -rect.left;
	   } else if (rect.right < overalyImageBounds.right) {
		   deltaX = (overalyImageBounds.right - rect.right);
		   mScrollEdge = EDGE_RIGHT;
	   } else {
		   mScrollEdge = EDGE_NONE;
	   }
	   // Finally actually translate the matrix
	   mSuppMatrix.postTranslate(deltaX, deltaY);
	   return true;
   }

   /**
	* Helper method that maps the supplied Matrix to the current Drawable
	*
	* @param matrix - Matrix to map Drawable against
	* @return RectF - Displayed Rectangle
	*/
   private RectF getDisplayRect(Matrix matrix) {
	   ImageView imageView = getImageView();

	   if (null != imageView) {
		   Drawable d = imageView.getDrawable();
		   if (null != d) {
			   mDisplayRect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
			   matrix.mapRect(mDisplayRect);
			   return mDisplayRect;
		   }
	   }
	   return null;
   }

   public Bitmap getVisibleRectangleBitmap() {
	   ImageView imageView = getImageView();
	   return imageView == null ? null : imageView.getDrawingCache();
   }

   @Override
   public void setZoomTransitionDuration(int milliseconds) {
	   if (milliseconds < 0)
		   milliseconds = DEFAULT_ZOOM_DURATION;
	   this.ZOOM_DURATION = milliseconds;
   }

   @Override
   public IPhotoView getIPhotoViewImplementation() {
	   return this;
   }

   /**
	* Helper method that 'unpacks' a Matrix and returns the required value
	*
	* @param matrix     - Matrix to unpack
	* @param whichValue - Which value from Matrix.M* to return
	* @return float - returned value
	*/
   private float getValue(Matrix matrix, int whichValue) {
	   matrix.getValues(mMatrixValues);
	   return mMatrixValues[whichValue];
   }

   /**
	* Resets the Matrix back to FIT_CENTER, and then displays it.s
	*/
   private void resetMatrix() {
	   mSuppMatrix.reset();
	   setImageViewMatrix(getDrawMatrix());
	   checkMatrixBounds();
   }

   private void setImageViewMatrix(Matrix matrix) {
	   ImageView imageView = getImageView();
	   if (null != imageView) {

		   checkImageViewScaleType();
		   imageView.setImageMatrix(matrix);

		   // Call MatrixChangedListener if needed
		   if (null != mMatrixChangeListener) {
			   RectF displayRect = getDisplayRect(matrix);
			   if (null != displayRect) {
				   mMatrixChangeListener.onMatrixChanged(displayRect);
			   }
		   }
	   }
   }

   /**
	* Calculate Matrix for FIT_CENTER
	*
	* @param d - Drawable being displayed
	*/
   private void updateBaseMatrix(Drawable d) {
	   ImageView imageView = getImageView();
	   if (null == imageView || null == d) {
		   return;
	   }

	   final float viewWidth = getImageViewWidth(imageView);
	   final float viewHeight = getImageViewHeight(imageView);
	   final int drawableWidth = d.getIntrinsicWidth();
	   final int drawableHeight = d.getIntrinsicHeight();

	   mBaseMatrix.reset();

	   final float widthScale = viewWidth / drawableWidth;
	   final float heightScale = viewHeight / drawableHeight;
	   if (mScaleType == ScaleType.CENTER) {
		   mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F,
				   (viewHeight - drawableHeight) / 2F);

	   } else if (mScaleType == ScaleType.CENTER_CROP) {
		   float scale = Math.max(widthScale, heightScale);
		   mBaseMatrix.postScale(scale, scale);
		   mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
				   (viewHeight - drawableHeight * scale) / 2F);

	   } else if (mScaleType == ScaleType.CENTER_INSIDE) {
		   float scale = Math.min(1.0f, Math.min(widthScale, heightScale));
		   mBaseMatrix.postScale(scale, scale);
		   mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
				   (viewHeight - drawableHeight * scale) / 2F);

	   } else {
		   RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);
		   RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);

		   switch (mScaleType) {
			   case FIT_CENTER:
				   mBaseMatrix
						   .setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER);
				   break;

			   case FIT_START:
				   mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START);
				   break;

			   case FIT_END:
				   mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END);
				   break;

			   case FIT_XY:
				   mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL);
				   break;

			   default:
				   break;
		   }
	   }
	   resetMatrix();
   }

   private int getImageViewWidth(ImageView imageView) {
	   if (null == imageView)
		   return 0;
	   return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight();
   }

   private int getImageViewHeight(ImageView imageView) {
	   if (null == imageView)
		   return 0;
	   return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom();
   }

   /**
	* Used for calculating crop overlay
	*
	*/
   public interface IGetImageBounds {
	   Rect getImageBounds();
   }

   /**
	* Interface definition for a callback to be invoked when the internal Matrix has changed for
	* this View.
	*
	* @author Chris Banes
	*/
   public interface OnMatrixChangedListener {
	   /**
		* Callback for when the Matrix displaying the Drawable has changed. This could be because
		* the View's bounds have changed, or the user has zoomed.
		*
		* @param rect - Rectangle displaying the Drawable's new bounds.
		*/
	   void onMatrixChanged(RectF rect);
   }

   /**
	* Interface definition for a callback to be invoked when the Photo is tapped with a single
	* tap.
	*
	* @author Chris Banes
	*/
   public interface OnPhotoTapListener {

	   /**
		* A callback to receive where the user taps on a photo. You will only receive a callback if
		* the user taps on the actual photo, tapping on 'whitespace' will be ignored.
		*
		* @param view - View the user tapped.
		* @param x    - where the user tapped from the of the Drawable, as percentage of the
		*             Drawable width.
		* @param y    - where the user tapped from the top of the Drawable, as percentage of the
		*             Drawable height.
		*/
	   void onPhotoTap(View view, float x, float y);
   }

   /**
	* Interface definition for a callback to be invoked when the ImageView is tapped with a single
	* tap.
	*
	* @author Chris Banes
	*/
   public interface OnViewTapListener {

	   /**
		* A callback to receive where the user taps on a ImageView. You will receive a callback if
		* the user taps anywhere on the view, tapping on 'whitespace' will not be ignored.
		*
		* @param view - View the user tapped.
		* @param x    - where the user tapped from the left of the View.
		* @param y    - where the user tapped from the top of the View.
		*/
	   void onViewTap(View view, float x, float y);
   }

   private class AnimatedZoomRunnable implements Runnable {

	   private final float mFocalX, mFocalY;
	   private final long mStartTime;
	   private final float mZoomStart, mZoomEnd;

	   public AnimatedZoomRunnable(final float currentZoom, final float targetZoom,
								   final float focalX, final float focalY) {
		   mFocalX = focalX;
		   mFocalY = focalY;
		   mStartTime = System.currentTimeMillis();
		   mZoomStart = currentZoom;
		   mZoomEnd = targetZoom;
	   }

	   @Override
	   public void run() {
		   ImageView imageView = getImageView();
		   if (imageView == null) {
			   return;
		   }

		   float t = interpolate();
		   float scale = mZoomStart + t * (mZoomEnd - mZoomStart);
		   float deltaScale = scale / getScale();

		   mSuppMatrix.postScale(deltaScale, deltaScale, mFocalX, mFocalY);
		   checkAndDisplayMatrix();

		   // We haven't hit our target scale yet, so post ourselves again
		   if (t < 1f) {
			   CropCompat.postOnAnimation(imageView, this);
		   }
	   }

	   private float interpolate() {
		   float t = 1f * (System.currentTimeMillis() - mStartTime) / ZOOM_DURATION;
		   t = Math.min(1f, t);
		   t = sInterpolator.getInterpolation(t);
		   return t;
	   }
   }

   private class FlingRunnable implements Runnable {

	   private final CropScrollerProxy mScroller;
	   private int mCurrentX, mCurrentY;

	   public FlingRunnable(Context context) {
		   mScroller = CropScrollerProxy.getScroller(context);
	   }

	   public void cancelFling() {
		   if (DEBUG) {
			   Log.d(LOG_TAG, "Cancel Fling");
		   }
		   mScroller.forceFinished(true);
	   }

	   public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) {
		   final RectF rect = getDisplayRect();
		   if (null == rect) {
			   return;
		   }

		   final int startX = Math.round(-rect.left);
		   final int minX, maxX, minY, maxY;

		   if (false && viewWidth < rect.width()) {
			   minX = 0;
			   maxX = Math.round(rect.width() - viewWidth);
		   } else {
			   minX = maxX = startX;
		   }

		   final int startY = Math.round(-rect.top);
		   if (false && viewHeight < rect.height()) {
			   minY = 0;
			   maxY = Math.round(rect.height() - viewHeight);
		   } else {
			   minY = maxY = startY;
		   }

		   mCurrentX = startX;
		   mCurrentY = startY;

		   if (DEBUG) {
			   Log.d(LOG_TAG, "fling. StartX:" + startX + " StartY:" + startY + " MaxX:" + maxX + " MaxY:" + maxY);
		   }

		   // If we actually can move, fling the scroller
		   if (startX != maxX || startY != maxY) {
			   mScroller.fling(startX, startY, velocityX, velocityY, minX,
					   maxX, minY, maxY, 0, 0);
		   }
	   }

	   @Override
	   public void run() {
		   if (mScroller.isFinished()) {
			   return; // remaining post that should not be handled
		   }

		   ImageView imageView = getImageView();
		   if (null != imageView && mScroller.computeScrollOffset()) {

			   final int newX = mScroller.getCurrX();
			   final int newY = mScroller.getCurrY();

			   if (DEBUG) {
				   Log.d(LOG_TAG, "fling run(). CurrentX:" + mCurrentX + " CurrentY:" + mCurrentY + " NewX:" + newX + " NewY:" + newY);
			   }

			   mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
			   setImageViewMatrix(getDrawMatrix());

			   mCurrentX = newX;
			   mCurrentY = newY;

			   // Post On animation
			   CropCompat.postOnAnimation(imageView, this);
		   }
	   }
   }
}