// Bot.cs created with MonoDevelop
// User: epsilon at 21:44 26.05.2008
//
// To change standard headers go to Edit->Preferences->Coding->Standard Headers
//

using System;
using Framework1;

namespace Bot1Delta
{
	public class Bot : Framework1.BotPluginTemplate
	{
		// TODO:
		// - ufo bullet collision detection
		// - exact phi (--> winkelbyte)
		// - ufo collision detection
		// - ufo hit radius
		// - asteroid IDs
		// - target priority list (asteroid on collision course --> min distance of line from point in next 300 frames)
		// - collsion predetection in another thread
		// - LogOutput-timer messages
		
		// constants
		//-----------
		private const double BIG_ASTEROID_RADIUS = 32;
		private const double MEDIUM_ASTEROID_RADIUS = 16;
		private const double SMALL_ASTEROID_RADIUS = 8;		
		private const double SHIP_RADIUS = 16;
		private const double UFO_RADIUS = 8;		

		private const double MAX_T = 90;
		
		private const int N_COLLISION_DETECTION = 5;
		private const double N_COLLISION_FRAME_PREDICTION = 5;
		private const int N_COLLISION_PREDETECTION_FRAMES = 300;
		

		// fields
		//--------
		private Framework1.KeySet theLastPressedKeys = new KeySet();
		private Framework1.KeySet theCurrentKeys = new KeySet();
		private Framework1.Playfield thePlayfield;
		
		
		// constructor
		//-------------
		public Bot()
		{
			LogOutput.LogInfo(this.ToString(), "Starting Bot1Delta");
		}
		
		
		// methods
		//---------
		// the predictor =)
		// ... has to be improved a bit ...
		private Framework1.Point2D CalculatePhiAndT(Framework1.Point2D targetPosition,
		                                            Framework1.Point2D targetSpeed,
		                                            Framework1.Point2D shipPosition)
		{
			double ar = targetSpeed.Abs();
			double arx = targetSpeed.X;
			double ary = targetSpeed.Y;

			double pr  = 8.0;
						
			// '-'-operator considers overwrapping
			double dx = (targetPosition - shipPosition).X;
			double dy = (targetPosition - shipPosition).Y;
			double d = (targetPosition - shipPosition).Abs();
						
			double t1 = -1.0/(ar*ar - pr*pr)*(dx*arx + dy*ary);
			double t2 = Math.Sqrt(t1*t1 - d*d/(ar*ar - pr*pr));
			//LogOutput.LogDebug(this.ToString(), "t1 + t2: " + (t1 + t2));
			//LogOutput.LogDebug(this.ToString(), "t1 - t2: " + (t1 - t2));
			double t = t1 + t2;
						
			if (0 < t && t < MAX_T)
			{
				double cosPhi = dx/(pr*t) + arx/pr;
				double sinPhi = dy/(pr*t) + ary/pr;
				double phi = Math.Atan2(sinPhi, cosPhi);
				
				return new Point2D(phi, t);
			}
			
			return null;
		}
		
		private Framework1.Point2D CalculatePhiAndT(Framework1.Sprite target, Framework1.Point2D shipPosition)
		{
			// check if speed is known
			if (target.SpeedKnown)
				return this.CalculatePhiAndT(target.Position, target.Speed, shipPosition);
			else
				return null;
		}
		
		private int CollisionCourse(Framework1.Sprite target)
		{
			if (target.SpeedKnown && thePlayfield.ShipPresent)
			{
				double targetRadius = BIG_ASTEROID_RADIUS;
				if (target.HitRadius == 14)
					targetRadius = SMALL_ASTEROID_RADIUS;
				if (target.HitRadius == 15)
					targetRadius = MEDIUM_ASTEROID_RADIUS;

				// number of wrap-arounds to be considered
				int nX = (int)Math.Ceiling(Math.Abs(target.Speed.X) * N_COLLISION_PREDETECTION_FRAMES / Framework1.Playfield.WORLD_WIDTH) + 1;
				int nY = (int)Math.Ceiling(Math.Abs(target.Speed.Y) * N_COLLISION_PREDETECTION_FRAMES / Framework1.Playfield.WORLD_HEIGHT) + 1;
				//LogOutput.LogDebug(this.ToString(), "Collision predetection field dimensions: " + nX + ", "+ nY);
				
				for (int iX=0; iX < nX; iX++)
				{
					for (int iY=0; iY < nY; iY++)
					{
						// transform ship position
						Framework1.Point2D shipPosition = thePlayfield.ShipSprite.Position
							+ new Framework1.Point2D(iX * Framework1.Playfield.WORLD_WIDTH * Math.Sign(target.Speed.X),
							                         iY * Framework1.Playfield.WORLD_HEIGHT * Math.Sign(target.Speed.X));
						
						// calculate coefficient t in line parameterisation
						// do not use wrapping in vector operations!
						double t = (shipPosition + (target.Position*(-1))) * target.Speed / (target.Speed.Abs()*target.Speed.Abs());
						
						// minimum distance to considered ship position
						double distance = (target.Speed*t + (target.Position + (shipPosition*(-1)))).Abs();
						
						// asteroid on collision course?
						if (t > 0 && distance < targetRadius + SHIP_RADIUS)
						{
							LogOutput.LogDebug(this.ToString(), "Asteroid on collision course: t = " + t + ", distance = " + distance);
							return (int)t;
						}
					}
				}
			}
			
			return -1;
		}
		
