// ComPhys File: GameOfLife.java
// Chapter 15 Section 1: Conway's Game of Life
// Interface similar to Mike Creutz's xautomalab program

import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import comphys.*;

public class GameOfLife extends Applet implements ActionListener, AdjustmentListener, ItemListener, Runnable {

    boolean[][] neighborhood = new boolean[3][3];
    boolean[] birthRule = new boolean[9];
    boolean[] deathRule = new boolean[9];

    void setConway () {

        for (int i = 0; i < 3; i++)
            for (int j = 0; j < 3; j++)
                neighborhood[i][j] = true;
        neighborhood[1][1] = false;
        
        for (int i = 0; i < 9; i++) {
            deathRule[i] = true;
            birthRule[i] = false;
        }
        birthRule[3] = true;
        deathRule[2] = deathRule[3] = false;
        
    }

    int maxL = 198;
    int L = 50;
    boolean[][] cell = new boolean[maxL + 2][maxL + 2];
    boolean[][] newCell = new boolean[maxL + 2][maxL + 2];

    static final int DEAD = 0;
    static final int ALIVE = DEAD + 1;
    static final int PERIODIC = ALIVE + 1;
    int boundary = PERIODIC;

    void updateCells () {

        if (boundary == PERIODIC) {

            for (int x = 1; x <= L; x++) {
                newCell[x][0] = newCell[x][L];
                newCell[x][L+1] = newCell[x][1];
            }

            for (int y = 1; y <= L; y++) {
                newCell[0][y] = newCell[L][y];
                newCell[L+1][y] = newCell[1][y];
            }

            newCell[0][0] = newCell[0][L];
            newCell[0][L+1] = newCell[0][1];
            newCell[L+1][0] = newCell[1][0];
            newCell[L+1][L+1] = newCell[L+1][1];

        } else if (boundary == DEAD) {

            for (int i = 0; i <= L+1; i++)
                newCell[0][i] = newCell[i][0]
                    = newCell[L+1][i] = newCell[i][L+1]
                    = false;

        } else if (boundary == ALIVE) {

            for (int i = 0; i <= L+1; i++)
                newCell[0][i] = newCell[i][0]
                    = newCell[L+1][i] = newCell[i][L+1]
                    = true;

        }

        for (int y = 0; y <= L + 1; y++)
            for (int x = 0; x <= L + 1; x++)
                cell[x][y] = newCell[x][y];

    }

    static final int RANDOM = ALIVE + 1;
    int initialConfiguration = RANDOM;

    void setConfiguration () {

        if (initialConfiguration == RANDOM) {

            for (int y = 1; y <= L; y++) {
                for (int x = 1; x <= L; x++) {
                    if (Math.random() < 0.5)
                        newCell[x][y] = true;
                    else
                        newCell[x][y] = false;
                }
            }

        } else if (initialConfiguration == DEAD) {

            for (int y = 1; y <= L; y++) {
                for (int x = 1; x <= L; x++) {
                    newCell[x][y] = false;

                }
            }

        } else if (initialConfiguration == ALIVE) {

            for (int y = 1; y <= L; y++) {
                for (int x = 1; x <= L; x++) {
                    newCell[x][y] = true;

                }
            }

        }

        updateCells();
        takeCensus();

    }

    int births;
    int adults;
    int deaths;
    boolean fossil;
    int fossils;

    void takeCensus () {

        births = adults = deaths = fossils = 0;

        for (int y = 1; y <= L; y++) {
            for (int x = 1; x <= L; x++) {

                if (newCell[x][y]) {
                    if (cell[x][y])
                        ++adults;
                    else
                        ++births;
                } else {
                    if (cell[x][y])
                        ++deaths;
                    else
                        if (fossil)
                            ++fossils;
                }
                
            }
        }

    }

    int t;
    boolean initialize;
    boolean userHasChangedCells;

    void step () {

        if (initialize)
            initial();

        if (userHasChangedCells) {

            updateCells();
            userHasChangedCells = false;
            
        }
        
        ++t;

        for (int y = 1; y <= L; y++) {
            for (int x = 1; x <= L; x++) {

                int sumOfNeighbors = 0;
                for (int i = 0; i < 3; i++)
                    for (int j = 0; j < 3; j++)
                        if (neighborhood[i][j])
                            if (cell[x + j - 1][y + i - 1])
                                ++sumOfNeighbors;

                if (cell[x][y]) {
                    if (deathRule[sumOfNeighbors])
                        newCell[x][y] = false;
                } else {
                    if (birthRule[sumOfNeighbors])
                        newCell[x][y] = true;
                }

            }
        }

        takeCensus();

    }

