package jrummikub.control;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;

import jrummikub.model.IHand;
import jrummikub.model.ITable;
import jrummikub.model.Position;
import jrummikub.model.Stone;
import jrummikub.model.StoneColor;
import jrummikub.model.StoneSet;
import jrummikub.util.Connection;
import jrummikub.util.Event;
import jrummikub.util.IEvent;
import jrummikub.util.IListener;
import jrummikub.util.IListener1;
import jrummikub.util.IListener2;
import jrummikub.util.Pair;
import jrummikub.view.IView;

/**
 * Controller for a single turn made by a human player
 */
public class TurnControl {
	private IHand hand;
	private ITable table;
	private ITurnTimer timer;
	private IView view;

	private List<Stone> selectedStones = new ArrayList<Stone>();

	private Event endOfTurnEvent = new Event();
	private List<Connection> connections = new ArrayList<Connection>();

	/**
	 * Create a new TurnControl using a given hand (of the active player), a
	 * given table and a given view for user interaction.
	 * 
	 * @param hand
	 *            active player's hand
	 * @param table
	 *            current table
	 * @param view
	 *            view for user interaction.
	 */
	public TurnControl(IHand hand, ITable table, IView view) {
		this.hand = hand;
		this.table = table;
		this.view = view;
		this.timer = new TurnTimer(view);
	}

	/** Test only constructor **/
	TurnControl(IHand hand, ITable table, IView view, ITurnTimer testTimer) {
		this.hand = hand;
		this.table = table;
		this.view = view;
		this.timer = testTimer;
	}

	/**
	 * Start the turn
	 */
	public void startTurn() {

		IListener endOfTurnListener = new IListener() {

			@Override
			public void handle() {
				endOfTurn();
			}
		};
		connections.add(timer.getTimeRunOutEvent().add(endOfTurnListener));
		connections.add(view.getPlayerPanel().getEndTurnEvent()
				.add(endOfTurnListener));

		addHandPanelHandlers();
		addStoneCollectionHandlers();
		addTablePanelHandlers();

		connections.add(view.getPlayerPanel().getSortByGroupsEvent()
				.add(new IListener() {
					@Override
					public void handle() {
						sortByGroups();
					}
				}));

		connections.add(view.getPlayerPanel().getSortByRunsEvent()
				.add(new IListener() {
					@Override
					public void handle() {
						sortByRuns();
					}
				}));

		view.getPlayerPanel().getHandPanel().setStones(hand.clone());
		view.enableStartTurnPanel(false);

		timer.startTimer();
	}

	private void addHandPanelHandlers() {
		connections.add(view.getPlayerPanel().getHandPanel().getClickEvent()
				.add(new IListener1<Position>() {
					@Override
					public void handle(Position pos) {
						handClick(pos);
					}
				}));

		connections.add(view.getPlayerPanel().getHandPanel().getStoneClickEvent()
				.add(new IListener2<Stone, Boolean>() {

					@Override
					public void handle(Stone stone, Boolean collect) {
						stoneClick(stone, collect);
					}
				}));

		connections.add(view.getPlayerPanel().getHandPanel().getRangeClickEvent()
				.add(new IListener2<Stone, Boolean>() {

					@Override
					public void handle(Stone stone, Boolean collect) {
						handRangeClick(stone, collect);
					}
				}));
	}

	private void addStoneCollectionHandlers() {
		connections.add(view.getTablePanel().getStoneCollectionPanel()
				.getStoneClickEvent().add(new IListener2<Stone, Boolean>() {

					@Override
					public void handle(Stone stone, Boolean collect) {
						collectionStoneClick(stone, collect);
					}
				}));

		connections.add(view.getTablePanel().getStoneCollectionPanel()
				.getSetClickEvent().add(new IListener2<Stone, Boolean>() {

					@Override
					public void handle(Stone stone, Boolean collect) {
						collectionSetClick(stone, collect);
					}
				}));
	}

	private void addTablePanelHandlers() {
		connections.add(view.getTablePanel().getStoneClickEvent()
				.add(new IListener2<Stone, Boolean>() {

					@Override
					public void handle(Stone stone, Boolean collect) {
						stoneClick(stone, collect);
					}
				}));

		connections.add(view.getTablePanel().getSetClickEvent()
				.add(new IListener2<Stone, Boolean>() {

					@Override
					public void handle(Stone stone, Boolean collect) {
						tableSetClick(stone, collect);
					}
				}));

		connections.add(view.getTablePanel().getRangeClickEvent()
				.add(new IListener2<Stone, Boolean>() {

					@Override
					public void handle(Stone stone, Boolean collect) {
						tableRangeClick(stone, collect);
					}
				}));

		connections.add(view.getTablePanel().getClickEvent()
				.add(new IListener1<Position>() {
					@Override
					public void handle(Position pos) {
						tableClick(pos);
					}
				}));

		addTablePanelConnectorClickHandlers();
	}