		public override void UpdatePlayfield (Framework1.Playfield playfield)
		{
			DateTime startingTime = DateTime.Now;
			
			thePlayfield = playfield;
			theCurrentKeys.ReleaseKeys();
			
			bool shouldFire = false;
			bool shouldTurnRight = false;
			bool shouldTurnLeft = false;	
			
			BotStatus currentStatus = new BotStatus();
			Framework1.Sprite nextTarget = null;			

			if (thePlayfield.ShipPresent)
			{
				LogOutput.LogDebug(this.ToString(), "Searching for targets");
				int minTimeUntilCollision = -1;
				double minDeltaPhi = 100; // huge number

				foreach (Framework1.Asteroid asteroid in thePlayfield.Asteroids)
				{
					if (asteroid.SpeedKnown)
					{
						int timeUntilCollision = this.CollisionCourse(asteroid);
						if (timeUntilCollision != -1)
						{
							// mark as asteroid on collision course
							currentStatus.Targets2.Add(asteroid);
							
							if (timeUntilCollision < minTimeUntilCollision || minTimeUntilCollision < 0)
							{
								LogOutput.LogDebug(this.ToString(), "Targetting asteroid on collision course");
								minTimeUntilCollision = timeUntilCollision;
								nextTarget = asteroid;
								currentStatus.Targets1.Add(asteroid);
							}
						}

						//LogOutput.LogDebug(this.ToString(), "Asteroid position: " + asteroid.Position);
						//LogOutput.LogDebug(this.ToString(), "Asteroid speed: " + asteroid.Speed);
						
						Framework1.Point2D phiAndT = this.CalculatePhiAndT(asteroid, thePlayfield.ShipSprite.Position);
						
						if (phiAndT != null)
						{
							double phi = phiAndT.X;
							double t = phiAndT.Y;
							
							double radius = BIG_ASTEROID_RADIUS;
							if (asteroid.HitRadius == 14)
								radius = SMALL_ASTEROID_RADIUS;
							if (asteroid.HitRadius == 15)
								radius = MEDIUM_ASTEROID_RADIUS;

							double hitRatio = 1.00;
							if (Math.Abs(thePlayfield.ShipSprite.Phi - phi) < Math.PI/2)
								hitRatio = Math.Abs(Math.Sin(thePlayfield.ShipSprite.Phi - phi))
								* (thePlayfield.ShipSprite.Position - asteroid.Position).Abs() / radius;

							
							if (hitRatio < 1.0)
							{
								LogOutput.LogDebug(this.ToString(), "Target found:");
								LogOutput.LogDebug(this.ToString(), "delta phi: " + (thePlayfield.ShipSprite.Phi - phi));
								LogOutput.LogDebug(this.ToString(), "t: " + t);

								LogOutput.LogDebug(this.ToString(), "Distance: " + (thePlayfield.ShipSprite.Position - asteroid.Position).Abs());
								LogOutput.LogDebug(this.ToString(), "Ratio: " + hitRatio);							

								shouldFire = true;
								currentStatus.Targets1.Add(asteroid);
							}
							
							if (Math.Abs(thePlayfield.ShipSprite.Phi - phi) < Math.Abs(minDeltaPhi)
							    && minTimeUntilCollision < 0)
							{
								LogOutput.LogDebug(this.ToString(), "Targetting asteroid");
								minDeltaPhi = thePlayfield.ShipSprite.Phi - phi;
								nextTarget = asteroid;
							}
						}
					}
				}
				
				// aim for saucer, if present
				if (thePlayfield.SaucerPresent)
				{
					LogOutput.LogDebug(this.ToString(), "Targetting saucer");
					nextTarget = thePlayfield.SaucerSprite;

					// mark ufo as target
					currentStatus.Targets1.Add(thePlayfield.SaucerSprite);
				}

				// turn towards target with highest priority
				if (nextTarget != null)
				{
					Framework1.Point2D phiAndT = this.CalculatePhiAndT(nextTarget, thePlayfield.ShipSprite.Position);
						
					// turn towards target (preliminary, will be updated after calculation)
					double phi = (nextTarget.Position - thePlayfield.ShipSprite.Position).Abs();

					if (phiAndT != null)
					{
						phi = phiAndT.X;
						double t = phiAndT.Y;
							
						double radius = BIG_ASTEROID_RADIUS;
						if (nextTarget.HitRadius == 14)
							radius = SMALL_ASTEROID_RADIUS;
						if (nextTarget.HitRadius == 15) // medium asteroid and saucer
							radius = MEDIUM_ASTEROID_RADIUS;
						

						double hitRatio = 1.00;
						if (Math.Abs(thePlayfield.ShipSprite.Phi - phi) < Math.PI/2)
							hitRatio = Math.Abs(Math.Sin(thePlayfield.ShipSprite.Phi - phi))
								* (thePlayfield.ShipSprite.Position - nextTarget.Position).Abs() / radius;

							
						if (hitRatio < 1.0)
						{
							LogOutput.LogDebug(this.ToString(), "main target:");
							LogOutput.LogDebug(this.ToString(), "delta phi: " + (thePlayfield.ShipSprite.Phi - phi));
							LogOutput.LogDebug(this.ToString(), "t: " + t);
							
							LogOutput.LogDebug(this.ToString(), "Distance: "
							                   + (thePlayfield.ShipSprite.Position - nextTarget.Position).Abs());
							LogOutput.LogDebug(this.ToString(), "Ratio: " + hitRatio);							
							
							shouldFire = true;
						}
					}

						// turn towards target
					if ( Math.Sin(thePlayfield.ShipSprite.Phi - phi) > 0)
						shouldTurnRight = true;
					if ( Math.Sin(thePlayfield.ShipSprite.Phi - phi) < 0)
						shouldTurnLeft = true;
					
				}
				
				// hyperspace if asteroid gets to close
				if (this.CollisionDetection())
					theCurrentKeys.PushKey(KeySet.Keys.HyperSpace);
					
				// fire?
				if (shouldFire)
				{
					// only fire if not fired in last frame
					//LogOutput.LogDebug(this.ToString(), "Old keys: " + theLastPressedKeys.ToString());
					if (!theLastPressedKeys.IsPushed(KeySet.Keys.Fire))
					{
						theCurrentKeys.PushKey(KeySet.Keys.Fire);
					}
				}
				//else
				{
					// //do not turn while shooting
					if (shouldTurnRight)
						theCurrentKeys.PushKey(KeySet.Keys.Right);
					
					if (shouldTurnLeft)
						theCurrentKeys.PushKey(KeySet.Keys.Left);
				}
					
				if (thePlayfield.ShipSprite.SpeedKnown)
					LogOutput.LogDebug(this.ToString(), "Current speed: " + thePlayfield.ShipSprite.Speed);
			}			
			
			if (thePlayfield.FrameNumber%100 == 0)
				LogOutput.LogInfo(this.ToString(), "Calculationd done in  " + (DateTime.Now - startingTime).TotalMilliseconds + " ms");
			else
				LogOutput.LogDebug(this.ToString(), "Calculationd done in  " + (DateTime.Now - startingTime).TotalMilliseconds + " ms");
			

			// request key pushing
			Framework1.ChangeKeysEventArgs keyArgs = new ChangeKeysEventArgs();
			keyArgs.keys = theCurrentKeys.Clone();
			
			LogOutput.LogDebug(this.ToString(), "Demanding keys: " + keyArgs.keys.ToString());
			OnChangeKeys(keyArgs);
			
			
			// submit bot status change
			Framework1.ChangeBotStatusEventArgs statusArgs = new ChangeBotStatusEventArgs();
			statusArgs.status = currentStatus;
			
			OnChangeBotStatus(statusArgs);
		}
		
