/*
 * bounce.java
 *
 * @author Bob Pendleton
 * @version 0.0.0
 */

import java.lang.*;
import java.math.*;
import java.util.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.Font.*;
import java.applet.Applet;

interface GamePage
{
    public void paint(Graphics g);
    public void setGame(Game a);
    public void mouseClicked(MouseEvent e);
    public void mouseEntered(MouseEvent e); 
    public void mouseExited(MouseEvent e);
    public void mousePressed(MouseEvent e); 
    public void mouseReleased(MouseEvent e); 
    public void mouseDragged(MouseEvent e);
    public void mouseMoved(MouseEvent e); 
    public void keyPressed(KeyEvent e);
    public void keyReleased(KeyEvent e); 
    public void keyTyped(KeyEvent e);
}

interface GameDriver
{
    public int getWidth();
    public int getHeight();
    public double getCurrentTime();
    public double getFPS();
    public long getFrameCount();
    public void setFPS(int fps);
    public void setGamePage(GamePage anim);
    public void setSize(int w, int h);
    public void setCursor(Cursor c);
    public FontMetrics getFontMetrics(Font f);
    public void start();
    public void stop();
}

class HowHard
{
    public int level = -1;
    public int minSpeed = -1;
    public int maxSpeed = -1;
    public int speedRange = -1;
    public int percentPost = -1;
    public int minPercentPost = -1;
    public int maxPercentPost = -1;
    public int incrPercentPost = -1;
    public int maxScore = -1;
    public int numBalls = -1;
    public int maxBalls = -1;
    public int gravity = -1;

    HowHard()
    {
    }
}

class winLose implements GamePage
{
    Game driver = null;
    Font bigFont = null;
    Font smallFont = null;
    FontMetrics bigFm = null;
    FontMetrics smallFm = null;
    Graphics oldg = null;
    int width = -1;
    int height = -1;
    boolean flip = true;
    boolean started = false;
    boolean wonLost = false;
    HowHard info = null;

    winLose(HowHard info, boolean wonLost)
    {
        this.info = info;
        this.wonLost = wonLost;

        if (wonLost)
        {
            info.level++;
            info.percentPost += info.incrPercentPost;

            if (info.percentPost > info.maxPercentPost)
            {
                info.minSpeed = Math.min(info.minSpeed + 1, info.maxSpeed);
                info.percentPost = info.minPercentPost;
            }

            if (0 == (info.level % 5))
            {
                info.numBalls = Math.min(info.numBalls + 1, info.maxBalls);
            }
            /*
            System.out.println("level=" + info.level +
                               " range=" + info.speedRange +
                               " speed=" + info.minSpeed +
                               " post=" + info.percentPost);
            */
        }
    }

    public void setGame(Game driver)
    {
        this.driver = driver;

        driver.setFPS(4);

        width = driver.getWidth();
        height = driver.getHeight();

        bigFont = new Font("SansSerif", Font.BOLD, 40);
        bigFm = driver.getFontMetrics(bigFont);

        smallFont = new Font("SansSerif", Font.BOLD, 20);
        smallFm = driver.getFontMetrics(smallFont);
    }

    public void paint(Graphics g)
    {
        int p1 = -1;
        int p2 = -1;
        String label = "";

        if (driver == null)
        {
            return;
        }

        if (g != null && g != oldg)
        {
            oldg = g;
        }

        g.setColor(Color.black);
        g.fillRect(0, 0, width, height);

        if (flip)
        {
            g.setColor(Color.white);
        }
        else
        {
            g.setColor(Color.red);         
        }
        flip = !flip;

        if (wonLost)
        {
            label = "You Won!";
        }
        else
        {
            label = "You Lost!";
        }

        p1 = (width / 2) - (bigFm.stringWidth(label) / 2);
        p2 = (height / 2);

        g.setFont(bigFont);
        g.drawString(label, p1, p2);

        label = "Click to start level " + info.level;

        p1 = (width / 2) - (smallFm.stringWidth(label) / 2);
        p2 = (height / 2) + smallFm.getHeight() + 2;

        g.setFont(smallFont);
        g.drawString(label, p1, p2);

    }

    // MouseListener

