package jrummikub.view.impl;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;

import jrummikub.model.Position;
import jrummikub.model.Stone;
import jrummikub.model.StoneColor;

/**
 * The StonePainter paints stones and converts between pixel and grid
 * coordinates
 */
class StonePainter {
	enum StoneState {
		NORMAL, SELECTED, INVALID
	}

	private static final double ASPECT_RATIO = 0.75f;
	private static final double DEFAULT_WIDTH = 40;
	private static final double TEXT_POS = 0.275f;
	private static final double FACE_WIDTH = 0.475f;
	private static final double CIRCLE_POS = 0.725f;
	private static final double CIRCLE_WIDTH = 0.45f;

	private static final Color BACKGROUND_COLOR = new Color(0.9f, 0.9f, 0.6f);

	private static final double BRIGHTER_SCALE = 1.15f;
	private static final double HOVER_RATIO = 0.7f;
	private static final double REDDEN_RATIO = 0.3f;

	private Map<StoneState, Map<StoneColor, Map<Integer, BufferedImage>>> defaultStones;
	private Map<StoneState, Map<StoneColor, Map<Integer, BufferedImage>>> hoveredStones;

	/**
	 * The width of one pixel in the scale of 1.0
	 */
	static final double WIDTH_SCALE = 1 / DEFAULT_WIDTH;
	/**
	 * The height of one pixel in the scale of 1.0
	 */
	static final double HEIGHT_SCALE = ASPECT_RATIO / DEFAULT_WIDTH;

	private double scale;

	private static int even(double f) {
		return 2 * (int) (f / 2);
	}

	private static Color brighter(Color color) {
		int r = (int) (color.getRed() * BRIGHTER_SCALE);
		int g = (int) (color.getGreen() * BRIGHTER_SCALE);
		int b = (int) (color.getBlue() * BRIGHTER_SCALE);

		return new Color(r > 255 ? 255 : r, g > 255 ? 255 : g, b > 255 ? 255
				: b);
	}

	private static Color hover(Color color) {
		int r = (int) (color.getRed() * HOVER_RATIO + 255 * (1 - HOVER_RATIO));
		int g = (int) (color.getGreen() * HOVER_RATIO + 255 * (1 - HOVER_RATIO));
		int b = (int) (color.getBlue() * HOVER_RATIO + 255 * (1 - HOVER_RATIO));

		return new Color(r > 255 ? 255 : r, g > 255 ? 255 : g, b > 255 ? 255
				: b);
	}

	private static Color redden(Color color) {
		int r = (int) (color.getRed() * REDDEN_RATIO + 255 * (1 - REDDEN_RATIO));
		int g = (int) (color.getGreen() * REDDEN_RATIO + 128 * (1 - REDDEN_RATIO));
		int b = (int) (color.getBlue() * REDDEN_RATIO + 128 * (1 - REDDEN_RATIO));

		return new Color(r > 255 ? 255 : r, g > 255 ? 255 : g, b > 255 ? 255
				: b);
	}

	static Color getColor(StoneColor color) {
		switch (color) {
		case BLACK:
			return new Color(0.0f, 0.0f, 0.0f);
		case BLUE:
			return new Color(0.0f, 0.0f, 1.0f);
		case ORANGE:
			return new Color(1.0f, 0.4f, 0.0f);
		case RED:
			return new Color(0.9f, 0.0f, 0.25f);
		case AQUA:
			return new Color(0.0f, 0.85f, 0.75f);
		case GREEN:
			return new Color(0.0f, 0.65f, 0.0f);
		case VIOLET:
			return new Color(0.75f, 0.325f, 0.75f);
		case GRAY:
			return new Color(0.5f, 0.5f, 0.5f);
		}

		return null;
	}

	/**
	 * Sets the new grid scale
	 * 
	 * @param scale
	 *            the new scale
	 */
	void setScale(double scale) {
		this.scale = scale;

		if (this.scale == 0) {
			this.scale = 1;
		}

		resetPrepaint();
	}

	/**
	 * @param x
	 *            x position in screen coordinates
	 * @param y
	 *            y position in screen coordinates
	 * @return position in grid coordinates
	 */
	Position calculatePosition(int x, int y) {
		double width = getStoneWidth();
		double height = getStoneHeight();

		return new Position(x / width, y / height);
	}

	/**
	 * @return the width of a stone in the current scale in pixels
	 */
	int getStoneWidth() {
		return Math.max(even(DEFAULT_WIDTH * scale), 1);
	}

