/* Copyright 2002 by Basil Vandegriend.  All rights reserved. */

package com.basilv.core.richui;

import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.List;

import javax.swing.ImageIcon;

import com.basilv.core.Assert;

/**
 * An implementation of a two-dimensional graphics rendering context with
 * support for layers. Objects drawn on higher layers will be rendered on top of
 * objects drawn on lower layers.
 * <P>
 * Layers are identified via an integer value. Layers are drawn from largest
 * number to smallest. Within a layer, objects are draw in the order in which
 * they were rendered. The class defines several specific layer number values:
 * <LI>Default layer. If a layer is not specified in a drawing command and has
 * not been set, then the default layer is used.</LI>
 * <LI>Topmost layer. This layer is always drawn on top (drawn last). No layer
 * is permitted to have a value greater than this layer. </LI>
 * <LI>Lowermost layer. This layer is always drawn on the bottom (drawn first).
 * <LI>Invisible layer. This layer is never actually rendered. No layer is
 * permitted to have a value less than this layer. </LI>
 * <P>
 * This class works by wrapping a Graphics2D instance. Calling the various draw
 * methods does not actually draw anything, but queues up the requests. Calling
 * the render() method actually does the drawing.
 */
public class LayeredGraphics
{
	public static final int TOP_LAYER = Integer.MAX_VALUE;

	public static final int DEFAULT_LAYER = 0;

	public static final int BOTTOM_LAYER = Integer.MIN_VALUE + 1;

	public static final int INVISIBLE_LAYER = Integer.MIN_VALUE;

	// TODO:Replace with enums.
	public static final int X_LAYOUT_CENTER = 0;
	public static final int Y_LAYOUT_CENTER_ASCENT = 1;
	public static final int Y_LAYOUT_AT_TOP = 2;
	public static final int Y_LAYOUT_AT_BOTTOM = 3;

	private Graphics2D graphics = null;

	private int layer = DEFAULT_LAYER;

	private Map<Integer, List<DrawingCommand>> layerToDrawingCommandsMap = 
		new HashMap<Integer, List<DrawingCommand>>();

	/**
	 * Create the instance to wrap the specified graphics context.
	 * 
	 * @param aGraphicsContext  Cannot be null.
	 */
	public LayeredGraphics(Graphics2D aGraphicsContext) {
		Assert.notNull("aGraphicsContext", aGraphicsContext);
		graphics = aGraphicsContext;
	}

	/**
	 * Set the layer to use for subsequent drawing commands.
	 * 
	 * @param aLayer  Must be greater than or equal INVISIBLE_LAYER and less
	 *        than or equal to TOP_LAYER.
	 */
	public void setLayer(int aLayer) {
		ensureValidLayer(aLayer);
		layer = aLayer;
	}

	/**
	 * Return the current layer to use for drawing commands not specifying a
	 * layer.
	 * 
	 * @return the current layer.
	 */
	public int getLayer() {
		return layer;
	}

	/**
	 * Actually render to the wrapped Graphics2D context all that has been drawn
	 * since the last render, in the specified order (higher layers on top).
	 */
	public void render() {
		// Retrieve list of layers that have been drawn to and sort them.
		List<Integer> layersDrawnTo = new ArrayList<Integer>(layerToDrawingCommandsMap.keySet());
		Collections.sort(layersDrawnTo);

		// Draw each layer in turn
		for (int drawingLayer : layersDrawnTo) {
			if (drawingLayer != INVISIBLE_LAYER) {
				List<DrawingCommand> layerDrawingCommands = getDrawingCommandsForLayer(drawingLayer);
				for (DrawingCommand drawingCommand : layerDrawingCommands) {
					drawingCommand.draw();
				}
			}
		}
	}

	public void setColor(Color newColor) {
		graphics.setColor(newColor);
	}

	public Font getFont() {
		return graphics.getFont();
	}
	
	public void setFont(Font font) {
		graphics.setFont(font);
	}
	
