package org.newdawn.slick.font.effects;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.font.Glyph;

/**
 * An effect to generate soft shadows beneath text 
 * 
 * @author Nathan Sweet <[email protected]>
 */
public class ShadowEffect implements ConfigurableEffect {
	/** The number of kernels to apply */
	public static final int NUM_KERNELS = 16;
	/** The blur kernels applied across the effect */
	public static final float[][] GAUSSIAN_BLUR_KERNELS = generateGaussianBlurKernels(NUM_KERNELS);

	/** The colour of the shadow to render */
	private Color color = Color.black;
	/** The transparency factor of the shadow */
	private float opacity = 0.6f;
	/** The distance on the x axis of the shadow from the text */
	private float xDistance = 2;
	/** The distance on the y axis of the shadow from the text */
	private float yDistance = 2;
	/** The size of the kernel used to blur the shadow */
	private int blurKernelSize = 0;
	/** The number of passes applied to create the blur */
	private int blurPasses = 1;

	/**
	 * Default constructor for injection
	 */
	public ShadowEffect() {
	}

	/**
	 * Create a new effect to apply a drop shadow to text
	 * 
	 * @param color The colour of the shadow to generate
	 * @param xDistance The distance from the text on the x axis the shadow should be rendered
	 * @param yDistance The distance from the text on the y axis the shadow should be rendered
	 * @param opacity The transparency factor of the shadow
	 */
	public ShadowEffect (Color color, int xDistance, int yDistance, float opacity) {
		this.color = color;
		this.xDistance = xDistance;
		this.yDistance = yDistance;
		this.opacity = opacity;
	}

