/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * 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.example.android.marslander;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.SoundPool;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Vibrator;
import android.speech.tts.TextToSpeech;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.TextView;


/**
 * View that draws, takes keystrokes, etc. for a simple MarsLander game.
 *
 * Has a mode which RUNNING, PAUSED, etc. Has a x, y, dx, dy, ... capturing the
 * current ship physics. All x/y etc. are measured with (0,0) at the lower left.
 * updatePhysics() advances the physics based on realtime. draw() renders the
 * ship, and does an invalidate() to prompt another draw() as soon as possible
 * by the system.
 */

class MarsView extends SurfaceView implements SurfaceHolder.Callback {

	class MarsThread extends Thread {

		boolean touchDown = false;   // is the user "pressing" the screen?
		float touch_x, touch_y;
		float touch_pressure;

		/*
		 * State-tracking constants
		 */
		public static final int STATE_LOSE = 1;
		public static final int STATE_PAUSE = 2;
		public static final int STATE_READY = 3;
		public static final int STATE_RUNNING = 4;
		public static final int STATE_WIN = 5;

		public static final int TEXT_SIZE = 24;
		
		public static final int METEOR_RADIUS = 20;
		public static final double METEOR_MIN_X_LIMIT = 0.0;
		public static final double METEOR_MAX_X_LIMIT = 800.0;
		public static final double METEOR_MIN_Y_LIMIT = 0.0;
		public static final double METEOR_MAX_Y_LIMIT = 400.0;
		public static final double METEOR_Y_START = 20.0;
		public static final double METEOR_MAX_SPEED = 15.0;  // 20.0
		public static final double METEOR_SPEED_FACTOR = 10.0;
		public static final double SHIP_MAX_SPEED = 40.0;  // 20.0
		public static final double SHIP_SPEED_FACTOR = 10.0;
		public static final double CRASH_DISTANCE = 50.0;
		double meteor_x;
		double meteor_y;
		boolean mCrash = false;
		int numCrashes = 0;
		
		long mTimeNow, mTimeStart;
		long maxTimeSinceCrash = 0;
		SoundPool SP;
		int sound_explosion;
		MediaPlayer MP;
		
		TextToSpeech mTTS;
				
		SensorManager SM;
		double accelX, accelY, accelZ;
		double accelMagnitude;
		boolean haveAccel = false;
		
		Vibrator V;
		
		/*
		 * Member (state) fields
		 */
		/** The drawable to use as the background of the animation canvas */
		private Bitmap mBackgroundImage;

		/**
		 * Current height of the surface/canvas.
		 *
		 * @see #setSurfaceSize
		 */
		private int mCanvasHeight = 1;

		/**
		 * Current width of the surface/canvas.
		 *
		 * @see #setSurfaceSize
		 */
		private int mCanvasWidth = 1;

		/** Is the engine burning? */
		private boolean mEngineFiring;

		/** What to draw for the Lander when the engine is firing */
		private Drawable mFiringImage;

		/** Message handler used by thread to interact with TextView */
		private Handler mHandler;

		/**
		 * Lander heading in degrees, with 0 up, 90 right. Kept in the range
		 * 0..360.
		 */
		private double mHeading;

		/** Pixel height of lander image. */
		private int mLanderHeight;

		/** What to draw for the Lander in its normal state */
		private Drawable mLanderImage;

		/** Pixel width of lander image. */
		private int mLanderWidth;

		/** Used to figure out elapsed time between frames */
		private long mLastTime;

		/** Paint to draw the lines on screen. */
		private Paint mLinePaint;
		private Paint mTextPaint;
		private Paint mBigTextPaint;
		
		/** "Bad" speed-too-high variant of the line color. */
		private Paint mLinePaintBad;

		/** The state of the game. One of READY, RUNNING, PAUSE, LOSE, or WIN */
		private int mMode;

		/** Indicate whether the surface has been created & is ready to draw */
		private boolean mRun = false;

		/** Scratch rect object. */
		private RectF mScratchRect;

		/** Handle to the surface manager object we interact with */
		private SurfaceHolder mSurfaceHolder;

		/** X of lander center. */
		private double mX;

		/** Y of lander center. */
		private double mY;

		class SensorListener implements SensorEventListener {
	
			public void onSensorChanged(SensorEvent event) {
				if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
					float[] values = event.values;
					// Movement
					
					accelX = values[0];
					accelY = values[1];
					accelZ = values[2];
					accelMagnitude = Math.sqrt(accelX*accelX + accelY*accelY + accelZ*accelZ);
					haveAccel = true;
		
				}

			}

		
			public void onAccuracyChanged(Sensor sensor, int accuracy) { }
				

		}
		