    public void mouseClicked(MouseEvent e) 
    {
        if (!started)
        {
            driver.setGamePage(new bouncer(info));
            started = true;
        }
    }

    public void mouseEntered(MouseEvent e) 
    {
    }

    public void mouseExited(MouseEvent e) 
    {
    }

    public void mousePressed(MouseEvent e) 
    {
        if (!started)
        {
            driver.setGamePage(new bouncer(info));
            started = true;
        }
    }

    public void mouseReleased(MouseEvent e) 
    {
    }
           
    // MouseMotionListener

    public void mouseDragged(MouseEvent e) 
    {
    }

    public void mouseMoved(MouseEvent e) 
    {
    }
           
    // KeyListener

    public void keyPressed(KeyEvent e) 
    {
        if (!started)
        {
            driver.setGamePage(new bouncer(info));
            started = true;
        }
    }

    public void keyReleased(KeyEvent e) 
    {
    }

    public void keyTyped(KeyEvent e) 
    {
    }
}

class startPage implements GamePage
{
    Game driver = null;
    Font tf = null;
    FontMetrics tfm = null;
    Font font = null;
    FontMetrics fm = null;
    Graphics oldg = null;
    int width = -1;
    int height = -1;
    boolean flip = true;
    boolean started = false;
    HowHard info = null;

    startPage(HowHard info)
    {
        //System.out.println("startPage");
        this.info = info;
    }

    public void setGame(Game driver)
    {
        //System.out.println("startPage::setGame");
        this.driver = driver;

        driver.setFPS(4);

        width = driver.getWidth();
        height = driver.getHeight();

        font = new Font("SansSerif", Font.BOLD, 40);
        fm = driver.getFontMetrics(font);

        tf = new Font("SansSerif", Font.BOLD, 10);
        tfm = driver.getFontMetrics(tf);
    }

    public void paint(Graphics g)
    {
        //System.out.println("startPage::paint");
        int p1 = -1;
        int p2 = -1;

        if (driver == null)
        {
            return;
        }

        if (g != null && g != oldg)
        {
            oldg = g;
        }

        g.setFont(font);
        g.setColor(Color.black);
        g.fillRect(0, 0, width, height);

        p1 = (width / 2) - (fm.stringWidth("Click to Start") / 2);
        p2 = (height / 2);

        if (flip)
        {
            g.setColor(Color.white);
        }
        else
        {
            g.setColor(Color.red);         
        }
        flip = !flip;

        g.drawString("Click to Start", p1, p2);

        g.setFont(tf);
        g.setColor(Color.white);

        p1 = (width / 2) - 
        (tfm.stringWidth("Copyright 2000 Robert C. Pendleton. All rights reserved") / 2);
        p2 = (height - tfm.getHeight()) - 2;

        g.drawString("Copyright 2000 Robert C. Pendleton. All rights reserved", p1, p2);
    }

    // MouseListener

    public void mouseClicked(MouseEvent e) 
    {
        //System.out.println("startPage::mouseClicked");
        if (!started)
        {
            driver.setGamePage(new bouncer(info));
            started = true;
        }
    }

    public void mouseEntered(MouseEvent e) 
    {
    }

    public void mouseExited(MouseEvent e) 
    {
    }

    public void mousePressed(MouseEvent e) 
    {
        //System.out.println("startPage::mousePressed");
        if (!started)
        {
            driver.setGamePage(new bouncer(info));
            started = true;
        }
    }

    public void mouseReleased(MouseEvent e) 
    {
    }
           
    // MouseMotionListener

    public void mouseDragged(MouseEvent e) 
    {
    }

    public void mouseMoved(MouseEvent e) 
    {
    }
           
    // KeyListener

    public void keyPressed(KeyEvent e) 
    {
        //System.out.println("startPage::keyPressed");
        if (!started)
        {
            driver.setGamePage(new bouncer(info));
            started = true;
        }
    }

    public void keyReleased(KeyEvent e) 
    {
    }

    public void keyTyped(KeyEvent e) 
    {
    }
}

class MyRandom
{
    Random r = null;

    MyRandom()
    {
        r = new Random();
    }

    public int nextInt(int i)
    {
        return (int)(r.nextFloat() * i);
    }
}