    boolean stillLife () {

        for (int y = 0; y <= L + 1; y++)
            for (int x = 0; x <= L + 1; x++)
                if (cell[x][y] != newCell[x][y])
                    return false;

        return true;

    }

    void initial () {

        t = 0;
        setConfiguration();
        initialize = false;
        
    }

    boolean clear;
    boolean small;
    boolean grow = true;
    boolean grid;
    boolean userHasChangedL;
    
    class Picture extends Canvas implements MouseListener, MouseMotionListener {

        int pixels = 400;

        Picture () {

            setSize(pixels, pixels);
            setBackground(Color.white);
            addMouseListener(this);
            addMouseMotionListener(this);

        }

        Image offScreen;
        Graphics osg;

        public void paint (Graphics g) {

            update(g);

        }

        int x0;
        int y0;
        int d;
        boolean scribble;
        int xScribble;
        int yScribble;

        public void update (Graphics g) {

            if (offScreen == null) {

                offScreen = createImage(pixels, pixels);
                osg = offScreen.getGraphics();

            }

            d = (int) (pixels / (double) (L + 2));
            if (d < 1)
                d = 1;
            int dRect = d - 1;
            if (small) 
                x0 = y0 = (pixels - L - 2) / 2;
            else
                x0 = y0 = (pixels - (L + 2) * d) / 2;

            if (grid) {

                osg.clearRect(0, 0, pixels, pixels);
                osg.setColor(Color.gray);
                int x1 = x0 + (L + 2) * d;
                if (small)
                    x1 = x0 + (L + 2);
                for (int i = 0; i <= L + 2; i++) {

                    int x = x0 + i * d;
                    if (small)
                        x = x0 + i;
                    osg.drawLine(x, x0, x, x1);
                    osg.drawLine(x0, x, x1, x);

                }
                g.drawImage(offScreen, 0, 0, null);
                grid = false;
                return;

            }

            if (scribble) {

                if (grow)
                    osg.setColor(Color.red);
                else
                    osg.setColor(Color.black);
                
                if (small) {

                    osg.drawLine(x0 + xScribble, y0 + yScribble,
                               x0 + xScribble, y0 + yScribble);

                } else {
                        
                    int x = x0 + xScribble * d;
                    int y = y0 + yScribble * d;
                    osg.fillRect(x, y, dRect, dRect);

                }

                g.drawImage(offScreen, 0, 0, null);
                scribble = false;
                return;

            }

            if (clear) {
                
                osg.clearRect(0, 0, pixels, pixels);
                clear = false;

            }

            // outline boundary
            if (!small) {

                if (fossil)
                    osg.setColor(Color.magenta);
                else
                    osg.setColor(Color.yellow);
                osg.drawRect(x0 - 1, y0 - 1, (L + 2) * d, (L + 2) * d);
                osg.drawRect(x0 + d - 1, y0 + d - 1, L * d, L * d);

            }
            
            for (int i = 0; i <= L + 1; i++) {
                for (int j = 0; j <= L + 1; j++) {

                    if (newCell[i][j]) {
                        if (cell[i][j] || i == 0 || i == L + 1
                               || j == 0 || j == L + 1)
                            osg.setColor(Color.green);
                        else
                            osg.setColor(Color.red);
                    } else {
                        if (!cell[i][j] || i == 0 || i == L + 1
                            || j == 0 || j == L + 1) {
                            if (fossil)
                                osg.setColor(Color.lightGray);
                            else
                                osg.setColor(Color.white);
                        } else
                            osg.setColor(Color.black);
                    }

                    if (small) {

                        osg.drawLine(x0 + j, y0 + i, x0 + j, y0 + i);

                    } else {
                        
                        int x = x0 + j * d;
                        int y = y0 + i * d;
                        osg.fillRect(x, y, dRect, dRect);

                    }

                }
            }

            g.drawImage(offScreen, 0, 0, null);

        }

