package view.joystick;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;

public class JoystickView extends View {
	public static final int INVALID_POINTER_ID = -1;

	// =========================================
	// Private Members
	// =========================================
	private final boolean D = false;
	String TAG = "JoystickView";

	private Paint dbgPaint1;
	private Paint dbgPaint2;

	private Paint bgPaint;
	private Paint handlePaint;
	private Paint bgPaint1;
	private Paint handlePaint1;

	private int innerPadding;
	private int bgRadius;
	private int handleRadius;
	private int movementRadius;
	private int handleInnerBoundaries;

	private JoystickMovedListener moveListener;
	private JoystickClickedListener clickListener;

	// # of pixels movement required between reporting to the listener
	private float moveResolution;

	private boolean yAxisInverted;
	private boolean autoReturnToCenter;
    private boolean autoReturnToMid;

	// Max range of movement in user coordinate system
	public final static int CONSTRAIN_BOX = 0;
	public final static int CONSTRAIN_CIRCLE = 1;
	private int movementConstraint;
	private float movementRange;
	private float maxMovementRange;
	private float minMovementRange;

	public final static int COORDINATE_CARTESIAN = 0; // Regular cartesian
														// coordinates
	public final static int COORDINATE_DIFFERENTIAL = 1; // Uses polar rotation
															// of 45 degrees to
															// calc differential
															// drive paramaters
	private int userCoordinateSystem;

	// Records touch pressure for click handling
	private float touchPressure;
	private boolean clicked;
	private float clickThreshold;

	// Last touch point in view coordinates
	private int pointerId = INVALID_POINTER_ID;

    public void setTouchY(float touchY) {
        this.touchY = touchY;
    }

    private float touchX, touchY;

	// Last reported position in view coordinates (allows different reporting
	// sensitivities)
	private float reportX, reportY;

	// Handle center in view coordinates
	private float handleX, handleY;

	// Center of the view in view coordinates
	private int cX, cY;

	// Size of the view in view coordinates
	@SuppressWarnings("unused")
	private int dimX, dimY;

	// Cartesian coordinates of last touch point - joystick center is (0,0)
	private int cartX, cartY;

	// Polar coordinates of the touch point from joystick center
	private double radial;
	private double angle;

	// User coordinates of last touch point
	private int userX, userY;

	// Offset co-ordinates (used when touch events are received from parent's
	// coordinate origin)
	private int offsetX;
	private int offsetY;

    private boolean isInit = true;
	// =========================================
	// Constructors
	// =========================================

	public JoystickView(Context context) {
		super(context);
		initJoystickView();
	}

	public JoystickView(Context context, AttributeSet attrs) {
		super(context, attrs);
		initJoystickView();
	}

