import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.net.URL;
import java.applet.Applet;
import java.applet.AudioClip;

/**
* A Java based "Daleks" game, from the Dr Who TV series.
 */
public class Daleks extends Applet {
    DalekControls controls;
    DalekCanvas canvas;
    boolean runAsApplication = false;

    public void init() {
        setLayout(new BorderLayout());
        canvas = new DalekCanvas(this);
        controls = new DalekControls(this, canvas);
        add("Center", canvas);
        add("South", controls);
    }
    public void start() {
        canvas.start();
    }
    public void stop() {
        canvas.stop();
    }
    public String getParam(String name, String defaultStr) {
        if (runAsApplication)
            return defaultStr;
        String s = getParameter(name);
        return s==null ? defaultStr : s;
    }
    public static void main(String args[]) {
        Frame f = new Frame("Daleks");
        Daleks daleks = new Daleks();
        daleks.runAsApplication = true;

        daleks.init();
        daleks.start();

        f.add("Center", daleks);
        f.setSize(800,600);
        f.show();
        f.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {System.exit(0);}
        });
    }
}
class DalekControls extends Panel implements ActionListener, ItemListener {
    String NEW, JUMP, ZAP, FINISH, gamenames[], gamespecs[];
    DalekCanvas canvas;
    Choice games;

    public DalekControls(Daleks applet, DalekCanvas canvas) {
        this.canvas = canvas;
        String s = applet.getParam("buttons","New|Jump|Zap|Finish");
        StringTokenizer t = new StringTokenizer(s, "|");
        Button btn=new Button(NEW = t.nextToken());
        btn.addActionListener(this); add(btn);
        btn=new Button(JUMP = t.nextToken());
        btn.addActionListener(this); add(btn);
        btn=new Button(ZAP = t.nextToken());
        btn.addActionListener(this); add(btn);
        btn=new Button(FINISH = t.nextToken());
        btn.addActionListener(this); add(btn);

        games = new Choice();
        s = applet.getParam("games",
                            "Classic:0 0, 1 0, 1 1|-1 10|5 2 15:" +
                            "Diamond:0 0, 1 0, 1 1, 2 0|-2 -2|7 3 22:" +
                            "Diagonal:0 0, 1 0, 1 1, 2 2|-1 -1|7 3 22");
        t = new StringTokenizer(s, ":");
        gamenames = new String[t.countTokens()/2];
        gamespecs = new String[gamenames.length];
        for (int i = 0; i<gamenames.length; i++) {
            gamenames[i] = t.nextToken().trim();
            gamespecs[i] = t.nextToken().trim();
            games.addItem(gamenames[i]);
        }
        add(games);
        games.addItemListener(this);
        canvas.newGame(gamespecs[0]);
    }

    public void actionPerformed(ActionEvent e) {
        String s = e.getActionCommand();
        if (s.equals(NEW)) {
            canvas.shuffle();
        } else if (s.equals(JUMP)) {
            canvas.teleport();
        } else if (s.equals(ZAP)) {
            canvas.sonicDriver();
        } else if (s.equals(FINISH)) {
            canvas.lastStand();
        }
    }
    public void itemStateChanged(ItemEvent e) {
        String s = (String)e.getItem();
        Choice c = (Choice)e.getItemSelectable();
        if (c.equals(games)) {	//gotta be; but check anyway
            for (int i = 0; i<gamenames.length; i++) {
                if (gamenames[i].equals(s)) {
                    canvas.newGame(gamespecs[i]);
                    // 		    canvas.repaint();
                    break;
                }
            }
        }
    }
}
class DalekCanvas extends Canvas implements Runnable {
    int boxSize, tickSize, deltaStep, delta;
    int lineWidth, lineHeight, x0, y0;
    int score, boards, jumps, zaps;
    DalekBoard board;
    Dimension boardSize = new Dimension(-1, -1);
    Daleks applet;
    Point moves[];
    int population[];		//3 ints: init, delta, max
    int escapes[];		//2 ints: zaps, jumps; negative => accumulate
    Image offscreen;
    boolean finishing;
    AudioClip nextNoise, WreckNoise, JumpNoise,
        ZapNoise, OopsNoise, LoseNoise, WinNoise;
    Thread painter;