	public Stroke getStroke() {
		return graphics.getStroke();
	}
	
	public void setStroke(Stroke newStroke) {
		graphics.setStroke(newStroke);
	}

	public void drawRectangle(final Rectangle rectangle, int drawingLayer) {
		ensureValidLayer(drawingLayer);
		Assert.notNull("rectangle", rectangle);

		DrawingCommand command = new StateRememberingDrawingCommand(graphics) {
			@Override
			public void doDraw() {
				graphics.drawRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
			}
		};

		registerDrawingCommand(command, drawingLayer);
	}

	public void fillRectangle(final Rectangle rectangle, int drawingLayer) {
		ensureValidLayer(drawingLayer);
		Assert.notNull("rectangle", rectangle);

		DrawingCommand command = new StateRememberingDrawingCommand(graphics) {
			@Override 
			public void doDraw() {
				graphics.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
			}
		};

		registerDrawingCommand(command, drawingLayer);
	}

	public void fillShape(final Shape shape, int drawingLayer) {
		ensureValidLayer(drawingLayer);
		Assert.notNull("shape", shape);

		DrawingCommand command = new StateRememberingDrawingCommand(graphics) {
			@Override
			public void doDraw() {
				graphics.fill(shape);
			}
		};

		registerDrawingCommand(command, drawingLayer);
	}
	
	
	public void fillPolygon(final Polygon polygon, int drawingLayer) {
		ensureValidLayer(drawingLayer);
		Assert.notNull("polygon", polygon);

		DrawingCommand command = new StateRememberingDrawingCommand(graphics) {
			@Override
			public void doDraw() {
				graphics.fillPolygon(polygon);
			}
		};

		registerDrawingCommand(command, drawingLayer);
	}

	public void drawPolygon(final Polygon polygon, int drawingLayer) {
		ensureValidLayer(drawingLayer);
		Assert.notNull("polygon", polygon);

		DrawingCommand command = new StateRememberingDrawingCommand(graphics) {
			@Override
			public void doDraw() {
				graphics.drawPolygon(polygon);
			}
		};

		registerDrawingCommand(command, drawingLayer);
	}

	public void drawLine(final int x1, final int y1, final int x2, final int y2, 
		int drawingLayer) {
		ensureValidLayer(drawingLayer);

		DrawingCommand command = new StateRememberingDrawingCommand(graphics) {
			@Override
			public void doDraw() {
				graphics.drawLine(x1, y1, x2, y2);
			}
		};

		registerDrawingCommand(command, drawingLayer);	
	}

	public void drawShape(final Shape shape, int drawingLayer) {
		ensureValidLayer(drawingLayer);
		Assert.notNull("shape", shape);

		DrawingCommand command = new StateRememberingDrawingCommand(graphics) {
			@Override
			public void doDraw() {
				graphics.draw(shape);
			}
		};

		registerDrawingCommand(command, drawingLayer);	
	}
	
	/**
	 * Draw the specified image, centered at the specified point, at the
	 * specified layer.
	 * 
	 * @param imageIcon  Cannot be null.
	 * @param centerPoint  Cannot be null.
	 * @param drawingLayer  Must be a valid drawing layer.
	 */
	public void drawImageCentered(ImageIcon imageIcon, Point2D centerPoint, int drawingLayer) {
		final int drawX = (int) (centerPoint.getX() - (imageIcon.getIconWidth() / 2.0));
		final int drawY = (int) (centerPoint.getY() - (imageIcon.getIconHeight() / 2.0));

		final Image image = imageIcon.getImage();

		DrawingCommand command = new StateRememberingDrawingCommand(graphics) {
			@Override
			public void doDraw() {
				graphics.drawImage(image, drawX, drawY, null);
			}
		};

		registerDrawingCommand(command, drawingLayer);
	}