	/**
	 * @return the height of a stone in the current scale in pixels
	 */
	int getStoneHeight() {
		return Math.max((int) (DEFAULT_WIDTH * scale / ASPECT_RATIO), 1);
	}

	private BufferedImage prepaintStone(Color fg, Color bg, int value) {
		Rectangle r = new Rectangle(0, 0, getStoneWidth(), getStoneHeight());
		BufferedImage img = new BufferedImage(r.width, r.height,
				BufferedImage.TYPE_INT_RGB);
		Graphics2D g = img.createGraphics();
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);

		paintStoneBackground(g, r, bg);

		if (value == 0) {
			paintJoker(g, r, fg);
		} else {
			paintStoneValue(g, r, fg, value);
		}

		paintCircle(g, r, bg);

		return img;
	}

	private BufferedImage getStoneImage(StoneColor color, int value,
			StoneState state, boolean hovered) {
		Map<StoneColor, Map<Integer, BufferedImage>> stoneMap;

		if (hovered) {
			stoneMap = hoveredStones.get(state);
		} else {
			stoneMap = defaultStones.get(state);
		}

		BufferedImage image = stoneMap.get(color).get(value);

		if (image == null) {
			Color background = BACKGROUND_COLOR;
			Color foreground = getColor(color);

			if (state == StoneState.SELECTED) {
				background = background.darker();
				foreground = foreground.darker();
			} else if (state == StoneState.INVALID) {
				background = redden(background);
				foreground = redden(foreground);
			}

			if (hovered) {
				background = hover(background);
				foreground = hover(foreground);
			}

			image = prepaintStone(foreground, background, value);

			stoneMap.get(color).put(value, image);
		}

		return image;
	}

	private void resetPrepaint() {
		defaultStones = new HashMap<StoneState, Map<StoneColor, Map<Integer, BufferedImage>>>();
		hoveredStones = new HashMap<StoneState, Map<StoneColor, Map<Integer, BufferedImage>>>();

		for (StoneState state : StoneState.values()) {
			Map<StoneColor, Map<Integer, BufferedImage>> defaultStateStones = new HashMap<StoneColor, Map<Integer, BufferedImage>>();
			Map<StoneColor, Map<Integer, BufferedImage>> hoveredStateStones = new HashMap<StoneColor, Map<Integer, BufferedImage>>();

			defaultStones.put(state, defaultStateStones);
			hoveredStones.put(state, hoveredStateStones);

			for (StoneColor color : StoneColor.values()) {
				defaultStateStones.put(color,
						new HashMap<Integer, BufferedImage>());
				hoveredStateStones.put(color,
						new HashMap<Integer, BufferedImage>());
			}
		}
	}

	/**
	 * @param scale
	 *            the scaling factor for the grid coordinates
	 */
	StonePainter(double scale) {
		setScale(scale);
	}

	private void paintStoneBackground(Graphics2D g, Rectangle r,
			Color background) {
		// Paint background
		g.setColor(background);
		g.fillRect(r.x, r.y, r.width, r.height);

		// Paint bevel border
		g.setColor(brighter(brighter(background)));
		g.fillRect(r.x, r.y, 1, r.height);
		g.setColor(brighter(background));
		g.fillRect(r.x + 1, r.y + 1, 1, r.height - 2);

		g.setColor(brighter(brighter(background)));
		g.fillRect(r.x, r.y, r.width, 1);
		g.setColor(brighter(background));
		g.fillRect(r.x + 1, r.y + 1, r.width - 2, 1);

		g.setColor(background.darker().darker());
		g.fillRect(r.x + r.width - 1, r.y, 1, r.height);
		g.setColor(background.darker());
		g.fillRect(r.x + r.width - 2, r.y + 1, 1, r.height - 2);

		g.setColor(background.darker().darker());
		g.fillRect(r.x, r.y + r.height - 1, r.width, 1);
		g.setColor(background.darker());
		g.fillRect(r.x + 1, r.y + r.height - 2, r.width - 2, 1);
	}

	private void paintJokerFace(Graphics2D g, Rectangle r) {
		Stroke oldStroke = g.getStroke();

		g.setStroke(new BasicStroke(2));
		g.drawOval(r.x, r.y, r.width, r.height);

		g.setStroke(new BasicStroke(1));
		GeneralPath path = new GeneralPath();
		// nose
		path.moveTo(r.x + 0.5f * r.width, r.y + 0.45f * r.height);
		path.lineTo(r.x + 0.53f * r.width, r.y + 0.6f * r.height);
		path.lineTo(r.x + 0.47f * r.width, r.y + 0.6f * r.height);
		path.closePath();
		g.fill(path);

		path.reset();
		// mouth, left
		path.moveTo(r.x + 0.23f * r.width, r.y + 0.75f * r.width);
		path.lineTo(r.x + 0.27f * r.width, r.y + 0.65f * r.width);
		// mouth, middle
		path.moveTo(r.x + 0.25f * r.width, r.y + 0.7f * r.width);
		path.lineTo(r.x + 0.5f * r.width, r.y + 0.8f * r.width);
		path.lineTo(r.x + 0.75f * r.width, r.y + 0.7f * r.width);
		// mouth, right
		path.moveTo(r.x + 0.77f * r.width, r.y + 0.75f * r.width);
		path.lineTo(r.x + 0.73f * r.width, r.y + 0.65f * r.width);
		g.draw(path);

		path.reset();
		// left eye
		path.moveTo(r.x + 0.3f * r.width, r.y + 0.41f * r.height);
		path.lineTo(r.x + 0.375f * r.width, r.y + 0.375f * r.height);
		path.lineTo(r.x + 0.3f * r.width, r.y + 0.34f * r.height);
		path.lineTo(r.x + 0.225f * r.width, r.y + 0.375f * r.height);
		path.closePath();
		g.draw(path);

		path.reset();
		// right eye
		path.moveTo(r.x + 0.7f * r.width, r.y + 0.41f * r.height);
		path.lineTo(r.x + 0.625f * r.width, r.y + 0.375f * r.height);
		path.lineTo(r.x + 0.7f * r.width, r.y + 0.34f * r.height);
		path.lineTo(r.x + 0.775f * r.width, r.y + 0.375f * r.height);
		path.closePath();
		g.draw(path);

		g.setStroke(oldStroke);
	}

	private void paintJoker(Graphics2D g, Rectangle r, Color color) {
		int faceSize = even(FACE_WIDTH * r.width);
		int pos = r.y + (int) (TEXT_POS * r.height);

		g.setColor(color);
		paintJokerFace(g, new Rectangle(r.x + r.width / 2 - faceSize / 2, pos
				- faceSize / 2, faceSize, faceSize));
	}

	private void paintStoneValue(Graphics2D g, Rectangle r, Color color, int v) {
		int pos = r.y + (int) (TEXT_POS * r.height);

		g.setFont(new Font("SansSerif", Font.BOLD, r.height / 4));
		FontMetrics fm = g.getFontMetrics();
		String value = (v > 0) ? Integer.toString(v) : Character
				.toString((char) (-v));
		Rectangle2D stringRect = fm.getStringBounds(value, g);

		if (scale > 1) {
			g.setColor(color.darker());
			g.drawString(value,
					(int) (r.x + r.width / 2 - stringRect.getWidth() / 2) + 1,
					pos + (fm.getAscent() - fm.getDescent()) / 2 + 1);
		}
		g.setColor(color);
		g.drawString(value,
				(int) (r.x + r.width / 2 - stringRect.getWidth() / 2), pos
						+ (fm.getAscent() - fm.getDescent()) / 2);
	}

	private void paintCircle(Graphics2D g, Rectangle r, Color background) {
		int size = even(r.width * CIRCLE_WIDTH);
		int pos = r.y + (int) (CIRCLE_POS * r.height);

		// Paint circle
		g.setColor(background.darker());
		g.drawArc(r.x + r.width / 2 - size / 2, pos - size / 2, size, size, 50,
				170);

		g.setColor(brighter(background));
		g.drawArc((int) (r.x + r.width / 2 - size / 2), pos - size / 2, size,
				size, -130, 170);
	}

	/**
	 * Paints a stone
	 * 
	 * @param g
	 *            the graphics context to paint the stone on
	 * @param stone
	 *            the stone to paint
	 * @param p
	 *            the position of the stone
	 * @param state
	 *            if the stone is selected the stone will be painted darker, if
	 *            it is invalid it will be painted in red
	 * @param hovered
	 *            if hovered is true the stone will be painted brighter
	 */
	void paintStone(Graphics2D g, Stone stone, Position p, StoneState state,
			boolean hovered) {
		int width = getStoneWidth();
		int height = getStoneHeight();
		int x = (int) Math.round(p.getX() * width), y = (int) Math.round(p
				.getY() * height);

		if (stone.isJoker()) {
			g.drawImage(getStoneImage(stone.getColor(), 0, state, hovered), x,
					y, null);
		} else {
			g.drawImage(
					getStoneImage(stone.getColor(), stone.getValue(), state,
							hovered), x, y, null);
		}
	}
}
