package com.docuverse.identicon;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.math.BigInteger;

/**
 * 9-block Identicon renderer.
 * 
 * <p>
 * Current implementation uses only the lower 32 bits of identicon code.
 * </p>
 * 
 * @author don
 * 
 */
public class NineBlockIdenticonRenderer2 implements IdenticonRenderer {

	/*
	 * Each patch is a polygon created from a list of vertices on a 5 by 5 grid.
	 * Vertices are numbered from 0 to 24, starting from top-left corner of the
	 * grid, moving left to right and top to bottom.
	 */

	private static final int PATCH_GRIDS = 5;

	private static final float DEFAULT_PATCH_SIZE = 20.0f;

	private static final byte PATCH_SYMMETRIC = 1;

	private static final byte PATCH_INVERTED = 2;

	private static final int PATCH_MOVETO = -1;

	private static final byte[] patch0 = { 0, 4, 24, 20 };

	private static final byte[] patch1 = { 0, 4, 20 };

	private static final byte[] patch2 = { 2, 24, 20 };

	private static final byte[] patch3 = { 0, 2, 20, 22 };

	private static final byte[] patch4 = { 2, 14, 22, 10 };

	private static final byte[] patch5 = { 0, 14, 24, 22 };

	private static final byte[] patch6 = { 2, 24, 22, 13, 11, 22, 20 };

	private static final byte[] patch7 = { 0, 14, 22 };

	private static final byte[] patch8 = { 6, 8, 18, 16 };

	private static final byte[] patch9 = { 4, 20, 10, 12, 2 };

	private static final byte[] patch10 = { 0, 2, 12, 10 };

	private static final byte[] patch11 = { 10, 14, 22 };

	private static final byte[] patch12 = { 20, 12, 24 };

	private static final byte[] patch13 = { 10, 2, 12 };

	private static final byte[] patch14 = { 0, 2, 10 };

	private static final byte[] patchTypes[] = { patch0, patch1, patch2,
			patch3, patch4, patch5, patch6, patch7, patch8, patch9, patch10,
			patch11, patch12, patch13, patch14, patch0 };

	private static final byte patchFlags[] = { PATCH_SYMMETRIC, 0, 0, 0,
			PATCH_SYMMETRIC, 0, 0, 0, PATCH_SYMMETRIC, 0, 0, 0, 0, 0, 0,
			PATCH_SYMMETRIC + PATCH_INVERTED };

	private static int centerPatchTypes[] = { 0, 4, 8, 15 };

	private float patchSize;

	private GeneralPath[] patchShapes;

	// used to center patch shape at origin because shape rotation works
	// correctly.
	private float patchOffset;

	private Color backgroundColor = Color.WHITE;

	/**
	 * Constructor.
	 * 
	 */
	public NineBlockIdenticonRenderer2() {
		setPatchSize(DEFAULT_PATCH_SIZE);
	}

	/**
	 * Returns the size in pixels at which each patch will be rendered before
	 * they are scaled down to requested identicon size.
	 * 
	 * @return the size
	 */
	public float getPatchSize() {
		return patchSize;
	}

	/**
	 * Set the size in pixels at which each patch will be rendered before they
	 * are scaled down to requested identicon size. Default size is 20 pixels
	 * which means, for 9-block identicon, a 60x60 image will be rendered and
	 * scaled down.
	 * 
	 * @param size
	 *            patch size in pixels
	 */
	public void setPatchSize(float size) {
		this.patchSize = size;
		this.patchOffset = patchSize / 2.0f; // used to center patch shape at
		float patchScale = patchSize / 4.0f;
		// origin.
		this.patchShapes = new GeneralPath[patchTypes.length];
		for (int i = 0; i < patchTypes.length; i++) {
			GeneralPath patch = new GeneralPath(GeneralPath.WIND_NON_ZERO);
			boolean moveTo = true;
			byte[] patchVertices = patchTypes[i];
			for (int j = 0; j < patchVertices.length; j++) {
				int v = (int) patchVertices[j];
				if (v == PATCH_MOVETO)
					moveTo = true;
				float vx = ((v % PATCH_GRIDS) * patchScale) - patchOffset;
				float vy = ((float) Math.floor(((float) v) / PATCH_GRIDS))
						* patchScale - patchOffset;
				if (!moveTo) {
					patch.lineTo(vx, vy);
				} else {
					moveTo = false;
					patch.moveTo(vx, vy);
				}
			}
			patch.closePath();
			this.patchShapes[i] = patch;
		}
	}

	public Color getBackgroundColor() {
		return backgroundColor;
	}

	public void setBackgroundColor(Color backgroundColor) {
		this.backgroundColor = backgroundColor;
	}

	public BufferedImage render(BigInteger code, int size) {
		return renderQuilt(code.intValue(), size);
	}

	/**
	 * Returns rendered identicon image for given identicon code.
	 * 
	 * <p>
	 * Size of the returned identicon image is determined by patchSize set using
	 * {@link #setPatchSize}. Since a 9-block identicon consists of 3x3 patches,
	 * width and height will be 3 times the patch size.
	 * </p>
	 * 
	 * @param code
	 *            identicon code
	 * @param size
	 *            image size
	 * @return identicon image
	 */
	public BufferedImage render(int code, int size) {
		return renderQuilt(code, size);
	}

