package jrummikub.model;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import jrummikub.util.Pair;

/**
 * A StoneTray is a collection of positioned objects (for example {@link Stone}s
 * or {@link StoneSet}s.
 * 
 * @param <E>
 *          Type of positioned objects (must implement Sizeable)
 */
public class StoneTray<E extends Sizeable> implements IStoneTray<E> {
	private static final long serialVersionUID = -6329309928640027222L;

	protected HashMap<E, Pair<E, Position>> objects = new HashMap<E, Pair<E, Position>>();

	/** Possible move directions in case of overlapping Stones/Sets */

	protected static enum Direction {
		LEFT, RIGHT, TOP, BOTTOM;
	}

	@Override
	public void drop(E object, Position position) {
		if (object != null) {
			drop(object, position, null);
		}
	}

	private void drop(E object, Position position, Direction direction) {
		Pair<Position, Direction> update = fixInvalidDrop(object, position,
				direction);
		if (update != null) {
			position = update.getFirst();
			direction = update.getSecond();
		}

		dropUnchecked(object, position, direction);
	}

	@SuppressWarnings("unchecked")
	private void dropUnchecked(E object, Position position, Direction direction) {
		objects.put(object, new Pair<E, Position>(object, position));
		for (Pair<E, Position> i : ((Map<E, Pair<E, Position>>) objects.clone())
				.values()) {
			Direction newDirection = direction;
			E currentObject = i.getFirst();
			if (currentObject == object)
				continue;
			Position currentPosition = getPosition(currentObject);
			if (!objectsOverlap(object, position, currentObject, currentPosition)) {
				continue;
			}
			// Object would be placed inside the current object
			if (newDirection == null) {
				newDirection = getMoveDirection(object, position, i);
			}
			Position newPosition = null;
			// Move object to avoid overlap
			switch (newDirection) {
				case TOP:
					newPosition = new Position(currentPosition.getX(), position.getY()
							- currentObject.getHeight());
					break;
				case BOTTOM:
					newPosition = new Position(currentPosition.getX(), position.getY()
							+ object.getHeight());
					break;
				case LEFT:
					newPosition = new Position(
							position.getX() - currentObject.getWidth(),
							currentPosition.getY());
					break;
				case RIGHT:
					newPosition = new Position(position.getX() + object.getWidth(),
							currentPosition.getY());
					break;
			}

			objects.remove(currentObject);
			drop(currentObject, newPosition, newDirection);
		}
	}

	/**
	 * Checks whether the object may be placed on the given position, computes new
	 * position if not
	 * 
	 * @param object
	 *          to be dropped
	 * @param dir
	 * @param pos
	 *          the object is dropped at
	 * @return null if the drop is valid, new position otherwise
	 */
	protected Pair<Position, Direction> fixInvalidDrop(E object, Position pos,
			Direction dir) {
		return null;
	}

	private static boolean lessOrEqual(double d, double e) {
		if (-0.000001 < e && e < 0.000001) {
			return (d < e + 0.000001);
		}

		double q = d / e;
		if (0.999999 < q && q < 1.000001) {
			return true;
		}

		return d < e;
	}

	/** Tests whether two objects overlap **/
	private boolean objectsOverlap(E object1, Position position1, E object2,
			Position position2) {
		// Tests if position is left of, above ... the current object
		if (lessOrEqual(position1.getX() + object1.getWidth(), position2.getX())) {
			return false;
		}
		if (lessOrEqual(position1.getY() + object1.getHeight(), position2.getY())) {
			return false;
		}
		if (lessOrEqual(position2.getX() + object2.getWidth(), position1.getX())) {
			return false;
		}
		if (lessOrEqual(position2.getY() + object2.getHeight(), position1.getY())) {
			return false;
		}
		return true;
	}

	private Direction getMoveDirection(E object, Position position,
			Pair<E, Position> blocking) {
		boolean isVertical = getMoveOrientation(object, position, blocking);
		double objectMidpointX = position.getX() + object.getWidth() / 2;
		double objectMidpointY = position.getY() + object.getHeight() / 2;
		double blockingMidpointX = blocking.getSecond().getX()
				+ blocking.getFirst().getWidth() / 2;
		double blockingMidpointY = blocking.getSecond().getY()
				+ blocking.getFirst().getHeight() / 2;
		if (isVertical) {
			if (objectMidpointY < blockingMidpointY) {
				return Direction.BOTTOM;
			} else {
				return Direction.TOP;
			}
		} else {
			if (objectMidpointX < blockingMidpointX) {
				return Direction.RIGHT;
			} else {
				return Direction.LEFT;
			}
		}
	}

	/**
	 * Will the object be moved horizontally or vertically
	 * 
	 * @return boolean vertical movement
	 */
	private boolean getMoveOrientation(E object, Position position,
			Pair<E, Position> blocking) {
		double objectRight = position.getX() + object.getWidth();
		double blockingRight = blocking.getSecond().getX()
				+ blocking.getFirst().getWidth();
		double overlapRight = Math.min(objectRight, blockingRight);
		double overlapLeft = Math.max(position.getX(), blocking.getSecond().getX());
		double overlapX = overlapRight - overlapLeft;
		double objectBottom = position.getY() + object.getHeight();
		double blockingBottom = blocking.getSecond().getY()
				+ blocking.getFirst().getHeight();
		double overlapBottom = Math.min(objectBottom, blockingBottom);
		double overlapTop = Math.max(position.getY(), blocking.getSecond().getY());
		double overlapY = overlapBottom - overlapTop;
		// vertical or horizontal Shift
		// TODO magic factor
		return overlapX > overlapY;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jrummikub.model.IStoneTray#getPosition(E)
	 */
	@Override
	public Position getPosition(E object) {
		Pair<E, Position> entry = objects.get(object);
		if (entry == null) {
			return null;
		}
		return entry.getSecond();
	}

	@Override
	public boolean contains(E object) {
		return objects.get(object) != null;
	}

	@Override
	public Iterator<Pair<E, Position>> iterator() {
		return objects.values().iterator();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jrummikub.model.IStoneTray#pickUp(E)
	 */
	@Override
	public boolean pickUp(E object) {
		return null != objects.remove(object);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jrummikub.model.IStoneTray#clone()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public IStoneTray<E> clone() {
		try {
			StoneTray<E> copy = (StoneTray<E>) super.clone();
			copy.objects = (HashMap<E, Pair<E, Position>>) objects.clone();

			return copy;
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();

			return null;
		}
	}

	@Override
	public int getSize() {
		return objects.size();
	}

}
