package jrummikub.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import jrummikub.util.Pair;
import static jrummikub.model.StoneSet.Type.*;

/** Class managing {@link Stone}s joined together to form sets */
public class StoneSet implements Iterable<Stone>, Sizeable {
	static final float VERTICAL_BORDER = 0.5f;
	static final float HORIZONTAL_BORDER = 0.125f;
	private List<Stone> stones;

	/**
	 * Create a new single stone stone set
	 * 
	 * @param stone
	 *            single stone of the set
	 */
	public StoneSet(Stone stone) {
		stones = Collections.singletonList(stone);
	}

	/**
	 * Create a stone set from a list of stones
	 * 
	 * @param stones
	 *            list of stones to build a set of
	 */
	public StoneSet(List<Stone> stones) {
		this.stones = new ArrayList<Stone>(stones);
	}

	/** Validity type of the set */
	public enum Type {
		/** Set is a valid group */
		GROUP,
		/** Set is a valid run */
		RUN,
		/** Set is invalid */
		INVALID
	}

	/**
	 * Test for rule conflict within the StoneSet
	 * 
	 * @return true when the set is valid according to the rules
	 */
	public boolean isValid() {
		return classify().getFirst() != INVALID;
	}

	/**
	 * Test for rule conflict within the StoneSet and determine whether the set
	 * is a group or a run
	 * 
	 * @return GROUP or RUN for valid sets, INVALID otherwise
	 */

	public Pair<Type, Integer> classify() {
		if (stones.size() < 3) {
			return new Pair<Type, Integer>(INVALID, 0);
		}
		int nonJoker1 = -1, nonJoker2 = -1;
		for (int i = 0; i < stones.size(); i++) {
			if (stones.get(i).isJoker()) {
				continue;
			}
			nonJoker2 = nonJoker1;
			nonJoker1 = i;
		}
		if (nonJoker2 == -1) {
			// Z first
			if (nonJoker1 == 0 && stones.get(nonJoker1).getValue() < 12) {
				return new Pair<Type, Integer>(RUN, isValidRun(nonJoker1));
			} else {
				int value = stones.get(nonJoker1).getValue() * stones.size();
				return new Pair<Type, Integer>(GROUP, value);
			}
		}

		// is run
		if (stones.get(nonJoker1).getColor() == stones.get(nonJoker2)
				.getColor()) {
			if (isValidRun(nonJoker1) > 0) {
				return new Pair<Type, Integer>(RUN, isValidRun(nonJoker1));
			}
			return new Pair<Type, Integer>(INVALID, 0);

		}
		// is group
		else {
			if (isValidGroup(stones.get(nonJoker1).getValue()) > 0) {
				return new Pair<Type, Integer>(GROUP, isValidGroup(stones.get(
						nonJoker1).getValue()));
			}
			return new Pair<Type, Integer>(INVALID, 0);
		}
	}

	/**
	 * Test for rule conflict within the StoneSet, assuming we have a run
	 * 
	 * @param referencePosition
	 *            position of stone used as reference (any non-joker stone)
	 */
	private int isValidRun(int referencePosition) {
		StoneColor runColor = stones.get(referencePosition).getColor();
		int startValue = stones.get(referencePosition).getValue()
				- referencePosition;
		int endValue = startValue + stones.size() - 1;
		if (startValue < 1 || endValue > 13) {
			return 0;
		}
		for (int i = 0; i < stones.size(); i++) {
			if (stones.get(i).isJoker()) {
				continue;
			}
			if (stones.get(i).getColor() != runColor) {
				// warum macht er das nicht?
				return 0;
			}
			if (stones.get(i).getValue() != i + startValue) {
				return 0;
			}
		}
		int value = 0;
		for (int i = 0; i < stones.size(); i++) {
			value += startValue + i;
		}
		return value;
	}

	/**
	 * Test for rule conflict within the StoneSet, assuming we have a group
	 */
	private int isValidGroup(int value) {
		if (stones.size() > 4) {
			return 0;
		}
		Set<StoneColor> seenColors = new HashSet<StoneColor>();
		for (Stone i : stones) {
			if (i.isJoker()) {
				continue;
			}
			if (i.getValue() != value) {
				return 0;
			}
			if (seenColors.contains(i.getColor())) {
				return 0;
			} else {
				seenColors.add(i.getColor());
			}
		}
		return value * stones.size();
	}

	/**
	 * Splits the StoneSet at a specified {@link Position} and returns two new
	 * Stone Sets
	 * 
	 * @param position
	 *            Splitting {@link Position}
	 * @return A pair of StoneSets, one for each split part
	 */
	public Pair<StoneSet, StoneSet> splitAt(int position) {
		if (position == 0) {
			return new Pair<StoneSet, StoneSet>(null, this);
		} else if (position == stones.size()) {
			return new Pair<StoneSet, StoneSet>(this, null);
		}
		StoneSet firstSet = new StoneSet(stones.subList(0, position));
		StoneSet secondSet = new StoneSet(stones.subList(position,
				stones.size()));
		return new Pair<StoneSet, StoneSet>(firstSet, secondSet);
	}

	/**
	 * Joins StoneSet to another StoneSet and returns the resulting new StoneSet
	 * 
	 * @param other
	 *            StoneSet to be joined to active StoneSet
	 * @return the combined StoneSet
	 */
	public StoneSet join(StoneSet other) {
		List<Stone> joinedList = new ArrayList<Stone>();
		joinedList.addAll(stones);
		joinedList.addAll(other.stones);
		return new StoneSet(joinedList);
	}

	/**
	 * Returns the number of stones in the set.
	 * 
	 * @return number of stones
	 */
	public int size() {
		return stones.size();
	}

	/**
	 * Returns the i-th stone of the set (starting with 0)
	 * 
	 * @param i
	 *            number of the stone to return
	 * @return the i-th stone
	 */
	public Stone get(int i) {
		return stones.get(i);
	}

	@Override
	public Iterator<Stone> iterator() {
		final Iterator<Stone> it = stones.iterator();

		return new Iterator<Stone>() {

			@Override
			public boolean hasNext() {
				return it.hasNext();
			}

			@Override
			public Stone next() {
				return it.next();
			}

			@Override
			public void remove() {
				// removing stones is impossible

				throw new UnsupportedOperationException();
			}
		};
	}

	@Override
	public float getWidth() {
		return stones.size() + 2 * VERTICAL_BORDER;
	}

	@Override
	public float getHeight() {
		return 1 + 2 * HORIZONTAL_BORDER;
	}

	@Override
	public String toString() {
		String ret = "StoneSet[";
		boolean first = true;

		for (Stone stone : stones) {
			if (!first)
				ret += ",";

			ret += stone.toString();
			first = false;
		}

		return ret + "]";
	}
}
