/*
 *  FBCellularAutomatonCanvas.
 *  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.*;

/**
 * FB cellular automaton canvas.
 *
 * @author      Frank Buß
 * @see step() for the rules for this CA.
 */
public class FBCellularAutomatonCanvas
	extends Canvas
	implements ImageProducer, MouseListener, MouseMotionListener {

	// 17 states used
	private static final int colors[] = { 
		0x000000, // empty
		0xff0000, // dataBit
		0xff4080, // dataStopperBit
		0x000000, // dataBit | dataStopperBit (unused)
		0x00ff00, // movingBit
		0x4040ff, // movingBit | dataBit 
		0xffff80, // movingBit | dataStopperBit
		0xffff00, // dataBit | dataStopperBit | movingBit 
		0x00ff00, // movingStopperBit
		0x000000, // movingStopperBit | dataBit (unused)
		0x000000, // movingStopperBit | dataStopperBit (unused)
		0x000000, // movingStopperBit | dataBit | dataStopperBit (unused)
		0xffff80, // movingStopperBit | movingBit
		0x00ffff, // movingStopperBit | movingBit | dataBit
		0x000000, // movingStopperBit | movingBit | dataStopperBit (unused)
		0x000000, // movingStopperBit | dataBit | dataStopperBit | movingBit (unused)
		0x000000, // directionBit (unused)
		0xe00000, // directionBit | dataBit
		0x000000, // directionBit | dataStopperBit (unused)
		0x000000, // directionBit | dataBit | dataStopperBit (unused)
		0x00c000, // directionBit | movingBit
		0x0000ff, // directionBit | movingBit | dataBit
		0xc000c0, // directionBit | movingBit | dataStopperBit
		0x80c000, // directionBit | dataBit | dataStopperBit | movingBit
		0x000000, // directionBit | movingStopperBit (unused)
		0x000000, // directionBit | movingStopperBit | dataBit (unused)
		0x000000, // directionBit | movingStopperBit | dataStopperBit (unused)
		0x000000, // directionBit | movingStopperBit | dataBit | dataStopperBit (unused)
		0x008000, // directionBit | movingStopperBit | movingBit
		0x4040ff, // directionBit | movingStopperBit | movingBit | dataBit
		0x000000, // directionBit | movingStopperBit | movingBit | dataStopperBit (unused)
		0x000000  // directionBit | movingStopperBit | dataBit | dataStopperBit | movingBit (unused)
	};

	private static final int dataBit = 1;
	private static final int dataStopperBit = 2;
	private static final int movingBit = 4;
	private static final int movingStopperBit = 8;
	private static final int directionBit = 16;

	private static final int dataMask = 31 ^ dataBit;
	private static final int dataStopperMaske = 31 ^ dataStopperBit;
	private static final int movingMask = 31 ^ movingBit;
	private static final int movingStopperMask = 31 ^ movingStopperBit;
	private static final int directionMask = 31 ^ directionBit;

	private final static String F_POINT_B_POINT =
		"0010010011100100000010010000010000100000100100000000011100011100001000001001000000001001000001000011110011100000";

	/**
	 * Width of the CA state array.
	 */
	private final static int width = 256;

	/**
	 * Height of the CA state array.
	 */
	private final static int height = 11;

	/**
	 * 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 = 3;

	/**
	 * 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 FBCellularAutomatonCanvas() {
		// 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);
	}

	/**
	 * Clears the CA array and initialized it with my serialized initials.
	 */
	public void init() {
		// clear cellular automaton
		int adr = width + 1;
		for (int y = 1; y < height - 1; y++) {
			for (int x = 1; x < width - 1; x++) current[adr++] = 0;
			adr += 2;
		}

		// init cellular automaton with serialized "F.B."
		int base = width * (height - 2) + 1;
		int len = F_POINT_B_POINT.length();
		for (int i = 0; i < len; i++) {
			if (F_POINT_B_POINT.charAt(i) == '1')
				current[base - 7 * width + i + 16] = dataBit;
			current[base + 2 * i + 16] = movingBit | directionBit;
			current[base + 2 * i + 17] = movingBit | directionBit;
		}
		current[base] = movingStopperBit;
		current[base - width + 15] = movingStopperBit;
		current[base - 2 * width] = movingStopperBit;
		current[base - 3 * width + 15] = movingStopperBit;
		current[base - 4 * width] = movingStopperBit;
		current[base - 5 * width + 15] = movingStopperBit;
		current[base - 6 * width] = movingStopperBit;
		current[base - width] = dataStopperBit;
		current[base - 2 * width + 15] = dataStopperBit;
		current[base - 3 * width] = dataStopperBit;
		current[base - 4 * width + 15] = dataStopperBit;
		current[base - 5 * width] = dataStopperBit;
		current[base - 6 * width + 15] = dataStopperBit;
		current[base - 7 * width] = dataStopperBit;
	}

	/**
	 * 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++) {
				int center = current[adr];
				int result = center;

				// rules for moving bits west, east, up and down
				
				int n = current[adr - width];
				int e = current[adr + 1];
				int s = current[adr + width];
				int w = current[adr - 1];

				// moving moving bit

				// testing for south moving stopper bit
				if ((s & movingStopperBit) != 0) {
					// moving up the moving bit
					if ((s & movingBit) == 0) {
						result &= movingMask;
					} else {
						result |= movingBit;
						// opposite direction
						if ((s & directionBit) == 0) {
							result |= directionBit;
						} else {
							result &= directionMask;
						}
					}
				} else {
					// west is set, west direction says east and west is no dataStopperBit
					if ((w & movingBit) != 0
						&& (w & directionBit) == 0
						&& (w & movingStopperBit) == 0) {
						// moving east
						result |= movingBit;
					}

					// west is clear, center is set and center direction says east
					if ((w & movingBit) == 0
						&& (center & movingBit) != 0
						&& (center & directionBit) == 0) {
						// west is empty and direction says east -> move empty bit
						result &= movingMask;
					}

					// east is set, east direction says west and east is no dataStopperBit
					if ((e & movingBit) != 0
						&& (e & directionBit) != 0
						&& (e & movingStopperBit) == 0) {
						// moving west
						result = result | movingBit | directionBit;
					}

					// east is empty, center is set and center direction says west
					if ((e & movingBit) == 0
						&& (center & movingBit) != 0
						&& (center & directionBit) != 0) {
						result = result & movingMask;
					}
				}

				// moving data bit, if moving bit is set

				if ((center & movingBit) != 0) {
					// testing for north stopper bit
					if ((n & dataStopperBit) != 0) {
						// moving down the data bit
						if ((n & dataBit) == 0) {
							result &= dataMask;
						} else {
							result |= dataBit;
							// opposite direction
							if ((n & directionBit) == 0) {
								result |= directionBit;
							} else {
								result &= directionMask;
							}
						}
					} else {
						// west is set, west direction says east and west is no dataStopperBit
						if ((w & dataBit) != 0
							&& (w & directionBit) != 0
							&& (w & dataStopperBit) == 0) {
							// moving east
							result |= dataBit | directionBit;
						}

						// west is clear, center is set and center direction says east
						if ((w & dataBit) == 0
							&& (center & dataBit) != 0
							&& (center & directionBit) != 0) {
							// west is empty and direction says east -> move empty bit
							result &= dataMask;
						}

						// east is set, east direction says west and east is no dataStopperBit
						if ((e & dataBit) != 0
							&& (e & directionBit) == 0
							&& (e & dataStopperBit) == 0) {
							// moving west
							result = result | dataBit;
						}

						// east is empty, center is set and center direction says west
						if ((e & dataBit) == 0
							&& (center & dataBit) != 0
							&& (center & directionBit) == 0) {
							result = result & dataMask;
						}
					}
				}

				// clear direction bit, if data and moving bit are cleared
				if ((result & dataBit) == 0 && (result & movingBit) == 0)
					result &= directionMask;

				// set next state
				next[adr++] = result;
			}
			adr += 2;
		}

		// swap buffers
		int tmp[] = next;
		next = current;
		current = tmp;
	}

	/**
	 * Show current CA state array.
	 * @see java.awt.Component#update(Graphics)
	 */
	synchronized public void update(Graphics g) {
		// 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++) {
				int t = current[adr++];
				int c = colors[t];
				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);
	}

	/**
	 * 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) {
	}
}