	private void addTablePanelConnectorClickHandlers() {
		connections.add(view.getTablePanel().getLeftConnectorClickEvent()
				.add(new IListener1<StoneSet>() {
					@Override
					public void handle(StoneSet set) {
						connectorClick(set, false);
					}
				}));

		connections.add(view.getTablePanel().getRightConnectorClickEvent()
				.add(new IListener1<StoneSet>() {
					@Override
					public void handle(StoneSet set) {
						connectorClick(set, true);
					}
				}));
	}

	private void handClick(Position pos) {
		List<Stone> handStones = new ArrayList<Stone>();
		for (Stone s : selectedStones) {
			if (hand.pickUp(s)) {
				handStones.add(s);
			}
		}

		int i = 0;
		for (Stone s : handStones) {
			float x = Math.max(0,
					Math.min(13, pos.getX() - handStones.size() / 2.0f + i));
			hand.drop(s, new Position(x, (float) Math.floor(pos.getY())));
			selectedStones.remove(s);
			i++;
		}
		view.setSelectedStones(selectedStones);
		view.getPlayerPanel().getHandPanel().setStones(hand);
	}

	private void sortStones(Comparator<Stone> comparator) {
		List<Stone> stones = new ArrayList<Stone>();
		for (Pair<Stone, Position> entry : hand) {
			stones.add(entry.getFirst());
		}
		for (Stone stone : stones) {
			hand.pickUp(stone);
		}

		Collections.sort(stones, comparator);
		int x = 0, y = 0;
		for (Stone stone : stones) {
			hand.drop(stone, new Position(x, y));
			x++;
			if (x >= GameControl.HAND_WIDTH) {
				x = 0;
				y++;

				if (y >= GameControl.HAND_HEIGHT) {
					throw new ArrayIndexOutOfBoundsException(); // TODO We can't
					// handle
					// this yet...
				}
			}
		}

		view.getPlayerPanel().getHandPanel().setStones(hand);
	}

	private void sortByRuns() {
		sortStones(new RunComparator());
	}

	private void sortByGroups() {
		sortStones(new GroupComparator());
	}

	private void stoneClick(Stone stone, boolean collect) {
		if (collect) {
			if (!selectedStones.remove(stone)) {
				selectedStones.add(stone);
			}
		} else {
			selectedStones.clear();
			selectedStones.add(stone);
		}

		view.setSelectedStones(selectedStones);
	}

	private void collectionStoneClick(Stone stone, boolean collect) {
		selectedStones.remove(stone);

		if (collect) {
			selectedStones.add(stone);
		}

		view.setSelectedStones(selectedStones);
	}

	private void collectionSetClick(Stone stone, Boolean collect) {
		selectedStones.clear();
		view.setSelectedStones(selectedStones);
	}

	private void pickUpSelectedStones() {
		for (Stone stone : selectedStones) {
			hand.pickUp(stone);
			table.pickUpStone(stone);
		}
	}

	private void tableClick(Position position) {
		if (selectedStones.isEmpty()) {
			return;
		}

		pickUpSelectedStones();
		table.drop(new StoneSet(selectedStones), new Position(position.getX()
				- selectedStones.size() * 0.5f, position.getY() - 0.5f));
		selectedStones.clear();

		view.getTablePanel().setStoneSets(table);
		view.getPlayerPanel().getHandPanel().setStones(hand);
		view.setSelectedStones(selectedStones);
	}

	private void tableSetClick(Stone stone, Boolean collect) {
		if (!collect) {
			selectedStones.clear();
		}
		StoneSet selectedSet = table.findStoneSet(stone);
		for (Stone setStone : selectedSet) {
			selectedStones.remove(setStone);
			selectedStones.add(setStone);
		}
		view.setSelectedStones(selectedStones);
	}

	private void tableRangeClick(Stone stone, Boolean collect) {
		if (selectedStones.isEmpty()) {
			stoneClick(stone, collect);
			return;
		}
		Stone lastStone = selectedStones.get(selectedStones.size() - 1);
		StoneSet lastSet = table.findStoneSet(lastStone);
		StoneSet selectedSet = table.findStoneSet(stone);
		if (lastSet != selectedSet) {
			stoneClick(stone, collect);
			return;
		}
		List<Stone> setStones = new ArrayList<Stone>();
		for (Stone s : selectedSet) {
			setStones.add(s);
		}
		int firstIndex = setStones.indexOf(lastStone);
		int lastIndex = setStones.indexOf(stone);
		if (firstIndex > lastIndex) {
			int temp = firstIndex;
			firstIndex = lastIndex;
			lastIndex = temp;
		}
		for (int i = firstIndex; i <= lastIndex; i++) {
			Stone s = setStones.get(i);
			selectedStones.remove(s);
			selectedStones.add(s);
		}

		view.setSelectedStones(selectedStones);
	}

