//  MainGame.java
//  KandyJump
//
// $Id: MainGame.java,v 1.10 2007/11/26 05:28:55 dtj Exp $
//
//  Created by Derek Jones on 11/13/07.
//  Copyright 2007. All rights reserved.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.Vector;
import java.util.Random;
import java.util.HashSet;
import java.util.Date;

public class MainGame extends JPanel implements KeyListener {
	
	public int worldHeight;
	public int worldWidth;
	
	public int hiScore = 0;
	public int totalScore = 0;
	public int flashScore = 10;
	public int flashMilestone = flashScore;
	public int levelSeconds = 60;
	
	// Each layer is a HashSet of sprites.
	private Vector layers = new Vector();
	private Vector allSprites = new Vector();
	
    Random rnd = new Random();
	
	public class Sprite {
		
		protected int width;
		protected int height;
		
		public int x, y;
		public double vx, vy;
		public int gravity;
		public boolean grounded;
		private Image image;
		public boolean solid;
		public int hitScore;
		public boolean inPlay;
		
		public HashSet layer;
		
		public Sprite () {
			width = 0;
			height = 0;
			this.image = null;
			x = 50; y = 50;
			vx = 0; vy = 0;
			grounded = false;
			solid = true;
			hitScore = 0;
			inPlay = true;
			gravity = 1;
		}
		
		public void setImage (Image image) {
			this.image = image;
			width = image.getWidth(null);
			height = image.getHeight(null);			
		}
		
		public void onGround () {
			vx = 0; vy = 0;
			grounded = true;
		}
		
		public void inAir () {
			vy -= gravity;
			grounded = false;			
		}
		
		public void update () {
			if (!inPlay)
				return;
			int ground = height;
			y = (int)Math.max(y + vy, ground);
			x = (int)(x + vx);
			// Wraparound!
			if (x > worldWidth) {
				x = 0;
			} else if (x < 0) {
				x = worldWidth;
			}

			// this is where the player wants to check for collision

			if (gravity > 0) {
				if (y > ground) {
					inAir();
				} else {
					onGround();
				}
			}
		} // update
		
		public void draw (Graphics g) {
			if (!inPlay)
				return;
			// future optimization:  skip drawing if no sprite
			// overlap or change since last update
			Rectangle clip = g.getClipBounds();
			Rectangle myBounds = new Rectangle(x, y, width, height);
			if (clip != null  &&  !clip.intersects(myBounds)) {
				return;
			}
			g.drawImage(image, x, worldHeight - y, null);			
		} // draw
	} // class Sprite
	
	public class Player extends Sprite {
		private ImageIcon normal;
		private ImageIcon flash;
		private int flashoff_x;
		private int flashoff_y;
		private boolean drawFlash = false;
		public Player () {
			normal = new ImageIcon(getClass().getResource("player.png"));
			flash = new ImageIcon(getClass().getResource("flash.png"));
			setImage(normal.getImage());
			// Remember the offset at which the flash will need to be
			// drawn.
			flashoff_x = flash.getIconWidth()/2 - normal.getIconWidth()/2;
			flashoff_y = flash.getIconHeight()/2 - normal.getIconHeight()/2;
			gravity = 1;
		}
		