		// constructor for thread

		public MarsThread(SurfaceHolder surfaceHolder, Context context, Handler handler) {
	
			// get handles to some important objects
			mSurfaceHolder = surfaceHolder;
			mHandler = handler;
			mContext = context;

			Resources res = context.getResources();
			
			// sound
			
			SP = new SoundPool(16, AudioManager.STREAM_MUSIC, 0);
			sound_explosion = SP.load(context, R.raw.grenade, 1);
			
			MP = MediaPlayer.create(mContext, R.raw.electronic_clav);
			try {
				MP.prepare();
			} catch (Exception e) { }
			
			mTTS = new TextToSpeech(context, new TextToSpeech.OnInitListener() {
				public void onInit(int status) {
					
				}
			});
			
			
			// sensors/actuators
			
			SM = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
			SM.registerListener(new SensorListener(),
					SM.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
					SM.SENSOR_DELAY_NORMAL);

			V = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
			
			// cache handles to our key sprites & other drawables
			mLanderImage = context.getResources().getDrawable(R.drawable.lander_plain);
			mFiringImage = context.getResources().getDrawable(R.drawable.lander_firing);

			// load background image as a Bitmap instead of a Drawable b/c
			// we don't need to transform it and it's faster to draw this way
			mBackgroundImage = BitmapFactory.decodeResource(res,R.drawable.marshorizon);

			// Use the regular lander image as the model size for all sprites
			mLanderWidth = mLanderImage.getIntrinsicWidth();
			mLanderHeight = mLanderImage.getIntrinsicHeight();

			// Initialize paints for drawing shapes
			mLinePaint = new Paint();
			mLinePaint.setAntiAlias(true);
			mLinePaint.setARGB(255, 255, 0, 255);
			mLinePaint.setStyle(Paint.Style.STROKE);
    	mLinePaint.setStrokeWidth(5);
    	
			mLinePaintBad = new Paint();
			mLinePaintBad.setAntiAlias(true);
			mLinePaintBad.setARGB(255, 120, 180, 0);

			mTextPaint = new Paint();
			mTextPaint.setAntiAlias(true);
			mTextPaint.setARGB(255, 255, 0, 0);
			mTextPaint.setTextSize(TEXT_SIZE);
			
			mBigTextPaint = new Paint();
			mBigTextPaint.setAntiAlias(true);
			mBigTextPaint.setARGB(255, 255, 0, 0);
			mBigTextPaint.setTextAlign(Paint.Align.CENTER);
			mBigTextPaint.setTextSize(48);
			
			mScratchRect = new RectF(0, 0, 0, 0);

			// initial location of lander (not yet playing)
			mX = mLanderWidth;
			mY = 3 * mLanderHeight / 2;
			mHeading = 0; 
			// mEngineFiring = true;
			respawnMeteor(true);
		}

		/*
		public void doStart() {
			synchronized (mSurfaceHolder) {
				// First set the game for Medium difficulty

				mEngineFiring = false;
				
				// pick a convenient initial location for the lander sprite
				mX = mCanvasWidth / 2;
				mY = mCanvasHeight / 2;

				mLastTime = System.currentTimeMillis() + 100;
				setState(STATE_RUNNING);
			}
		}
*/
		/**
		 * Pauses the physics update & animation.
		 */
		public void pause() {
			synchronized (mSurfaceHolder) {
				if (mMode == STATE_RUNNING) {
					MP.stop();
					setState(STATE_PAUSE);
				}
			}
		}

		@Override
		public void run() {
			while (mRun) {
				
				mTimeNow = System.currentTimeMillis();

				Canvas c = null;
				try {
					c = mSurfaceHolder.lockCanvas(null);
					synchronized (mSurfaceHolder) {
						if (mMode == STATE_RUNNING) 
							updatePhysics();
						doDraw(c);
					}
				} finally {
					// do this in a finally so that if an exception is thrown
					// during the above, we don't leave the Surface in an
					// inconsistent state
					if (c != null) {
						mSurfaceHolder.unlockCanvasAndPost(c);
					}
				}
			}
		}

		/**
		 * Sets if the engine is currently firing.
		 */
		public void setFiring(boolean firing) {
			synchronized (mSurfaceHolder) {
				mEngineFiring = firing;
			}
		}

		/**
		 * Used to signal the thread whether it should be running or not.
		 * Passing true allows the thread to run; passing false will shut it
		 * down if it's already running. Calling start() after this was most
		 * recently called with false will result in an immediate shutdown.
		 *
		 * @param b true to run, false to shut down
		 */
		public void setRunning(boolean b) {
			mRun = b;
		}

