/*
 *  MRMAutomatonCanvas.
 *  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.*;
import java.io.*;
import java.util.zip.*;

//import javax.imageio.ImageIO;

/**
 * Minsky register machine cellular automaton canvas.
 *
 * @author      Frank Buß
 * @see step() for the rules for this CA.
 */
public class MRMAutomatonCanvas
	extends Canvas
	implements ImageProducer, MouseListener, MouseMotionListener {

	/**
	 * Width of the CA state array.
	 */
	private final static int width = 821;

	/**
	 * Height of the CA state array.
	 */
	private final static int height = 290;

	/**
	 * CA states as read from *.dat file in constructor.
	 */
	private byte startData[];

	/**
	 * Current CA state array.
	 */
	private byte current[];

	/**
	 * Destination CA state array, used by step(), swapped with 'current', after
	 * filled.
	 */
	private byte next[];

	/**
	 * State O: dead.
	 */
	private final static byte O = 0;
	private final static byte O3 = 0;

	/**
	 * State M: memory.
	 */
	private final static byte M = 1;
	private final static byte M3 = 3;

	/**
	 * State H: head.
	 */
	private final static byte T = 2;
	private final static byte T3 = 6;

	/**
	 * State T: tail.
	 */
	private final static byte H = 3;
	private final static byte H3 = 9;

	/**
	 * Colors for every state.
	 */
	private final static int colors[] = { 0, 0x00ffff, 0xffff00, 0xffffff };

	/**
	 * 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 MRMAutomatonCanvas() {
		/* uncomment this, if you want to create a new *.dat file from an image,
		 * then start the program once from command line
		try {
			// get image data
			BufferedImage automatonImage =
				ImageIO.read(getClass().getResource("10thprime.gif"));
			int automatonBits[] = new int[width * height];
			PixelGrabber pg =
				new PixelGrabber(
					automatonImage,
					0,
					0,
					width,
					height,
					automatonBits,
					0,
					width);
			pg.grabPixels();
		
			// parse pixels
			byte converted[] = new byte[width * height];
			for (int y = 0; y < height; y++) {
				for (int x = 0; x < width; x++) {
					byte b = 0;
					switch (automatonBits[x + width * y] & 0xffffff) {
						case 0x00ffff :
							b = 1;
							break;
						case 0xffff00 :
							b = 2;
							break;
						case 0xffffff :
							b = 3;
							break;
					}
					converted[x + y * width] = b;
				}
			}
		
			// pack data
			Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
			deflater.setInput(converted);
			deflater.finish();
			byte out[] = new byte[2 * converted.length + 100];
			int size = deflater.deflate(out);
		
			// save packed data		
			FileOutputStream automatonData =
				new FileOutputStream("10thprime.dat");
			automatonData.write(out, 0, size);
			automatonData.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		*/

		// init arrays
		startData = new byte[width * height];
		current = new byte[width * height];
		next = new byte[width * height];
		pixels = new int[width * height];

		try {
			// load data
			DataInputStream in =
				new DataInputStream(
					getClass().getResourceAsStream("10thprime.dat"));
			byte packed[] = new byte[1308];
			in.readFully(packed);

			// unpack data to automaton array
			Inflater inflater = new Inflater();
			inflater.setInput(packed);
			inflater.inflate(startData);
		} catch (Exception e) {
			e.printStackTrace();
		}

		// 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() {
		System.arraycopy(startData, 0, current, 0, startData.length);
	}

	/**
	 * 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++) {

				/*
				There are four states:

					O: Dead
					H: Head (of pulse)
					T: Tail (of pulse)
					M: Memory

				The default next-states for these states, where no further rule is specified, are:

					O -> O
					H -> T
					T -> O
					M -> M

				The rest of the rules are stated in the form c/q/d -> s, where c is the centre cell
				state, q and d are the totals of the orthogonal and diagonal neighbour states
				respectively, and s is the new state.  O states are omitted from q and d for
				readability.

					O/H/- -> H (move)
					O/H/M -> H (pass)
					O/HM/- -> H (pass)
					O/HM/H -> T (copy diode)
					O/H/HH -> M (push)
					O/H/MT -> T (pull)
					M/-/HH -> H (push)
					M/H/TT -> O (pull)
					H/MT/- -> H (long head)
					H/H/M -> O (copy enable)
					H/MTT/- -> M (pull)
					T/H/M -> T (long tail)
					

				So eg M/-/HH means centre cell is M, orthogonal neighbours are all Os, diagonal
				neighbours are 2 Hs and 2 Os in any order.  Written out in full, this would
				be M/OOOO/HHOO.
				*/

				// clear sums
				int q = 0;
				int d = 0;

				// calculate new sums
				q += 1 << (3 * current[adr - 1]);
				q += 1 << (3 * current[adr + 1]);
				q += 1 << (3 * current[adr - width]);
				q += 1 << (3 * current[adr + width]);
				d += 1 << (3 * current[adr - 1 - width]);
				d += 1 << (3 * current[adr + 1 - width]);
				d += 1 << (3 * current[adr + 1 + width]);
				d += 1 << (3 * current[adr - 1 + width]);

				// calculate next cell value				
				byte center = current[adr];
				byte result = center;

				boolean found = false;
				switch (center) {
					case O :
						if ((q == (3 | (1 << H3))) && (d == 4)) {
							result = H;
							found = true;
						} else if (
							(q == (3 | (1 << H3))) && (d == (3 | (1 << M3)))) {
							result = H;
							found = true;
						} else if (
							(q == (2 | (1 << H3) | (1 << M3))) && (d == 4)) {
							result = H;
							found = true;
						} else if (
							(q == (2 | (1 << H3) | (1 << M3)))
								&& (d == (3 | (1 << H3)))) {
							result = T;
							found = true;
						} else if (
							(q == (3 | (1 << H3))) && (d == (2 | (2 << H3)))) {
							result = M;
							found = true;
						} else if (
							(q == (3 | (1 << H3)))
								&& (d == (2 | (1 << M3) | (1 << T3)))) {
							result = T;
							found = true;
						}
						break;
					case M :
						if ((q == 4) && (d == (2 | (2 << H3)))) {
							result = H;
							found = true;
						} else if (
							(q == (3 | (1 << H3))) && (d == (2 | (2 << T3)))) {
							result = O;
							found = true;
						}
						break;
					case H :
						if ((q == (2 | (1 << M3) | (1 << T3))) && (d == 4)) {
							result = H;
							found = true;
						} else if (
							(q == (3 | (1 << H3))) && (d == (3 | (1 << M3)))) {
							result = O;
							found = true;
						} else if (
							(q == (1 | (1 << M3) | (2 << T3))) && (d == 4)) {
							result = M;
							found = true;
						}
						break;
					case T :
						if ((q == (3 | (1 << H3))) && (d == (3 | (1 << M3)))) {
							result = T;
							found = true;
						}
						break;
				}

				if (!found) {
					switch (center) {
						case H :
							result = T;
							break;
						case T :
							result = O;
							break;
					}
				}
				
				// set next state
				next[adr++] = result;
			}
			adr += 2;
		}

		// swap buffers
		byte 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++) {
			for (int x = 1; x < width - 1; x++) {
				int c = colors[current[adr]];
				pixels[adr++] = c;
			}
			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) {
		if (x >= 0 && x < width && y >= 0 && y < height) {
			current[x + y * width] = pixel ? (byte) 0 : (byte) 1;
		}
		repaint();
	}

	// Java specific code (listeners etc.)

	/**
	 * @see java.awt.Component#getPreferredSize()
	 */
	public Dimension getPreferredSize() {
		return new Dimension(width, height);
	}

	/**
	 * @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, height);
			consumer.setProperties(null);
			consumer.setColorModel(cm);
			consumer.setHints(
				ImageConsumer.TOPDOWNLEFTRIGHT
					| ImageConsumer.COMPLETESCANLINES
					| ImageConsumer.SINGLEPASS
					| ImageConsumer.SINGLEFRAME);
			consumer.setPixels(0, 0, width, height, cm, pixels, 0, width);
			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) {
	}
}
