/**
 * This software is released as part of the Pumpernickel project.
 * 
 * All com.pump resources in the Pumpernickel project are distributed under the
 * MIT License:
 * https://github.com/mickleness/pumpernickel/raw/master/License.txt
 * 
 * More information about the Pumpernickel project is available here:
 * https://mickleness.github.io/pumpernickel/
 */
package com.pump.awt;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.JComponent;

import com.pump.blog.ResourceSample;
import com.pump.geom.ShapeBounds;
import com.pump.util.ObservableProperties;
import com.pump.util.ObservableProperties.Key;
import com.pump.util.ObservableProperties.NonNullBoundsChecker;

/**
 * This renders a block of text with optional attributes.
 * 
 * <!-- ======== START OF AUTOGENERATED SAMPLES ======== -->
 * <p>
 * Here are some samples:
 * <table summary="Resource&#160;Samples&#160;for&#160;com.pump.awt.TextBlock">
 * <tr>
 * <td></td>
 * <td><img src=
 * "https://github.com/mickleness/pumpernickel/raw/master/resources/samples/TextBlock/sample.png"
 * alt="new&#160;com.pump.awt.TextBlock(0,&#160;&#34;Example&#160;Red&#34;)"></td>
 * <td><img src=
 * "https://github.com/mickleness/pumpernickel/raw/master/resources/samples/TextBlock/sample2.png"
 * alt =
 * "new&#160;com.pump.awt.TextBlock(.2,&#160;&#34;Greenish&#160;Example&#34;)"></td>
 * <td><img src=
 * "https://github.com/mickleness/pumpernickel/raw/master/resources/samples/TextBlock/sample3.png"
 * alt="new&#160;com.pump.awt.TextBlock(.5,&#160;&#34;Example&#160;Cyan&#34;)"></td>
 * </tr>
 * <tr>
 * <td></td>
 * </tr>
 * </table>
 * <!-- ======== END OF AUTOGENERATED SAMPLES ======== -->
 */
@ResourceSample(sample = { "new com.pump.awt.TextBlock(0, \"Example Red\")",
		"new com.pump.awt.TextBlock(.2, \"Greenish Example\")",
		"new com.pump.awt.TextBlock(.5, \"Example Cyan\")" })
public class TextBlock implements Paintable {

	protected static final Key<String> KEY_TEXT = new Key<String>("text",
			String.class, new NonNullBoundsChecker());
	protected static final Key<Font> KEY_FONT = new Key<Font>("font",
			Font.class, new NonNullBoundsChecker());
	protected static final Key<Boolean> KEY_IS_ANTIALIASING = new Key<Boolean>(
			"antialiasing", Boolean.class, new NonNullBoundsChecker());
	protected static final Key<Boolean> KEY_USING_FRACTIONAL_METRICS = new Key<Boolean>(
			"fractional-metrics", Boolean.class, new NonNullBoundsChecker());
	protected static final Key<Integer> KEY_CURVE_WIDTH = new Key<Integer>(
			"curve-width", Integer.class, new NonNullBoundsChecker());
	protected static final Key<Insets> KEY_TEXT_INSETS = new Key<Insets>(
			"text-insets", Insets.class, new NonNullBoundsChecker());
	protected static final Key<Insets> KEY_INSETS = new Key<Insets>("insets",
			Insets.class, new NonNullBoundsChecker());
	protected static final Key<Paint> KEY_BORDER_PAINT = new Key<Paint>(
			"border-paint", Paint.class);
	protected static final Key<Paint> KEY_BACKGROUND = new Key<Paint>("paint",
			Paint.class);
	protected static final Key<Paint> KEY_TEXT_PAINT = new Key<Paint>(
			"text-paint", Paint.class, new NonNullBoundsChecker());
	protected static final Key<Stroke> KEY_STROKE = new Key<Stroke>("stroke",
			Stroke.class);
	protected static final Key<Boolean> KEY_TEXT_SHADOW = new Key<Boolean>(
			"text-shadow", Boolean.class, new NonNullBoundsChecker());
	protected static final Key<Boolean> KEY_BODY_SHADOW = new Key<Boolean>(
			"shadow", Boolean.class, new NonNullBoundsChecker());
	protected static final Key<Color> KEY_SHADOW_BACKGROUND_COLOR = new Key<Color>(
			"background-shadow-color", Color.class, new NonNullBoundsChecker());

	ObservableProperties properties = new ObservableProperties();

	/**
	 * If true then calls to getWidth()/getHeight()/paint() will revalidate the
	 * layout of this TextBlock.
	 */
	protected boolean dirty = true;

	protected int width = 0;
	protected int height = 0;
	protected GeneralPath body = new GeneralPath();

	public TextBlock(String text) {
		this(.45f, text);
	}