	protected BufferedImage renderQuilt(int code, int size) {
		// -------------------------------------------------
		// PREPARE
		//

		// decode the code into parts
		// bit 0-1: middle patch type
		// bit 2: middle invert
		// bit 3-6: corner patch type
		// bit 7: corner invert
		// bit 8-9: corner turns
		// bit 10-13: side patch type
		// bit 14: side invert
		// bit 15: corner turns
		// bit 16-20: blue color component
		// bit 21-26: green color component
		// bit 27-31: red color component
		int middleType = centerPatchTypes[code & 0x3];
		boolean middleInvert = ((code >> 2) & 0x1) != 0;
		int cornerType = (code >> 3) & 0x0f;
		boolean cornerInvert = ((code >> 7) & 0x1) != 0;
		int cornerTurn = (code >> 8) & 0x3;
		int sideType = (code >> 10) & 0x0f;
		boolean sideInvert = ((code >> 14) & 0x1) != 0;
		int sideTurn = (code >> 15) & 0x3;
		int blue = (code >> 16) & 0x01f;
		int green = (code >> 21) & 0x01f;
		int red = (code >> 27) & 0x01f;

		// color components are used at top of the range for color difference
		// use white background for now.
		// TODO: support transparency.
		Color fillColor = new Color(red << 3, green << 3, blue << 3);

		// outline shapes with a noticeable color (complementary will do) if
		// shape color and background color are too similar (measured by color
		// distance).
		Color strokeColor = null;
		if (getColorDistance(fillColor, backgroundColor) < 32.0f)
			strokeColor = getComplementaryColor(fillColor);

		// -------------------------------------------------
		// RENDER
		//

		BufferedImage targetImage = new BufferedImage(size, size,
				BufferedImage.TYPE_INT_RGB);
		Graphics2D g = targetImage.createGraphics();
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);

		g.setBackground(backgroundColor);
		g.clearRect(0, 0, size, size);

		float blockSize = size / 3.0f;
		float blockSize2 = blockSize * 2.0f;

		// middle patch
		drawPatch(g, blockSize, blockSize, blockSize, middleType, 0,
				middleInvert, fillColor, strokeColor);

		// side patchs, starting from top and moving clock-wise
		drawPatch(g, blockSize, 0, blockSize, sideType, sideTurn++, sideInvert,
				fillColor, strokeColor);
		drawPatch(g, blockSize2, blockSize, blockSize, sideType, sideTurn++,
				sideInvert, fillColor, strokeColor);
		drawPatch(g, blockSize, blockSize2, blockSize, sideType, sideTurn++,
				sideInvert, fillColor, strokeColor);
		drawPatch(g, 0, blockSize, blockSize, sideType, sideTurn++, sideInvert,
				fillColor, strokeColor);

		// corner patchs, starting from top left and moving clock-wise
		drawPatch(g, 0, 0, blockSize, cornerType, cornerTurn++, cornerInvert,
				fillColor, strokeColor);
		drawPatch(g, blockSize2, 0, blockSize, cornerType, cornerTurn++,
				cornerInvert, fillColor, strokeColor);
		drawPatch(g, blockSize2, blockSize2, blockSize, cornerType,
				cornerTurn++, cornerInvert, fillColor, strokeColor);
		drawPatch(g, 0, blockSize2, blockSize, cornerType, cornerTurn++,
				cornerInvert, fillColor, strokeColor);

		g.dispose();

		return targetImage;
	}

	private void drawPatch(Graphics2D g, float x, float y, float size,
			int patch, int turn, boolean invert, Color fillColor,
			Color strokeColor) {
		assert patch >= 0;
		assert turn >= 0;
		patch %= patchTypes.length;
		turn %= 4;
		if ((patchFlags[patch] & PATCH_INVERTED) != 0)
			invert = !invert;

		Shape shape = patchShapes[patch];
		double scale = ((double) size) / ((double) patchSize);
		float offset = size / 2.0f;

		// paint background
		g.setColor(invert ? fillColor : backgroundColor);
		g.fill(new Rectangle2D.Float(x, y, size, size));

		AffineTransform savet = g.getTransform();
		g.translate(x + offset, y + offset);
		g.scale(scale, scale);
		g.rotate(Math.toRadians(turn * 90));

		// if stroke color was specified, apply stroke
		// stroke color should be specified if fore color is too close to the
		// back color.
		if (strokeColor != null) {
			g.setColor(strokeColor);
			g.draw(shape);
		}

		// render rotated patch using fore color (back color if inverted)
		g.setColor(invert ? backgroundColor : fillColor);
		g.fill(shape);

		g.setTransform(savet);
	}

	/**
	 * Returns distance between two colors.
	 * 
	 * @param c1
	 * @param c2
	 * @return the distance
	 */
	private float getColorDistance(Color c1, Color c2) {
		float dx = c1.getRed() - c2.getRed();
		float dy = c1.getGreen() - c2.getGreen();
		float dz = c1.getBlue() - c2.getBlue();
		return (float) Math.sqrt(dx * dx + dy * dy + dz * dz);
	}

	/**
	 * Returns complementary color.
	 * 
	 * @param color
	 * @return the complement
	 */
	private Color getComplementaryColor(Color color) {
		return new Color(color.getRGB() ^ 0x00FFFFFF);
	}
}