	private void handRangeClick(Stone stone, Boolean collect) {
		if (selectedStones.isEmpty()) {
			stoneClick(stone, collect);
			return;
		}
		Stone lastStone = selectedStones.get(selectedStones.size() - 1);
		StoneSet lastSet = table.findStoneSet(lastStone);
		if (lastSet != null) {
			stoneClick(stone, collect);
			return;
		}
		List<Pair<Stone, Position>> handPairs = new ArrayList<Pair<Stone, Position>>();
		for (Pair<Stone, Position> entry : hand) {
			handPairs.add(entry);
		}

		Collections.sort(handPairs, new HandStonePositionComparator());

		List<Stone> handStones = new ArrayList<Stone>();
		for (Pair<Stone, Position> entry : handPairs) {
			handStones.add(entry.getFirst());
		}

		int firstIndex = handStones.indexOf(lastStone);
		int lastIndex = handStones.indexOf(stone);
		if (firstIndex > lastIndex) {
			int temp = firstIndex;
			firstIndex = lastIndex;
			lastIndex = temp;
		}
		for (int i = firstIndex; i <= lastIndex; i++) {
			Stone s = handStones.get(i);
			selectedStones.remove(s);
			selectedStones.add(s);
		}
		view.setSelectedStones(selectedStones);
	}

	private void connectorClick(StoneSet set, boolean right) {
		List<Stone> stones = new LinkedList<Stone>();
		Position pos = table.getPosition(set);
		for (Stone stone : set) {
			stones.add(stone);
		}
		stones.removeAll(selectedStones);
		if (right) {
			Collections.reverse(stones);
		}
		pickUpSelectedStones();
		StoneSet newSet = null;
		for (Stone stone : stones) {
			newSet = table.findStoneSet(stone);
			if (newSet != null) {
				break;
			}
		}
		if (newSet != null) {
			Position newPos = table.getPosition(newSet);
			table.pickUp(newSet);
			if (right) {
				StoneSet joinedSet = newSet.join(new StoneSet(selectedStones));
				table.drop(joinedSet, newPos);
			} else {
				StoneSet joinedSet = new StoneSet(selectedStones).join(newSet);
				table.drop(joinedSet,
						new Position(newPos.getX() - selectedStones.size(), newPos.getY()));
			}
		} else {
			table.drop(
					new StoneSet(selectedStones),
					new Position(
							pos.getX() + (set.size() - selectedStones.size()) * 0.5f, pos
									.getY()));
		}

		selectedStones.clear();

		view.getTablePanel().setStoneSets(table);
		view.getPlayerPanel().getHandPanel().setStones(hand);
		view.setSelectedStones(selectedStones);
	}

	private void endOfTurn() {
		timer.stopTimer();
		endOfTurnEvent.emit();
		for (Connection c : connections) {
			c.remove();
		}
		view.setSelectedStones(new ArrayList<Stone>());
	}

	/**
	 * Get the event that is emitted when the turn is over
	 * 
	 * @return end of turn event
	 */
	public IEvent getEndOfTurnEvent() {
		return endOfTurnEvent;
	}

	static private int compareJokers(Stone s1, Stone s2) {
		if (!s1.isJoker() && s2.isJoker()) {
			return -1;
		} else if (s1.isJoker() && !s2.isJoker()) {
			return 1;
		} else {
			return 0;
		}
	}

	static private int compareColors(Stone s1, Stone s2) {
		if (s1.getColor() == s2.getColor())
			return 0;

		for (StoneColor color : StoneColor.values()) {
			if (s1.getColor() == color) {
				return -1;
			} else if (s2.getColor() == color) {
				return 1;
			}
		}

		return 0;
	}

	static private int compareValues(Stone s1, Stone s2) {
		if (s1.getValue() < s2.getValue()) {
			return -1;
		} else if (s1.getValue() > s2.getValue()) {
			return 1;
		} else {
			return 0;
		}
	}

	private static class RunComparator implements Comparator<Stone> {
		@Override
		public int compare(Stone s1, Stone s2) {
			int jokerCompare = compareJokers(s1, s2);
			if (jokerCompare != 0) {
				return jokerCompare;
			}

			int colorCompare = compareColors(s1, s2);
			if (colorCompare != 0) {
				return colorCompare;
			}

			return compareValues(s1, s2);
		}
	}

	private static class GroupComparator implements Comparator<Stone> {
		@Override
		public int compare(Stone s1, Stone s2) {
			int jokerCompare = compareJokers(s1, s2);
			if (jokerCompare != 0) {
				return jokerCompare;
			}

			int valueCompare = compareValues(s1, s2);
			if (valueCompare != 0) {
				return valueCompare;
			}

			return compareColors(s1, s2);
		}
	}

	static class HandStonePositionComparator implements
			Comparator<Pair<Stone, Position>> {
		@Override
		public int compare(Pair<Stone, Position> pair1, Pair<Stone, Position> pair2) {
			Position pos1 = pair1.getSecond(), pos2 = pair2.getSecond();
			if (pos1.getY() < pos2.getY()) {
				return -1;
			} else if (pos1.getY() > pos2.getY()) {
				return 1;
			} else {
				if (pos1.getX() < pos2.getX()) {
					return -1;
				} else if (pos1.getX() > pos2.getX()) {
					return 1;
				} else {
					return 0;
				}
			}
		}
	}

}
