/**
 * PongPod.<br>
 * Play a pong with a wheel.<br><br>
 *
 * Copyright (C) 2005 Kenta Cho. Some rights reserved.
 * 
 * @p5 description
 * - 画面下の灰色の丸（ホイール）にカーソルを合わせてください
 * - クリックするとゲームが始まります
 * - ホイールに沿って丸くカーソルを動かすとラケットが動きます
 * - ラケットでボールをはじき返してください
 * - ボールが中央の黒丸に入るとミスです
 * - ミス3つでゲームオーバー
 */
//import processing.core.*;

//public class PongPod extends PApplet {
public class PongPod extends BApplet {
	Field field;
	Wheel wheel;
	Racket racket;
	Ball[] ball = new Ball[25];
	//PFont font;
	BFont font;
	int TITLE = 0;
	int IN_GAME = 1;
	int GAMEOVER = 2;
	int state;
	Title title;
	Gameover gameover;
	int score;
	Se se;

	public void setup() {
		size(340, 550);
		framerate(30);
		cursor(ARROW);
		noStroke();
		//
		ellipseMode(CENTER_DIAMETER);
		//font = loadFont("Verdana-48.vlw");
		font = loadFont("Futura-BoldCon.vlw.gz");
		textFont(font);
		field = new Field();
		field.init();
		wheel = new Wheel();
		wheel.init();
		racket = new Racket();
		racket.init();
		for (int i = 0; i < ball.length; i++)
			ball[i] = new Ball();
		title = new Title();
		gameover = new Gameover();
		se = new Se();
		se.load();
		startTitle();
	}

	void startTitle() {
		state = TITLE;
		title.start();
	}

	void startInGame() {
		state = IN_GAME;
		score = 0;
		se.init();
		initBalls();
		field.start();
		racket.start();
	}

	void startGameover() {
		state = GAMEOVER;
		gameover.start();
	}

	//public void draw() {
	public void loop() {
		background(250, 250, 250);
		smooth();
		switch (state) {
			case 0 : // TITLE
				field.draw();
				wheel.draw();
				wheel.move();
				title.draw();
				break;
			case 1 : // IN_GAME
				field.draw();
				wheel.draw();
				field.move();
				wheel.move();
				moveBalls();
				racket.move();
				racket.draw();
				drawBalls();
				drawStatus();
				se.playAllSes();
				break;
			case 2 : // GAMEOVER
				field.draw();
				wheel.draw();
				wheel.move();
				gameover.draw();
				drawStatus();
				break;
		}
	}

	void drawStatus() {
		textSize(26);
		//textAlign(RIGHT);
		textMode(ALIGN_RIGHT);
		fill(255, 255, 255);
		text(score, width - 14, 35);
	}

	float adjustDeg(float d) {
		if (d > PI)
			return d - PI * 2;
		else if (d < -PI)
			return d + PI * 2;
		else
			return d;
	}

	/**
	 * 2D vector.
	 */
	class Vector {
		float x, y;
	}

	/**
	 * My racket.
	 */
	class Racket {
		float SENSITIVITY = 0.7f;
		float radius;
		float deg;
		Vector[] pos;
		Vector p1, p2;
		float rWidth, rHeight;

		void init() {
			radius = field.bwRadius * 1.5f;
			pos = new Vector[2];
			for (int i = 0; i < pos.length; i++)
				pos[i] = new Vector();
			p1 = new Vector();
			p2 = new Vector();
			rWidth = 70;
			rHeight = 7;
		}

		void start() {
			deg = 0;
		}

		void move() {
			deg -= wheel.angVel * SENSITIVITY;
			deg = adjustDeg(deg);
			float d = deg;
			for (int i = 0; i < pos.length; i++) {
				pos[i].x = field.centerPos.x - sin(d) * radius;
				pos[i].y = field.centerPos.y + cos(d) * radius;
				p1.x = pos[i].x - sin(d + HALF_PI) * rWidth / 2;
				p1.y = pos[i].y + cos(d + HALF_PI) * rWidth / 2;
				p2.x = pos[i].x - sin(d - HALF_PI) * rWidth / 2;
				p2.y = pos[i].y + cos(d - HALF_PI) * rWidth / 2;
				for (int j = 0; j < ball.length; j++)
					if (ball[j].isExist)
						if (ball[j].checkHit(p1, p2))
							ball[j].reflectWithRacket(d + HALF_PI);
				d += PI;
			}
		}

		void draw() {
			fill(0, 0, 0);
			//rectMode(CENTER);
			rectMode(CENTER_DIAMETER);
			float d = deg;
			for (int i = 0; i < pos.length; i++) {
				//pushMatrix();
				push();
				translate(pos[i].x + field.pos.x, pos[i].y + field.pos.y);
				rotate(d);
				rect(0, 0, rWidth, rHeight);
				//popMatrix();
				pop();
				d += PI;
			}
			rectMode(CORNER);
		}
	}