class Ball
{
    public int x = -1;
    public int y = -1;
    public int dx = -1;
    public int dy = -1;
    public int minSpeed = -1;
    public int speedRange = -1;
    public int gravity = -1;
    public int g = 0;
    public int ballSize = -1;
    public int halfSize = -1;
    public Color color = null;
    public MyRandom rand = null;

    Ball(int size, 
         int minSpeed, 
         int speedRange, 
         int gravity,
         Color color, 
         MyRandom rand)
    {
        ballSize = size;
        halfSize = size / 2;
        this.minSpeed = minSpeed;
        this.speedRange = speedRange;
        this.gravity = gravity;
        g = gravity;
        this.color = color;
        this.rand = rand;
    }

    public void randomPos(int width, int height)
    {
        x = rand.nextInt(width);
        dx = rand.nextInt(1) > 0 ? 2 : -2;

        y = rand.nextInt(height);
        dy = rand.nextInt(1) > 0 ? 2 : -2;
    }

    void update()
    {
        x += dx;
        y += dy;
        if (-1 != gravity)
        {
            g--;
            if (g == 0)
            {
                dy++;
                g = gravity;
            }
        }
    }

    int testFiddle(int i)
    {
        if (i > 0)
        {
            i = 1;
        }
        else
        {
            i = -1;
        }

        return i;
    }

    int fiddle(int i)
    {
        if (i > 0)
        {
            i = minSpeed + rand.nextInt(speedRange);
        }
        else
        {
            i = (-minSpeed) - rand.nextInt(speedRange);
        }

        return i;
    }

    boolean inSide(int left, int right, int top, int bottom)
    {
        return (x > left) && (x < right) && (y > top) && (y < bottom);
    }

    void boxBounce(int width, int height)
    {
        if (x < halfSize || x > (width - halfSize))
        {
            x = Math.max(x, halfSize);
            x = Math.min(x, width - halfSize);

            dx = -dx;
            dx = fiddle(dx);
        }

        if (y < halfSize || y > (height - halfSize))
        {
            y = Math.max(y, halfSize);
            y = Math.min(y, height - halfSize);

            dy = -dy;
            dy = fiddle(dy);
        }

    }

    void pointBounce(int objX, int objY)
    {
        int hitX = x - objX;
        int hitY = y - objY;
        int absX = Math.abs(hitX);
        int absY = Math.abs(hitY);
        int hitDist = (absX * absX) + (absY * absY);

        if (absX > absY) // closest in Y
        {
            if (hitY > 0) // above ball
            {
                if (dy > 0) // going down
                {
                    dy = fiddle(dy);
                }
                else       // going up
                {
                    dy = -dy;
                    dy = fiddle(dy);
                }
            }
            else          // below ball
            {
                if (dy > 0) // going down
                {
                    dy = -dy;
                    dy = fiddle(dy);
                }
                else      // going up
                {
                    dy = fiddle(dy);
                }
            }
        }
        else             // closest in X
        {
            if (hitX > 0) // left of ball
            {
                if (dx > 0) // going right
                {
                    dx = fiddle(dx);
                }
                else      // going left
                {
                    dx = -dx;
                    dx = fiddle(dx);
                }
            }
            else          // right of ball
            {
                if (dx > 0) // going right
                {
                    dx = -dx;
                    dx = fiddle(dx);
                }
                else      // going left
                {
                    dx = fiddle(dx);
                }
            }
        }
    }

    boolean hit(int objX, int objY, int dist)
    {
        int hitX = x - objX;
        int hitY = y - objY;
        int hitDist = (hitX * hitX) + (hitY * hitY);

        return hitDist < (dist * dist);
    }

    void hide(Graphics g)
    {
        g.setColor(Color.black);
        g.fillArc(x - halfSize, y - halfSize, ballSize, ballSize, 0, 360);
    }

    void show(Graphics g)
    {
        g.setColor(color);
        g.fillArc(x - halfSize, y - halfSize, ballSize, ballSize, 0, 360);
    }
}