	public TextBlock(float hue, String text) {
		properties.addListener(new PropertyChangeListener() {
			@Override
			public void propertyChange(PropertyChangeEvent evt) {
				dirty = true;
			}
		});
		setAntiAliased(true);
		setTextPaint(Color.black);
		setUsingFractionalMetrics(true);
		setFont(new Font("default", Font.BOLD, 14));
		setText(text);
		setInsets(new Insets(2, 2, 2, 2));
		setShadowActive(true);
		setTextShadowActive(true);
		Color c1 = new Color(Color.HSBtoRGB(hue, .8f, 1));
		Color c2 = new Color(Color.HSBtoRGB(hue, .8f, .7f));
		setBackground(new GradientPaint(0, 0, c1, 0, 20, c2));
		setBorderPaint(new Color(0, 0, 0, 90));
		setStroke(new BasicStroke(1));
		setCurveWidth(18);
		setTextInsets(new Insets(2, 8, 2, 8));
		setBackgroundShadowColor(new Color(0, 0, 0, 50));
	}

	@Override
	public int hashCode() {
		return getText().hashCode();
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof TextBlock))
			return false;
		TextBlock block = (TextBlock) obj;
		return properties.equals(block.properties);
	}

	private void setUsingFractionalMetrics(boolean b) {
		properties.set(KEY_USING_FRACTIONAL_METRICS, b);
	}

	private void setAntiAliased(boolean b) {
		properties.set(KEY_IS_ANTIALIASING, b);
	}

	public void setText(String text) {
		properties.set(KEY_TEXT, text);
	}

	public String getText() {
		return properties.get(KEY_TEXT);
	}

	public Font getFont() {
		return properties.get(KEY_FONT);
	}

	public void setFont(Font font) {
		properties.set(KEY_FONT, font);
	}

	public FontRenderContext createFontRenderContext() {
		boolean isAliased = isAntiAliased();
		boolean fractionalMetrics = usesFractionalMetrics();
		return new FontRenderContext(new AffineTransform(), isAliased,
				fractionalMetrics);
	}

	public boolean usesFractionalMetrics() {
		return properties.get(KEY_USING_FRACTIONAL_METRICS);
	}

	public boolean isTextShadowActive() {
		return properties.get(KEY_TEXT_SHADOW);
	}

	public boolean isShadowActive() {
		return properties.get(KEY_BODY_SHADOW);
	}

	public void setTextShadowActive(boolean active) {
		properties.set(KEY_TEXT_SHADOW, active);
	}

	public void setShadowActive(boolean active) {
		properties.set(KEY_BODY_SHADOW, active);
	}

	public boolean isAntiAliased() {
		return properties.get(KEY_IS_ANTIALIASING);
	}

	public int getCurveWidth() {
		return properties.get(KEY_CURVE_WIDTH);
	}

	public void setCurveWidth(int curveWidth) {
		properties.set(KEY_CURVE_WIDTH, curveWidth);
	}

	public Insets getTextInsets() {
		return properties.get(KEY_TEXT_INSETS);
	}

	public void setTextInsets(Insets insets) {
		properties.set(KEY_TEXT_INSETS, insets);
	}

	public Insets getInsets() {
		return properties.get(KEY_INSETS);
	}

	public void setInsets(Insets insets) {
		properties.set(KEY_INSETS, insets);
	}

	protected Rectangle2D getStringBounds() {
		Font font = getFont();
		String text = getText();
		FontRenderContext frc = createFontRenderContext();
		return font.getStringBounds(text, frc);
	}

	/**
	 * This defines width, height and body and may make other adjustments to
	 * prepare this TextBlock to be rendered/measured. This should be called in
	 * a synchronized block.
	 */
	public void revalidate() {
		Rectangle2D stringBounds = getStringBounds();
		Insets insets = getInsets();
		Insets textInsets = getTextInsets();
		int curveWidth = getCurveWidth();
		RoundRectangle2D roundRect = new RoundRectangle2D.Double(insets.left,
				insets.top, stringBounds.getWidth() + textInsets.left
						+ textInsets.right, stringBounds.getHeight()
						+ textInsets.top + textInsets.bottom, curveWidth,
				curveWidth);
		body.reset();
		body.append(roundRect, false);
		width = (int) Math.ceil(roundRect.getWidth() + insets.left
				+ insets.right);
		height = (int) Math.ceil(roundRect.getHeight() + insets.top
				+ insets.bottom);
	}

	@Override
	public synchronized int getWidth() {
		if (dirty)
			revalidate();
		return width;
	}

	@Override
	public synchronized int getHeight() {
		if (dirty)
			revalidate();
		return height;
	}

	@Override
	public synchronized void paint(Graphics2D g) {
		if (dirty)
			revalidate();
		FontRenderContext frc = createFontRenderContext();
		if (frc.isAntiAliased()) {
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
		} else {
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_OFF);
		}
		if (frc.usesFractionalMetrics()) {
			g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
					RenderingHints.VALUE_FRACTIONALMETRICS_ON);
		} else {
			g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
					RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
		}

		paintBackground(g, body);
		paintBorder(g, body);
		paintText(g, body);
	}

	protected void paintText(Graphics2D g, GeneralPath bodyOutline) {
		Font font = getFont();
		String text = getText();
		g.setFont(font);
		FontRenderContext frc = g.getFontRenderContext();
		Rectangle2D r = font.getStringBounds(text, frc);
		LineMetrics lineMetrics = font.getLineMetrics(text, frc);

		Rectangle2D shapeBounds = ShapeBounds.getBounds(bodyOutline);
		float shapeCenterX = (float) shapeBounds.getCenterX();
		float shapeCenterY = (float) shapeBounds.getCenterY();
		float textX = (float) (shapeCenterX - r.getWidth() / 2f);
		float textY = (float) (shapeCenterY + lineMetrics.getAscent() / 2f - lineMetrics
				.getDescent() / 3f);

		if (isTextShadowActive()) {
			Paint paint = this.getTextPaint();
			boolean isTextDark = true;
			if (paint instanceof Color) {
				Color color = (Color) paint;
				isTextDark = (color.getRed() + color.getGreen() + color
						.getBlue()) / 3 < 120;
			}
			g.setColor(isTextDark ? new Color(255, 255, 255, 40) : new Color(0,
					0, 0, 40));
			g.translate(0, 1);
			g.drawString(text, textX, textY);
			g.translate(0, 1);
			g.drawString(text, textX, textY);
			g.translate(0, -2);
		}

		g.setPaint(getTextPaint());
		g.drawString(text, textX, textY);
	}

	protected void paintBorder(Graphics2D g, GeneralPath bodyOutline) {
		Paint border = getBorderPaint();
		Stroke stroke = getStroke();
		if (border != null && stroke != null) {
			g.setStroke(stroke);
			g.setPaint(border);
			g.draw(bodyOutline);
		}
	}

	public Stroke getStroke() {
		return properties.get(KEY_STROKE);
	}

	public Paint getBorderPaint() {
		return properties.get(KEY_BORDER_PAINT);
	}

	public Paint getBackground() {
		return properties.get(KEY_BACKGROUND);
	}

	public void setTextPaint(Paint textColor) {
		properties.set(KEY_TEXT_PAINT, textColor);
	}

	public Paint getTextPaint() {
		return properties.get(KEY_TEXT_PAINT);
	}

	public void setStroke(Stroke stroke) {
		properties.set(KEY_STROKE, stroke);
	}

	public void setBorderPaint(Paint borderPaint) {
		properties.set(KEY_BORDER_PAINT, borderPaint);
	}

	public void setBackground(Paint background) {
		properties.set(KEY_BACKGROUND, background);
	}

	protected void paintBackground(Graphics2D g, GeneralPath bodyOutline) {
		Paint background = getBackground();
		if (background != null) {

			if (isShadowActive()) {
				Graphics2D g2 = (Graphics2D) g.create();
				double dy = 0;
				Stroke stroke = getStroke();
				if (stroke != null) {
					dy = (ShapeBounds.getBounds(
							stroke.createStrokedShape(bodyOutline)).getHeight() - ShapeBounds
							.getBounds(bodyOutline).getHeight()) / 2;
				}
				g2.translate(0, dy);
				g2.setColor(getBackgroundShadowColor());
				g2.translate(0, 1);
				g2.fill(bodyOutline);
				g2.translate(0, 1);
				g2.fill(bodyOutline);
				g2.dispose();
			}

			g.setPaint(background);
			g.fill(bodyOutline);
		}
	}

	public void setBackgroundShadowColor(Color color) {
		properties.set(KEY_SHADOW_BACKGROUND_COLOR, color);
	}

	public Color getBackgroundShadowColor() {
		return properties.get(KEY_SHADOW_BACKGROUND_COLOR);
	}

	public JComponent createComponent() {
		JComponent jc = new JComponent() {
			@Override
			protected void paintComponent(Graphics g) {
				super.paintComponent(g);
				TextBlock.this.paint((Graphics2D) g);
			}
		};
		jc.setOpaque(false);
		jc.setMinimumSize(new Dimension(getWidth(), getHeight()));
		jc.setPreferredSize(new Dimension(getWidth(), getHeight()));
		return jc;
	}

	public BufferedImage createImage() {
		BufferedImage bi = new BufferedImage(getWidth(), getHeight(),
				BufferedImage.TYPE_INT_ARGB);
		Graphics2D g = bi.createGraphics();
		paint(g);
		g.dispose();
		return bi;
	}
}