/*
 * AutomatonCanvas.java 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.math.*;

public class AutomatonCanvas extends Canvas {
	private Automaton parent;

	private int automatonWidth = 250;

	private boolean automaton[][];

	private boolean automaton2[][];

	private Image buffer;

	private Graphics gBuffer;

	private int cornerCount;

	private boolean rules[] = new boolean[512];

	private int baseGridWidth = 30;

	private int gridWidth;

	private int baseGridHeight = 60;

	private int gridHeight;

	private int baseEdge = 4;

	private int edge;

	private int hexagonWidth = 8;

	private int ofs = 20;

	private BigInteger rule;

	private Color white = new Color(255, 255, 255);

	private Color black = new Color(0, 0, 0);

	private int zoom = 4;

	public AutomatonCanvas(Automaton parent) {
		this.parent = parent;

		// Double-Buffering
		buffer = parent.createImage(2 * automatonWidth, 2 * automatonWidth);
		gBuffer = buffer.getGraphics();
		init();
	}

	private void drawSquare(int x0, int y0, boolean fill) {
		if (fill) {
			gBuffer.fillRect(x0, y0, edge + 1, edge + 1);
		} else {
			gBuffer.drawRect(x0, y0, edge, edge);
		}
	}

	private void updateSquareResultDraw(int x, int y, boolean fill) {
		drawSquare(x * gridWidth + edge + ofs, y * gridHeight + 7 * edge + ofs, fill);
	}

	private void updateSquareResult(int x, int y, boolean set) {
		if (set) {
			updateSquareResultDraw(x, y, true);
		} else {
			gBuffer.setColor(white);
			updateSquareResultDraw(x, y, true);
			gBuffer.setColor(black);
			updateSquareResultDraw(x, y, false);
		}
	}

	private void drawHexagon(int x0, int y0, boolean fill) {
		Polygon p = new Polygon();
		p.addPoint(x0 + hexagonWidth / 2, y0);
		p.addPoint(x0 + hexagonWidth, y0 + edge / 2);
		p.addPoint(x0 + hexagonWidth, y0 + edge / 2 + edge);
		p.addPoint(x0 + hexagonWidth / 2, y0 + 2 * edge);
		p.addPoint(x0, y0 + edge / 2 + edge);
		p.addPoint(x0, y0 + edge / 2);
		if (fill) {
			gBuffer.fillPolygon(p);
		} else {
			gBuffer.drawPolygon(p);
		}
	}

	private void updateHexagonResultDraw(int x, int y, boolean fill) {
		drawHexagon(x * gridWidth + hexagonWidth + ofs, y * gridHeight + edge * 7 + ofs, fill);
	}

	private void updateHexagonResult(int x, int y, boolean set) {
		if (set) {
			updateHexagonResultDraw(x, y, true);
		} else {
			gBuffer.setColor(white);
			updateHexagonResultDraw(x, y, true);
			gBuffer.setColor(black);
			updateHexagonResultDraw(x, y, false);
		}
	}

	public void init() {
		automaton = new boolean[automatonWidth][automatonWidth];
		automaton2 = new boolean[automatonWidth][automatonWidth];
		if (parent.isInitPoint())
			automaton[automatonWidth / zoom][automatonWidth / zoom] = true;
		gBuffer.setColor(white);
		gBuffer.fillRect(0, 0, 2 * automatonWidth, 2 * automatonWidth);
		gBuffer.setColor(black);
		cornerCount = parent.getCornerCount();
		rule = parent.getRule();
		for (int i = 0; i < 512; i++) {
			rules[i] = rule.testBit(i);
		}
		if (parent.isRuleMode()) {
			int bit = 0;
			if (cornerCount == 4) {
				gridWidth = baseGridWidth * 2;
				gridHeight = baseGridHeight * 2;
				edge = baseEdge * 2;
				for (int y = 0; y < 4; y++) {
					for (int x = 0; x < 8; x++) {
						updateSquareResult(x, y, rule.testBit(bit));
						gBuffer.drawLine(x * gridWidth + edge * 3 / 2 + ofs, y * gridHeight + edge * 4 + ofs, x
								* gridWidth + edge * 3 / 2 + ofs, y * gridHeight + edge * 6 + ofs);
						gBuffer.drawLine(x * gridWidth + edge * 3 / 2 + ofs, y * gridHeight + edge * 6 + ofs, x
								* gridWidth + edge * 3 / 2 - edge / 2 + ofs, y * gridHeight + edge * 5 + ofs);
						gBuffer.drawLine(x * gridWidth + edge * 3 / 2 + ofs, y * gridHeight + edge * 6 + ofs, x
								* gridWidth + edge * 3 / 2 + edge / 2 + ofs, y * gridHeight + edge * 5 + ofs);
						drawSquare(x * gridWidth + edge + ofs, y * gridHeight + edge * 2 + ofs, (bit & 1) > 0);
						drawSquare(x * gridWidth + edge * 2 + ofs, y * gridHeight + edge + ofs, (bit & 2) > 0);
						drawSquare(x * gridWidth + edge + ofs, y * gridHeight + ofs, (bit & 4) > 0);
						drawSquare(x * gridWidth + ofs, y * gridHeight + edge + ofs, (bit & 8) > 0);
						drawSquare(x * gridWidth + edge + ofs, y * gridHeight + edge + ofs, (bit & 16) > 0);
						bit++;
					}
				}
			} else if (cornerCount == 6) {
				gridWidth = baseGridWidth;
				gridHeight = baseGridHeight;
				edge = baseEdge;
				for (int y = 0; y < 8; y++) {
					for (int x = 0; x < 16; x++) {
						updateHexagonResult(x, y, rule.testBit(bit));
						gBuffer.drawLine(x * gridWidth + hexagonWidth * 3 / 2 + ofs, y * gridHeight + edge * 5 + ofs, x
								* gridWidth + hexagonWidth * 3 / 2 + ofs, y * gridHeight + edge * 7 + ofs);
						gBuffer.drawLine(x * gridWidth + hexagonWidth * 3 / 2 + ofs, y * gridHeight + edge * 7 + ofs, x
								* gridWidth + hexagonWidth * 3 / 2 - edge / 2 + ofs, y * gridHeight + edge * 6 + ofs);
						gBuffer.drawLine(x * gridWidth + hexagonWidth * 3 / 2 + ofs, y * gridHeight + edge * 7 + ofs, x
								* gridWidth + hexagonWidth * 3 / 2 + edge / 2 + ofs, y * gridHeight + edge * 6 + ofs);
						drawHexagon(x * gridWidth + hexagonWidth * 3 / 2 + ofs + 1,
								y * gridHeight + edge * 3 + ofs + 1, (bit & 1) > 0);
						drawHexagon(x * gridWidth + 2 * hexagonWidth + ofs + 1, y * gridHeight + edge * 3 / 2 + ofs,
								(bit & 2) > 0);
						drawHexagon(x * gridWidth + hexagonWidth * 3 / 2 + ofs + 1, y * gridHeight + ofs - 2,
								(bit & 4) > 0);
						drawHexagon(x * gridWidth + hexagonWidth / 2 + ofs - 1, y * gridHeight + ofs - 2, (bit & 8) > 0);
						drawHexagon(x * gridWidth + ofs - 1, y * gridHeight + edge * 3 / 2 + ofs, (bit & 16) > 0);
						drawHexagon(x * gridWidth + hexagonWidth / 2 + ofs - 1, y * gridHeight + edge * 3 + ofs + 2,
								(bit & 32) > 0);
						drawHexagon(x * gridWidth + hexagonWidth + ofs, y * gridHeight + edge * 3 / 2 + ofs,
								(bit & 64) > 0);
						bit++;
					}
				}
			} else if (cornerCount == 8) {
				gridWidth = baseGridWidth / 2;
				gridHeight = baseGridHeight / 2;
				edge = baseEdge / 2;
				for (int y = 0; y < 16; y++) {
					for (int x = 0; x < 32; x++) {
						updateSquareResult(x, y, rule.testBit(bit));
						gBuffer.drawLine(x * gridWidth + edge * 3 / 2 + ofs, y * gridHeight + edge * 4 + ofs, x
								* gridWidth + edge * 3 / 2 + ofs, y * gridHeight + edge * 6 + ofs);
						gBuffer.drawLine(x * gridWidth + edge * 3 / 2 + ofs, y * gridHeight + edge * 6 + ofs, x
								* gridWidth + edge * 3 / 2 - edge / 2 + ofs, y * gridHeight + edge * 5 + ofs);
						gBuffer.drawLine(x * gridWidth + edge * 3 / 2 + ofs, y * gridHeight + edge * 6 + ofs, x
								* gridWidth + edge * 3 / 2 + edge / 2 + ofs, y * gridHeight + edge * 5 + ofs);
						drawSquare(x * gridWidth + edge + ofs, y * gridHeight + edge * 2 + ofs + 1, (bit & 1) > 0);
						drawSquare(x * gridWidth + edge * 2 + ofs + 1, y * gridHeight + edge * 2 + ofs + 1,
								(bit & 2) > 0);
						drawSquare(x * gridWidth + edge * 2 + ofs + 1, y * gridHeight + edge + ofs, (bit & 4) > 0);
						drawSquare(x * gridWidth + edge * 2 + ofs + 1, y * gridHeight + ofs - 1, (bit & 8) > 0);
						drawSquare(x * gridWidth + edge + ofs, y * gridHeight + ofs - 1, (bit & 16) > 0);
						drawSquare(x * gridWidth + ofs - 1, y * gridHeight + ofs - 1, (bit & 32) > 0);
						drawSquare(x * gridWidth + ofs - 1, y * gridHeight + edge + ofs, (bit & 64) > 0);
						drawSquare(x * gridWidth + ofs - 1, y * gridHeight + edge * 2 + ofs + 1, (bit & 128) > 0);
						drawSquare(x * gridWidth + edge + ofs, y * gridHeight + edge + ofs, (bit & 256) > 0);
						bit++;
					}
				}
			}
		} else {
			automaton = new boolean[automatonWidth][automatonWidth];
			automaton2 = new boolean[automatonWidth][automatonWidth];
			if (parent.isInitPoint())
				automaton[automatonWidth / zoom][automatonWidth / zoom] = true;
		}
		repaint();
	}

	public Dimension getSize() {
		return new Dimension(automatonWidth, automatonWidth);
	}

	public Dimension getMinimumSize() {
		return getSize();
	}

	public Dimension getPreferredSize() {
		return getSize();
	}

	public Dimension getMaximumSize() {
		return getSize();
	}

	/**
	 * Overload base method to avoid background clear for flicker-free animation.
	 */
	public void update(Graphics g) {
		paint(g);
	}

	/**
	 * Show picture.
	 */
	public void paint(Graphics g) {
		g.drawImage(buffer, 0, 0, this);
	}

	private void mouseCellAction(int x, int y, boolean pixel) {
		x /= zoom;
		y /= zoom;
		if (x >= 0 && x < automatonWidth && y >= 0 && y < automatonWidth) {
			automaton[x][y] = pixel;
		}
		if (parent.isPaused()) {
			copyBuffer();
			repaint();
		}
	}

	public boolean mouseDrag(Event evt, int x, int y) {
		if (parent.isRuleMode()) {
		} else {
			mouseCellAction(x, y, !evt.metaDown());
		}
		return true;
	}

	public boolean mouseDown(Event evt, int x, int y) {
		if (parent.isRuleMode()) {
			x = (x - ofs) / gridWidth;
			y = (y - ofs) / gridHeight;
			if (cornerCount == 4) {
				if (x >= 0 && x < 8 && y >= 0 && y < 4) {
					int bit = x + y * 8;
					rule = rule.flipBit(bit);
					updateSquareResult(x, y, rule.testBit(bit));
					parent.setRule(rule);
					repaint();
				}
			} else if (cornerCount == 6) {
				if (x >= 0 && x < 16 && y >= 0 && y < 8) {
					int bit = x + y * 16;
					rule = rule.flipBit(bit);
					updateHexagonResult(x, y, rule.testBit(bit));
					parent.setRule(rule);
					repaint();
				}
			} else if (cornerCount == 8) {
				if (x >= 0 && x < 32 && y >= 0 && y < 16) {
					int bit = x + y * 32;
					rule = rule.flipBit(bit);
					updateSquareResult(x, y, rule.testBit(bit));
					parent.setRule(rule);
					repaint();
				}
			}
		} else {
			mouseCellAction(x, y, !evt.metaDown());
		}
		return true;
	}

	private void copyBuffer() {
		int xp, yp;
		for (xp = 0; xp < 2 * automatonWidth / zoom; xp++) {
			for (yp = 0; yp < 2 * automatonWidth / zoom; yp++) {
				Color clr = null;
				if (automaton[xp][yp])
					clr = black;
				else
					clr = white;
				gBuffer.setColor(clr);
				int x = zoom * xp;
				int y = zoom * yp;
				if ((yp % 2) == 1 && cornerCount == 6)
					x += zoom / 2;
				gBuffer.fillRect(x, y, zoom, zoom);
			}
		}
	}

	/**
	 * Calculate next generation.
	 */
	public void step() {
		int xp, yp;
		if (cornerCount == 4) {
			for (xp = 1; xp < automatonWidth - 1; xp++) {
				for (yp = 1; yp < automatonWidth - 1; yp++) {
					int rule = 0;
					if (automaton[xp][yp + 1])
						rule |= 1;
					if (automaton[xp + 1][yp])
						rule |= 2;
					if (automaton[xp][yp - 1])
						rule |= 4;
					if (automaton[xp - 1][yp])
						rule |= 8;
					if (automaton[xp][yp])
						rule |= 16;
					automaton2[xp][yp] = rules[rule];
				}
			}
		} else if (cornerCount == 6) {
			for (xp = 1; xp < automatonWidth - 1; xp++) {
				for (yp = 1; yp < automatonWidth - 1; yp++) {
					int rule = 0;
					if (automaton[xp - 1][yp])
						rule |= 16;
					if (automaton[xp + 1][yp])
						rule |= 2;
					if ((yp % 2) == 0) {
						if (automaton[xp - 1][yp - 1])
							rule |= 8;
						if (automaton[xp - 1][yp + 1])
							rule |= 32;
						if (automaton[xp][yp + 1])
							rule |= 1;
						if (automaton[xp][yp - 1])
							rule |= 4;
					} else {
						if (automaton[xp][yp - 1])
							rule |= 8;
						if (automaton[xp][yp + 1])
							rule |= 32;
						if (automaton[xp + 1][yp + 1])
							rule |= 1;
						if (automaton[xp + 1][yp - 1])
							rule |= 4;
					}
					if (automaton[xp][yp])
						rule |= 64;
					automaton2[xp][yp] = rules[rule];
				}
			}
		} else if (cornerCount == 8) {
			for (xp = 1; xp < automatonWidth - 1; xp++) {
				for (yp = 1; yp < automatonWidth - 1; yp++) {
					int rule = 0;
					if (automaton[xp][yp + 1])
						rule |= 1;
					if (automaton[xp + 1][yp + 1])
						rule |= 2;
					if (automaton[xp + 1][yp])
						rule |= 4;
					if (automaton[xp + 1][yp - 1])
						rule |= 8;
					if (automaton[xp][yp - 1])
						rule |= 16;
					if (automaton[xp - 1][yp - 1])
						rule |= 32;
					if (automaton[xp - 1][yp])
						rule |= 64;
					if (automaton[xp - 1][yp + 1])
						rule |= 128;
					if (automaton[xp][yp])
						rule |= 256;
					automaton2[xp][yp] = rules[rule];
				}
			}
		}
		boolean tmp[][] = automaton;
		automaton = automaton2;
		automaton2 = tmp;

		// copy to double buffer
		copyBuffer();
	}

	public void setZoom(int zoom) {
		this.zoom = zoom;
	}
}