    DalekCanvas(Daleks applet) {
        URL url=null;
        if (applet.runAsApplication) {
            try {
                url = new URL("file:"+System.getProperty("user.dir")+"/");
                WreckNoise = Applet.newAudioClip(new URL(url, "audio/ding.au"));
                JumpNoise  = Applet.newAudioClip(new URL(url, "audio/clank.au"));
                ZapNoise   = Applet.newAudioClip(new URL(url, "audio/beep.au"));
                OopsNoise  = Applet.newAudioClip(new URL(url, "audio/bubble1.au"));
                LoseNoise  = Applet.newAudioClip(new URL(url, "audio/bong.au"));
                WinNoise   = Applet.newAudioClip(new URL(url, "audio/joy.au"));
                System.out.println(WinNoise.toString());
            } catch (Exception e) {
                System.err.println(e.getMessage());
            }
        } else {
            url = applet.getCodeBase();
            WreckNoise = applet.getAudioClip(url, "audio/ding.au");
            JumpNoise  = applet.getAudioClip(url, "audio/clank.au");
            ZapNoise   = applet.getAudioClip(url, "audio/beep.au");
            OopsNoise  = applet.getAudioClip(url, "audio/bubble1.au");
            LoseNoise  = applet.getAudioClip(url, "audio/bong.au");
            WinNoise   = applet.getAudioClip(url, "audio/joy.au");
        }
        this.applet = applet;
        parseGrid(applet.getParam("grid", "15 5").trim());
        newGame("0 0,1 0,1 1|-1 10|5 2 15"); //will be overridden by ctls
        this.addMouseListener(new MyMouseListener());
    }
    class MyMouseListener extends MouseAdapter {
        public void mousePressed(MouseEvent e) {
            Point pt = xyToGrid (e.getX(), e.getY());
            moveWho(pt, false);
        }
    }
    void parseGrid(String grid) {
        StringTokenizer t = new StringTokenizer(grid);
        boxSize   = Integer.parseInt(t.nextToken());
        deltaStep = Integer.parseInt(t.nextToken());
        tickSize  = (boxSize<20) ? 3:5;
    }
    void parseGame(String game) {
        StringTokenizer t = new StringTokenizer(game, "|");
        String s1 = t.nextToken().trim();
        String s2 = t.nextToken().trim();
        String s3 = t.nextToken().trim();
        t = new StringTokenizer(s1, ",");
        int p=0;
        Point pts[] = new Point[100];
        while (t.hasMoreTokens()) {
            String s = t.nextToken().trim();
            int i = s.indexOf(' ');
            int x = Integer.parseInt(s.substring(0,i));
            int y = Integer.parseInt(s.substring(i+1));
            pts[p++] = new Point(x, y);
            if (y!=0)	pts[p++] = new Point(x, -y);
            if (x!=0)	pts[p++] = new Point(-x, y);
            if (x!=0&&y!=0)	pts[p++] = new Point(-x, -y);
            if (x==y)	continue;
            pts[p++] = new Point(y, x);
            if (x!=0)	pts[p++] = new Point(y, -x);
            if (y!=0)	pts[p++] = new Point(-y, x);
            if (x!=0&&y!=0)	pts[p++] = new Point(-y, -x);
        }
        moves = new Point[p];
        for (int i=0; i<p; i++) {
            moves[i]=pts[i];
        }

        escapes = new int[2];
        t = new StringTokenizer(s2);
        escapes[0] = Integer.parseInt(t.nextToken());
        escapes[1] = Integer.parseInt(t.nextToken());

        population = new int[3];
        t = new StringTokenizer(s3);
        population[0] = Integer.parseInt(t.nextToken());
        population[1] = Integer.parseInt(t.nextToken());
        population[2] = Integer.parseInt(t.nextToken());
    }
    void newBoard () {
        int minMargin = boxSize/2;
        FontMetrics fm = getFontMetrics(getFont());
        boardSize = getSize();
        board = new DalekBoard((boardSize.width - 2*minMargin)/boxSize,
                               (boardSize.height - 2*minMargin -
                                fm.getHeight())/boxSize,
                               Math.min(population[2],population[0]+
                                        (boards-1)*population[1]), moves);
        lineWidth = (board.maxX * boxSize);
        lineHeight = (board.maxY * boxSize);
        x0 = (boardSize.width - lineWidth)/2;
        y0 = (boardSize.height - lineHeight - fm.getHeight())/2;
        offscreen=createImage(boardSize.width,boardSize.height);
        finishing = false;
    }
    //     public void paint(Graphics g) {
    // 	update(g);	// avoid background repainting
    //     }

    Color BoardColor	= Color.pink;
    Color LineColor	= Color.red;
    Color MovesColor	= Color.black;
    Color DalekColor	= Color.yellow;
    Color WreckColor	= Color.black;
    Color DeadWhoColor	= Color.black;
    Color WhoColor	= Color.white;
    Color TextFillColor	= Color.white;
    Color TextColor	= Color.black;

