/**
 *  Filename: ZoomAndScrollImageView.java (in org.repin.android.ui.mapview)
 *  This file is part of the Redpin project.
 * 
 *  Redpin is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published
 *  by the Free Software Foundation, either version 3 of the License, or
 *  any later version.
 *
 *  Redpin is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with Redpin. If not, see <http://www.gnu.org/licenses/>.
 *
 *  (c) Copyright ETH Zurich, Pascal Brogle, Philipp Bolliger, 2010, ALL RIGHTS RESERVED.
 * 
 *  www.redpin.org
 */
package org.redpin.android.ui.mapview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Picture;
import android.graphics.drawable.BitmapDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.widget.Scroller;
import android.widget.ZoomButtonsController;
import android.widget.ZoomButtonsController.OnZoomListener;

/**
 * ImageView that is capable of zooming and scrolling an image.
 * 
 * @author Pascal Brogle ([email protected])
 * 
 */
public class ZoomAndScrollImageView extends View implements OnZoomListener,
		OnDoubleTapListener, OnGestureListener, OnScaleGestureListener {

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

	private static final float ZOOM_STEP = 0.5f;

	private static final float DPAD_MOVEMENT_STEP = 20;

	static float MAX_ZOOM = 4.0f;
	static float MIN_ZOOM = 1f;

	float scale = 1.0f;

	private Scroller scroller;
	private GestureDetector gestureDetector;
	private ScaleGestureDetector scaleGestureDetector;

	private ZoomButtonsController zoomController;
	private Matrix matrix;

	private float currentX;
	private float currentY;
	private float contentWidth;
	private float contentHeight;

	private ZoomAndScrollViewListener listener;

	private static float[] ORIGIN = new float[] { 0, 0 };
	private float[] destination;

	private Picture picture;

	/**
	 * Construct a new ZoomAndScrollImageView with layout parameters and a
	 * default style.
	 * 
	 * @param context
	 *            A Context object used to access application assets.
	 * @param attrs
	 *            An AttributeSet passed to our parent.
	 * @param defStyle
	 *            The default style resource ID.
	 */
	public ZoomAndScrollImageView(Context context, AttributeSet attrs,
			int defStyle) {
		super(context, attrs, defStyle);
		init(context);

	}

	/**
	 * Construct a new ZoomAndScrollImageView with layout parameters.
	 * 
	 * @param context
	 *            A Context object used to access application assets.
	 * @param attrs
	 *            An AttributeSet passed to our parent.
	 */
	public ZoomAndScrollImageView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context);
	}

	/**
	 * Construct a new ZoomAndScrollImageView with a Context object.
	 * 
	 * @param context
	 *            A Context object used to access application assets.
	 */
	public ZoomAndScrollImageView(Context context) {
		super(context);
		init(context);
	}

	/**
	 * Initializes the view
	 * 
	 * @param context
	 *            {@link Context}
	 */
	private void init(Context context) {
		setFocusable(true);
		scroller = new Scroller(context);
		gestureDetector = new GestureDetector(context, this);
		scaleGestureDetector = new ScaleGestureDetector(context, this);

		zoomController = new ZoomButtonsController(this);
		zoomController.setOnZoomListener(this);

		matrix = new Matrix();
		destination = new float[2];

		// setVerticalScrollBarEnabled(true);
		// setHorizontalScrollBarEnabled(true);

	}

	/**
	 * Displays a bitmap
	 * 
	 * @param bitmap
	 *            {@link Bitmap}
	 */
	public void setImageBitmap(Bitmap bitmap) {
		setZoom(1.0f, false);
		setContentSize(bitmap.getWidth(), bitmap.getHeight());

		picture = new Picture();
		Canvas c = picture
				.beginRecording(bitmap.getWidth(), bitmap.getHeight());
		c.drawBitmap(bitmap, 0, 0, null);
		picture.endRecording();
	}

	/**
	 * Displays a drawable
	 * 
	 * @param bDrawable
	 *            {@link BitmapDrawable}
	 */
	public void setImageDrawable(BitmapDrawable bDrawable) {
		setImageBitmap(bDrawable.getBitmap());
	}

	/**
	 * Adjusts minimal zoom level depending on the image size
	 */
	public void adjustMinZoom() {
		int w = getWidth();
		int h = getHeight();
		if (w == 0 || h == 0) {
			return;
		}

		MIN_ZOOM = Math.max(w / contentWidth, h / contentHeight);
	}

	/**
	 * Sets the image content size
	 * 
	 * @param width
	 *            Image width
	 * @param height
	 *            Image height
	 */
	public void setContentSize(int width, int height) {
		contentWidth = width;
		contentHeight = height;
		adjustMinZoom();
	}

	/**
	 * 
	 * @return Current zoom scale
	 */
	public float getScale() {
		return scale;
	}

	/**
	 * 
	 * @param scale
	 *            Desired zoom scale
	 */
	public void setScale(float scale) {
		this.scale = scale;
	}

	/**
	 * Notifies the listener about the changed matrix
	 * 
	 * @param m
	 *            Changed matrix
	 */
	private void notifyMatrix(Matrix m) {
		if (listener != null) {
			listener.onMatrixChange(m, this);
		}

	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void scrollBy(int x, int y) {

	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void scrollTo(int x, int y) {
		currentX = -x;
		currentY = -y;

		currentX = Math.max(getWidth() - contentWidth, Math.min(0, currentX));
		currentY = Math.max(getHeight() - contentHeight, Math.min(0, currentY));

		invalidate();
	}

	/**
	 * 
	 * @return current x and y coordinate of the
	 */
	public float[] getCurrentXY() {
		return new float[] { currentX, currentY };
	}

	/*
	 * {@link OnDoubleTapListener}
	 */

	private boolean zoomedIn = false;

	@Override
	public boolean onDoubleTap(MotionEvent e) {

		float oldX, oldY;
		oldX = currentX;
		oldY = currentY;
		currentX -= (e.getX() - getWidth() / 2) / scale;
		currentY -= (e.getY() - getHeight() / 2) / scale;

		currentX = Math.max(getWidth() - contentWidth, Math.min(0, currentX));

		zoomedIn = !zoomedIn;

		changeZoom(zoomedIn ? 1 : -1, oldX, currentX, oldY, currentY);

		return true;
	}

	@Override
	public boolean onDoubleTapEvent(MotionEvent e) {
		return false;
	}

	@Override
	public boolean onSingleTapConfirmed(MotionEvent e) {
		if (listener != null) {
			listener.onSingleTab(e);
		}
		return true;
	}

	/*
	 * {@link OnGestureListener}
	 */
	@Override
	public boolean onDown(MotionEvent e) {
		zoomController.setVisible(false);
		return true;
	}

	@Override
	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
			float velocityY) {

		final float velocityFactor = 1.5f;
		int minX = (int) (getWidth() - contentWidth);
		int minY = (int) (getHeight() - contentHeight);
		scroller.fling((int) currentX, (int) currentY,
				(int) (velocityX / velocityFactor),
				(int) (velocityY / velocityFactor), minX, 0, minY, 0);
		return true;
	}

	@Override
	public void onLongPress(MotionEvent e) {
		zoomController.setVisible(true);
	}

	@Override
	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
			float distanceY) {

		currentX -= distanceX / scale;
		currentY -= distanceY / scale;

		currentX = Math.max(getWidth() - contentWidth, Math.min(0, currentX));
		currentY = Math.max(getHeight() - contentHeight, Math.min(0, currentY));

		// notifyOnScroll();

		invalidate();
		return true;
	}

	@Override
	public void onShowPress(MotionEvent e) {
	}

	@Override
	public boolean onSingleTapUp(MotionEvent e) {
		return false;
	}

	/*
	 * {@link OnZoomListener}
	 */
	@Override
	public void onVisibilityChanged(boolean visible) {
	}

	@Override
	public void onZoom(boolean zoomIn) {
		float toX = currentX;
		float toY = currentY;
		changeZoom(zoomIn ? ZOOM_STEP : -ZOOM_STEP, currentX, toX, currentY,
				toY);
	}

	public void changeZoom(float amount, float fromX, float toX, float fromY,
			float toY) {

		myTranslation.start(amount, fromX, toX, fromY, toY);

	}

	public void setZoom(float zoom, boolean adjust) {
		Log.d(TAG, "Before: " + zoom + "(max: " + MAX_ZOOM + ",min: "
				+ MIN_ZOOM + ")");
		scale = zoom;
		if (adjust) {
			scale = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, scale));
		}
		Log.d(TAG, "After: " + scale);
		zoomController.setZoomInEnabled(scale != MAX_ZOOM);
		zoomController.setZoomOutEnabled(scale != MIN_ZOOM);
		invalidate();
	}

	/*
	 * {@link View}
	 */
	@Override
	protected void onDetachedFromWindow() {
		super.onDetachedFromWindow();
		zoomController.setVisible(false);
	}

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		boolean h1 = scaleGestureDetector.onTouchEvent(ev);
		boolean h2 = gestureDetector.onTouchEvent(ev);

		return h1 || h2;

	}

	@Override
	protected void onDraw(Canvas canvas) {
		if (myTranslation != null && myTranslation.hasStarted()
				&& !myTranslation.hasEnded()) {
			myTranslation.getTransformation(AnimationUtils
					.currentAnimationTimeMillis(), null);
		}

		int saveCount = canvas.save();

		if (scroller.computeScrollOffset()) {
			currentX = scroller.getCurrX();
			currentY = scroller.getCurrY();
			invalidate();
		}

		int width = getWidth();
		int height = getHeight();

		matrix.reset();

		float scaledWidth = contentWidth * scale;
		float scaledHeigth = contentHeight * scale;

		float transX = scaledWidth > width ? currentX * scale
				: (width - scaledWidth) / 2;
		float transY = scaledHeigth > height ? currentY * scale
				: (height - scaledHeigth) / 2;

		matrix.preTranslate(transX, transY);

		float pivotX = 0;
		if (scaledWidth > width) {
			pivotX = Math.max(Math.min(-currentX, width / 2), 2 * width
					- contentWidth - currentX);
		}

		float pivotY = 0;
		if (scaledHeigth > height) {
			pivotY = Math.max(Math.min(-currentY, height / 2), 2 * height
					- contentHeight - currentY);
		}

		matrix.preScale(scale, scale, pivotX, pivotY);

		notifyMatrix(matrix);

		canvas.concat(matrix);

		if (picture != null) {
			picture.draw(canvas);
		}

		canvas.restoreToCount(saveCount);

	}

	@Override
	protected int computeHorizontalScrollExtent() {
		return Math.round(computeHorizontalScrollRange() * getWidth()
				/ (contentWidth * scale));
	}

	@Override
	protected int computeHorizontalScrollOffset() {
		matrix.mapPoints(destination, ORIGIN);
		float x = -destination[0] / scale;
		return Math.round(computeHorizontalScrollRange() * x / contentWidth);
	}

	@Override
	protected int computeVerticalScrollExtent() {
		return Math.round(computeVerticalScrollRange() * getHeight()
				/ (contentHeight * scale));
	}

	@Override
	protected int computeVerticalScrollOffset() {
		matrix.mapPoints(destination, ORIGIN);
		float y = -destination[1] / scale;
		return Math.round(computeVerticalScrollRange() * y / contentHeight);
	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		boolean handeled = false;
		switch (keyCode) {
		case KeyEvent.KEYCODE_DPAD_LEFT:
			currentX += DPAD_MOVEMENT_STEP / scale;
			currentX = Math.max(getWidth() - contentWidth, Math
					.min(0, currentX));

			invalidate();
			handeled = true;
			break;
		case KeyEvent.KEYCODE_DPAD_RIGHT:
			currentX -= DPAD_MOVEMENT_STEP / scale;
			currentX = Math.max(getWidth() - contentWidth, Math
					.min(0, currentX));

			invalidate();
			handeled = true;
			break;
		case KeyEvent.KEYCODE_DPAD_UP:

			currentY += DPAD_MOVEMENT_STEP / scale;
			currentY = Math.max(getHeight() - contentHeight, Math.min(0,
					currentY));

			invalidate();
			handeled = true;
			break;
		case KeyEvent.KEYCODE_DPAD_DOWN:

			currentY -= DPAD_MOVEMENT_STEP / scale;
			currentY = Math.max(getHeight() - contentHeight, Math.min(0,
					currentY));

			invalidate();
			handeled = true;
			break;

		case KeyEvent.KEYCODE_DPAD_CENTER:
			zoomedIn = !zoomedIn;
			changeZoom(zoomedIn ? 1 : -1, currentX, currentX, currentX,
					currentY);
			handeled = true;
			break;
		default:
			break;
		}

		return handeled;
	}

	@Override
	public boolean onTrackballEvent(MotionEvent e) {

		boolean handeled = false;

		switch (e.getAction()) {
		case MotionEvent.ACTION_DOWN:
			zoomedIn = !zoomedIn;
			changeZoom(zoomedIn ? 1 : -1, currentX, currentX, currentX,
					currentY);
			handeled = true;
			break;

		case MotionEvent.ACTION_MOVE:
			currentX -= e.getX() * DPAD_MOVEMENT_STEP / scale;
			currentY -= e.getY() * DPAD_MOVEMENT_STEP / scale;

			currentX = Math.max(getWidth() - contentWidth, Math
					.min(0, currentX));
			currentY = Math.max(getHeight() - contentHeight, Math.min(0,
					currentY));

			handeled = true;
			invalidate();
			break;

		default:
			break;
		}

		return handeled;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		adjustMinZoom();
	}

	ZoomAndTranslate myTranslation = new ZoomAndTranslate();

	/**
	 * Animation that zoom and translates to a given position
	 * 
	 * @author Pascal Brogle ([email protected])
	 * 
	 */
	class ZoomAndTranslate extends Animation {
		private static final int DURATION = 1000;

		private float mFrom;
		private float mTo;
		private Interpolator translateInterpolator;

		private float fromX;
		private float toX;
		private float fromY;
		private float toY;
		private Interpolator zoomInterpolator;

		public ZoomAndTranslate() {
			setDuration(DURATION);
			setAnimationListener(new AnimationListener() {

				@Override
				public void onAnimationStart(Animation animation) {
					notifyScaleBegin();
				}

				@Override
				public void onAnimationRepeat(Animation animation) {
				}

				@Override
				public void onAnimationEnd(Animation animation) {
					notifyScaleEnd();
				}
			});
		}

		/**
		 * Starts the animation
		 * 
		 * @param amount
		 *            Zoom amount
		 * @param fromX
		 *            From x coordinate
		 * @param toX
		 *            To x coordinate
		 * @param fromY
		 *            From y coordinate
		 * @param toY
		 *            To y coordinate
		 */
		public void start(float amount, float fromX, float toX, float fromY,
				float toY) {
			this.fromX = fromX;
			this.toX = toX;
			this.fromY = fromY;
			this.toY = toY;

			translateInterpolator = new DecelerateInterpolator(); // new
			// LinearInterpolator();//
			// new
			// DecelerateInterpolator();
			zoomInterpolator = new AccelerateDecelerateInterpolator(); // new
			// LinearInterpolator();//
			// AccelerateDecelerateInterpolator();
			mFrom = scale;
			mTo = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, scale + amount));

			start();
			long t = AnimationUtils.currentAnimationTimeMillis();
			getTransformation(t, null);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		protected void applyTransformation(float interpolatedTime,
				Transformation t) {
			float time = interpolatedTime;

			float tInterpolatedTime = translateInterpolator
					.getInterpolation(time);
			float zInterpolatedTime = zoomInterpolator.getInterpolation(time);
			currentX = fromX + (toX - fromX) * tInterpolatedTime;
			currentY = fromY + (toY - fromY) * tInterpolatedTime;
			setZoom(mFrom + (mTo - mFrom) * zInterpolatedTime, false);

		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean onScale(ScaleGestureDetector detector) {
		// Log.i(TAG, "onScale, factor:" + detector.getScaleFactor());
		float factor = detector.getScaleFactor();

		scale *= factor;
		setZoom(scale, true);
		Log.i(TAG, "onScale, " + scale + "(factor: +" + factor + ")");

		return true;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean onScaleBegin(ScaleGestureDetector detector) {
		notifyScaleBegin();
		Log.i(TAG, "onScale Begin");
		return true;
	}

	/**
	 * Notifies the beginning of the scaling to the listener
	 */
	private void notifyScaleBegin() {
		if (listener != null) {
			listener.onScaleBegin(this);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void onScaleEnd(ScaleGestureDetector detector) {

		notifyScaleEnd();
		Log.i(TAG, "onScale End");
		System.out.println("End scale: " + scale);

	}

	/**
	 * Notifies the ending of the scaling to the listener
	 */
	private void notifyScaleEnd() {
		if (listener != null) {
			listener.onScaleEnd(this);
		}
	}

	public void setListener(ZoomAndScrollViewListener l) {
		listener = l;
	}

	/**
	 * Listener-Interface for {@link ZoomAndScrollImageView}
	 * 
	 * @author Pascal Brogle ([email protected])
	 * 
	 */
	public interface ZoomAndScrollViewListener {
		/**
		 * Called when a change in the drawing matrix occours
		 * 
		 * @param m
		 *            New matrix
		 * @param view
		 *            View that calls the method
		 */
		public void onMatrixChange(Matrix m, ZoomAndScrollImageView view);

		/**
		 * Called when the user begins to scale
		 * 
		 * @param view
		 *            View that calls the method
		 */
		public void onScaleBegin(ZoomAndScrollImageView view);

		/**
		 * Called when the user ends scaling
		 * 
		 * @param view
		 *            View that calls the method
		 */
		public void onScaleEnd(ZoomAndScrollImageView view);

		/**
		 * Called when the user tabs the view
		 * 
		 * @param e
		 *            MotionEvent
		 */
		public void onSingleTab(MotionEvent e);
	}

}