	/**
	 * Draw the specified image whose bottom left corner is aligned with the specified point
	 * 
	 * @param imageIcon  Cannot be null.
	 * @param point  Cannot be null.
	 * @param drawingLayer  Must be a valid drawing layer.
	 */
	public void drawImageAlignedAtBottomLeft(ImageIcon imageIcon, Point2D point, int drawingLayer) {
		
		final int drawX = (int) point.getX();
		final int drawY = (int) (point.getY() - imageIcon.getIconHeight()) ;

		final Image image = imageIcon.getImage();

		DrawingCommand command = new StateRememberingDrawingCommand(graphics) {
			@Override
			public void doDraw() {
				graphics.drawImage(image, drawX, drawY, null);
			}
		};

		registerDrawingCommand(command, drawingLayer);
	}
	
	public void drawText(final String text, Point2D positionRelativeTo, int xLayout, int yLayout, int drawingLayer) {
		// TODO: Add preconditions, refactor how layout is done

		FontMetrics metrics = graphics.getFontMetrics();
		Rectangle2D textBounds = metrics.getStringBounds(text, graphics);

		float textX = 0.0f;
		float textY = 0.0f;

		if (xLayout == X_LAYOUT_CENTER) {
			textX = (float) (positionRelativeTo.getX() - textBounds.getWidth() / 2.0);
		} else {
			Assert.shouldNotReach("Invalid value for xLayout");
		}

		if (yLayout == Y_LAYOUT_AT_TOP) {
			textY = (float) (positionRelativeTo.getY() + textBounds.getHeight());
		} else if (yLayout == Y_LAYOUT_CENTER_ASCENT) {
			textY = (float) (positionRelativeTo.getY() + (metrics.getAscent() / 2.0));
		} else if (yLayout == Y_LAYOUT_AT_BOTTOM) {
			textY = (float) (positionRelativeTo.getY());
		} else {
			Assert.shouldNotReach("Invalid value for yLayout");
		}

		final float finalTextX = textX;
		final float finalTextY = textY;

		DrawingCommand command = new StateRememberingDrawingCommand(graphics) {
			@Override
			public void doDraw() {
				graphics.drawString(text, finalTextX, finalTextY);
			}
		};

		registerDrawingCommand(command, drawingLayer);
	}

	private void registerDrawingCommand(DrawingCommand command, int drawingLayer) {
		getDrawingCommandsForLayer(drawingLayer).add(command);
	}

	private List<DrawingCommand> getDrawingCommandsForLayer(int aLayer) {
		ensureValidLayer(aLayer);

		Integer layerKey = new Integer(aLayer);

		List<DrawingCommand> drawingCommands = layerToDrawingCommandsMap.get(layerKey);
		if (drawingCommands == null) {
			drawingCommands = new ArrayList<DrawingCommand>();
			layerToDrawingCommandsMap.put(layerKey, drawingCommands);
		}

		return drawingCommands;
	}

	private void ensureValidLayer(int aLayer) {
		Assert.isTrue("layer >= INVISIBLE_LAYER", aLayer >= INVISIBLE_LAYER);
		Assert.isTrue("layer <= TOP_LAYER", aLayer <= TOP_LAYER);
	}

	private static interface DrawingCommand
	{
		void draw();
	}
	
	private static abstract class StateRememberingDrawingCommand implements DrawingCommand
	{
		private Graphics2D graphics;
		private Paint paint;
		private Stroke stroke;
		private Font font;
		
		public StateRememberingDrawingCommand(Graphics2D graphics) {
			this.graphics = graphics;
			rememberGraphicsState();
		}

		public final void draw() {
			restoreGraphicsState();
			doDraw();
		}

		public abstract void doDraw();

		private void rememberGraphicsState() {
			paint = graphics.getPaint();
			stroke = graphics.getStroke();
			font = graphics.getFont();
		}

		private void restoreGraphicsState() {
			graphics.setPaint(paint);
			graphics.setStroke(stroke);
			graphics.setFont(font);
		}
		
		
	}

}