		private bool CollisionDetection()
		{
			for (int iAsteroid = 0; iAsteroid < Math.Min(thePlayfield.AsteroidsByDistance.Count, N_COLLISION_DETECTION); iAsteroid++)
			{
				Framework1.Asteroid asteroid = thePlayfield.AsteroidsByDistance[iAsteroid];
				
				double asteroidRadius = BIG_ASTEROID_RADIUS;
				if (asteroid.HitRadius == 14)
					asteroidRadius = SMALL_ASTEROID_RADIUS;
				if (asteroid.HitRadius == 15)
					asteroidRadius = MEDIUM_ASTEROID_RADIUS;
				
				// collision now?
				double distance = (thePlayfield.ShipSprite.Position - asteroid.Position).Abs();
				if (distance < (SHIP_RADIUS + asteroidRadius))
					return true;
				
				// collision in the next X frames?
				if (asteroid.SpeedKnown)
				{
					distance = (thePlayfield.ShipSprite.Position
					            - (asteroid.Position + asteroid.Speed*N_COLLISION_FRAME_PREDICTION)).Abs();
					if (distance < (SHIP_RADIUS + asteroidRadius))
						return true;
					
				}
			}
			
			return false;
		}
		
		public override void UpdateCurrentKeys (KeySet keys)
		{
			theLastPressedKeys = keys;
			//LogOutput.LogDebug(this.ToString(), "Current keys: " + keys.ToString());
		}

		
		public override string ToString ()
		{
			return "Bot1Delta";
		}

	}
}
