import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
import java.util.Stack;

import javax.swing.ButtonGroup;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
 * Program to demonstrate recursion by flood filling an area.
 * Used as a first assignment in CIT 594, Spring 2005.
 * <p>
 * This program implements some features not in the assignment:
 * <ul>
 *   <li>It allows the user to pick an existing color from
 *       the dislpay,</li>
 *   <li>it displays the currently selected color, and</li>
 *   <li>it displays, in the status area, the current mode.</li>
 * </ul>
 * 
 * @author David Matuszek
 * @version Jan 11, 2005
 */
public class FloodFillApplet extends JApplet implements Observer {
    Container c;
    ColorArrayComponent canvas;
    Color[][] colors = new Color[20][30];
    JPanel controlPanel = new JPanel();
    JRadioButton pickButton = new JRadioButton("Pick");
    JRadioButton drawButton = new JRadioButton("Draw");
    JRadioButton fillButton = new JRadioButton("Fill");
    JCheckBox recursiveCheckBox = new JCheckBox("Recursive");
    JButton chooseButton = new JButton("Choose");
    JTextField colorDisplay = new JTextField(3);
    JButton clearButton = new JButton("Clear");
    ButtonGroup mode = new ButtonGroup();
    JColorChooser chooser = new JColorChooser();
    
    Color chosenColor = Color.BLACK;
    Random rand = new Random();
    
    /** 
     * Creates the GUI.
     * 
     * @see java.applet.Applet#init()
     */
    public void init() {
        setSize(600, 500); // I had not realized this would work
        canvas = new ColorArrayComponent(colors, this);
        c = getContentPane();
        c.add(canvas, BorderLayout.CENTER);
        c.add(controlPanel, BorderLayout.SOUTH);
        mode.add(pickButton);
        mode.add(drawButton);
        mode.add(fillButton);
        controlPanel.add(pickButton);
        controlPanel.add(drawButton);
        controlPanel.add(fillButton);
        controlPanel.add(recursiveCheckBox);
        controlPanel.add(chooseButton);
        controlPanel.add(colorDisplay);
        colorDisplay.setBackground(chosenColor);
        controlPanel.add(clearButton);
        drawButton.setSelected(true);
        clear();
        chooseButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                chooseColor();
            }
        });
        clearButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                clear();
            }
        });
        ChangeListener myChangeListener = new ChangeListener() {
            public void stateChanged(ChangeEvent arg0) {
                showMode();                
            }
            
        };
        pickButton.addChangeListener(myChangeListener);
        drawButton.addChangeListener(myChangeListener);
        fillButton.addChangeListener(myChangeListener);
        recursiveCheckBox.addChangeListener(myChangeListener);
    }

    
    /**
     * Displays, in the applet's status area, what will happen
     * the next time the user clicks on the color array display.
     */
    protected void showMode() {
        if (pickButton.isSelected()) {
            showStatus("Pick an existing color from the display");
        }
        else if (drawButton.isSelected()) {
            showStatus("Click on a pixel to change its color");
        }
        else if (fillButton.isSelected()) {
            if (recursiveCheckBox.isSelected()) {
                showStatus("Click on a pixel to flood fill recursively");
            } else {
                showStatus("Click on a pixel to flood fill nonrecursively");
            }
        }
    }

    /**
     * Puts up a JColorChooser dialog box to allow the user to
     * select a new color.
     */
    protected void chooseColor() {
        Color c = chooser.showDialog(this, "Choose a color", Color.BLACK);
        if (c != null) {
            chosenColor = c;
            colorDisplay.setBackground(chosenColor);
        }
    }

    /**
     * Clears (sets to white) the color array. Does not change the
     * user-selected color.
     */
    protected void clear() {
        for (int i = 0; i < colors.length; i++) {
            for (int j = 0; j < colors[i].length; j++) {
                colors[i][j] = Color.WHITE;
            }
        }
        repaint();
    }

    /**
     * Performs the chosen operation on the color array and
     * updates the display to reflect the changes.
     * 
     * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
     */
    public void update(Observable arg0, Object obj) {
        Point p = (Point)obj;
        Color oldColor = colors[p.x][p.y];
        if (oldColor.equals(chosenColor)) {
            return;
        }
        else if (drawButton.isSelected()) {
            colors[p.x][p.y] = chosenColor;
        }
        else if (pickButton.isSelected()) {
            chosenColor = colors[p.x][p.y];
            colorDisplay.setBackground(chosenColor);           
        }
        else if (oldColor != chosenColor) {
            if (recursiveCheckBox.isSelected()) {
                fill(colors, p.x, p.y, oldColor, chosenColor);
            }
            else {
                nonrecursiveFill(colors, p.x, p.y, oldColor, chosenColor);
            }
        }
        repaint();
    }

    /**
     * Flood fills the area containing the given array element in
     * the color array with the new color.
     * 
     * @param colors The array of colors.
     * @param i The row number of the given array element.
     * @param j The column number of the given array element.
     * @param oldColor The color to be replaced.
     * @param newColor The replacement color.
     */
    private static void fill(Color[][] colors, int i, int j,
                             Color oldColor, Color newColor) {
        if (!isLegal(colors, i, j)) return;
        if (!(colors[i][j].equals(oldColor))) return;
        colors[i][j] = newColor;
        fill(colors, i - 1, j, oldColor, newColor);
        fill(colors, i + 1, j, oldColor, newColor);
        fill(colors, i, j - 1, oldColor, newColor);
        fill(colors, i, j + 1, oldColor, newColor);
    }
    
    /**
     * Flood fills the area containing the given array element in
     * the color array with the new color.
     * 
     * @param colors The array of colors.
     * @param i The row number of the given array element.
     * @param j The column number of the given array element.
     * @param oldColor The color to be replaced.
     * @param newColor The replacement color.
     */
    private static void nonrecursiveFill(Color[][] colors, int i, int j,
                                         Color oldColor, Color newColor) {
        Stack pending = new Stack();
        pending.push(new Point(i, j));
        while (!pending.empty()) {
            Point p = (Point)pending.pop();
            if (isLegal(colors, p.x, p.y) && colors[p.x][p.y].equals(oldColor)) {
                colors[p.x][p.y] = newColor;
                pending.push(new Point(p.x - 1, p.y));
                pending.push(new Point(p.x + 1, p.y));
                pending.push(new Point(p.x, p.y - 1));
                pending.push(new Point(p.x, p.y + 1));
            }
        }
    }

    /**
     * Tests whether a given array location is legal or out of bounds.
     * 
     * @param array The array allegedly containing the given location.
     * @param i The row number to check.
     * @param j The column number to check.
     * @return <code>true</code> if [i][j] is within the array.
     */
    private static boolean isLegal(Object[][] array, int i, int j) {
        if (i < 0 || j < 0) return false;
        if (i >= array.length || j >= array[i].length) return false;
        return true;
    }
}