class bouncer implements GamePage
{
    Game driver = null;
    MyRandom rand = null;
    Graphics oldg = null;
    Font font = null;
    FontMetrics fm = null;
    int ballSize = 19;
    int width = -1;
    int height = -1;
    int cursorX = -1;
    int cursorY = -1;
    int oldYou = 0;
    int oldMe = 0;
    int scoreYou = 0;
    int scoreMe = 0;
    int startYou = -1;
    int startYouScore = -1;
    int startMe = -1;
    int startMeScore = -1;
    int startHeight = -1;
    int scoreWidth = -1;
    int postHeight = 20;
    int postWidth = 4;
    int leftPost = -1;
    int rightPost = -1;
    HowHard info = null;
    int numBalls = -1;
    Ball b[] = null;

    bouncer(HowHard info)
    {
        Color c[] = new Color[5];
        c[0] = Color.red;
        c[1] = Color.green;
        c[2] = Color.yellow;
        c[3] = Color.orange;
        c[4] = Color.magenta;

        int ci = -1;
        this.info = info;
        this.numBalls = info.numBalls;
        rand = new MyRandom();

        b = new Ball[numBalls];
        for (int i = 0; i < numBalls; i++)
        {
            ci = i % 5;
            b[i] = new Ball(ballSize + (5 * i), 
                            info.minSpeed, 
                            info.speedRange,
                            info.gravity,
                            c[ci],
                            rand);
        }
    }

    public void setGame(Game driver)
    {
        this.driver = driver;

        driver.setFPS(60);

        width = driver.getWidth();
        height = driver.getHeight();

        for (int i = 0; i < numBalls; i++)
        {
            b[i].randomPos(width, height);
        }

        font = new Font("SansSerif", Font.BOLD, 20);
        fm = driver.getFontMetrics(font);
    }

    public void paint(Graphics g)
    {
        int p1 = -1;
        int p2 = -1;

        if (driver == null)
        {
            return;
        }

        if (g != null && g != oldg)
        {
            oldg = g;
            
            g.setFont(font);
            g.setColor(Color.black);
            g.fillRect(0, 0, width, height);

            p1 = width / 3;
            p2 = p1 * 2;

            startYou = p1 - (fm.stringWidth("YOU") / 2);
            startMe = p2 - (fm.stringWidth("ME") / 2);

            startYouScore = startYou + (fm.stringWidth("0") / 2);
            startMeScore = startMe + (fm.stringWidth("0") / 2);
            startHeight = fm.getHeight();
            scoreWidth = fm.stringWidth("000");

            p1 = (width - ((width * info.percentPost) / 100)) / 2;
            p2 = width - p1;

            leftPost = p1;
            rightPost = p2;
        }

        for (int i = 0; i < numBalls; i++)
        {
            b[i].hide(g);
        }

        for (int i = 0; i < numBalls; i++)
        {
            b[i].update();
        }

        for (int i = 0; i < numBalls; i++)
        {
            if (b[i].inSide(leftPost, rightPost, (height - (b[i].halfSize + 3)), height))
            {
                scoreMe++;
                if (scoreMe >= info.maxScore)
                {
                    driver.setGamePage(new winLose(info, false));
                    return;
                }
                b[i].randomPos(width, height);
            }
        }
        
        for (int i = 0; i < numBalls; i++)
        {
            b[i].boxBounce(width, height);
        }

        for (int i = 0; i < numBalls; i++)
        {
            if (b[i].hit(cursorX, cursorY, b[i].halfSize))
            {
                b[i].pointBounce(cursorX, cursorY);
                scoreYou++;

                if (scoreYou >= info.maxScore)
                {
                    driver.setGamePage(new winLose(info, true));
                    return;
                }
            }
        }

        for (int i = 0; i < numBalls; i++)
        {
            for (int j = (i + 1); j < numBalls; j++)
            {
                if (b[i].hit(b[j].x, b[j].y, b[i].halfSize + b[j].halfSize))
                {
                    b[i].pointBounce(b[j].x, b[j].y);
                    b[j].pointBounce(b[i].x, b[i].y);
                    break;
                }
            }
        }
        g.setColor(Color.black);
        g.fillRect(startYouScore, startHeight, scoreWidth, startHeight);
        g.fillRect(startMeScore, startHeight, scoreWidth, startHeight);

        g.setColor(Color.white);
        g.drawString("YOU", startYou, startHeight);
        g.drawString("ME", startMe, startHeight);
        g.drawString("" + scoreYou, startYouScore, startHeight * 2);
        g.drawString("" + scoreMe, startMeScore, startHeight * 2);

        g.setColor(Color.white);
        g.fillRect(leftPost, height - postHeight, postWidth, postHeight);
        g.fillRect(rightPost, height - postHeight, postWidth, postHeight);

        for (int i = 0; i < numBalls; i++)
        {
            b[i].show(g);
        }
    }