    public synchronized void update(Graphics g) {
        Dimension d = getSize();
        if (d.width*d.height == 0) { // this sometimes happens...
            return;
        }
        if (boardSize.width!=d.width || boardSize.height!=d.height) {
            nextBoard(false);
        }
        if (board==null) {
            newBoard();
        }
        Graphics gOff = offscreen.getGraphics();

        checkNoise();

        int x, y;
        gOff.setColor(BoardColor);
        gOff.fillRect(0, 0, boardSize.width, boardSize.height);
        gOff.setColor(LineColor);
        for (x = x0; x <= lineWidth + x0; x += boxSize) {
            gOff.drawLine(x, y0, x, lineHeight+y0);
        }
        for (y = y0; y <= lineHeight + y0; y += boxSize) {
            gOff.drawLine(x0, y, lineWidth+x0, y);
        }
        gOff.setColor(MovesColor);
        for (int i=0; i<board.moves.length; i++) {
            if (board.deltaOnBoard(moves[i].x, moves[i].y)) {
                x = x0 + boxSize*(board.drWho.x+moves[i].x) - tickSize/2;
                y = y0 + boxSize*(board.drWho.y+moves[i].y) - tickSize/2;
                gOff.fillRect(x, y, tickSize, tickSize);
            }
        }

        for (int i=0; i<=board.maxX; i++) {
            for (int j=0; j<=board.maxY; j++) {
                switch (board.cell[i][j]) {
                    case DalekBoard.Dalek:
                        gOff.setColor(DalekColor);
                        x = x0 + boxSize*i - boxSize/2;
                        y = y0 + boxSize*j - boxSize/2;
                        if (evolving()) {
                            x += board.dx[i][j]*delta;
                            y += board.dy[i][j]*delta;
                        }
                            gOff.fillRect(x+1, y+1, boxSize-2, boxSize-2);
                        break;
                    case DalekBoard.Wreck:
                        gOff.setColor(WreckColor);
                        x = x0 + boxSize*i - boxSize/2;
                        y = y0 + boxSize*j + boxSize/2;
                        int xs[]={x,x+boxSize,x+boxSize/2};
                        int ys[]={y, y, y-boxSize};
                        gOff.fillPolygon(xs, ys, 3);
                        break;
                    case DalekBoard.DeadWho:
                        gOff.setColor(DeadWhoColor);
                        x = x0 + boxSize*i - boxSize/2;
                        y = y0 + boxSize*j - boxSize/2;
                        gOff.fillRect(x, y, boxSize, boxSize);
                        break;
                }
            }
        }
        if (!board.whoCaptured()) {
            x = x0 + boxSize*board.drWho.x - boxSize/2;
            y = y0 + boxSize*board.drWho.y - boxSize/2;
            gOff.setColor(WhoColor);
            gOff.fillOval(x, y, boxSize, boxSize);
        }
        String s="Score: "+score +"("+(board.daleks)+" left)"
            +", board: "+boards+"("+(board.population)+"%)";
        s += (jumps<=0&&zaps<=0) ?
            " ..Final jump/zap!" : ", jumps: "+jumps+", zaps: "+zaps;
        gOff.setFont(getFont());
        FontMetrics fm=gOff.getFontMetrics();
        int h=fm.getHeight();
        int w=fm.stringWidth(s);
        x = (boardSize.width - w)/2;
        y = boardSize.height - h;
        gOff.setColor(TextFillColor);
        gOff.fillRect(x-10, y, w+20, h);
        gOff.setColor(TextColor);
        gOff.drawRect(x-10, y, w+20-1, h-1);
        y = boardSize.height - fm.getDescent();
        gOff.drawString(s, x, y);

        g.drawImage(offscreen, 0, 0, null);
        // 	evolveNext();
        if (evolving()) {
            delta += deltaStep;
            if (delta > boxSize - deltaStep) {
                evolveStop();
            }
            // 	    repaint();
        } else if (finishing) {
            evolveStart(board.drWho, true);
        }
    }
    boolean evolving() {
        return delta != 0;
    }
    public synchronized void evolveStart(Point pt, boolean finish) {
        if (evolving()) {
            evolveStop();
        }
        board.evolveStart(pt.x, pt.y);
        delta = deltaStep;
        finishing = finish;
        // 	repaint();	// starts the drawing cycle
    }
    //     public void evolveNext() {
    // 	if (evolving()) {
    // 	    delta += deltaStep;
    // 	    if (delta > boxSize - deltaStep) {
    // 		evolveStop();
    // 	    }
    // // 	    repaint();
    // 	} else if (finishing) {
    // 	    evolveStart(board.drWho, true);
    // 	}
    //     }
    public void evolveStop() {
        int deltaDaleks = board.evolveStop ();
        if (board.whoCaptured()) {
            queueNoise(LoseNoise);
        } else {
            score += finishing ? 2*deltaDaleks : deltaDaleks;
            if (board.whoWon()) {
                queueNoise(WinNoise);
            } else if (deltaDaleks!=0) {
                queueNoise(WreckNoise);
            }
        }
        delta = 0;
        finishing = (finishing && !board.gameOver());
    }