	/**
	 * @see org.newdawn.slick.font.effects.Effect#draw(java.awt.image.BufferedImage, java.awt.Graphics2D, org.newdawn.slick.UnicodeFont, org.newdawn.slick.font.Glyph)
	 */
	public void draw(BufferedImage image, Graphics2D g, UnicodeFont unicodeFont, Glyph glyph) {
		g = (Graphics2D)g.create();
		g.translate(xDistance, yDistance);
		g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), Math.round(opacity * 255)));
		g.fill(glyph.getShape());

		// Also shadow the outline, if one exists.
		for (Iterator iter = unicodeFont.getEffects().iterator(); iter.hasNext();) {
			Effect effect = (Effect)iter.next();
			if (effect instanceof OutlineEffect) {
				Composite composite = g.getComposite();
				g.setComposite(AlphaComposite.Src); // Prevent shadow and outline shadow alpha from combining.

				g.setStroke(((OutlineEffect)effect).getStroke());
				g.draw(glyph.getShape());

				g.setComposite(composite);
				break;
			}
		}

		g.dispose();
		if (blurKernelSize > 1 && blurKernelSize < NUM_KERNELS && blurPasses > 0) blur(image);
	}

	/**
	 * Apply blurring to the generate image
	 * 
	 * @param image The image to be blurred
	 */
	private void blur(BufferedImage image) {
		float[] matrix = GAUSSIAN_BLUR_KERNELS[blurKernelSize - 1];
		Kernel gaussianBlur1 = new Kernel(matrix.length, 1, matrix);
		Kernel gaussianBlur2 = new Kernel(1, matrix.length, matrix);
		RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
		ConvolveOp gaussianOp1 = new ConvolveOp(gaussianBlur1, ConvolveOp.EDGE_NO_OP, hints);
		ConvolveOp gaussianOp2 = new ConvolveOp(gaussianBlur2, ConvolveOp.EDGE_NO_OP, hints);
		BufferedImage scratchImage = EffectUtil.getScratchImage();
		for (int i = 0; i < blurPasses; i++) {
			gaussianOp1.filter(image, scratchImage);
			gaussianOp2.filter(scratchImage, image);
		}
	}

	/**
	 * Get the colour of the shadow generated
	 * 
	 * @return The colour of the shadow generated
	 */
	public Color getColor() {
		return color;
	}

	/**
	 * Set the colour of the shadow to be generated
	 * 
	 * @param color The colour ofthe shadow to be generated
	 */
	public void setColor(Color color) {
		this.color = color;
	}

	/**
	 * Get the distance on the X axis from the text the shadow should
	 * be generated at
	 * 
	 * @return The distance on the X axis the shadow will be from the text
	 */
	public float getXDistance() {
		return xDistance;
	}

	/**
	 * Sets the pixels to offset the shadow on the x axis. The glyphs will need padding so the 
	 * shadow doesn't get clipped.
	 * 
	 * @param distance The offset on the x axis
	 */
	public void setXDistance(float distance) {
		xDistance = distance;
	}

	/**
	 * Get the distance on the Y axis from the text the shadow should
	 * be generated at
	 * 
	 * @return The distance on the Y axis the shadow will be from the text
	 */
	public float getYDistance() {
		return yDistance;
	}

	/**
	 * Sets the pixels to offset the shadow on the y axis. The glyphs will need 
	 * padding so the shadow doesn't get clipped.
	 * 
	 * @param distance The offset on the y axis
	 */
	public void setYDistance (float distance) {
		yDistance = distance;
	}

	/**
	 * Get the size of the kernel used to apply the blur
	 * 
	 * @return The blur kernel size
	 */
	public int getBlurKernelSize() {
		return blurKernelSize;
	}

	/**
	 * Sets how many neighboring pixels are used to blur the shadow. Set to 0 for no blur.
	 * 
	 * @param blurKernelSize The size of the kernel to apply the blur with
	 */
	public void setBlurKernelSize (int blurKernelSize) {
		this.blurKernelSize = blurKernelSize;
	}

	/**
	 * Get the number of passes to apply the kernel for blurring
	 * 
	 * @return The number of passes
	 */
	public int getBlurPasses() {
		return blurPasses;
	}

	/**
	 * Sets the number of times to apply a blur to the shadow. Set to 0 for no blur.
	 * 
	 * @param blurPasses The number of passes to apply when blurring
	 */
	public void setBlurPasses (int blurPasses) {
		this.blurPasses = blurPasses;
	}

	/**
	 * Get the opacity of the shadow, i.e. how transparent it is
	 * 
	 * @return The opacity of the shadow
	 */
	public float getOpacity() {
		return opacity;
	}

	/**
	 * Set the opacity of the shadow, i.e. how transparent it is
	 * 
	 * @param opacity The opacity of the shadow
	 */
	public void setOpacity(float opacity) {
		this.opacity = opacity;
	}

	/**
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		return "Shadow";
	}

	/**
	 * @see org.newdawn.slick.font.effects.ConfigurableEffect#getValues()
	 */
	public List getValues() {
		List values = new ArrayList();
		values.add(EffectUtil.colorValue("Color", color));
		values.add(EffectUtil.floatValue("Opacity", opacity, 0, 1, "This setting sets the translucency of the shadow."));
		values.add(EffectUtil.floatValue("X distance", xDistance, Float.MIN_VALUE, Float.MAX_VALUE, "This setting is the amount of pixels to offset the shadow on the"
			+ " x axis. The glyphs will need padding so the shadow doesn't get clipped."));
		values.add(EffectUtil.floatValue("Y distance", yDistance, Float.MIN_VALUE, Float.MAX_VALUE, "This setting is the amount of pixels to offset the shadow on the"
			+ " y axis. The glyphs will need padding so the shadow doesn't get clipped."));

		List options = new ArrayList();
		options.add(new String[] {"None", "0"});
		for (int i = 2; i < NUM_KERNELS; i++)
			options.add(new String[] {String.valueOf(i)});
		String[][] optionsArray = (String[][])options.toArray(new String[options.size()][]);
		values.add(EffectUtil.optionValue("Blur kernel size", String.valueOf(blurKernelSize), optionsArray,
			"This setting controls how many neighboring pixels are used to blur the shadow. Set to \"None\" for no blur."));

		values.add(EffectUtil.intValue("Blur passes", blurPasses,
			"The setting is the number of times to apply a blur to the shadow. Set to \"0\" for no blur."));
		return values;
	}

	/**
	 * @see org.newdawn.slick.font.effects.ConfigurableEffect#setValues(java.util.List)
	 */
	public void setValues(List values) {
		for (Iterator iter = values.iterator(); iter.hasNext();) {
			Value value = (Value)iter.next();
			if (value.getName().equals("Color")) {
				color = (Color)value.getObject();
			} else if (value.getName().equals("Opacity")) {
				opacity = ((Float)value.getObject()).floatValue();
			} else if (value.getName().equals("X distance")) {
				xDistance = ((Float)value.getObject()).floatValue();
			} else if (value.getName().equals("Y distance")) {
				yDistance = ((Float)value.getObject()).floatValue();
			} else if (value.getName().equals("Blur kernel size")) {
				blurKernelSize = Integer.parseInt((String)value.getObject());
			} else if (value.getName().equals("Blur passes")) {
				blurPasses = ((Integer)value.getObject()).intValue();
			}
		}
	}

	/**
	 * Generate the blur kernels which will be repeatedly applied when blurring images
	 * 
	 * @param level The number of kernels to generate
	 * @return The kernels generated
	 */
	private static float[][] generateGaussianBlurKernels(int level) {
		float[][] pascalsTriangle = generatePascalsTriangle(level);
		float[][] gaussianTriangle = new float[pascalsTriangle.length][];
		for (int i = 0; i < gaussianTriangle.length; i++) {
			float total = 0.0f;
			gaussianTriangle[i] = new float[pascalsTriangle[i].length];
			for (int j = 0; j < pascalsTriangle[i].length; j++)
				total += pascalsTriangle[i][j];
			float coefficient = 1 / total;
			for (int j = 0; j < pascalsTriangle[i].length; j++)
				gaussianTriangle[i][j] = coefficient * pascalsTriangle[i][j];
		}
		return gaussianTriangle;
	}

	/**
	 * Generate Pascal's triangle
	 * 
	 * @param level The level of the triangle to generate
	 * @return The Pascal's triangle kernel
	 */
	private static float[][] generatePascalsTriangle(int level) {
		if (level < 2) level = 2;
		float[][] triangle = new float[level][];
		triangle[0] = new float[1];
		triangle[1] = new float[2];
		triangle[0][0] = 1.0f;
		triangle[1][0] = 1.0f;
		triangle[1][1] = 1.0f;
		for (int i = 2; i < level; i++) {
			triangle[i] = new float[i + 1];
			triangle[i][0] = 1.0f;
			triangle[i][i] = 1.0f;
			for (int j = 1; j < triangle[i].length - 1; j++)
				triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j];
		}
		return triangle;
	}
}