		// Used to animate temporary actions, like flashing.
		public Timer flashTimer = new Timer(750, new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				drawFlash = false;
				flashTimer.stop();
			}
		});
		
		public void draw (Graphics g) {
			if (drawFlash) {
				g.drawImage(flash.getImage(), x - flashoff_x,
							worldHeight - (y + flashoff_y), null);			
			}
			super.draw(g);
		}
		
		public void collidedWith (Sprite s) {
			if (s.hitScore != 0) {
				s.inPlay = false;
				int score = s.hitScore;
				if (!grounded) {
					// Double points!
					score *= 2;
				}
				int next_score = Math.max(totalScore + score, 0);
				if (score > 0  &&  totalScore < flashMilestone  &&
					next_score >= flashMilestone) {
					drawFlash = true;
					flashTimer.start();
					flashMilestone += flashScore;
				}
				totalScore = next_score;
				hiScore = Math.max(totalScore, hiScore);
			}
		}
		
		public void update () {
			// Check it against all other sprites except me.
			// This is done before positions are updated, so that
			// what the player sees on screen agrees with this calculation.
			if (solid) {
				Rectangle r1 = new Rectangle(x, worldHeight - y, width, height);
				for (Object o : allSprites) {
					Sprite check = (Sprite)o;
					if (this != check  &&  check.solid) {
						Rectangle r2 = new Rectangle(check.x,
													 worldHeight - check.y,
													 check.width,
													 check.height);
						Rectangle hit = r2.intersection(r1);
						if (hit.getHeight() > 0.0  &&  hit.getWidth() > 0.0) {
							collidedWith(check);
						}
					}
				}
			}
			super.update();
		} // Player.update
	}
	
	// Declared here so that later Sprite types can access it directly.
	private Sprite player = null; 
	
	public class Cloud extends Sprite {
		private int sinceLastDrop = 0;
		public Cloud (int which) {
			String icon_name = "cloud" + which + ".png";
			ImageIcon icon = new ImageIcon(getClass().getResource(icon_name));
			setImage(icon.getImage());
			solid = false;
			gravity = 0;
			x = rnd.nextInt(worldWidth);
			y = 300 + rnd.nextInt(100);
			vy = 0;
		}
		public void update () {
			super.update();
			// Don't drop too near the edges.
			if (x <= width  ||  x > (worldWidth - width))
				return;
			// Don't drop too often, or too rarely.
			// Within these constraints, it can be random.
			if (++sinceLastDrop < 5) {
				return;
			}
			if (sinceLastDrop < 40  &&  rnd.nextInt(10) != 0) {
				return;
			}
			Sprite drop = null;
			if (rnd.nextInt(5) <= 1) {
				drop = new Apple();
			} else {
				drop = new Gumdrop(rnd.nextInt(5) + 1);
			}
			drop.x = x + width / 2;
			drop.y = y - height / 2;
			drop.vx = 0;
			drop.vy = 0;
			addSprite(drop, 0);
			sinceLastDrop = 0;
		}
		
	}
	
	public class Gumdrop extends Sprite {
		public Gumdrop (int which) {
			String icon_name = "gumdrop" + which + ".png";
			ImageIcon icon = new ImageIcon(getClass().getResource(icon_name));
			setImage(icon.getImage());
			hitScore = 1;
		}
		public void inAir () {
			vy = Math.max(vy - gravity, -3);
			grounded = false;			
		}
		public void onGround () {
			inPlay = false;
		}
	}
	
	public class Apple extends Sprite {
		public Apple () {
			String icon_name = "apple.png";
			ImageIcon icon = new ImageIcon(getClass().getResource(icon_name));
			setImage(icon.getImage());
			hitScore = -10;
		}
		public void onGround () {
			inPlay = false;
		}
		public void inAir () {
			vy = Math.max(vy - gravity, -5);
			grounded = false;			
		}
	}
	
	private boolean pressingLeft = false;
	private boolean pressingRight = false;
	private boolean pressingUp = false;
		
	private Timer timer = new Timer(50, new AbstractAction() {
		public void actionPerformed (ActionEvent e) {
			// I could dispatch this logic to the Sprite class
			// but the player object is unique in the
			// game as it is the only one that responds to any controls.
			if (pressingLeft) {
				if (player.grounded) {
					player.vx = -5;
				}
			}
			if (pressingRight) {
				if (player.grounded) {
					player.vx = 5;
				}
			}
			Vector next_sprites = new Vector();
			// The updates inside this loop may add to allSprites,
			// so the choice to continue the loop until the very end of
			// allSprites.size() is in fact critical; otherwise they
			// won't make it into next_sprites.
			for (int ii = 0; ii < allSprites.size(); ++ii) {
				Sprite s = (Sprite)(allSprites.get(ii));
				if (s.inPlay) {
					s.update();
					next_sprites.add(s);
				} else {
					HashSet layer = s.layer;
					layer.remove(s);
				}
			}
			allSprites = next_sprites;
			repaint();
		}
	});
	
	public void addSprite (Sprite s, int layer_index) {
		allSprites.add(s);
		HashSet layer = (HashSet)(layers.get(layer_index));
		layer.add(s);
		s.layer = layer;
	}
	
	public void start () {
		totalScore = 0;
		flashMilestone = flashScore;
		// Date().getTime() returns a number of milliseconds.  Compute the
		// time of the level termination.
		endTime = new Date().getTime() + 1000 * levelSeconds;
		layers.clear();
		allSprites.clear();
		player = null;  // signal to rebuild the level
		timer.start();
	}
	
	private long endTime;
	
	public void startLevel () {
		layers.add(new HashSet());
		layers.add(new HashSet());
		layers.add(new HashSet());
		
		player = new Player();
		addSprite(player, 2);  // in front of everything
		
		Sprite cloud1 = new Cloud(1); cloud1.vx = -(rnd.nextInt(3) + 1);
		Sprite cloud2 = new Cloud(2); cloud2.vx = rnd.nextInt(3) + 1;
		addSprite(cloud1, 1);
		addSprite(cloud2, 1);
	}
	
	private Font scoreFont = new Font("SansSerif", Font.BOLD, 14);

	public void paint (Graphics g) {
		worldHeight = getHeight();
		worldWidth = getWidth();
		if (player == null) {
			startLevel();
			return;
		}
		// Background
		g.setColor(Color.white);
		g.fillRect(0, 0, worldWidth, worldHeight);
		
		// Draw the sprite layers in their specific order.
		for (Object lyr : layers) {
			for (Object o : (HashSet)lyr) {
				((Sprite)o).draw(g);
			}
		}
		
		g.setFont(scoreFont);
		g.setColor(Color.black);
		String score_str = "Score: " + totalScore;
		int text_h = (int)g.getFontMetrics(scoreFont).
			getStringBounds(score_str, g).getHeight();
		g.drawString(score_str, 10, 10 + text_h);
		
		score_str = "High Score: " + hiScore;
		text_h = (int)g.getFontMetrics(scoreFont).
			getStringBounds(score_str, g).getHeight();
		int text_w = (int)g.getFontMetrics(scoreFont).
			getStringBounds(score_str, g).getWidth();
		g.drawString(score_str, worldWidth/2-text_w/2, 10 + text_h);
		
		long now = new Date().getTime();
		if (now >= endTime) {
			timer.stop();
			String game_over = "Game Over!";
			text_w = (int)g.getFontMetrics(scoreFont).
				getStringBounds(game_over, g).getWidth();
			g.drawString("Game Over",
						 worldWidth/2 - text_w/2,
						 worldHeight/2 - text_h/2);
		} else {
			String timer_str = "Time Left: " + (endTime - now) / 1000;
			text_w = (int)g.getFontMetrics(scoreFont).
				getStringBounds(timer_str, g).getWidth();
			g.drawString(timer_str, worldWidth - 10 - text_w, 10 + text_h);
		}
	}
	
	// Key methods
	
	public void keyPressed (KeyEvent e) {
		// Soon as a control key goes down, don't consider it
		// "up" until it has been noted as released, below.
		if (e.getKeyCode() == KeyEvent.VK_LEFT) {
			pressingLeft = true;
		} else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
			pressingRight = true;
		} else if (e.getKeyCode() == KeyEvent.VK_UP) {
			pressingUp = true;
		} else if (e.getKeyCode() == KeyEvent.VK_SPACE) {
			if (player.grounded) {
				player.vy = 10;
			}
		} else if (e.getKeyCode() == KeyEvent.VK_N  &&  !timer.isRunning()) {
			start();
		}
	}
	
	public void keyReleased (KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_LEFT) {
			pressingLeft = false;
		} else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
			pressingRight = false;
		} else if (e.getKeyCode() == KeyEvent.VK_UP) {
			pressingUp = false;
		}
	}
	
	public void keyTyped (KeyEvent e) {
	}
	
}