	public JoystickView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		initJoystickView();
	}

	// =========================================
	// Initialization
	// =========================================

	private void initJoystickView() {
		setFocusable(true);

		dbgPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
		dbgPaint1.setColor(Color.RED);
		dbgPaint1.setAlpha(60);
		dbgPaint1.setStrokeWidth(1);
		dbgPaint1.setStyle(Paint.Style.STROKE);

		dbgPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
		dbgPaint2.setColor(Color.GRAY);
		dbgPaint2.setStrokeWidth(1);
		dbgPaint2.setStyle(Paint.Style.STROKE);

		bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
		bgPaint.setColor(Color.WHITE);
		bgPaint.setAlpha(140);
		bgPaint.setStrokeWidth(3);
		bgPaint.setStyle(Paint.Style.STROKE);
		
		bgPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
		bgPaint1.setColor(Color.GRAY);
		bgPaint1.setAlpha(80);
		bgPaint1.setStyle(Paint.Style.FILL);

		handlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
		handlePaint.setColor(Color.WHITE);
		handlePaint.setStrokeWidth(2);
		bgPaint.setAlpha(140);
		handlePaint.setStyle(Paint.Style.STROKE);
		
		handlePaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
		handlePaint1.setColor(Color.GRAY);
		handlePaint1.setAlpha(180);
		handlePaint1.setStyle(Paint.Style.FILL);

		innerPadding = 10;

		setMovementRange(500);
		setMoveResolution(1.0f);
		setClickThreshold(0.4f);
		setYAxisInverted(true);
		setUserCoordinateSystem(COORDINATE_CARTESIAN);
		setAutoReturnToCenter(true);
        setAutoReturnToMid(false);


	}

    public void setAutoReturnToMid(boolean b) {
        this.autoReturnToMid = b;
    }

    public boolean getAutoReturnToMid(){
        return this.autoReturnToMid;
    }

    public void setAutoReturnToCenter(boolean autoReturnToCenter) {
		this.autoReturnToCenter = autoReturnToCenter;
	}

	public boolean isAutoReturnToCenter() {
		return autoReturnToCenter;
	}

	public void setUserCoordinateSystem(int userCoordinateSystem) {
		if (userCoordinateSystem < COORDINATE_CARTESIAN
				|| movementConstraint > COORDINATE_DIFFERENTIAL)
			Log.e(TAG, "invalid value for userCoordinateSystem");
		else
			this.userCoordinateSystem = userCoordinateSystem;
	}

	public int getUserCoordinateSystem() {
		return userCoordinateSystem;
	}

	public void setMovementConstraint(int movementConstraint) {
		if (movementConstraint < CONSTRAIN_BOX
				|| movementConstraint > CONSTRAIN_CIRCLE)
			Log.e(TAG, "invalid value for movementConstraint");
		else
			this.movementConstraint = movementConstraint;
	}

	public int getMovementConstraint() {
		return movementConstraint;
	}

	public boolean isYAxisInverted() {
		return yAxisInverted;
	}

	public void setYAxisInverted(boolean yAxisInverted) {
		this.yAxisInverted = yAxisInverted;
	}

	/**
	 * Set the pressure sensitivity for registering a click
	 * 
	 * @param clickThreshold
	 *            threshold 0...1.0f inclusive. 0 will cause clicks to never be
	 *            reported, 1.0 is a very hard click
	 */
	public void setClickThreshold(float clickThreshold) {
		if (clickThreshold < 0 || clickThreshold > 1.0f)
			Log.e(TAG, "clickThreshold must range from 0...1.0f inclusive");
		else
			this.clickThreshold = clickThreshold;
	}

	public float getClickThreshold() {
		return clickThreshold;
	}

	public void setMovementRange(float movementRange) {
		this.movementRange = movementRange;
		this.maxMovementRange = movementRange / 2;
		this.minMovementRange = -maxMovementRange;
	}

	public float getMovementRange() {
		return movementRange;
	}

	public void setMoveResolution(float moveResolution) {
		this.moveResolution = moveResolution;
	}

	public float getMoveResolution() {
		return moveResolution;
	}

	public float getMoveResolutionMax() {
		return this.maxMovementRange;
	}

	public float getMoveResolutionMin() {
		return this.minMovementRange;
	}

	// =========================================
	// Public Methods
	// =========================================

	public void setOnJostickMovedListener(JoystickMovedListener listener) {
		this.moveListener = listener;
	}

	public void setOnJostickClickedListener(JoystickClickedListener listener) {
		this.clickListener = listener;
	}

	// =========================================
	// Drawing Functionality
	// =========================================

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// Here we make sure that we have a perfect circle
		int measuredWidth = measure(widthMeasureSpec);
		int measuredHeight = measure(heightMeasureSpec);
		setMeasuredDimension(measuredWidth, measuredHeight);
	}


    @Override
	protected void onLayout(boolean changed, int left, int top, int right,
			int bottom) {
		super.onLayout(changed, left, top, right, bottom);

		int d = Math.min(getMeasuredWidth(), getMeasuredHeight());

		dimX = d;
		dimY = d;

		cX = d / 2;
		cY = d / 2;

		bgRadius = dimX / 2 - innerPadding;
		handleRadius = (int) (d * 0.25);
		handleInnerBoundaries = handleRadius;
		movementRadius = Math.min(cX, cY) - handleInnerBoundaries;
        if(isInit == true && autoReturnToMid == true){
            isInit = false;
            touchY = movementRadius;
        }
	}

	private int measure(int measureSpec) {
		int result = 0;
		// Decode the measurement specifications.
		int specMode = MeasureSpec.getMode(measureSpec);
		int specSize = MeasureSpec.getSize(measureSpec);
		if (specMode == MeasureSpec.UNSPECIFIED) {
			// Return a default size of 200 if no bounds are specified.
			result = 200;
		} else {
			// As you want to fill the available space
			// always return the full available bounds.
			result = specSize;
		}
		return result;
	}

	@Override
	protected void onDraw(Canvas canvas) {
		canvas.save();
		// Draw the background
		canvas.drawCircle(cX, cY, bgRadius, bgPaint);
		canvas.drawCircle(cX, cY, bgRadius, bgPaint1);
		
		// Draw the handle
		handleX = touchX + cX;
		handleY = touchY + cY;
		canvas.drawCircle(handleX, handleY, handleRadius, handlePaint);
		canvas.drawCircle(handleX, handleY, handleRadius, handlePaint1);
		
		if (D) {
			canvas.drawRect(1, 1, getMeasuredWidth() - 1,
					getMeasuredHeight() - 1, dbgPaint1);

			canvas.drawCircle(handleX, handleY, 3, dbgPaint1);

			if (movementConstraint == CONSTRAIN_CIRCLE) {
				canvas.drawCircle(cX, cY, this.movementRadius, dbgPaint1);
			} else {
				canvas.drawRect(cX - movementRadius, cY - movementRadius, cX
						+ movementRadius, cY + movementRadius, dbgPaint1);
			}

			// Origin to touch point
			canvas.drawLine(cX, cY, handleX, handleY, dbgPaint2);

			int baseY = (int) (touchY < 0 ? cY + handleRadius : cY
					- handleRadius);
			canvas.drawText(
					String.format("%s (%.0f,%.0f)", TAG, touchX, touchY),
					handleX - 20, baseY - 7, dbgPaint2);
			canvas.drawText(
					"("
							+ String.format("%.0f, %.1f", radial,
									angle * 57.2957795) + (char) 0x00B0 + ")",
					handleX - 20, baseY + 15, dbgPaint2);
		}

		// Log.d(TAG, String.format("touch(%f,%f)", touchX, touchY));
		// Log.d(TAG, String.format("onDraw(%.1f,%.1f)\n\n", handleX, handleY));
		canvas.restore();
	}

	// Constrain touch within a box
	private void constrainBox() {
		touchX = Math.max(Math.min(touchX, movementRadius), -movementRadius);
		touchY = Math.max(Math.min(touchY, movementRadius), -movementRadius);
	}

	// Constrain touch within a circle
	private void constrainCircle() {
		float diffX = touchX;
		float diffY = touchY;
		double radial = Math.sqrt((diffX * diffX) + (diffY * diffY));
		if (radial > movementRadius) {
			touchX = (int) ((diffX / radial) * movementRadius);
			touchY = (int) ((diffY / radial) * movementRadius);
		}
	}

	public void setPointerId(int id) {
		this.pointerId = id;
	}

	public int getPointerId() {
		return pointerId;
	}

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		final int action = ev.getAction();
		switch (action & MotionEvent.ACTION_MASK) {
		case MotionEvent.ACTION_MOVE: {
            return processMoveEvent(ev);
		}
		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP: {
			
			if (pointerId != INVALID_POINTER_ID) {
				// Log.d(TAG, "ACTION_UP");
				returnHandleToCenter();
				setPointerId(INVALID_POINTER_ID);
			}
			break;
		}
		case MotionEvent.ACTION_POINTER_UP: {
			
			if (pointerId != INVALID_POINTER_ID) {
				final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
				final int pointerId = ev.getPointerId(pointerIndex);
				if (pointerId == this.pointerId) {
					// Log.d(TAG, "ACTION_POINTER_UP: " + pointerId);
					returnHandleToCenter();
					setPointerId(INVALID_POINTER_ID);
					return true;
				}
			}
			break;
		}
		case MotionEvent.ACTION_DOWN: {
         	if (pointerId == INVALID_POINTER_ID) {
				int x = (int) ev.getX();
                if (x >= offsetX && x < offsetX + dimX ) {
					setPointerId(ev.getPointerId(0));
                    return true;
				}
			}
			break;
		}
		case MotionEvent.ACTION_POINTER_DOWN: {
		//	Log.d("JoyStickView", "ACTION_POINTER_MOVE");
			
			if (pointerId == INVALID_POINTER_ID) {
				final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
				final int pointerId = ev.getPointerId(pointerIndex);
				int x = (int) ev.getX(pointerId);
				if (x >= offsetX && x < offsetX + dimX) {
					// Log.d(TAG, "ACTION_POINTER_DOWN: " + pointerId);
					setPointerId(pointerId);
					return true;
				}
			}
			break;
		}
		}
		return false;
	}

	private boolean processMoveEvent(MotionEvent ev) {
		if (pointerId != INVALID_POINTER_ID) {
			final int pointerIndex = ev.findPointerIndex(pointerId);
			// Translate touch position to center of view
			float x = ev.getX(pointerIndex);
			touchX = x - cX - offsetX;
			float y = ev.getY(pointerIndex);
			touchY = y - cY - offsetY;

			// Log.d(TAG,
			// String.format("ACTION_MOVE: (%03.0f, %03.0f) => (%03.0f, %03.0f)",
			// x, y, touchX, touchY));

			reportOnMoved();
			invalidate();

			touchPressure = ev.getPressure(pointerIndex);
			reportOnPressure();

			return true;
		}
		return false;
	}

	private void reportOnMoved() {
		if (movementConstraint == CONSTRAIN_CIRCLE)
			constrainCircle();
		else
			constrainBox();

		calcUserCoordinates();

		if (moveListener != null) {
			boolean rx = Math.abs(touchX - reportX) >= moveResolution;
			boolean ry = Math.abs(touchY - reportY) >= moveResolution;
			if (rx || ry) {
				this.reportX = touchX;
				this.reportY = touchY;

				// Log.d(TAG, String.format("moveListener.OnMoved(%d,%d)",
				// (int)userX, (int)userY));
				moveListener.OnMoved(userX, userY);
			}
		}
	}

	private void calcUserCoordinates() {
		// First convert to cartesian coordinates
		cartX = (int) (touchX / movementRadius * movementRange);
		cartY = (int) (touchY / movementRadius * movementRange);

		radial = Math.sqrt((cartX * cartX) + (cartY * cartY));
		angle = Math.atan2(cartY, cartX);

		// Invert Y axis if requested
		if (!yAxisInverted)
			cartY *= -1;

		if (userCoordinateSystem == COORDINATE_CARTESIAN) {
			userX = cartX;
			userY = cartY;
		} else if (userCoordinateSystem == COORDINATE_DIFFERENTIAL) {
			userX = cartY + cartX / 4;
			userY = cartY - cartX / 4;

			if (userX < -movementRange)
				userX = (int) -movementRange;
			if (userX > movementRange)
				userX = (int) movementRange;

			if (userY < -movementRange)
				userY = (int) -movementRange;
			if (userY > movementRange)
				userY = (int) movementRange;
		}

	}

	// Simple pressure click
	private void reportOnPressure() {
		// Log.d(TAG, String.format("touchPressure=%.2f", this.touchPressure));
		if (clickListener != null) {
			if (clicked && touchPressure < clickThreshold) {
				clickListener.OnReleased();
				this.clicked = false;
				// Log.d(TAG, "reset click");
				invalidate();
			} else if (!clicked && touchPressure >= clickThreshold) {
				clicked = true;
				clickListener.OnClicked();
				// Log.d(TAG, "click");
				invalidate();
				performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
			}
		}
	}

	public void setCoordinates(float touchX, float touchY) {
		if (!this.isPressed()) {
			this.touchX = touchX;
			this.touchY = touchY;
			reportOnMoved();
			invalidate();
		}
	}
    public void returnHandleToMid(){
        final int numberOfFrames = 5;
        final double intervalsX = (0 - touchX) / numberOfFrames;
        for (int i = 0; i < numberOfFrames; i++) {
            final int j = i;
            postDelayed(new Runnable() {
                @Override
                public void run() {
                    touchX += intervalsX;

                    reportOnMoved();
                    invalidate();

                    if (moveListener != null && j == numberOfFrames - 1) {
                        moveListener.OnReturnedToCenter();
                    }
                }
            }, i * 40);
        }

        if (moveListener != null) {
            moveListener.OnReleased();
        }
    }
	public void returnHandleToCenter() {
		if (autoReturnToCenter) {
			final int numberOfFrames = 5;
			final double intervalsX = (0 - touchX) / numberOfFrames;
			final double intervalsY = (0 - touchY) / numberOfFrames;

			for (int i = 0; i < numberOfFrames; i++) {
				final int j = i;
				postDelayed(new Runnable() {
					@Override
					public void run() {
						touchX += intervalsX;
						touchY += intervalsY;

						reportOnMoved();
						invalidate();

						if (moveListener != null && j == numberOfFrames - 1) {
							moveListener.OnReturnedToCenter();
						}
					}
				}, i * 40);
			}

			if (moveListener != null) {
				moveListener.OnReleased();
			}
		}
        else if(autoReturnToMid){
            this.returnHandleToMid();
        }
	}

	public void setTouchOffset(int x, int y) {
		offsetX = x;
		offsetY = y;
	}

    public float getTouchY() {
        return this.touchY;
    }
}