	/**
	 * Balls.
	 */
	class Ball {
		Vector pos = new Vector();
		Vector ppos = new Vector();
		float deg;
		float speed;
		float size, trgSize;
		boolean isExist = false;
		int reflectCnt;
		int idx;

		void set(float x, float y, float d, float sp, float sz) {
			pos.x = x;
			pos.y = y;
			deg = adjustDeg(d);
			speed = sp;
			trgSize = sz;
			size = 0.1f;
			reflectCnt = 0;
			isExist = true;
		}

		void move() {
			ppos.x = pos.x;
			ppos.y = pos.y;
			pos.x -= sin(deg) * speed;
			pos.y += cos(deg) * speed;
			size += (trgSize - size) * 0.1f;
			if (pos.x < size || pos.x >= field.size.x - size)
				reflect(0);
			if (pos.y < size || pos.y >= field.size.y - size)
				reflect(HALF_PI);
			if (reflectCnt > 0)
				reflectCnt--;
			if (dist(pos.x, pos.y, field.centerPos.x, field.centerPos.y)
				< field.bwRadius * 0.75f + size) {
				field.incOutCnt();
				isExist = false;
				return;
			}
		}

		void reflect(float d) {
			float aod1 = abs(adjustDeg(deg - d));
			float aod2 = abs(adjustDeg(deg - (d + PI)));
			float od, aod;
			if (aod1 < aod2) {
				od = adjustDeg(deg - d);
				aod = aod1;
			} else {
				od = adjustDeg(deg - (d + PI));
				aod = aod2;
			}
			if (od < 0)
				deg += aod * 2;
			else
				deg -= aod * 2;
			pos.x = ppos.x;
			pos.y = ppos.y;
			pos.x -= sin(deg) * speed;
			pos.y += cos(deg) * speed;
			se.playSe(idx % 7);
		}

		void reflectWithRacket(float d) {
			if (reflectCnt <= 0) {
				reflect(d);
				se.playSe(7);
				score++;
			}
			reflectCnt = 3;
		}

		boolean checkHit(Vector p, Vector pp) {
			float bmvx, bmvy, inaa;
			bmvx = pp.x;
			bmvy = pp.y;
			bmvx -= p.x;
			bmvy -= p.y;
			inaa = bmvx * bmvx + bmvy * bmvy;
			if (inaa > 0.00001) {
				float sofsx, sofsy, inab, hd;
				sofsx = pos.x;
				sofsy = pos.y;
				sofsx -= p.x;
				sofsy -= p.y;
				inab = bmvx * sofsx + bmvy * sofsy;
				if (inab >= 0 && inab <= inaa) {
					hd = sofsx * sofsx + sofsy * sofsy - inab * inab / inaa;
					if (hd >= 0 && hd <= size * 0.7f + 7.0f) {
						return true;
					}
				}
			}
			return false;
		}

		void draw() {
			fill(255, 255, 255);
			ellipse(
				field.pos.x + pos.x,
				field.pos.y + pos.y,
				size * 2,
				size * 2);
		}
	}

	void initBalls() {
		for (int i = 0; i < ball.length; i++) {
			ball[i].idx = i;
			ball[i].isExist = false;
		}
		ballIdx = 0;
	}
	void moveBalls() {
		for (int i = 0; i < ball.length; i++)
			if (ball[i].isExist)
				ball[i].move();
	}
	void drawBalls() {
		for (int i = 0; i < ball.length; i++)
			if (ball[i].isExist)
				ball[i].draw();
	}
	int ballIdx = 0;
	Ball getBallInstance() {
		for (int i = 0; i < ball.length; i++) {
			if (!ball[ballIdx].isExist)
				return ball[ballIdx];
			ballIdx++;
			if (ballIdx >= ball.length)
				ballIdx = 0;
		}
		return null;
	}

	/**
	 * Game field (main screen).
	 */
	class Field {
		Vector pos = new Vector();
		Vector size = new Vector();
		float bwRadius;
		Vector centerPos = new Vector();
		float brightness;
		float bwSize;
		int cnt;
		int outCnt;

		void init() {
			size.x = 320;
			size.y = 240;
			pos.x = pos.y = (width - size.x) / 2;
			bwRadius = size.y * 0.1f;
			centerPos.x = size.x / 2;
			centerPos.y = size.y / 2;
			brightness = 0;
			bwSize = 1.0f;
		}

		void start() {
			bwSize = 1;
			cnt = 0;
			outCnt = 0;
			for (int i = 0; i < 3; i++)
				addBall();
		}

		void addBall() {
			Ball b = getBallInstance();
			if (b == null)
				return;
			float x, y;
			if (random(2) >= 1) {
				x = random(field.size.x - 40) + 20;
				if (random(2) >= 1)
					y = 20;
				else
					y = field.size.y - 20;
			} else {
				y = random(field.size.y - 40) + 20;
				if (random(2) >= 1)
					x = 20;
				else
					x = field.size.x - 20;
			}
			b.set(x, y, random(TWO_PI), 1.5f + random(1.0f), 4 + random(3.0f));
		}