    // MouseListener

    public void mouseClicked(MouseEvent e) 
    {
    }

    public void mouseEntered(MouseEvent e) 
    {
        cursorX = e.getX();
        cursorY = e.getY();
    }

    public void mouseExited(MouseEvent e) 
    {
        cursorX = e.getX();
        cursorY = e.getY();
    }

    public void mousePressed(MouseEvent e) 
    {
    }

    public void mouseReleased(MouseEvent e) 
    {
    }
           
    // MouseMotionListener

    public void mouseDragged(MouseEvent e) 
    {
        cursorX = e.getX();
        cursorY = e.getY();
    }

    public void mouseMoved(MouseEvent e) 
    {
        cursorX = e.getX();
        cursorY = e.getY();
    }
           
    // KeyListener

    public void keyPressed(KeyEvent e) 
    {
    }

    public void keyReleased(KeyEvent e) 
    {
    }

    public void keyTyped(KeyEvent e) 
    {
    }
}

class Game
    extends Canvas
    implements GameDriver,
               Runnable, 
               ComponentListener,
               MouseListener,
               MouseMotionListener,
               KeyListener
{
    private double startTime = -1;
    private double frameTime = -1;
    private long frameCount = -1;
    private double fps = -1;
    private double delay = -1; // milliseconds per frame
    private Thread GameThread = null;
    private Image backBuf = null;
    private Graphics backG = null;
    private GamePage anim = null;

    Game()
    {
        //System.out.println("Game::init");

        setFPS(10);

        addComponentListener(this);
        addMouseListener(this);
        addMouseMotionListener(this);
        addKeyListener(this);
    }

    public void setFPS(int fps)
    {
        //System.out.println("Game::SetFPS");

        if (fps > 0)
        {
            delay = (1000.0 / fps);
        }
    }

    public double getFPS()
    {
        //System.out.println("Game::GetFPS");

        return fps;
    }

    public long getFrameCount()
    {
        //System.out.println("Game::getFrameCount");

        return frameCount;
    }

    public double getCurrentTime()
    {
        //System.out.println("Game::getCurrentTime");

        return frameTime;
    }

    public void setGamePage(GamePage anim)
    {
        //System.out.println("Game::setGamePage");

        cleanUpBuffers();
        this.anim = anim;
        if (this.anim != null)
        {
            this.anim.setGame(this);
        }
    }

    public void start() 
    {
        //System.out.println("Game::start");

        //Start animating!
        if (GameThread == null) 
        {
            GameThread = new Thread(this);
            GameThread.start();
        }
    }

    public void stop() 
    {
        //System.out.println("Game::stop");

        //Stop the animating thread.
        GameThread = null;
    }

    public void run() 
    {
        //System.out.println("Game::run");

        Thread currentThread = Thread.currentThread();
        long diff = -1;

        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
        startTime = frameTime = ((double)System.currentTimeMillis());

        //This is the animation loop.
        while (currentThread == GameThread) 
        {
            frameCount++;
            if (frameTime > startTime)
            {
                fps =  (frameCount * 1000) / (frameTime - startTime);
            }

            repaint();

            try 
            {
                frameTime += delay;
                diff = (long)(frameTime - ((double)System.currentTimeMillis()));
                Thread.sleep(Math.max(0, diff));
            }
            catch (InterruptedException e) 
            {
                break;
            }
        }
    }

    public void cleanUpBuffers()
    {
        //System.out.println("Game::cleanUpBuffers");

        if (backG != null)
        {
            backG.dispose();
            backG = null;
        }

        if (backBuf != null)
        {
            backBuf.flush();
            backBuf = null;
        }
    }

    public void setSize(int w, int h)
    {
        //System.out.println("Game::setSize");

        super.setSize(w, h);
        cleanUpBuffers();
    }

    public int getWidth()
    {
        return super.getBounds().width;
    }

    public int getHeight()
    {
        return super.getBounds().height;
    }

    public void update(Graphics g)
    {
        //System.out.println("Game::update");

        paint(g);
    }

    //Draw the current frame of animation.
    public void paint(Graphics g) 
    {
        //System.out.println("Game::paint");

        if (null == backBuf)
        {
            cleanUpBuffers();

            backBuf = createImage(getWidth(), getHeight());
            backG = backBuf.getGraphics();
        }

        if (anim != null)
        {
            anim.paint(backG);

            if (backBuf != null)
            {
                g.drawImage(backBuf, 0, 0, this);
            }
        }
    }

    // CompoenentListener
    public void componentHidden(ComponentEvent e)
    {
        stop();
        cleanUpBuffers();
    }

    public void componentMoved(ComponentEvent e) 
    {
    }

    public void componentResized(ComponentEvent e) 
    {
        cleanUpBuffers();
    }

    public void componentShown(ComponentEvent e) 
    {
        start();
    }

    // MouseListener

    public void mouseClicked(MouseEvent e) 
    {
        if (anim != null)
        {
            anim.mouseClicked(e);
        }
    }

    public void mouseEntered(MouseEvent e) 
    {
        if (anim != null)
        {
            anim.mouseEntered(e);
        }
    }

    public void mouseExited(MouseEvent e) 
    {
        if (anim != null)
        {
            anim.mouseExited(e);
        }
    }

    public void mousePressed(MouseEvent e) 
    {
        if (anim != null)
        {
            anim.mousePressed(e);
        }
    }

    public void mouseReleased(MouseEvent e) 
    {
        if (anim != null)
        {
            anim.mouseReleased(e);
        }
    }
           
    // MouseMotionListener

    public void mouseDragged(MouseEvent e) 
    {
        if (anim != null)
        {
            anim.mouseDragged(e);
        }
    }

    public void mouseMoved(MouseEvent e) 
    {
        if (anim != null)
        {
            anim.mouseMoved(e);
        }
    }
           
    // KeyListener

    public void keyPressed(KeyEvent e) 
    {
        if (anim != null)
        {
            anim.keyPressed(e);
        }
    }

    public void keyReleased(KeyEvent e) 
    {
        if (anim != null)
        {
            anim.keyReleased(e);
        }
    }

    public void keyTyped(KeyEvent e) 
    {
        if (anim != null)
        {
            anim.keyTyped(e);
        }
    }
}