        int mouseX;
        int mouseY;
        boolean toggle;
        
        void editCell () {

            int x = mouseX - x0;
            int y = mouseY - y0;
            if (!small) {
                x = (int) (x / (double) d);
                y = (int) (y / (double) d);
            }
            
            if (x > 0 && x <= L && y > 0 && y <= L) {
                
                boolean newValue = grow;
                if (toggle)
                    newValue = !newCell[y][x];
                toggle = false;
                newCell[y][x] = newValue;
                if (boundary == PERIODIC) {

                    if (x == 1)
                        newCell[y][L + 1] = newValue;
                    if (x == L)
                        newCell[y][0] = newValue;
                    if (y == 1)
                        newCell[L + 1][x] = newValue;
                    if (y == L)
                        newCell[0][x] = newValue;
                    if (y == 1 && x == 1)
                        newCell[L + 1][L + 1] = newValue;
                    if (y == 1 && x == L)
                        newCell[L + 1][0] = newValue;
                    if (y == L && x == 1)
                        newCell[0][L + 1] = newValue;
                    if (y == L && x == L)
                        newCell[0][0] = newValue;


                }
                xScribble = x;
                yScribble = y;
                scribble = true;
                repaint();

            }

        }

        public void mouseClicked (MouseEvent me) { }
        public void mouseEntered (MouseEvent me) {

            if (userHasChangedL) {

                initial();
                clear = true;
                userHasChangedL = false;
                repaint();

            }

        }

        public void mouseExited (MouseEvent me) { }
        public void mousePressed (MouseEvent me) {

            mouseX = me.getX();
            mouseY = me.getY();
            toggle = true;
            editCell();
            
        }

        public void mouseReleased (MouseEvent me) {

            repaint();

        }

        public void mouseMoved (MouseEvent me) { }
        public void mouseDragged (MouseEvent me) {

            mouseX = me.getX();
            mouseY = me.getY();
            editCell();
            
        }

    }

    class RuleWindow extends Canvas implements MouseListener {

        int xPixels = 280;
        int yPixels = 150;

        RuleWindow () {

            setSize(xPixels, yPixels);
            setBackground(new Color(255, 250, 245));
            addMouseListener(this);

        }

        public void paint (Graphics g) {

            update(g);

        }

        Image offScreen;
        Graphics osg;

        // locations of clickable squares of side unit pixels
        int unit, maxNeighbors;
        int xNeighborhood, xBirth, xDeath;
        int yNeighborhood, yBirth, yDeath;

        public void update (Graphics g) {

            if (offScreen == null) {

                offScreen = createImage(xPixels, yPixels);
                osg = offScreen.getGraphics();

            }

            osg.clearRect(0, 0, xPixels, yPixels);

            int d = unit = 20;
            int dy = d;
            int y = yNeighborhood = d / 2;
            int x = xNeighborhood = xPixels - d / 2 - 3 * d;

            maxNeighbors = 0;
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j < 3; j++) {
                    if (neighborhood[i][j]) {
                        osg.setColor(Color.blue);
                        ++maxNeighbors;
                    } else
                        osg.setColor(Color.white);
                    osg.fillRect(x + j * d, y + i * d, d - 1, d - 1);
                }
            }
            osg.setColor(Color.cyan);
            osg.fillRect(x + d, y + d, d - 1, d - 1);

            xBirth = x = xPixels - 9 * d - d / 2;
            yBirth = y += 4 * d - d / 2;
            for (int i = 0; i <= maxNeighbors; i++) {
                osg.setColor(Color.blue);
                osg.drawRect(x + i * d, y, d, d);
                if (birthRule[i]) {
                    osg.setColor(Color.red);
                    osg.fillRect(x + i * d + 1, y + 1, d - 1, d - 1);
                } else if (fossil) {
                    osg.setColor(Color.lightGray);
                    osg.fillRect(x + i * d + 1, y + 1, d - 1, d - 1);
                }
            }
            xDeath = x;
            yDeath = y += 2 * d;
            for (int i = 0; i <= maxNeighbors; i++) {
                osg.setColor(Color.yellow);
                osg.drawRect(x + i * d, y, d, d);
                if (deathRule[i]) {
                    osg.setColor(Color.black);
                    osg.fillRect(x + i * d + 1, y + 1, d - 1, d - 1);
                } else {
                    osg.setColor(Color.green);
                    osg.fillRect(x + i * d + 1, y + 1, d - 1, d - 1);
                }
            }