		void incOutCnt() {
			se.playSe(8);
			outCnt++;
			if (outCnt >= 3)
				startGameover();
		}

		void move() {
			cnt++;
			if (cnt % 300 == 0)
				addBall();
		}

		void draw() {
			fill(80 * brightness, 200 * brightness, 80 * brightness);
			rect(pos.x, pos.y, size.x, size.y);
			fill(0, 0, 0);
			ellipse(
				pos.x + size.x / 2,
				pos.y + size.y / 2,
				bwRadius * 2 * bwSize,
				bwRadius * 2 * bwSize);
			if (state == IN_GAME) {
				fill(255, 255, 255);
				for (int i = 0; i < outCnt; i++)
					ellipse(
						pos.x + size.x / 2 - 10 + i * 10,
						pos.y + size.y / 2,
						10,
						10);
			}
		}
	}

	/**
	 * Wheel that controls a racket.
	 */
	class Wheel {
		Vector centerPos;
		float radFrom, radTo;
		boolean cursorOn;
		float angVel;
		float pdeg;

		void init() {
			centerPos = new Vector();
			centerPos.x = width / 2;
			centerPos.y = height * 0.75f;
			radFrom = width * 0.35f / 2;
			radTo = width * 0.7f / 2;
			cursorOn = false;
			angVel = 0;
			pdeg = 0;
		}

		void move() {
			float d = dist(mouseX, mouseY, centerPos.x, centerPos.y);
			if (d >= radFrom * 0.05f && d <= radTo * 1.4f) {
				if (!cursorOn) {
					cursorOn = true;
					cursor(HAND);
					pdeg = atan2(mouseX - centerPos.x, mouseY - centerPos.y);
				}
			} else {
				if (cursorOn) {
					cursorOn = false;
					cursor(ARROW);
				}
			}
			if (cursorOn) {
				float deg = atan2(mouseX - centerPos.x, mouseY - centerPos.y);
				angVel = adjustDeg(deg - pdeg);
				pdeg = deg;
			} else {
				angVel = 0;
			}
		}

		void draw() {
			fill(210, 210, 210);
			ellipse(centerPos.x, centerPos.y, radTo * 2, radTo * 2);
			fill(255, 255, 255);
			ellipse(centerPos.x, centerPos.y, radFrom * 2, radFrom * 2);
		}
	}

	/**
	 * Title screen.
	 */
	class Title {
		int cnt;

		void start() {
			cnt = 0;
			field.brightness = 0;
			field.bwSize = 0;
		}

		void draw() {
			cnt++;
			if (cnt > 30 && mousePressed && wheel.cursorOn) {
				startInGame();
				return;
			}
			if (cnt <= 30)
				field.brightness = cnt * (1.0f / 30);
			if (cnt > 45) {
				textSize(42);
				//textAlign(CENTER);
				textMode(ALIGN_CENTER);
				fill(255, 255, 255);
				text(
					"PongPod",
					field.pos.x + field.size.x / 2,
					field.pos.y + field.size.y / 2);
			}
		}
	}

	/**
	 * Gameover screen.
	 */
	class Gameover {
		int cnt;

		void start() {
			cnt = 0;
		}

		void draw() {
			cnt++;
			if (cnt > 300 || (cnt > 30 && mousePressed && wheel.cursorOn)) {
				startTitle();
				return;
			}
			if (cnt <= 30) {
				field.bwSize = 1.0f + cnt * 0.1f;
				field.brightness = 1.0f - cnt * (1.0f / 30);
			}
			if (cnt > 45) {
				textSize(32);
				//textAlign(CENTER);
				textMode(ALIGN_CENTER);
				fill(255, 255, 255);
				text(
					"GameOver",
					field.pos.x + field.size.x / 2,
					field.pos.y + field.size.y / 2);
			}
		}
	}

	/**
	 * Sound effect.
	 */
	class Se {
		int SE_NUM = 9;
		//PSound[] se;
		BSound[] se;
		boolean[] play;
		int cnt;
		int interval = 7;

		void load() {
			//se = new PSound[SE_NUM];
			se = new BSound[SE_NUM];
			play = new boolean[SE_NUM];
			for (int i = 0; i < SE_NUM; i++)
				se[i] = loadSound("se0" + (i + 1) + ".wav");
		}

		void init() {
			for (int i = 0; i < SE_NUM; i++)
				play[i] = false;
			cnt = 0;
		}

		void playSe(int n) {
			play[n] = true;
		}

		void playAllSes() {
			cnt++;
			int ci = cnt % interval;
			if (ci == interval - 1) {
				for (int i = 0; i < SE_NUM; i++) {
					if (play[i])
						//se[i].stop();
						stop(se[i]);
				}
			}
			if (ci == 0) {
				for (int i = 0; i < SE_NUM; i++) {
					if (play[i]) {
						//se[i].play();
						play(se[i]);
						play[i] = false;
					}
				}
			}
		}
	}
}
