/*
 *  TotalisticCellularAutomatonCanvas.
 *  Copyright (C) 2002  Frank Buß (fb@frank-buss.de)
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You can get the GNU General Public License at
 *  http://www.gnu.org/licenses/gpl.html
 */

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;

/**
 * Totalistic explorer cellular automaton canvas.
 *
 * @author      Frank Buß
 * @see step() for the rules for this CA.
 */
public class TotalisticCellularAutomatonCanvas
	extends Canvas
	implements ImageProducer, MouseListener, MouseMotionListener {

	/**
	 * Birth table.
	 */
	private static int table0[] = new int[5];

	/**
	 * Survival table.
	 */
	private static int table1[] = new int[5];

	private boolean disableFlickering = true;
	private boolean flickeringFlag = true;

	private boolean ruleError;

	/**
	 * Width of the CA state array.
	 */
	private final static int width = 256;

	/**
	 * Height of the CA state array.
	 */
	private final static int height = 256;

	/**
	 * Current CA state array.
	 */
	private int current[];

	/**
	 * Destination CA state array, used by step(), swapped with 'current', after
	 * filled.
	 */
	private int next[];

	/**
	 * Display with and height for one CA cell.
	 */
	private final static int zoom = 2;

	/**
	 * Image buffer.
	 */
	private final int pixels[];

	/**
	 * ColorModel for the pixels array.
	 */
	private final ColorModel cm =
		new DirectColorModel(32, 0xFF0000, 0xFF00, 0xFF);

	/**
	 * Image for displaying the CA.
	 */
	private Image image;

	/**
	 * Reference to the consumer of the image for displaying the CA.
	 */
	private ImageConsumer consumer;

	/**
	 * Creates a new automaton canvas.
	 */
	public TotalisticCellularAutomatonCanvas() {
		// init arrays
		current = new int[width * height];
		next = new int[width * height];
		pixels = new int[width * height * zoom * zoom];

		// add listener
		addMouseListener(this);
		addMouseMotionListener(this);

		// init CA
		init();

		// create rendering image with this as ImageProducer
		image = createImage(this);
	}

	public void init() {
		try {
			initRule("B134S3");
		} catch (Exception e) {
		}
	}

	/**
	 * Set new rule.
	 */
	public void initRule(String rule) {
		ruleError = false;
		try {
			for (int i = 0; i < 5; i++) {
				table0[i] = 0;
				table1[i] = 0;
			}
			boolean birth = true;
			boolean initialized = false;
			for (int i = 0; i < rule.length(); i++) {
				char c = rule.charAt(i);
				switch (c) {
					case 'B' :
					case 'b' :
						birth = true;
						initialized = true;
						break;
					case 'S' :
					case 's' :
						birth = false;
						initialized = true;
						break;
					default :
						if (!initialized)
							throw new Exception();
						int s = c - '0';
						if (s < 0 || s > 4)
							throw new Exception();
						if (birth)
							table0[s] = 1;
						else
							table1[s] = 1;
				}
			}
		} catch (Exception e) {
			ruleError = true;
		}
	}

	/**
	 * Set or reset disableFlickering flag.
	 */
	public void switchFlickering() {
		disableFlickering = !disableFlickering;
	}

	/**
	 * Init with a random rule.
	 */
	public String initRandomRule() {
		ruleError = false;
		String rule = "";
		int numberOfBirths = 0;
		int numberOfSurvivals = 0;
		while (numberOfBirths + numberOfSurvivals == 0) {
			// random rule
			for (int i = 0; i < 5; i++) {
				table0[i] = (int) (Math.random() * 2.0);
				table1[i] = (int) (Math.random() * 2.0);
			}

			// count number of births and survivals
			numberOfBirths = 0;
			numberOfSurvivals = 0;
			for (int i = 0; i < 5; i++) {
				if (table0[i] == 1)
					numberOfBirths++;
				if (table1[i] == 1)
					numberOfSurvivals++;
			}
		}

		// return string representation of the rule
		if (numberOfBirths > 0) {
			rule += "B";
			for (int i = 0; i < 5; i++) {
				if (table0[i] == 1) {
					rule += i;
				}
			}
		}
		if (numberOfSurvivals > 0) {
			rule += "S";
			for (int i = 0; i < 5; i++) {
				if (table1[i] == 1) {
					rule += i;
				}
			}
		}
		return rule;
	}

	/**
	 * Seed.
	 */
	public void seed(float filling) {
		if (filling > 1)
			filling = 1;
		if (filling < 0)
			filling = 0;

		// fill cellular automaton with random values
		int adr = width + 1;
		for (int y = 1; y < height - 1; y++) {
			for (int x = 1; x < width - 1; x++)
				current[adr++] = Math.random() > filling ? 1 : 0;
			adr += 2;
		}
	}

	/**
	 * Calculate next CA step.
	 */
	public void step() {
		int adr = width + 1;
		for (int y = 1; y < height - 1; y++) {
			for (int x = 1; x < width - 1; x++) {
				// count states
				int n = current[adr - width];
				int e = current[adr + 1];
				int s = current[adr + width];
				int w = current[adr - 1];
				int sum = n + e + s + w;

				// set next state
				if (current[adr] == 1)
					next[adr++] = table1[sum];
				else
					next[adr++] = table0[sum];
			}
			adr += 2;
		}

		// swap buffers
		int tmp[] = next;
		next = current;
		current = tmp;

		// switch flickering flag
		if (disableFlickering)
			flickeringFlag = !flickeringFlag;
	}

	/**
	 * Show current CA state array.
	 * @see java.awt.Component#update(Graphics)
	 */
	synchronized public void update(Graphics g) {
		if (!ruleError) {
			// copy to pixels
			int adr = width + 1;
			for (int y = 1; y < height - 1; y++) {
				int adr2 = zoom * zoom * width * y + zoom;
				for (int x = 1; x < width - 1; x++) {
					boolean t = current[adr++] == 1 ? true : false;
					if (disableFlickering)
						t ^= flickeringFlag;
					int c = t ? 0xffffff : 0;
					int i = adr2;
					for (int cy = 0; cy < zoom; cy++) {
						for (int cx = 0; cx < zoom; cx++) {
							pixels[i++] = c;
						}
						i += (width - 1) * zoom;
					}
					adr2 += zoom;
				}
				adr += 2;
			}

			// draw
			if (consumer != null)
				startProduction(consumer);
			g.drawImage(image, 0, 0, null);
		} else {
			g.setColor(new Color(0, 0, 0));
			g.fillRect(0, 0, width * 2, height * 2);
			g.setColor(new Color(255, 255, 255));
			g.drawString("rule parsing error", 50, 50);
		}
	}

	/**
	 * Set states.
	 * @param x x coordinate in the state array.
	 * @param y y coordinate in the state array.
	 * @param pixel true, if state should be set, false otherwise.
	 */
	private void mouseCellAction(int x, int y, boolean pixel) {
		x /= zoom;
		y /= zoom;
		if (x >= 0 && x < width && y >= 0 && y < height) {
			current[x + y * width] = pixel ? 0 : 1;
		}
		repaint();
	}

	// Java specific code (listeners etc.)

	/**
	 * @see java.awt.Component#getPreferredSize()
	 */
	public Dimension getPreferredSize() {
		return new Dimension(width * zoom, height * zoom);
	}

	/**
	 * @see java.awt.Component#getMinimumSize()
	 */
	public Dimension getMinimumSize() {
		return getPreferredSize();
	}

	/**
	 * @see java.awt.Component#getMaximumSize()
	 */
	public Dimension getMaximumSize() {
		return getPreferredSize();
	}

	/**
	 * @see java.awt.image.ImageProducer#addConsumer(java.awt.image.ImageConsumer)
	 */
	public void addConsumer(ImageConsumer c) {
	}

	/**
	 * @see java.awt.image.ImageProducer#isConsumer(java.awt.image.ImageConsumer)
	 */
	public boolean isConsumer(ImageConsumer consumer) {
		return consumer != null;
	}

	/**
	 * @see java.awt.image.ImageProducer#removeConsumer(java.awt.image.ImageConsumer)
	 */
	public void removeConsumer(ImageConsumer consumer) {
	}

	/**
	 * @see java.awt.image.ImageProducer#startProduction(java.awt.image.ImageConsumer)
	 */
	public void startProduction(ImageConsumer c) {
		if (c != null)
			consumer = c;
		if (consumer != null) {
			consumer.setDimensions(width * zoom, height * zoom);
			consumer.setProperties(null);
			consumer.setColorModel(cm);
			consumer.setHints(
				ImageConsumer.TOPDOWNLEFTRIGHT
					| ImageConsumer.COMPLETESCANLINES
					| ImageConsumer.SINGLEPASS
					| ImageConsumer.SINGLEFRAME);
			consumer.setPixels(
				0,
				0,
				width * zoom,
				height * zoom,
				cm,
				pixels,
				0,
				width * zoom);
			consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
		}
	}

	/**
	 * @see java.awt.image.ImageProducer#requestTopDownLeftRightResend(java.awt.image.ImageConsumer)
	 */
	public void requestTopDownLeftRightResend(ImageConsumer consumer) {
	}

	/**
	 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
	 */
	public void mouseClicked(MouseEvent e) {
	}

	/**
	 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
	 */
	public void mousePressed(MouseEvent e) {
		mouseCellAction(
			e.getX(),
			e.getY(),
			(e.getModifiers() & MouseEvent.META_MASK) > 0);
	}

	/**
	 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
	 */
	public void mouseReleased(MouseEvent e) {
	}

	/**
	 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
	 */
	public void mouseEntered(MouseEvent e) {
	}

	/**
	 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
	 */
	public void mouseExited(MouseEvent e) {
	}

	/**
	 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
	 */
	public void mouseDragged(MouseEvent e) {
		mouseCellAction(
			e.getX(),
			e.getY(),
			(e.getModifiers() & MouseEvent.META_MASK) > 0);
	}
	/**
	 * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
	 */
	public void mouseMoved(MouseEvent e) {
	}
}
