package edu.stanford.nick;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.view.MotionEvent;
import android.view.View;


/**
 * This custom view that demonstrates custom drawing, clicking, dragging, and invalidate/animation
 * running timer code off a handler.
 * It also handles handles pause/resume correctly and kill/create correctly.
 * Nick Parlante
 */
public class MonkeyView extends View {
	/** Takes context, handler to run ui-thread code,
	 and screen density (DisplayMetrics.density). */
	public MonkeyView(Context context, Handler handler, float densityScale) {
		super(context);

		// Background image for view. background.jpg has a vertical
		// orientation, so it looks right when the device is vertical.
		// "drawable-land" folder contains a landscape version of background.jpg.
		// The system automatically provides that background when device is
		// horizontal, so the setBackground() call below just works either way.

		// (this can also be done in XML with android:background="@drawable/background")
		setBackgroundResource(R.drawable.background);

		mScale = densityScale;

		mMonkey = makeScaled(BitmapFactory.decodeResource(context.getResources(), R.drawable.monkey), 160 * mScale);
		mAlternate = makeScaled(BitmapFactory.decodeResource(context.getResources(), R.drawable.alternate), 160 * mScale);
		// Note: density scaling: here we choose to make the monkey 160 device-independent-pixels wide, exactly 1 inch.
		// By multiplying by the passed in scale, the monkey will be the right number of actual pixels
		// wide to be 1 inch for whatever the display density.

		// Start the monkey off at 50,50 DIPs
		mX = 50 * mScale;
		mY = 50 * mScale;

		mHandler = handler;
		// todo: getHandler() returns null sometimes, so we pass it in instead.
		// Is that an android bug?
	}

	/** Utility makes a scaled version of the given bitmap to the given width,
	 but keeping the original proportions. */
	private Bitmap makeScaled(Bitmap base, float width) {
		float scale = width / base.getWidth();
		return Bitmap.createScaledBitmap(base, (int)(base.getWidth()*scale), (int)(base.getHeight()*scale), false);

	}

	// Images used -- set in constructor
	private Bitmap mMonkey;
	private Bitmap mAlternate;  // alternate image to use when clicked

	private float mScale;
	// DIP scale factor. Multiply DIP pixel measures, e.g. width 160 DIPs
	// by this to yield the actual number of pixels for our display.

	// Monkey upper left
	private float mX;
	private float mY;

	// During click/drag
	private boolean mDragging;
	private float mLastX;
	private float mLastY;

	// For "motoring" -- animate monkey moving on its own by handler timer
	private boolean mMotoring;
	private float mDX;  // pixels per second
	private float mDY;

	// When clicked upon during motoring .. change to alternate image
	private boolean mClicked;
	private long mClickTime;

	// Handler we use to run code repeatedly
	private Handler mHandler;


	// Draw ourselves
	public void onDraw(Canvas canvas) {                		
		if (!mClicked) {
			canvas.drawBitmap(mMonkey, mX, mY, null);
		}
		else {
			canvas.drawBitmap(mAlternate, mX, mY, null);
		}
	}


	// Notification of touch events on the view
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		float x = event.getX();
		float y = event.getY();
		int action = event.getAction();

		// Not motoring, just sitting there -- several cases.
		if (!mMotoring) {

			// ACTION_DOWN = click down (i.e. not a drag) -- 2 cases
			if (action == MotionEvent.ACTION_DOWN)  {
				// 1. On the monkey, set state to begin dragging
				if (mX <= x && x < mX + mMonkey.getWidth() &&
						mY <= y && y < mY + mMonkey.getHeight()) {

					mLastX = x;
					mLastY = y;
					mDragging = true;
				}
				else {
					// 2. Out in space -- move the monkey to there
					mX = x - mMonkey.getWidth() / 2;
					mY = y - mMonkey.getHeight() / 2;
					invalidate();

					// Width adjustment so monkey appears centered where the user clicks.
					// Recall the pattern: change model, then invalidate.
				}
			}

			// ACTION_MOVE = during a finger drag
			// Compute delta of gesture, apply that to the model.
			else if (action == MotionEvent.ACTION_MOVE && mDragging) {
				// Compute delta
				float deltaX = x - mLastX;
				float deltaY = y - mLastY;

				// Not-optimized strategy:
				// invalidate();

				// Invalidate-rect optimization. Rather than invalidate the entire view,
				// compute just the area of pixels to redraw. For a move, want to invalidate the pre-move
				// rect and the post-move rect. This can be a significant speedup, as the number of pixels
				// to redraw could easily be 10x less.

				// 1. Compute rect of the monkey pre-move. Cast float down to int, and add 1 to width/height to avoid
				// rounding problems. Invalidate just this rect.
				Rect rect = new Rect((int)mX, (int)mY, (int)mX + mMonkey.getWidth() + 1, (int)mY + mMonkey.getHeight() + 1);
				invalidate(rect);

				// 2. Now move the rect by the delta for this, case, and invalidate again. The two invalidate
				// rects are added together by the system, forming the overall area to draw.
				rect.offset((int)deltaX, (int)deltaY);
				invalidate(rect);

				// Apply that delta to the monkey
				mX += deltaX;
				mY += deltaY;

				// Store lastX/lastY for next time.
				mLastX = x;
				mLastY = y;
			}
		}  // !mMoving case


		else {
			// Monkey is motoring. Detect click on monkey -- set mClicked in that case.
			if (action == MotionEvent.ACTION_DOWN) {
				if (mX <= x && x < mX + mMonkey.getWidth() &&
						mY <= y && y < mY + mMonkey.getHeight()) {

					mClickTime = System.currentTimeMillis();
					mClicked = true;
					invalidate();
					// invalidate() here to follow the change-invalidate pattern
					// (mClicked in this case), although here
					// we're drawing 50 times a second anyway, so
					// it makes little difference.
				}
			}
		}


