import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Vector;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;

/** A demonstration of model-view-controller separation in a simple Java GUI
  * program.  Models a die and controls to roll it, and multiple different
  * views to see the value on the face of the die.
  *
  * @author Martin Stepp
  * @version Thursday, November 2, 2000
  */
public class DiceGameDemo {
	// turn this on for trippy multiple views
	public static final boolean MULTI_VIEW = false;
	
	public static void main(String[] args) {
		new DiceFrame("Have a Dice Day!", new Die()).show();
	}
}


/** Models one die, the "model" in this game.
  * The die is observable and can have listeners added to it.
  * The listeners will be notified whenever the die's state changes.
  */
class Die {
	public static final int NUM_SIDES = 6;
	int value = NUM_SIDES;
	Vector myListeners = new Vector();


	/** Rolls the die. */
	public void roll() {
		value = (int)(Math.random() * NUM_SIDES + 1);
		notifyListeners();
	}


	/** Returns the die's current value. */
	public int getValue() {
		return value;
	}

	
	/** Adds the given listener to this die's set of listeners to notify. */
	public void addDiceListener(DiceListener listener) {
		myListeners.add(listener);
		notifyListeners();
	}

	
	/** Notifies everyone listening to this die that its state has changed. */
	private void notifyListeners() {
		int size = myListeners.size();
		for (int i = 0;  i < size;  i++)
			((DiceListener)myListeners.get(i)).gameUpdated(this);
	}
	

	/** Removes the given listener from this die's set of listeners to notify. */
	public void removeDiceListener(DiceListener listener) {
		myListeners.remove(listener);
		notifyListeners();
	}
}


/** The interface that must be implemented by all die views. */
interface DiceListener {
	public void gameUpdated(Die d);
}


/** The main frame to hold everything; the "controller" in this game. */
class DiceFrame extends JFrame {
	JPanel centerPanel;
	JPanel myView;
	Die    myDie;
	
	/** Constructs a new frame with the given window title to view the given die. */
	public DiceFrame(String title, Die d) {
		setTitle(title);
		myDie = d;
		setDefaultCloseOperation(EXIT_ON_CLOSE);

		DiceActionListener listener = new DiceActionListener();

		JButton rollButton = new JButton("Roll");
		rollButton.addActionListener(listener);

		JButton viewButton = new JButton("View");
		viewButton.addActionListener(listener);

		JPanel northPanel = new JPanel();
		northPanel.add(rollButton);
		northPanel.add(viewButton);

		JPanel contentPane = new JPanel(new BorderLayout());
		contentPane.add(northPanel, BorderLayout.NORTH);
			
		centerPanel = new JPanel();
		contentPane.add(centerPanel, BorderLayout.CENTER);
		setContentPane(contentPane);

		pack();  // shrink window to fit its contents
	}
	
	
	/** Listens to button presses in this frame. */
	private class DiceActionListener implements ActionListener {
		/** Called when user clicks one of the buttons in the game. */
		public void actionPerformed(ActionEvent event) {
			String command = event.getActionCommand();
			if (command.equalsIgnoreCase("Roll"))
				myDie.roll();
			else if (command.equalsIgnoreCase("View")) {
				if (myView != null  &&  !DiceGameDemo.MULTI_VIEW) {
					// disassociate old view
					centerPanel.remove(myView);
					myDie.removeDiceListener((DiceListener)myView);
				}

				// swap between views
				if (myView == null  ||  myView instanceof TextViewPanel) 
					myView = new GraphicalViewPanel();
				else
					myView = new TextViewPanel();

				// notify the die that this new view is now listening to it
				myDie.addDiceListener((DiceListener)myView);

				centerPanel.add(myView);
				centerPanel.revalidate();
				pack();  // shrink window to fit its contents
			}
		}
	}
}


/** Text area crap view panel that listens to a die.
  * The first "view" in this game.
  */
class TextViewPanel extends JPanel implements DiceListener {
	JTextArea area;
	
	/** Constructs a new text view panel. */
	public TextViewPanel() {
		area = new JTextArea();
		area.setFont(new Font("SansSerif", Font.BOLD, 14));
		area.setEditable(false);
		add(area);
	}
	
	/** Called by the die that this text view panel is listening to. */
	public void gameUpdated(Die d) {
		area.setText("Die's value is " + d.getValue());
	}
}


/** Graphical view panel that depicts the die's current value.
  * The second "view" in this game.
  */
class GraphicalViewPanel extends JPanel implements DiceListener {
	int value;
	Image[] diceImages;
	
	/** Constructs a new graphical view panel. */
	public GraphicalViewPanel() {
		value = Die.NUM_SIDES;
		
		// get die images
		diceImages = new Image[Die.NUM_SIDES];
		Toolkit tk = Toolkit.getDefaultToolkit();
		MediaTracker mt = new MediaTracker(this);
		for (int i = 0;  i < Die.NUM_SIDES;  i++) {
			diceImages[i] = tk.getImage("die" + (i+1) + ".gif");
			mt.addImage(diceImages[i], 0);
		}
		
		// using a MediaTracker, we are assured that the image
		// files have finished downloading before the panel appears
		// on the screen (unpredictable things happen otherwise)
		try {
			mt.waitForAll();
		}
		catch (InterruptedException ie) {
			System.out.println("Could not load images: " + ie);
		}
		
		Dimension dieSize = new Dimension(
			diceImages[0].getWidth(this),
			diceImages[0].getHeight(this)
		);
		setPreferredSize(dieSize);
	}
	
	/** Called by the die that this text view panel is listening to. */
	public void gameUpdated(Die d) {
		value = d.getValue();
		repaint();
	}
	
	/** Paints this view panel on the screen. */
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		g.drawImage(diceImages[value - 1], 0, 0, this);
	}
}