		/**
		 * Sets the game mode. That is, whether we are running, paused, in the
		 * failure state, in the victory state, etc.
		 *
		 * @see #setState(int, CharSequence)
		 * @param mode one of the STATE_* constants
		 */
		public void setState(int mode) {
			synchronized (mSurfaceHolder) {
				setState(mode, null);
			}
		}

		/**
		 * Sets the game mode. That is, whether we are running, paused, in the
		 * failure state, in the victory state, etc.
		 *
		 * @param mode one of the STATE_* constants
		 * @param message string to add to screen or null
		 */
		public void setState(int mode, CharSequence message) {
			/*
			 * This method optionally can cause a text message to be displayed
			 * to the user when the mode changes. Since the View that actually
			 * renders that text is part of the main View hierarchy and not
			 * owned by this thread, we can't touch the state of that View.
			 * Instead we use a Message + Handler to relay commands to the main
			 * thread, which updates the user-text View.
			 */
			synchronized (mSurfaceHolder) {
				mMode = mode;

				if (mMode == STATE_RUNNING) {
					Message msg = mHandler.obtainMessage();
					Bundle b = new Bundle();
					b.putString("text", "");
					b.putInt("viz", View.INVISIBLE);
					msg.setData(b);
					mHandler.sendMessage(msg);
				} else {
					mEngineFiring = false;
					Resources res = mContext.getResources();
					CharSequence str = "";
					if (mMode == STATE_READY)
						str = res.getText(R.string.mode_ready);
					else if (mMode == STATE_PAUSE)
						str = res.getText(R.string.mode_pause);

					else if (mMode == STATE_WIN)
						str = res.getString(R.string.mode_win_prefix)
						+ " "
						+ res.getString(R.string.mode_win_suffix);

					if (message != null) {
						str = message + "\n" + str;
					}

					Message msg = mHandler.obtainMessage();
					Bundle b = new Bundle();
					b.putString("text", str.toString());
					b.putInt("viz", View.VISIBLE);
					msg.setData(b);
					mHandler.sendMessage(msg);

				}
			}
		}  // setState


		/* Callback invoked when the surface dimensions change. */
		public void setSurfaceSize(int width, int height) {
			// synchronized to make sure these all change atomically
			synchronized (mSurfaceHolder) {
				mCanvasWidth = width;
				mCanvasHeight = height;

				// don't forget to resize the background image
				mBackgroundImage = Bitmap.createScaledBitmap(mBackgroundImage, width, height, true);
			}
		}

		/*
		public void unpause() {
			// Move the real time clock up to now
			synchronized (mSurfaceHolder) {
				mLastTime = System.currentTimeMillis() + 100;
			}
			setState(STATE_RUNNING);
		}
*/
		
		// handle "finger down" event
		
		void doTouchDown(float x, float y, int count) {
			synchronized (mSurfaceHolder) {

				// here's where a touch starts the game...
				
				if (mMode != STATE_RUNNING) {
					mTimeStart = System.currentTimeMillis();
					setState(STATE_RUNNING);
					MP.start();
					MP.setVolume(.5f, .5f);
					MP.setLooping(true);
				}
				
				touchDown = true;
				touch_x = x;
				touch_y = y;
				mEngineFiring = true;
			}
		}

		// handle "finger up" event
		
		void doTouchUp(float x, float y, int count) {
			synchronized (mSurfaceHolder) {

				touchDown = false; 
				mEngineFiring = false;
				mHeading = 0f;
			}
		}

		// handle "finger drag" event
		
		void doTouchMove(float x, float y, float dx, float dy, int count) {
			synchronized (mSurfaceHolder) {

				touchDown = true;      		
				touch_x = x;
				touch_y = y; 	
				
				// make ship "lean" in direction of lateral motion
				
				if (dx > 90)
					mHeading = 90;
				else if (dx < -90)
					mHeading = -90;
				else
					mHeading = dx;
			}
		}