public class bounce extends Applet 
{
    static Applet applet = null;
    Game c = null;

    public bounce()
    {
	// Empty
    }

    public void init()
    {
        //System.out.println("bounce::init");
        HowHard info = null;

        info = new HowHard();
        info.level = 1;
        info.minPercentPost = 24;
        info.percentPost = info.minPercentPost;
        info.maxPercentPost = 40;
        info.incrPercentPost = 4;
        info.minSpeed = 1;
        info.maxSpeed = 5;
        info.speedRange = 10;
        info.maxScore = 20;
        info.numBalls = 1;
        info.maxBalls = 5;
        info.gravity = 7;

        c = new Game();

        add((Canvas)c);
        c.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
        c.setSize(300, 300);
        c.setGamePage(new startPage(info));
    }

    public void start()
    {
        //System.out.println("bounce::start");
        if (c != null)
        {
            c.start();
        }
    }

    public void stop()
    {
        //System.out.println("bounce::stop");
        if (c != null)
        {
            c.stop();
        }
    }

    public void destroy()
    {
	// Empty
    }

    public void update(Graphics g)
    {
        //System.out.println("bounce::update");

        paint(g);
    }

    public static void main(String[] args)
    {
	applet = new bounce();
	final Frame f = new Frame("bounce");
	f.addWindowListener(new WindowAdapter()
            {
                public void windowClosing(WindowEvent e)
                {
                    applet.stop();
                    applet.destroy();
                    System.exit(0);
                }

                public void windowDeiconified(WindowEvent e) 
                {
                    applet.start();
                }

                public void windowIconified(WindowEvent e) 
                {
                    applet.stop();
                }
            });
	f.add(applet, BorderLayout.CENTER);
	applet.init();
	applet.start();
        f.pack();
	f.setVisible(true);
    }
    
} // bounce