		// true = we handled this event (and we want future notifications)
		return true;
	}

	// How motoring works.
	// "timer" strategy -- a bit of code that runs 50x a second,
	// updates game state each time run() executes.

	private long mUpdateTime;  // time of last run

	// Code we'll have run 50x a second.
	private Runnable mUpdateState = new Runnable() {
		public void run() {

			// Running 50x per second is approximate -- fit the computation
			// to the exact elapsed time.
			long now = System.currentTimeMillis();
			float elapsed = (now - mUpdateTime)/1000.0f;
			mUpdateTime = now;

			float deltaX = mDX * elapsed;  // mDX is pixels per second, so scale to elapsed time
			float deltaY = mDY * elapsed;

			// Invalidate-rect optimization -- see above.
			Rect rect = new Rect((int)mX, (int)mY, (int)mX + mMonkey.getWidth() + 1, (int)mY + mMonkey.getHeight() + 1);
			invalidate(rect);
			rect.offset((int)deltaX, (int)deltaY);
			invalidate(rect);

			mX += deltaX;
			mY += deltaY;

			// if wraparound happens ... invalidate the whole thing
			if (wrapAround()) {
				invalidate();
			}

			// Let mClicked be true for 1 second.
			if (mClicked) {
				if (now - mClickTime >= 1000) {
					mClicked = false;
				}
			}

			// We post a pointer to ourselves to run again in 20ms
			mHandler.postDelayed(this, 20);


			// Debug that this code stops/starts running properly as activity does pause/resume.
			//if (now % 20 == 0) {
			//	System.err.println("Note: handler animation is still happening");
			//}
		}

	};

	/** Wraps the image back on screen if it goes off.
	 Might look better using the center instead of the upper left.
	 Returns true if it moved mX/mY.
	 */
	public boolean wrapAround() {
		// Note: rarely, getWidth() returns 0. I'm not sure if this is an
		// android bug or just something that happens. We need to screen that case out
		// as it puts as in an infinite loop.
		if (getWidth() == 0 || getHeight() == 0) return false;

		float saveX = mX;
		float saveY = mY;

		// Use *while* here in case the screen size has changed, and we are suddenly
		// a few widths off screen.
		while (mX < 0) mX += getWidth();
		while (mX >= getWidth()) mX -= getWidth();

		while (mY < 0) mY += getHeight();
		while (mY >= getHeight()) mY -= getHeight();

		return (saveX == mX && saveY == mY);
	}

	/** Randomizes dx/dy */
	public void randomizeDirection() {
		mDX = (float)(Math.random() * 600 - 300) * mScale;
		mDY = (float)(Math.random() * 600 - 300) * mScale;
	}

	/** Starts or restarts the motoring process.
	 * @param randomize  randomize the speed and direction
	 * @param varSet set the mMotoring variable
	 */
	public void startMotoring(boolean randomize, boolean varSet) {
		mUpdateTime = System.currentTimeMillis();

		mHandler.removeCallbacks(mUpdateState);  // Here being cautious -- avoid proliferating runnables
		mHandler.post(mUpdateState);

		if (randomize) {
			randomizeDirection();
		}

		if (varSet) {
			mMotoring = true;
		}
	}

	/** Stops the motoring process. */
	public void stopMotoring(boolean varSet) {
		mHandler.removeCallbacks(mUpdateState);
		if (varSet) {
			mMotoring = false;
		}
	}

	/** Pauses the motoring (but does not change mMotoring state). 
     Call from the activity onPause(). */
	public void pause() {
		System.err.println("pause");
		stopMotoring(false);  // false = don't change mMotoring var, as we need it at resume time
	}

	/** Resumes motoring if appropriate.
	 Call from activity onResume().
	 */
	public void resume() {
		System.err.println("resume:" + mMotoring);
		if (mMotoring) {
			startMotoring(false, false);
		}
	}

	/**
	 * View save-code for kill/recreate case. Note that this is similar but different
	 * from the activity onSaveInstaneState() code.
	 * This method and the resume below are sent automatically by the system for the
	 * kill/recreate case. We need to save any mXXX variables we want to preserve.
	 * Result of this code is: monkey could be motoring, but when it goes through
	 * a kill/recreate cycle, it keeps the same position and speed and keeps motoring.
	 */
	@Override
	protected Parcelable onSaveInstanceState() {
		System.err.println("save");

		// 1. Make Bundle, save mXXX vars we want to preserve.
		Bundle bundle = new Bundle();
		bundle.putFloat("x", mX);
		bundle.putFloat("y", mY);
		bundle.putFloat("dx", mDX);
		bundle.putFloat("dy", mDY);
		bundle.putBoolean("motoring", mMotoring);
		// not saving "clicked"

		// 2. Also should call super class, and save whatever
		// it returns.
		Parcelable s = super.onSaveInstanceState();
		bundle.putParcelable("super", s);

		return bundle;
	}


	/** Restore from kill/recreate cycle. */
	@Override
	protected void onRestoreInstanceState(Parcelable state) {
		System.err.println("restore");

		// This gets passed whatever we make above, so it should be a Bundle.
		Bundle bundle = (Bundle) state;
		mX = bundle.getFloat("x");
		mY = bundle.getFloat("y");
		mDX = bundle.getFloat("dx");
		mDY = bundle.getFloat("dy");
		mMotoring = bundle.getBoolean("motoring");
		// mClicked we'll leave as false on resume

		// Restore for our superclass too.
		// todo: should this be first or last?
		super.onRestoreInstanceState(bundle.getParcelable("super"));
	}
}
