/*
 * Copyright (c) 2012 Jason Polites
 *
 * 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.polites.android;

import android.content.res.Configuration;
import android.graphics.PointF;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;

public class GestureImageViewTouchListener implements OnTouchListener {

	private GestureImageView image;
	private OnClickListener onClickListener;

	private final PointF current = new PointF();
	private final PointF last = new PointF();
	private final PointF next = new PointF();
	private final PointF midpoint = new PointF();

	private final VectorF scaleVector = new VectorF();
	private final VectorF pinchVector = new VectorF();

	private boolean touched = false;
	private boolean inZoom = false;

	private float initialDistance;
	private float lastScale = 1.0f;
	private float currentScale = 1.0f;

	private float boundaryLeft = 0;
	private float boundaryTop = 0;
	private float boundaryRight = 0;
	private float boundaryBottom = 0;

	private float maxScale = 5.0f;
	private float minScale = 0.25f;
	private float fitScaleHorizontal = 1.0f;
	private float fitScaleVertical = 1.0f;

	private int canvasWidth = 0;
	private int canvasHeight = 0;

	private float centerX = 0;
	private float centerY = 0;

	private float startingScale = 0;

	private boolean canDragX = false;
	private boolean canDragY = false;

	private boolean multiTouch = false;

	private int displayWidth;
	private int displayHeight;

	private int imageWidth;
	private int imageHeight;

	private FlingListener flingListener;
	private FlingAnimation flingAnimation;
	private ZoomAnimation zoomAnimation;
	private MoveAnimation moveAnimation;
	private GestureDetector tapDetector;
	private GestureDetector flingDetector;
	private GestureImageViewListener imageListener;

	public GestureImageViewTouchListener(final GestureImageView image, int displayWidth, int displayHeight) {
		super();

		this.image = image;

		this.displayWidth = displayWidth;
		this.displayHeight = displayHeight;

		this.centerX = (float) displayWidth / 2.0f;
		this.centerY = (float) displayHeight / 2.0f;

		this.imageWidth = image.getImageWidth();
		this.imageHeight = image.getImageHeight();

		startingScale = image.getScale();

		currentScale = startingScale;
		lastScale = startingScale;

		boundaryRight = displayWidth;
		boundaryBottom = displayHeight;
		boundaryLeft = 0;
		boundaryTop = 0;

		next.x = image.getImageX();
		next.y = image.getImageY();

		flingListener = new FlingListener();
		flingAnimation = new FlingAnimation();
		zoomAnimation = new ZoomAnimation();
		moveAnimation = new MoveAnimation();

		flingAnimation.setListener(new FlingAnimationListener() {
			@Override
			public void onMove(float x, float y) {
				handleDrag(current.x + x, current.y + y);
			}

			@Override
			public void onComplete() {}
		});

		zoomAnimation.setZoom(2.0f);
		zoomAnimation.setZoomAnimationListener(new ZoomAnimationListener() {
			@Override
			public void onZoom(float scale, float x, float y) {
				if(scale <= maxScale && scale >= minScale) {
					handleScale(scale, x, y);
				}
			}

			@Override
			public void onComplete() {
				inZoom = false;
				handleUp();
			}
		});

		moveAnimation.setMoveAnimationListener(new MoveAnimationListener() {

			@Override
			public void onMove(float x, float y) {
				image.setPosition(x, y);
				image.redraw();
			}
		});

		tapDetector = new GestureDetector(image.getContext(), new SimpleOnGestureListener() {
			@Override
			public boolean onDoubleTap(MotionEvent e) {
				startZoom(e);
				return true;
			}

			@Override
			public boolean onSingleTapConfirmed(MotionEvent e) {
				if(!inZoom) {
					if(onClickListener != null) {
						onClickListener.onClick(image);
						return true;
					}
				}

				return false;
			}
		});

		flingDetector = new GestureDetector(image.getContext(), flingListener);
		imageListener = image.getGestureImageViewListener();

		calculateBoundaries();
	}

	private void startFling() {
		flingAnimation.setVelocityX(flingListener.getVelocityX());
		flingAnimation.setVelocityY(flingListener.getVelocityY());
		image.animationStart(flingAnimation);
	}

	private void startZoom(MotionEvent e) {
		inZoom = true;
		zoomAnimation.reset();

		float zoomTo = 1.0f;

		if(image.isLandscape()) {
			if(image.getDeviceOrientation() == Configuration.ORIENTATION_PORTRAIT) {
				int scaledHeight = image.getScaledHeight();

				if(scaledHeight < canvasHeight) {
					zoomTo = fitScaleVertical / currentScale;
					zoomAnimation.setTouchX(e.getX());
					zoomAnimation.setTouchY(image.getCenterY());
				}
				else {
					zoomTo = fitScaleHorizontal / currentScale;
					zoomAnimation.setTouchX(image.getCenterX());
					zoomAnimation.setTouchY(image.getCenterY());
				}
			}
			else {
				int scaledWidth = image.getScaledWidth();

				if(scaledWidth == canvasWidth) {
					zoomTo = currentScale*4.0f;
					zoomAnimation.setTouchX(e.getX());
					zoomAnimation.setTouchY(e.getY());
				}
				else if(scaledWidth < canvasWidth) {
					zoomTo = fitScaleHorizontal / currentScale;
					zoomAnimation.setTouchX(image.getCenterX());
					zoomAnimation.setTouchY(e.getY());
				}
				else {
					zoomTo = fitScaleHorizontal / currentScale;
					zoomAnimation.setTouchX(image.getCenterX());
					zoomAnimation.setTouchY(image.getCenterY());
				}
			}
		}
		else {
			if(image.getDeviceOrientation() == Configuration.ORIENTATION_PORTRAIT) {

				int scaledHeight = image.getScaledHeight();

				if(scaledHeight == canvasHeight) {
					zoomTo = currentScale*4.0f;
					zoomAnimation.setTouchX(e.getX());
					zoomAnimation.setTouchY(e.getY());
				}
				else if(scaledHeight < canvasHeight) {
					zoomTo = fitScaleVertical / currentScale;
					zoomAnimation.setTouchX(e.getX());
					zoomAnimation.setTouchY(image.getCenterY());
				}
				else {
					zoomTo = fitScaleVertical / currentScale;
					zoomAnimation.setTouchX(image.getCenterX());
					zoomAnimation.setTouchY(image.getCenterY());
				}
			}
			else {
				int scaledWidth = image.getScaledWidth();

				if(scaledWidth < canvasWidth) {
					zoomTo = fitScaleHorizontal / currentScale;
					zoomAnimation.setTouchX(image.getCenterX());
					zoomAnimation.setTouchY(e.getY());
				}
				else {
					zoomTo = fitScaleVertical / currentScale;
					zoomAnimation.setTouchX(image.getCenterX());
					zoomAnimation.setTouchY(image.getCenterY());
				}
			}
		}

		zoomAnimation.setZoom(zoomTo);
		image.animationStart(zoomAnimation);
	}


	private void stopAnimations() {
		image.animationStop();
	}

	@Override
	public boolean onTouch(View v, MotionEvent event) {

		if(!inZoom) {

			if(!tapDetector.onTouchEvent(event)) {
				if(event.getPointerCount() == 1 && flingDetector.onTouchEvent(event)) {
					startFling();
				}

				if(event.getAction() == MotionEvent.ACTION_UP) {
					handleUp();
				}
				else if(event.getAction() == MotionEvent.ACTION_DOWN) {
					stopAnimations();

					last.x = event.getX();
					last.y = event.getY();

					if(imageListener != null) {
						imageListener.onTouch(last.x, last.y);
					}

					touched = true;
				}
				else if(event.getAction() == MotionEvent.ACTION_MOVE) {
					if(event.getPointerCount() > 1) {
						multiTouch = true;
						if(initialDistance > 0) {

							pinchVector.set(event);
							pinchVector.calculateLength();

							float distance = pinchVector.length;

							if(initialDistance != distance) {

								float newScale = (distance / initialDistance) * lastScale;

								if(newScale <= maxScale) {
									scaleVector.length *= newScale;

									scaleVector.calculateEndPoint();

									scaleVector.length /= newScale;

									float newX = scaleVector.end.x;
									float newY = scaleVector.end.y;

									handleScale(newScale, newX, newY);
								}
							}
						}
						else {
							initialDistance = MathUtils.distance(event);

							MathUtils.midpoint(event, midpoint);

							scaleVector.setStart(midpoint);
							scaleVector.setEnd(next);

							scaleVector.calculateLength();
							scaleVector.calculateAngle();

							scaleVector.length /= lastScale;
						}
					}
					else {
						if(!touched) {
							touched = true;
							last.x = event.getX();
							last.y = event.getY();
							next.x = image.getImageX();
							next.y = image.getImageY();
						}
						else if(!multiTouch) {
							if(handleDrag(event.getX(), event.getY())) {
								image.redraw();
							}
						}
					}
				}
			}
		}

		return true;
	}

	protected void handleUp() {

		multiTouch = false;

		initialDistance = 0;
		lastScale = currentScale;

		if(!canDragX) {
			next.x = centerX;
		}

		if(!canDragY) {
			next.y = centerY;
		}

		boundCoordinates();

		if(!canDragX && !canDragY) {

			if(image.isLandscape()) {
				currentScale = fitScaleHorizontal;
				lastScale = fitScaleHorizontal;
			}
			else {
				currentScale = fitScaleVertical;
				lastScale = fitScaleVertical;
			}
		}

		image.setScale(currentScale);
		image.setPosition(next.x, next.y);

		if(imageListener != null) {
			imageListener.onScale(currentScale);
			imageListener.onPosition(next.x, next.y);
		}

		image.redraw();
	}

	protected void handleScale(float scale, float x, float y) {

		currentScale = scale;

		if(currentScale > maxScale) {
			currentScale = maxScale;
		}
		else if (currentScale < minScale) {
			currentScale = minScale;
		}
		else {
			next.x = x;
			next.y = y;
		}

		calculateBoundaries();

		image.setScale(currentScale);
		image.setPosition(next.x, next.y);

		if(imageListener != null) {
			imageListener.onScale(currentScale);
			imageListener.onPosition(next.x, next.y);
		}

		image.redraw();
	}

	protected boolean handleDrag(float x, float y) {
		current.x = x;
		current.y = y;

		float diffX = (current.x - last.x);
		float diffY = (current.y - last.y);

		if(diffX != 0 || diffY != 0) {

			if(canDragX) next.x += diffX;
			if(canDragY) next.y += diffY;

			boundCoordinates();

			last.x = current.x;
			last.y = current.y;

			if(canDragX || canDragY) {
				image.setPosition(next.x, next.y);

				if(imageListener != null) {
					imageListener.onPosition(next.x, next.y);
				}

				return true;
			}
		}

		return false;
	}

	public void reset() {
		currentScale = startingScale;
		next.x = centerX;
		next.y = centerY;
		calculateBoundaries();
		image.setScale(currentScale);
		image.setPosition(next.x, next.y);
		image.redraw();
	}


	public float getMaxScale() {
		return maxScale;
	}

	public void setMaxScale(float maxScale) {
		this.maxScale = maxScale;
	}

	public float getMinScale() {
		return minScale;
	}

	public void setMinScale(float minScale) {
		this.minScale = minScale;
	}

	public void setOnClickListener(OnClickListener onClickListener) {
		this.onClickListener = onClickListener;
	}

	protected void setCanvasWidth(int canvasWidth) {
		this.canvasWidth = canvasWidth;
	}

	protected void setCanvasHeight(int canvasHeight) {
		this.canvasHeight = canvasHeight;
	}

	protected void setFitScaleHorizontal(float fitScale) {
		this.fitScaleHorizontal = fitScale;
	}

	protected void setFitScaleVertical(float fitScaleVertical) {
		this.fitScaleVertical = fitScaleVertical;
	}

	protected void boundCoordinates() {
		if(next.x < boundaryLeft) {
			next.x = boundaryLeft;
		}
		else if(next.x > boundaryRight) {
			next.x = boundaryRight;
		}

		if(next.y < boundaryTop) {
			next.y = boundaryTop;
		}
		else if(next.y > boundaryBottom) {
			next.y = boundaryBottom;
		}
	}

	protected void calculateBoundaries() {

		int effectiveWidth = Math.round( (float) imageWidth * currentScale );
		int effectiveHeight = Math.round( (float) imageHeight * currentScale );

		canDragX = effectiveWidth > displayWidth;
		canDragY = effectiveHeight > displayHeight;

		if(canDragX) {
			float diff = (float)(effectiveWidth - displayWidth) / 2.0f;
			boundaryLeft = centerX - diff;
			boundaryRight = centerX + diff;
		}

		if(canDragY) {
			float diff = (float)(effectiveHeight - displayHeight) / 2.0f;
			boundaryTop = centerY - diff;
			boundaryBottom = centerY + diff;
		}
	}
}
