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> {
	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 E pickUp(Position position) {
		for (Map.Entry<E, Pair<E, Position>> i : objects.entrySet()) {
			Position currentPosition = i.getValue().getSecond();
			E currentObject = i.getKey();
			// Tests if position is left of, above ... the current object
			if (position.getX() < currentPosition.getX()) {
				continue;
			}
			if (position.getY() < currentPosition.getY()) {
				continue;
			}
			if (position.getX() > currentPosition.getX() + currentObject.getWidth()) {
				continue;
			}
			if (position.getY() > currentPosition.getY() + currentObject.getHeight()) {
				continue;
			}
			// Position is inside the current object
			objects.remove(i.getKey());
			return currentObject;
		}
		return null;
	}

	@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 p
	 *          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;
	}

	/** 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 (position1.getX() + object1.getWidth() <= position2.getX()) {
			return false;
		}
		if (position1.getY() + object1.getHeight() <= position2.getY()) {
			return false;
		}
		if (position1.getX() >= position2.getX() + object2.getWidth()) {
			return false;
		}
		if (position1.getY() >= position2.getY() + object2.getHeight()) {
			return false;
		}
		return true;
	}

	private Direction getMoveDirection(E object, Position position,
			Pair<E, Position> blocking) {
		boolean isVertical = getMoveOrientation(object, position, blocking);
		float objectMidpointX = position.getX() + object.getWidth() / 2;
		float objectMidpointY = position.getY() + object.getHeight() / 2;
		float blockingMidpointX = blocking.getSecond().getX()
				+ blocking.getFirst().getWidth() / 2;
		float 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;
			}
		}
	}

	private boolean getMoveOrientation(E object, Position position,
			Pair<E, Position> blocking) {
		float objectRight = position.getX() + object.getWidth();
		float blockingRight = blocking.getSecond().getX()
				+ blocking.getFirst().getWidth();
		float overlapRight = Math.min(objectRight, blockingRight);
		float overlapLeft = Math.max(position.getX(), blocking.getSecond().getX());
		float overlapX = overlapRight - overlapLeft;
		float objectBottom = position.getY() + object.getHeight();
		float blockingBottom = blocking.getSecond().getY()
				+ blocking.getFirst().getHeight();
		float overlapBottom = Math.min(objectBottom, blockingBottom);
		float overlapTop = Math.max(position.getY(), blocking.getSecond().getY());
		float 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) {
		return objects.get(object).getSecond();
	}

	@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();
	}

}
