package jrummikub.view.impl;

import java.awt.Insets;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Rectangle2D;
import java.util.Collections;

import javax.swing.JPanel;

import jrummikub.model.Position;
import jrummikub.model.Stone;
import jrummikub.util.Event1;
import jrummikub.util.Event2;
import jrummikub.util.Pair;
import jrummikub.view.IClickable;
import jrummikub.view.IStonePanel;

/**
 * Base class for panels that draw stones
 */
@SuppressWarnings("serial")
abstract class AbstractStonePanel extends JPanel implements IStonePanel,
		IClickable {
	private StonePainter stonePainter;

	private Event1<Position> clickEvent = new Event1<Position>();
	private Event2<Stone, Boolean> stoneClickEvent = new Event2<Stone, Boolean>();
	private Event2<Stone, Boolean> rangeClickEvent = new Event2<Stone, Boolean>();
	private Event2<Stone, Boolean> setClickEvent = new Event2<Stone, Boolean>();

	private Iterable<Pair<Stone, Position>> stones = Collections.emptySet();
	private Stone hoveredStone = null;

	/**
	 * @return the stone painter
	 */
	protected StonePainter getStonePainter() {
		return stonePainter;
	}

	/**
	 * Create a new StonePanel with default scale factor
	 */
	public AbstractStonePanel() {
		this(1);
	}

	/**
	 * Create a new StonePanel with a given scale factor
	 * 
	 * @param scale
	 *            the grid scale
	 */
	public AbstractStonePanel(float scale) {
		super(true); // Set double buffered

		stonePainter = new StonePainter(scale);

		addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				clickAt(e.getPoint(), e.getClickCount(), e.isShiftDown(),
						e.isControlDown(), e.isAltDown());
			}

			@Override
			public void mouseExited(MouseEvent e) {
				setHoveredStone(null);
			}
		});

		addMouseMotionListener(new MouseMotionAdapter() {
			@Override
			public void mouseMoved(MouseEvent e) {
				Insets insets = getInsets();
				Pair<Integer, Integer> trans = getTranslation();
				Position pos = stonePainter.calculatePosition(e.getX()
						- insets.left - trans.getFirst(), e.getY() - insets.top
						- trans.getSecond());

				setHoveredStone(getStoneAt(pos));

				handleOtherMoveEvent(pos);
			}
		});
	}

	/**
	 * clickAt is called when a click has occurred
	 * 
	 * @param p
	 *            the point in component coordinates
	 * @param clickCount
	 *            the click count
	 * @param shift
	 *            is shift down?
	 * @param control
	 *            is control down?
	 * @param alt
	 *            is alt down?
	 */
	protected void clickAt(Point p, int clickCount, boolean shift,
			boolean control, boolean alt) {
		Insets insets = getInsets();
		Pair<Integer, Integer> trans = getTranslation();
		Position pos = stonePainter.calculatePosition(
				p.x - insets.left - trans.getFirst(),
				p.y - insets.top - trans.getSecond());
		if (alt) {
			clickEvent.emit(pos);
			return;
		}

		Stone stone = getStoneAt(pos);

		if (stone == null) {
			if (!handleOtherClickEvent(pos))
				clickEvent.emit(pos);

			return;
		}

		Event2<Stone, Boolean> event = stoneClickEvent;

		if (shift)
			event = rangeClickEvent;
		else if (clickCount >= 2)
			event = setClickEvent;

		event.emit(stone, control);
	}

	/**
	 * Sets the stone over which the cursor hovers
	 * 
	 * @param stone
	 *            stone under cursor
	 */
	private void setHoveredStone(Stone stone) {
		Stone oldStone = hoveredStone;
		hoveredStone = stone;

		if (oldStone != hoveredStone)
			repaint();
	}

	/**
	 * Returns the stone the mouse pointer is hovering over
	 * 
	 * @return the hovered stone
	 */
	protected Stone getHoveredStone() {
		return hoveredStone;
	}

	/**
	 * Overwrite this method to signal if special zone was clicked
	 * 
	 * @param pos
	 *            the clicked position
	 * 
	 * @return special zone clicked
	 */
	protected boolean handleOtherClickEvent(Position pos) {
		return false;
	}

	/**
	 * Overwrite this method to signal if special zone was hovered
	 * 
	 * @param pos
	 *            the hovered position
	 */
	protected void handleOtherMoveEvent(Position pos) {
	}

	/**
	 * Gets the stone at the specified position
	 * 
	 * @param pos
	 *            position
	 * @return the stone
	 */
	private Stone getStoneAt(Position pos) {
		for (Pair<Stone, Position> entry : stones) {
			Stone stone = entry.getFirst();
			Position p = entry.getSecond();
			Rectangle2D rect = new Rectangle2D.Double(p.getX(), p.getY(), 1, 1);

			if (rect.contains(pos.getX(), pos.getY()))
				return stone;
		}

		return null;
	}

	/**
	 * Sets the list of stones that can be clicked on
	 * 
	 * @param stones
	 *            the stones and positions
	 */
	protected void setStones(Iterable<Pair<Stone, Position>> stones) {
		this.stones = stones;
	}

	/**
	 * Returns the translation in pixels the stones in this panel are painted
	 * with
	 * 
	 * @return the translation
	 */
	protected Pair<Integer, Integer> getTranslation() {
		return new Pair<Integer, Integer>(0, 0);
	}

	/**
	 * Returns the list of stones and positions currently set
	 * 
	 * @return the stones
	 */
	protected Iterable<Pair<Stone, Position>> getStones() {
		return stones;
	}

	@Override
	public Event1<Position> getClickEvent() {
		return clickEvent;
	}

	@Override
	public Event2<Stone, Boolean> getStoneClickEvent() {
		return stoneClickEvent;
	}

	@Override
	public Event2<Stone, Boolean> getRangeClickEvent() {
		return rangeClickEvent;
	}

	@Override
	public Event2<Stone, Boolean> getSetClickEvent() {
		return setClickEvent;
	}

}