		/**
		 * Draws the ship, background, and everything else to the provided Canvas.
		 */
		 private void doDraw(Canvas canvas) {
			 // Draw the background image. Operations on the Canvas accumulate
			 // so this is like clearing the screen.
			 canvas.drawBitmap(mBackgroundImage, 0, 0, null);

			 // make ship follow touch
			 
//			 if (touchDown) {
				 mX = (int) touch_x;
				 mY = (int) touch_y - 50f;   // offset so ship is visible above your finger
//			 }


			 int xLeft = (int) mX - mLanderWidth / 2;
			 int yTop = (int) mY - mLanderHeight / 2;

			 // Draw the ship with its current rotation

			 canvas.save();
			 canvas.rotate((float) mHeading, (float) mX, (float) mY);

			 if (mEngineFiring) {
				 mFiringImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop + mLanderHeight);
				 mFiringImage.draw(canvas);
			 } else {
				 mLanderImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop + mLanderHeight);
				 mLanderImage.draw(canvas);
			 }
			 canvas.restore();
			 
			 // draw "meteor"
			 
			 int mx = (int) meteor_x;
			 int my = (int) meteor_y;
			 
			 mScratchRect.set(mx - METEOR_RADIUS, my - METEOR_RADIUS, mx + METEOR_RADIUS, my + METEOR_RADIUS);
			 canvas.drawOval(mScratchRect, mLinePaint);

			 // don't draw if we are in pause mode
			 if (mMode == STATE_RUNNING)
				 canvas.drawText("Time since last crash: " + (mTimeNow - mTimeStart) + " (" + maxTimeSinceCrash + ") Lander position = (" + mX + ", " + mY + ")", 10, TEXT_SIZE, mTextPaint);
			 
			 if (haveAccel) {
				 String s = String.format("(%+5.3f, %+5.3f, %+5.3f)", accelX, accelY, accelZ);
				 canvas.drawText(s, 10, 2*TEXT_SIZE, mTextPaint);
			 }
			 
			 if (accelMagnitude > 2*SensorManager.GRAVITY_EARTH) 
				 canvas.drawText("SHAKE!!!", 400, 100, mBigTextPaint);
			 
			 if (mCrash) 
				 canvas.drawText("CRASH!!!", 400, 200, mBigTextPaint);
		 }

		 // create a meteor at a random spot
		 
		 void respawnMeteor(boolean top) {
			 meteor_x = Math.random()*METEOR_MAX_X_LIMIT;
			 if (top)
				 meteor_y = METEOR_MIN_Y_LIMIT + 20.0;
			 else
				 meteor_y = METEOR_MAX_Y_LIMIT - 20.0;
		 }
		 
		 // move meteor
		 
		 void updateMeteor() {
			 
			 if (meteor_y > METEOR_MAX_Y_LIMIT) 
				 respawnMeteor(true);			 
			 else if (meteor_y < METEOR_MIN_Y_LIMIT) 
				 respawnMeteor(false);	
			 else { 
				 // constant velocity
				 //meteor_y += METEOR_MAX_SPEED;
				 
				 // determined by device tilt, up to maximum
				 double speed = METEOR_SPEED_FACTOR * accelX / Math.abs(accelZ);   // abs value so it works upside down
				 if (speed > METEOR_MAX_SPEED)
					 speed = METEOR_MAX_SPEED;
				 else if (speed < -METEOR_MAX_SPEED)
					 speed = -METEOR_MAX_SPEED;
				 if (haveAccel)
					 meteor_y += speed;
			 }
		 }
		 
		 // move ship
		 
		 void updateShip() {
			
			 if (haveAccel && !touchDown) {
				 double speed = SHIP_SPEED_FACTOR * accelY;
				 if (speed > SHIP_MAX_SPEED)
					 speed = SHIP_MAX_SPEED;
				 else if (speed < -SHIP_MAX_SPEED)
					 speed = -SHIP_MAX_SPEED;
			
					 touch_x += speed;
					 
					 if (touch_x > METEOR_MAX_X_LIMIT)
						 touch_x = (float) METEOR_MAX_X_LIMIT;
					 else if (touch_x < METEOR_MIN_X_LIMIT)
						 touch_x = (float) METEOR_MIN_X_LIMIT;
			 }
		 }
		 
		 // see if meteor hit ship
		 
		 void checkForShipMeteorCollision() {

			 if (Math.sqrt((meteor_x - mX)*(meteor_x - mX)+(meteor_y - mY)*(meteor_y - mY)) <= CRASH_DISTANCE) {
				 if (mCrash != true) {
					 
					 numCrashes++;

					 // sound

					 SP.play(sound_explosion, 1f, 1f, 0, 0, 1f);

					 // vibration
					 
					// V.vibrate(500);
					 
					/*
					 long[] pattern = { 
							 0,                         // delay before starting
							 50, 10, 50, 10, 50, 10 };  // on/off/etc. millis
							
					 V.vibrate(pattern, -1);  // -1 means don't repeat, 0 means repeat from start
					 */
					 
					 // speech

					 String myText;
					 if (numCrashes == 1)
						 myText = new String("One crash");
					 else
						 myText = new String(Integer.toString(numCrashes) + " crashes");
					 mTTS.speak(myText, TextToSpeech.QUEUE_FLUSH, null);

					 if ((mTimeNow - mTimeStart) > maxTimeSinceCrash) {
						 myText = "New best time!";
						 mTTS.speak(myText, TextToSpeech.QUEUE_ADD, null);  // vs. QUEUE_ADD

						 maxTimeSinceCrash = mTimeNow - mTimeStart;
					 }
				 }

				 // physics 

				 mCrash = true;
				 respawnMeteor(true);
				 mTimeStart = mTimeNow = System.currentTimeMillis();
			 }
			 else
				 mCrash = false;
		 }

		 /**
		  * This is where physics can be simulated if necessary... Called at the start of draw()
		  */

		 private void updatePhysics() {

			 // drop meteor
			 
			 updateMeteor();
			 
			 // slide ship
			 
			 updateShip();
			 
			 // check for collision
			 
			 checkForShipMeteorCollision();

		 }
	}  // end of MarsThread class definition

	/** Handle to the application context, used to e.g. fetch Drawables. */
	private Context mContext;

	/** Pointer to the text view to display "Paused.." etc. */
	private TextView mStatusText;

	/** The thread that actually draws the animation */
	private MarsThread thread;

	private float x_previous_touch, y_previous_touch;

	// view constructor

	public MarsView(Context context, AttributeSet attrs) {
		super(context, attrs);

		// register our interest in hearing about changes to our surface
		SurfaceHolder holder = getHolder();
		holder.addCallback(this);

		// create thread only; it's started in surfaceCreated()
		// note inline definition of Handler class
		thread = new MarsThread(holder, context, new Handler() {
			@Override
			public void handleMessage(Message m) {
				mStatusText.setVisibility(m.getData().getInt("viz"));
				mStatusText.setText(m.getData().getString("text"));
			}
		});

		setFocusable(true); // make sure we get key events
	}

	/**
	 * Fetches the animation thread corresponding to this MarsView.
	 *
	 * @return the animation thread
	 */
	public MarsThread getThread() {
		return thread;
	}

	/**
	 * Standard window-focus override. Notice focus lost so we can pause on
	 * focus lost. e.g. user switches to take a call.
	 */
	@Override
	public void onWindowFocusChanged(boolean hasWindowFocus) {
		if (!hasWindowFocus) thread.pause();
	}

	boolean moved = false;

	// handle touch events in the view...pass them along to the drawing thread
	
	@Override 
	public boolean onTouchEvent(MotionEvent e) {

		float x_touch = e.getX();
		float y_touch = e.getY();

		int action = e.getAction();

		switch (action & MotionEvent.ACTION_MASK)
		{

		case MotionEvent.ACTION_MOVE: 

			float dx_touch = x_touch - x_previous_touch;
			float dy_touch = y_touch - y_previous_touch;

			thread.doTouchMove(x_touch, y_touch, dx_touch, dy_touch, e.getPointerCount());
			break;

		case MotionEvent.ACTION_DOWN:
			thread.doTouchDown(x_touch, y_touch, e.getPointerCount());
			break;

		case MotionEvent.ACTION_UP:
			thread.doTouchUp(x_touch, y_touch, e.getPointerCount());
			break;

		}

		x_previous_touch = x_touch;
		y_previous_touch = y_touch;

		return true;
	}

	/**
	 * Installs a pointer to the text view used for messages.
	 */
	public void setTextView(TextView textView) {
		mStatusText = textView;
	}

	/* Callback invoked when the surface dimensions change. */
	public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
		thread.setSurfaceSize(width, height);
	}

	/*
	 * Callback invoked when the Surface has been created and is ready to be
	 * used.
	 */
	public void surfaceCreated(SurfaceHolder holder) {
		// start the thread here so that we don't busy-wait in run()
		// waiting for the surface to be created
		thread.setRunning(true);
		thread.start();
	}

	/*
	 * Callback invoked when the Surface has been destroyed and must no longer
	 * be touched. WARNING: after this method returns, the Surface/Canvas must
	 * never be touched again!
	 */
	public void surfaceDestroyed(SurfaceHolder holder) {
		// we have to tell thread to shut down & wait for it to finish, or else
		// it might touch the Surface after we return and explode
		boolean retry = true;
		thread.setRunning(false);
		while (retry) {
			try {
				thread.join();
				retry = false;
			} catch (InterruptedException e) {  }
		}
	}
}