    void checkNoise() {
        if (nextNoise != null) {
            nextNoise.play();
        }
        nextNoise = null;
    }
    void queueNoise(AudioClip s) {
        nextNoise = s;
    }
    void makeNoise(AudioClip s) {
        queueNoise(s);
        checkNoise();
    }
    public void lastStand() {
        moveWho(board.drWho, true);
    }
    public synchronized void teleport() {
        if (checkGameNotOver() && checkNotBusy() && checkOK(jumps!=0||zaps==0))
        {
            board.teleport();
            queueNoise(JumpNoise); //note: order below counts!
            evolveStart(board.drWho, jumps-- == 0 && zaps == 0);
        }
    }
    public synchronized void sonicDriver() {
        if (checkGameNotOver() && checkNotBusy() && checkOK(zaps!=0||jumps==0))
        {
            board.sonicDriver(1);
            queueNoise(ZapNoise); //note: order below counts!
            evolveStart(board.drWho, zaps-- == 0 && jumps == 0);
        }
    }
    public void newGame(String game) {
        parseGame (game);
        nextBoard(false);
    }
    public void nextBoard(boolean advance) {
        if (advance) {
            boards++;
            zaps  = (escapes[0]<0) ? Math.max(0,zaps) -escapes[0] : escapes[0];
            jumps = (escapes[1]<0) ? Math.max(0,jumps)-escapes[1] : escapes[1];
        } else {
            boards=1;
            score=0;
            zaps  = Math.abs(escapes[0]);
            jumps = Math.abs(escapes[1]);
        }
        board = null;
    }
    public synchronized void shuffle() {
        if (checkNotBusy() && checkOK(board!=null)) {
            nextBoard(board.whoWon());
            repaint();
        }
    }
    Point xyToGrid (int x, int y) {
        x = (x-x0+boxSize/2)/boxSize;
        y = (y-y0+boxSize/2)/boxSize;
        return new Point (
                          Math.min(board.maxX, Math.max(0, x)),
                          Math.min(board.maxY, Math.max(0, y))
                          );
    }
    void moveWho (Point pt, boolean finish) {
        if (checkGameNotOver() && checkNotBusy() && checkOK(board.moveOK(pt)))
        {
            evolveStart(pt, finish);
        }
    }
    boolean checkNotBusy() {
        return checkOK(!evolving() && !finishing);
    }
    boolean checkGameNotOver() {
        return checkOK(!board.gameOver());
    }
    boolean checkOK(boolean OK) {
        if (!OK)
            makeNoise(OopsNoise);
        return OK;
    }
    boolean running;
    public void run() {
        running=true;
        while (running) {
            repaint();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
    public void start() {
        painter = new Thread(this);
        painter.start();
    }
    public void stop() {
        //painter.stop();
        running=false;
    }
}
class DalekBoard {
    public static final int Empty = 0;
    public static final int Dalek = 1;
    public static final int Wreck = 2;
    public static final int DeadWho = 3;

    int maxX;
    int maxY;
    int population;
    int cell[][];
    Point drWho;
    Point moves[];

    int newcell[][];
    int dx[][];
    int dy[][];
    int daleks, newDaleks;

    DalekBoard (int width, int height, int num, Point moves[]) {
        maxX = Math.max(2, width);
        maxY = Math.max(2, height);
        population = num;
        num = Math.max(2, (num*(maxX+1)*(maxY+1))/100);
        this.moves = moves;
        cell = new int[maxX+1][maxY+1];

        while (daleks <= num) {
            drWho = randomPt();
            if (cell[drWho.x][drWho.y]==Empty) {
                cell[drWho.x][drWho.y]=Dalek;
                daleks++;
            }
        }
        cell[drWho.x][drWho.y]=Empty;
        daleks--;
    }
    Point randomPt() {
        return new Point( (int)((maxX+1)*Math.random()),
                          (int)((maxY+1)*Math.random()) );
    }
    public void teleport() {
        Point pt = randomPt();
        while (cell[pt.x][pt.y] != Empty || pt.equals(drWho)) {
            pt = randomPt();
        }
        drWho = pt;
    }
    public boolean gameOver() {
        return whoCaptured() || whoWon();
    }
    public boolean whoCaptured() {
        return cell[drWho.x][drWho.y] != Empty;
    }
    public boolean whoWon() {
        return daleks==0;
    }
    boolean ptOnBoard(int x, int y) {
        return (x>=0 && x<=maxX && y>=0 && y<=maxY);
    }
    boolean deltaOnBoard(int i, int j) {
        return ptOnBoard(drWho.x+i,drWho.y+j);
    }
    boolean deltaLegal(int dx, int dy) {
        dx = Math.abs(dx);
        dy = Math.abs(dy);
        Point p1 = new Point(dx, dy);
        Point p2 = new Point(dy, dx);
        for (int p=0; p<moves.length; p++) {
            if ( moves[p].equals(p1) || moves[p].equals(p2) )
                return true;
        };
        return false;
    }
    public boolean moveOK(Point pt) {
        return deltaLegal(drWho.x-pt.x, drWho.y-pt.y) && (cell[pt.x][pt.y] != Wreck);
    }
    void deleteDalek(int x, int y) {
        if (ptOnBoard(x,y) && cell[x][y] == Dalek) {
            cell[x][y] = Empty;
            daleks--;
        }
    }
    public void sonicDriver(int radius) {
        for (int i=-radius; i<=radius; i++) {
            for (int j=-radius; j<=radius; j++) {
                deleteDalek(drWho.x+i, drWho.y+j);
            }
        }
    }
    public void evolveStart(int x, int y) {
        drWho.x = x;
        drWho.y = y;
        newcell = new int[maxX+1][maxY+1];
        newDaleks = 0;
        dx = new int[maxX+1][maxY+1];
        dy = new int[maxX+1][maxY+1];
        for (int ix=0; ix<=maxX; ix++) {
            for (int iy=0; iy<=maxY; iy++) {
                switch (cell[ix][iy]) {
                    case Dalek:
                        int i = (drWho.x>ix) ?
                        ix+1 : (drWho.x<ix) ? ix-1 : ix;
                        int j = (drWho.y>iy) ?
                            iy+1 : (drWho.y<iy) ? iy-1 : iy;
                        dx[ix][iy]=i-ix;
                        dy[ix][iy]=j-iy;
                        if (newcell[i][j]==Dalek) {
                            newDaleks--;
                            newcell[i][j]=Wreck;
                        } else if (cell[i][j]!=Wreck && newcell[i][j]!=Wreck) {
                            newDaleks++;
                            newcell[i][j]=Dalek;
                        }
                            break;
                    case Wreck:
                        newcell[ix][iy]=Wreck;
                        break;
                }
            }
        }
    }
    public int evolveStop() {
        int deltaDaleks = daleks - newDaleks;
        cell = newcell;
        daleks = newDaleks;
        if (whoCaptured()) {
            cell[drWho.x][drWho.y] = DeadWho;
        }
        newcell = null;
        dx = null;
        dy = null;
        return deltaDaleks;
    }
}

/* -----------------------------------------------------------------------------
Liked:
- Compilation not as bad as I thought it would be.
- Toolkit feels like it has a lot of "craftmanship".
- Type safty: progs run if compile!
- Implicit call to super in constructor.
- The way println allows str + int + str + ...
- Applet parameters ok for crude "resource file" (i18n)

Misc Issues:
- reshape/resize: which is safe bottleneck to override to catch
    new size.  which one is guarenteed to be called.  Partially
    caused by multiple versions of foo based on diff args rather
    than a single foo that used polymorphism.

- callbacks awkward .. switch statement in parent vs proc style
    callbacks.

- References to atomic types makes it slightly difficult to
    "return" two ints; you have to use arrays of ints as a return
    value.

- The thread model/topology of awt was hard to figure out.
    Finally chose a state machine operating inside the repaint
    thread.  This included anamation and sound synch.

- Wishes: Boolean wrapper class seems incomplete.  Array
    wrapper needed; subarray similar to substring methods would
    be great.  applet.getParameter should take a default.  +
    should work with arrays, concatinating. Dimension needs
    "equals".  Why is "instanceof" an operator rather than a
    method of class Object? switch/case statements/labels still
    need breaks; NOT safe!

- Bug .. a substring with 0 index in init() caused strange
    problem; applet did not correctly initialize, "start: did not
    initialize" or some such message in applet.  Tough to figure
    out.  Caused by leading blank in input string.  Fixed with
    trim().

    ToDo:
- Use duke/usoft as daleks, or colors as parameters.
- Add mute button
- Use format string for score.  "Score: %, board: %, ...

----------------------------------------------------------------------------- */