            x = d / 2;
            y = 2 * dy - d / 2 - 4;
            osg.setColor(Color.blue);
            osg.drawString("Time step = " + t, x, y);
            y += dy;
            osg.setColor(Color.green);
            osg.drawString("Adults: " + adults, x, y);
            osg.setColor(Color.gray);
            if (fossil)
                osg.drawString("Fossils: " + fossils, x + 100, y);
            y += dy;
            osg.setColor(Color.red);
            osg.drawString("Births: " + births, x, y);
            osg.setColor(Color.black);
            osg.drawString("Deaths: " + deaths, x + 100, y);
            y += dy + d / 2;
            osg.setColor(Color.cyan);
            osg.drawString("Dead cell =>", x, y);
            y += dy;
            osg.setColor(Color.blue);
            osg.drawString("Neighbors:", x, y);
            x = xPixels - 9 * d - d / 2 + 8;
            for (int i = 0; i <= maxNeighbors; i++)
                osg.drawString("" + i, x + i * d, y);
            x = d / 2;
            y += dy;
            osg.setColor(Color.cyan);
            osg.drawString("Live cell =>", x, y);

            g.drawImage(offScreen, 0, 0, null);
            
        }

        public void mouseClicked (MouseEvent me) { }
        public void mouseEntered (MouseEvent me) { }
        public void mouseExited (MouseEvent me) { }

        public void mousePressed (MouseEvent me) {

            int x = me.getX();
            int y = me.getY();

            if (y > yNeighborhood && y < yNeighborhood + 3 * unit) {
                if (x > xNeighborhood && x < xNeighborhood + 3 * unit) {

                    int i = (int) ( (y - yNeighborhood) / (double) unit );
                    int j = (int) ( (x - xNeighborhood) / (double) unit );
                    if (!(i == 1 && j == 1))
                        neighborhood[i][j] = !neighborhood[i][j];
                    repaint();

                }
            }

            if (y > yBirth && y < yBirth + unit) {
                if (x > xBirth && x < xBirth + (maxNeighbors + 1) * unit) {

                    int j = (int) ( (x - xBirth) / (double) unit );
                    birthRule[j] = !birthRule[j];
                    repaint();

                }
            }

            if (y > yDeath && y < yDeath + unit) {
                if (x > xDeath && x < xDeath + (maxNeighbors + 1) * unit) {

                    int j = (int) ( (x - xDeath) / (double) unit );
                    deathRule[j] = !deathRule[j];
                    repaint();

                }
            }

        }

        public void mouseReleased (MouseEvent me) { }

    }
    
    Picture picture;
    RuleWindow ruleWindow;
    Label LLabel;
    Scrollbar LSlider;
    Choice boundaryChooser;
    Choice initialChooser;
    Checkbox growBox, killBox;
    Scrollbar delaySlider;
    Button stepButton;
    Button runButton;
    Button resetButton;
    Checkbox smallBox;
    Checkbox fossilBox;
    
    int delay = 16;

    public void init () {

        setConway();
        initial();

        add(picture = new Picture());

        Panel rightPanel = new Panel();
        rightPanel.setLayout(new BorderLayout(5, 5));

        rightPanel.add(ruleWindow = new RuleWindow(), "North");

        Panel panel = new Panel();
        panel.setLayout(new GridLayout(0, 2, 2, 2));

        panel.add(LLabel = new Label("No. of cells = " + L + "x" + L));
        panel.add(LSlider = new Scrollbar(Scrollbar.HORIZONTAL,
                                             0, 1, 1, maxL + 1));
        LSlider.setValue(L);
        LSlider.addAdjustmentListener(this);

        panel.add(initialChooser = new Choice());
        initialChooser.add("Start random");
        initialChooser.add("Start dead");
        initialChooser.add("Start alive");
        initialChooser.addItemListener(this);

        panel.add(boundaryChooser = new Choice());
        boundaryChooser.add("Periodic bndry");
        boundaryChooser.add("Dead boundary");
        boundaryChooser.add("Live boundary");
        boundaryChooser.addItemListener(this);

        panel.add(new Label("Scribble to play"));
        Panel panel1 = new Panel();
        CheckboxGroup cg = new CheckboxGroup();
        panel1.add(growBox = new Checkbox("Cupid", cg, true));
        growBox.addItemListener(this);
        panel1.add(killBox = new Checkbox("Mars", cg, false));
        killBox.addItemListener(this);
        panel.add(panel1);

        panel.add(new Label("Animation speed:"));
        panel.add(delaySlider = new Scrollbar(Scrollbar.HORIZONTAL,
                                              0, 1, 0, 13));
        delaySlider.setValue(5);
        delaySlider.addAdjustmentListener(this);

        panel.add(stepButton = new Button("Step"));
        stepButton.addActionListener(this);
        panel.add(runButton = new Button("Animate"));
        runButton.addActionListener(this);

        panel.add(resetButton = new Button("Reset"));
        resetButton.addActionListener(this);

        Panel panel2 = new Panel();
        panel2.add(smallBox = new Checkbox("Small", small));
        smallBox.addItemListener(this);
        panel2.add(fossilBox = new Checkbox("Fossil", fossil));
        fossilBox.addItemListener(this);
        panel.add(panel2);

        rightPanel.add(panel, "South");
        add(rightPanel);

    }

    Thread runThread;
    boolean running;
    boolean needToUpdate;

    public void actionPerformed (ActionEvent event) {

        if (needToUpdate) {
            updateCells();
            needToUpdate = false;
        }
        
        if (event.getSource() == stepButton) {

            running = false;
            step();
            repaint();
            needToUpdate = true;

        } else if (event.getSource() == runButton) {

            if (runThread == null) {

                running = true;
                runThread = new Thread(this);
                runThread.start();

            } else {

                running = false;

            }

        } else if (event.getSource() == resetButton) {

            running = false;
            initial();
            clear = true;
            repaint();

        }

        if (running)
            runButton.setLabel("Stop");
        else
            runButton.setLabel("Animate");
        
    }

    public void adjustmentValueChanged (AdjustmentEvent event) {

        if (event.getSource() == LSlider) {
    
            running = false;
            L = LSlider.getValue();
            LLabel.setText("No. of cells = " + L + "x" + L);
            initialize = clear = grid = true;
            userHasChangedL = true;
            repaint();

        } else if (event.getSource() == delaySlider) {

            int i = delaySlider.getValue();
            delay = 2;
            while (--i > 0)
                delay *= 2;

        }

    }

    public void itemStateChanged (ItemEvent event) {

        grow = growBox.getState();
        fossil = fossilBox.getState();

        if (event.getSource() == smallBox) {

            small = smallBox.getState();
            clear = true;

        } else if (event.getSource() == initialChooser) {

            if (initialChooser.getSelectedItem().equals("Start random"))
               initialConfiguration = RANDOM; 
            else if (initialChooser.getSelectedItem().equals("Start dead"))
               initialConfiguration = DEAD; 
            else
               initialConfiguration = ALIVE;

            running = false;
            initial();

        } else if (event.getSource() == boundaryChooser) {

            if (boundaryChooser.getSelectedItem().equals("Periodic bndry"))
               boundary = PERIODIC; 
            else if (boundaryChooser.getSelectedItem().equals("Dead boundary"))
               boundary = DEAD; 
            else
               boundary = ALIVE;

            updateCells();

        }

        takeCensus();
        repaint();

    }

    public void run () {

        runThread.setPriority(Thread.MIN_PRIORITY);

        while (running) {

            step();
            repaint();

            try {
                runThread.sleep(delay);
            } catch (InterruptedException ie) { }

            if (stillLife())
                running = false;
            else
                updateCells();

        }

        runButton.setLabel("Animate");
        runThread = null;

    }

    public void stop () {

        running = false;
        runButton.setLabel("Animate");

    }

    public void paint (Graphics g) {

        picture.repaint();
        ruleWindow.repaint();

    }

    public static void main (String[] args) {

        GameOfLife gameOfLife = new GameOfLife();
        CPFrame aFrame = new CPFrame("Conway's Game of Life");
        
        aFrame.add(gameOfLife);
        gameOfLife.init();
        aFrame.setSize(700, 450);
        aFrame.setLocation(50, 50);
        aFrame.setVisible(true);

    }

}

