/*
 * This code is protected under the Gnu General Public License (Copyleft), 2005 by
 * IBM and the Computer Science Teachers of America organization. It may be freely
 * modified and redistributed under educational fair use.
 */

/*
 * Pong Game
 * By Philip Peng
 * October 4th, 2007
 * For Mr. Benum
 * 
 * Over 600 lines of code due to extreme
 * commenting and text formatting.
 * Two little easter eggs in checkWin() method.
 * Otherwise, stays true to the classic Pong ^_^
 * 
 */

/*
 * CHANGELOG
 * 
 * Note that the attached documents may not reflect the changes
 * made in the new revisions.
 * 
 * RevB:
 * - Changed ball starting position, allowing for more human reaction time
 * - Removed all hard-coded values for the JLabels
 * - Added smallInfoLabel2 and shifted previous info JLabel locations
 * - Changed score JLabel font to Courier New (more readable) and left aligned them
 * - Changed fixPaddle1() to fixLocations() - now re-locates all objects needing it
 * 
 */

/*
 * TODO
 * 
 * Make magnetic version; rather than using
 * collision bounces, actively alter the ball's
 * dX and dY based on distance from paddle.
 *  
 */

import csta.ibm.pong.Game;
import java.awt.Color;
import java.awt.Font;
import javax.swing.JLabel;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class Pong extends Game {

////////////////////////////////////////////////////////////////////////////
// Class Variables block
////////////////////////////////////////////////////////////////////////////
	
// Class variables
	
	// Objects

		// Ball Object
		Ball ballObj;
		
		// Paddle Objects
		// Keep in mind that paddle1 is the bottom one and paddle2 is the top one.
		Paddle paddle1, paddle2;
		
		// JLabel Objects
		JLabel paddle1Score, paddle2Score;
		JLabel bigInfoLabel, smallInfoLabel1, smallInfoLabel2;
		
		// Font objects
		// Hard-coded font sizes since application field height has
		// not yet been declared
		Font scoreFont = new Font("courier new", Font.BOLD, 13);
		Font smallFont = new Font("papyrus", Font.PLAIN, 14);
		Font bigFont = new Font("papyrus", Font.BOLD, 20);
		
	// Variables
		boolean gameStopped = false;
		boolean magneticMode = false;

////////////////////////////////////////////////////////////////////////////
// Setup block
////////////////////////////////////////////////////////////////////////////

// Add Keypresses method; called by setup()
	public void addKeypresses() {
		
		// Direct copy/modify of Game class code
		// Adds a pause/resume key (P) and a restart key (R)
			addKeyListener(new KeyListener() {
				
				public void keyTyped(KeyEvent e) {}
		
				public void keyPressed(KeyEvent e) {
					char pressed = Character.toUpperCase(e.getKeyChar());
					switch (pressed) {
						case 'R' : 
							if (gameStopped) {
								gameStopped = false;
								bigInfoLabel.setText("");
								smallInfoLabel1.setText("");
								smallInfoLabel2.setText("");
								repaint();
								resetGame(); // Return objects to original locations
								startGame(); // Restart game
							}
							break;
						case 'P' :
							// Note: since pausing uses the same flag as restarting,
							// P can also be used to start the initial game or to
							// continue a game after reaching 10 points
							// (though following the next point the game will pause again).
							// This isn't really an issue worth addressing and can be used
							// as a hidden "feature".
							if (!gameStopped) {
								gameStopped = true;
								bigInfoLabel.setText("Game Paused");
								smallInfoLabel1.setText("Press P to resume game");
								smallInfoLabel2.setText("or press R to restart.");
								repaint();
								stopGame(); // Pause game
							} else {
								gameStopped = false;
								bigInfoLabel.setText("");
								smallInfoLabel1.setText("");
								smallInfoLabel2.setText("");
								repaint();
								startGame(); // Resume game
							}
					}
				}
		
				public void keyReleased(KeyEvent e) {}
	       } );
			
	} // addKeypressQ() method
		
//Setup Screen method; called by setup()
	public void setupScreen() {
		
		// Screen colour
			Color screenColour = Color.BLACK;
			this.setBackground(screenColour);
			this.repaint();
		
	} // setupScreen() method

// Setup Ball method; called by setup()
	public void setupBall() {

		// Instantiation of ball object
			ballObj = new Ball();

		// Setup ball variables
			setupBallVariables();
			
		// Add to Pong
			this.add(ballObj);
			this.repaint();
				
	} // setupBall() method

// Setup Ball Variables method; called by setupBall() and checkTopBottomBound()
	public void setupBallVariables() {
		
		ballObj.setColor(Color.LIGHT_GRAY);
		ballObj.setSize(
				( this.getFieldWidth() / 30 ), 			
				( this.getFieldWidth() / 30 ) // Want round ball!
				);
		ballObj.setX( this.getFieldWidth() / 2 - ( ballObj.getWidth() / 2) ); // Middle of screen
		ballObj.setY( this.getFieldHeight() / 2 - ( ballObj.getHeight() / 2) 
				- ( this.getFieldHeight() / 4 - ( ballObj.getHeight() / 4) )
				); // Three quarters of screen away from paddle
		ballObj.dX = 0; // Straight down
		ballObj.dY = ( this.getFieldHeight() / 70 ); // Goes down to paddle1
		ballObj.speedUpCounter = 0;
		
	}
	
// Setup Paddle1 method; called by setup()
	public void setupPaddle1() {

		// Instantiation of ball object
			paddle1 = new Paddle();

		// Setup paddle2 variables
			setupPaddle1Variables();
			
		// Add to Pong
			this.add(paddle1);
			this.repaint();
		
	} // setupPaddle1() method

// Setup Paddle1 Variables method; called by setupPaddle1()
	public void setupPaddle1Variables() {
		
		paddle1.setColor(Color.WHITE);
		paddle1.setSize(
				this.getFieldWidth() / 5,
				this.getFieldHeight() / 30
				);
		paddle1.setX( ( this.getFieldWidth() / 2 ) - ( paddle1.getWidth() / 2 ) ); // Centred
		setupPaddle1setY(); // Call setY method
		paddle1.dX = this.getFieldWidth() / 40; // Pretty reasonable speed
		paddle1.score = 0;
		// paddle1.score = 1336; // For easy testing
		paddle1.moveL = false;
		paddle1.moveR = false;
		
	} // setupPaddle1Variables() method
	
// Setup Paddle1 setY method; called by setupPaddle1() and fixPaddle1()
	public void setupPaddle1setY() {
		
		// Paddle thickness above bottom
		// Needs triple subtraction since reference is from top-left corner
		paddle1.setY ( this.getFieldHeight() - 3*paddle1.getHeight() );
		
	} // setupPaddle1setY() method
	
// Setup Paddle2 method; called by setup()
	public void setupPaddle2() {
			
		// Instantiation of ball object
			paddle2 = new Paddle();

		// Setup paddle2 variables
			setupPaddle2Variables();
		
		// Add to Pong
			this.add(paddle2);
			this.repaint();
		
	} // setupPaddle2() method

// Setup Paddle1 Variables method; called setupPaddle2()
	public void setupPaddle2Variables() {
		
		paddle2.setColor(Color.WHITE);
		paddle2.setSize (
				this.getFieldWidth() / 5,
				this.getFieldHeight() / 30
				);
		paddle2.setX( ( this.getFieldWidth() / 2 ) - ( paddle2.getWidth() / 2 ) ); // Centred
		setupPaddle2setY(); // Call setY method
		paddle2.dX = this.getFieldWidth() / 40; // Pretty reasonable speed
		paddle2.score = 0;
		// paddle2.score = 1336; // For easy testing
		paddle2.moveL = false;
		paddle2.moveR = false;
		
	}
	
// Setup Paddle2 setY method; called by setupPaddle2() only
	public void setupPaddle2setY() {
		
		// Paddle thickness below top
		paddle2.setY( 2*paddle2.getHeight() );
		
	} // setupPaddle2setY() method
	
// Setup Paddle1 Score method; called by setup()
	public void setupPaddle1Score() {
		
		// Instantiation of Paddle 1 score object
			paddle1Score = new JLabel("", JLabel.LEFT);
			
		// Set text colour
			paddle1Score.setForeground(Color.WHITE);
			
		// Set text size
			paddle1Score.setFont(scoreFont);
			
		// Set label location
			setupPaddle1ScoreLocation();
			
		// Add to Pong
			this.add(paddle1Score);
			this.repaint();
		
	} // setupPaddle1Score() method

// Setup Paddle1 Score Location method; called by setupPaddle1Score()
	public void setupPaddle1ScoreLocation() {
		
		paddle1Score.setBounds(
			this.getFieldWidth() / 60,
			this.getFieldHeight() - paddle1Score.getFont().getSize() - this.getFieldHeight() / 60,
			this.getFieldWidth(),
			paddle1Score.getFont().getSize()
			);
		
	} // setupPaddle1ScoreLocation() method

// Setup Paddle2 Score method; called by setup()
	public void setupPaddle2Score() {
	
		// Instantiation of Paddle 2 score object
			paddle2Score = new JLabel("", JLabel.LEFT);
			
		// Set text colour
			paddle2Score.setForeground(Color.WHITE);
			
		// Set text size
			paddle2Score.setFont(scoreFont);
			
		// Set label location
			setupPaddle2ScoreLocation();
			
		// Add to Pong
			this.add(paddle2Score);
			this.repaint();
			
	} // setupPaddle2Score() method

// Setup Paddle2 Score Location method; called by setupPaddle2Score() only
	public void setupPaddle2ScoreLocation() {
		
		paddle2Score.setBounds(
			this.getFieldWidth() / 60,
			this.getFieldHeight() / 60,
			this.getFieldWidth(),
			paddle2Score.getFont().getSize()
			);
		
	} // setupPaddle2ScoreLocation() method

// Setup Big Info Label; called by setup ()
	public void setupBigInfoLabel() {
		
		// Instantiation of info label object
			bigInfoLabel = new JLabel("", JLabel.CENTER);
			
		// Set text colour
			bigInfoLabel.setForeground(Color.WHITE);
			
		// Set text size
			bigInfoLabel.setFont(bigFont);
			
		// Set label location
			setupBigInfoLabelLocation();
			
		// Set label text
			bigInfoLabel.setText("Welcome to Pong");
			
		// Add to Pong
			this.add(bigInfoLabel);
			this.repaint();
			
	} // setupBigInfoLabel() method

// Setup Big Info Label Location method; called by setupInfoLabel()
	public void setupBigInfoLabelLocation() {
		
		bigInfoLabel.setBounds(
				0,
				(this.getFieldHeight() / 2) - ( this.getFieldHeight() / 10 ),
				this.getFieldWidth(),
				2*bigInfoLabel.getFont().getSize()
				);
		
	} // setupBigInfoLabelLocation() method

// Setup Small Info Label 1; called by setup ()
	public void setupSmallInfoLabel1() {
		
		// Instantiation of info label object
			smallInfoLabel1 = new JLabel("", JLabel.CENTER);
			
		// Set text colour
			smallInfoLabel1.setForeground(Color.WHITE);
			
		// Set text size
			smallInfoLabel1.setFont(smallFont);
			
		// Set label location
			setupSmallInfoLabel1Location();
			
		// Set label text
			smallInfoLabel1.setText("Press R to start game and P any time to pause.");
			
		// Add to Pong
			this.add(smallInfoLabel1);
			this.repaint();
				
	} // setupSmallInfoLabel1() method
	
// Setup Small Info Label 1 Location method; called by setupInfoLabel()
	public void setupSmallInfoLabel1Location() {
		
		smallInfoLabel1.setBounds(
				0,
				( this.getFieldHeight() / 2),
				this.getFieldWidth(),
				2*smallInfoLabel1.getFont().getSize()
				);
		
	} // setupSmallInfoLabel1Location() method
	
	
// Setup Small Info Label 2; called by setup ()
	public void setupSmallInfoLabel2() {
		
		// Instantiation of info label object
			smallInfoLabel2 = new JLabel("", JLabel.CENTER);
			
		// Set text colour
			smallInfoLabel2.setForeground(Color.WHITE);
			
		// Set text size
			smallInfoLabel2.setFont(smallFont);
			
		// Set label location
			setupSmallInfoLabel2Location();
			
		// Set label text
			smallInfoLabel2.setText("First paddle to score 10 points wins!");
			
		// Add to Pong
			this.add(smallInfoLabel2);
			this.repaint();
				
	} // setupSmallInfoLabel2() method
	
// Setup Small Info Label 2 Location method; called by setupInfoLabel()
	public void setupSmallInfoLabel2Location() {
		
		smallInfoLabel2.setBounds(
				0,
				( this.getFieldHeight() / 2) + ( this.getFieldHeight() / 15 ),
				this.getFieldWidth(),
				2*smallInfoLabel2.getFont().getSize()
				);
		
	} // setupSmallInfoLabel2Location() method
	
// Setup method; runs at start of game
	public void setup() {
		
		// Calls all the separate setup methods
			addKeypresses();
			setupScreen();
			setupBall();
			setupPaddle1();
			setupPaddle2();
			setupPaddle1Score();
			setupPaddle2Score();
			setupBigInfoLabel();
			setupSmallInfoLabel1();
			setupSmallInfoLabel2();
			updateScores();

	} // setup() method

////////////////////////////////////////////////////////////////////////////
// Act block
////////////////////////////////////////////////////////////////////////////

// Fix Locations method; called by act()
	public void fixLocations() {
		
		//This is just here in the case that the screen dimensions are
		//changed during gameplay. This basically keeps relocating
		//everything to their correct relative location. 
		 		
		setupPaddle1setY();
		setupPaddle2setY();
		setupPaddle1ScoreLocation();
		setupPaddle2ScoreLocation();
		setupBigInfoLabelLocation();
		setupSmallInfoLabel1Location();
		setupSmallInfoLabel2Location();
		
	} // fixLocations() method

// Act Paddle1 method; called by act()
	public void actPaddle1() {
		// Paddle Movements
			// If a key is pressed and the corresponding paddle is
			// not at its limits, the paddle is allowed to move in the desired direction.
					
			if (
					( this.XKeyPressed() )
					&& ( paddle1.getX() <
							( this.getFieldWidth() - paddle1.getWidth() - paddle1.getHeight() )
							) // Right bound of screen
				)
			{
				paddle1.moveR = true;
			} else {
				paddle1.moveR = false;
			}
			
			if (
					( this.ZKeyPressed() )
					&& ( paddle1.getX() > 0 + paddle1.getHeight() ) // Left bound of screen
				)
			{
				paddle1.moveL = true;
			} else {
				paddle1.moveL = false;
			}
			
	} // actPaddle1() method

// Act Paddle1 method; called by act()
	public void actPaddle2() {
		
		// Paddle Movements
			// If a key is pressed and the corresponding paddle is
			// not at its limits, the paddle is allowed to move in the desired direction.
			
			if (
					( this.MKeyPressed() )
					&& ( paddle2.getX() <
							( this.getFieldWidth() - paddle2.getWidth() - paddle2.getHeight() ) 
							) // Right bound of screen
				)
			{
				paddle2.moveR = true;
			} else {
				paddle2.moveR = false;
			}
			
			if (
					( this.NKeyPressed() )
					&& ( paddle2.getX() > 0  + paddle2.getHeight() ) // Left bound of screen
				)
			{
				paddle2.moveL = true;
			} else {
				paddle2.moveL = false;
			}
			
	} // actPaddle2() method

// Check Side Boundary method; called by act()
	public void checkSideBound() {
		
		// If the ball's reaches either the left or right sides of
		// the screen, reverse the ball's X movement.
		if (
				( ballObj.getX() >= ( this.getFieldWidth() - ballObj.getWidth() ) )
				|| ( ballObj.getX() <= 0 )
			)
		{
			ballObj.dX = -ballObj.dX;
		}
	
	} // checkSideBound() method

// Check Top/Bottom Boundary method; called by act()
	public void checkTopBottomBound() {
		
		// If the ball reaches the bottom of the screen (ie past paddle1)
			if ( ballObj.getY() >= ( this.getFieldHeight() - ballObj.getHeight() ) ) {
				// Update score
					paddle2.score++;
					updateScores();
					checkWin();
				// Resetup ball
					setupBallVariables();
					ballObj.setX( ( paddle1.getX() + ( paddle1.getWidth() / 2 ) ) ); // Centre to paddle
			}
		
		// If the ball reaches the top of the screen (ie past paddle2)
			if ( ballObj.getY() <= 0 ) {
				// Update score
					paddle1.score++;
					updateScores();
					checkWin();
				// Resetup ball
					setupBallVariables();
					ballObj.setX( ( paddle2.getX() + ( paddle2.getWidth() / 2 ) ) ); // Centre to paddle 
					ballObj.dY = -ballObj.dY; // Reverse Y direction since giving to paddle2
					ballObj.setY( this.getFieldHeight() - ballObj.getY() ); // Reverse relative start location
			}
		
	} // checkTopBottomBound() method

// Check Paddle Collide method; called by act()
	public void checkPaddleCollision(Paddle paddleObj) {
		
		/* 
		 * Note that if the ball is contacted by the side of the paddle, the
		 * ball movement gets buggy; however, this is a bug with the .collides()
		 * method of the GameObject class. It is also a familiar bug that is
		 * usually found with all simple Pong clones, thus the "bug" is left
		 * "unfixed" for simplicity and historical reasons. ; )
		 */
		
		// If collision with a paddle is detected...
		if ( ballObj.collides(paddleObj) ) {
			
			// Reverse the ball's Y movements
				ballObj.dY = -ballObj.dY;
			
			// Change angle depending on collision angle
				// Find x co-ordinate difference
					int collisionPoint = (
						( ballObj.getX() + ( ballObj.getWidth() / 2 ) ) // Middle of ball 
						- ( paddleObj.getX() + ( paddleObj.getWidth() / 2 ) ) // Middle of paddle
					);
					
				// Alter ball's X movements
					// If contacted at paddle's left quarter section
						if ( collisionPoint < -( paddleObj.getWidth() / 4) ) {
							ballObj.dX--;
						} else
					// If contacted at paddle's right quarter section
						if (collisionPoint > ( paddleObj.getWidth() / 4) ) {
							ballObj.dX++;
						} else
					// If contacted at paddle's middle section
						{
							// Do nothing
						}
			
			// Increase speedUpCounter
			ballObj.speedUpCounter++;
		}
		
	} // checkPaddleCollision(Paddle paddleObj) method

// Act Paddle Repel method; called by act() when in Magnetic Mode
	public void actPaddleRepel(Paddle paddleObj) {
		
		// Repel ball based on distance from paddle
		
		int ballXDiffFromPaddle = paddleObj.getX() - ballObj.getX();
		int ballYDiffFromPaddle = paddleObj.getY() - ballObj.getY();
		
		// X repulsion
		if ( ballXDiffFromPaddle > ( this.getFieldHeight() / 3 ) ) {
		ballObj.dX =
				( ( ballObj.dX + ( ballXDiffFromPaddle / 130 ) )
				* ( ballYDiffFromPaddle / this.getFieldHeight() )
				);
		}
		// Y repulsion
		ballObj.dY =
				( ( ballObj.dY + ( ballYDiffFromPaddle / 130 ) )
				//* ( ballXDiffFromPaddle / this.getFieldWidth() )
				);
		
	} // actPaddleRepel(Paddle paddleObj) method

// Update Scores method; called by checkTopBottomBound()
	public void updateScores() {
		
		paddle1Score.setText("Paddle 1 Score: " + paddle1.score);
		paddle2Score.setText("Paddle 2 Score: " + paddle2.score);
		
	} // updateScores() method
	
// Check Win method; called by act()
	public void checkWin() {
		
		// First paddle to accumulate 10 points wins
		// Game is then stopped and awaits the "R" keypress
		
		
			if ( ( paddle1.score == 1337 ) || ( paddle2.score == 1337 ) ) {
				
				// Easter Egg!
				// I wonder how to access this one... ; )
				gameStopped = true;
				bigInfoLabel.setText("Thou art 1337!");
				smallInfoLabel1.setText("Press R to restart game.");
				this.stopGame(); // Stop the game
				
			} else if ( ( paddle1.score >= 10 ) && ( paddle2.score >= 10 ) ) {
				
				// Easter Egg!
				// Accessible if you keep pressing P instead of R ; )
				gameStopped = true;
				bigInfoLabel.setText("Paddle... Wait, what???");
				smallInfoLabel1.setText("Press R to restart game.");
				this.stopGame(); // Stop the game
				
			} else if  ( ( paddle1.score >= 10 ) && ( paddle2.score < 10 ) ) {
				
				//this.p1Wins(); // The pop-up looks ugly >_<
				gameStopped = true;
				bigInfoLabel.setText("Paddle 1 Wins");
				smallInfoLabel1.setText("Press R to restart game.");
				this.stopGame(); // Stop the game
				
			} else if ( ( paddle2.score >= 10 ) && ( paddle1.score < 10 ) ) {
				
				//this.p2Wins(); // The pop-up looks ugly >_<
				gameStopped = true;
				bigInfoLabel.setText("Paddle 2 Wins");
				smallInfoLabel1.setText("Press R to restart game.");
				this.stopGame(); // Stop the game

			} else {
				// Nothing
			}
			
		
	} // checkWin() method

// Reset Game method; called by the keylistener
	public void resetGame() {
		
		setupPaddle1Variables();
		setupPaddle2Variables();
		setupBallVariables();
		updateScores();
		
	} // resetGame() method
	
// Act method; automatically loops every few milliseconds
// FIXME - Testing magneticMode
	public void act() {
		
		if (!gameStopped) {
			// Fix pbject locations
				fixLocations();
			
			// Paddle 1 movements
				actPaddle1();
			
			// Paddle 2 movements
				actPaddle2();
					
			// Check Ball side boundary
				checkSideBound();
				
			// Check Ball top/bottom boundary
				checkTopBottomBound();
			
			// Check Paddle collision
				checkPaddleCollision(paddle1);
				checkPaddleCollision(paddle2);
			
			// FIXME - Testing
				//if (magneticMode) {
					actPaddleRepel(paddle1);
					actPaddleRepel(paddle2);
				//}
		}
		
	} // act() method
	

////////////////////////////////////////////////////////////////////////////
// Main block
////////////////////////////////////////////////////////////////////////////

// Main method; what makes it all happen...
// Note: modified to initiate with game paused
	public static void main(String[] args) {
		
		// Creates new Pong game
			Pong p = new Pong();
			p.setVisible(true);
			p.initComponents();
		
		// Initiates with game paused
		// Press R or P to start game
			p.stopGame();
			p.gameStopped = true;
		
	} // main(String[] args) method
	
} // End of Pong class