package jrummikub.model;

import java.io.Serializable;
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, Serializable {
	private static final long serialVersionUID = -3852631195648599398L;

	static final float VERTICAL_BORDER = 0.5f;
	static final float HORIZONTAL_BORDER = 0.125f;
	private ArrayList<Stone> stones;

	/**
	 * Create a new single stone stone set
	 * 
	 * @param stone
	 *          single stone of the set
	 */
	public StoneSet(Stone stone) {
		stones = new ArrayList<Stone>(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
	 * 
	 * @param settings
	 *          GameSettings
	 * 
	 * @return true when the set is valid according to the rules
	 */
	public boolean isValid(GameSettings settings) {
		return classify(settings).getFirst() != INVALID;
	}

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

	public Pair<Type, Integer> classify(GameSettings settings) {
		if (stones.size() < 3) {
			return new Pair<Type, Integer>(INVALID, 0);
		}
		int nonJoker = -1;
		for (int i = 0; i < stones.size(); i++) {
			if (stones.get(i).isJoker()) {
				continue;
			}
			nonJoker = i;
		}

		if (nonJoker == -1) {
			return classifyJokersOnly(settings);
		}

		int runScore = isValidRun(nonJoker, settings);
		int groupScore = isValidGroup(stones.get(nonJoker).getValue(), settings);

		if (runScore > groupScore) {
			return new Pair<Type, Integer>(RUN, runScore);
		} else if (groupScore == 0) {
			return new Pair<Type, Integer>(INVALID, 0);
		} else {
			return new Pair<Type, Integer>(GROUP, groupScore);
		}
	}

	/**
	 * Test for rule conflict within a StoneSet with jokers only and determine
	 * whether the set is a group or a run
	 * 
	 * @param settings
	 *          the game settings
	 * @return GROUP or RUN for valid sets, INVALID otherwise and the points
	 */
	private Pair<Type, Integer> classifyJokersOnly(GameSettings settings) {
		if (stones.size() > settings.getHighestValue()
				&& stones.size() > settings.getStoneColors().size()
				&& !settings.isNoLimits()) {
			return new Pair<Type, Integer>(INVALID, 0);
		} else if (stones.size() > settings.getStoneColors().size()) {
			int value = 0;
			int stoneValue = settings.getHighestValue();
			for (int i = 0; i < stones.size(); i++) {
				value += stoneValue;
				stoneValue--;
				if (stoneValue == 0) {
					stoneValue = settings.getHighestValue();
				}
			}
			return new Pair<Type, Integer>(RUN, value);
		} else {
			return new Pair<Type, Integer>(GROUP, stones.size()
					* settings.getHighestValue());
		}
	}

	/**
	 * Test for rule conflict within the StoneSet, assuming we have a run
	 * 
	 * @param referencePosition
	 *          position of stone used as reference (any non-joker stone)
	 * @param settings
	 *          the game settings
	 * @return the set's points
	 */
	private int isValidRun(int referencePosition, GameSettings settings) {
		StoneColor runColor = stones.get(referencePosition).getColor();
		int startValue = stones.get(referencePosition).getValue()
				- referencePosition;
		int endValue = startValue + stones.size() - 1;
		if (!settings.isNoLimits()) {
			if (startValue < 1 || endValue > settings.getHighestValue()) {
				return 0;
			}
		}
		int value = 0;
		for (int i = 0; i < stones.size(); i++) {
			int expectedValue = i + startValue;
			while (expectedValue < 1) {
				expectedValue += settings.getHighestValue();
			}
			while (expectedValue > settings.getHighestValue()) {
				expectedValue -= settings.getHighestValue();
			}
			value += expectedValue;

			if (stones.get(i).isJoker()) {
				continue;
			}
			if (stones.get(i).getColor() != runColor) {
				return 0;
			}

			if (stones.get(i).getValue() != expectedValue) {
				return 0;
			}
		}
		return value;
	}

	/**
	 * Test for rule conflict within the StoneSet, assuming we have a group
	 * 
	 * @param value
	 *          the value of the stones (all have the same in a group)
	 * 
	 * @param settings
	 *          the game settings
	 * @return the set's points
	 */
	private int isValidGroup(int value, GameSettings settings) {
		if (stones.size() > settings.getStoneColors().size()) {
			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 getSize() {
		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 + "]";
	}
}
