package jrummikub.control;

import static jrummikub.model.PlayerSettings.Type.*;

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

import jrummikub.control.turn.AIControl;
import jrummikub.control.turn.HumanTurnControl;
import jrummikub.control.turn.ITurnControl;
import jrummikub.control.turn.TurnControlFactory;
import jrummikub.control.turn.TurnMode;
import jrummikub.model.Hand;
import jrummikub.model.IHand;
import jrummikub.model.IPlayer;
import jrummikub.model.IRoundState;
import jrummikub.model.ITable;
import jrummikub.model.PlayerSettings.Type;
import jrummikub.model.Position;
import jrummikub.model.Score;
import jrummikub.model.Stone;
import jrummikub.model.StoneSet;
import jrummikub.util.Connection;
import jrummikub.util.Event;
import jrummikub.util.Event1;
import jrummikub.util.IEvent;
import jrummikub.util.IEvent1;
import jrummikub.util.IListener;
import jrummikub.util.IListener3;
import jrummikub.util.Pair;
import jrummikub.view.IView;
import jrummikub.view.IView.BottomPanelType;

/**
 * Controller that manages a single round of rummikub
 */
public class RoundControl {
	private ITurnControl turnControl;
	protected IRoundState roundState;
	private IView view;
	private ITable clonedTable;
	IHand clonedHand;
	private Event restartRoundEvent = new Event();
	private Event1<IRoundState> roundStateUpdateEvent = new Event1<IRoundState>();
	private Event1<Score> endOfRoundEvent = new Event1<Score>();
	protected List<Connection> connections = new ArrayList<Connection>();
	private boolean roundFinished;
	private boolean lastTurnNotEnoughPoints;
	private boolean lastTurnMeldError;

	/**
	 * Create a new RoundControl using the given gameState and view
	 * 
	 * @param roundState
	 *          initial round state
	 * @param view
	 *          view used for user interaction
	 */
	public RoundControl(IRoundState roundState, IView view) {
		this.roundState = roundState;
		this.view = view;
	}

	/**
	 * Is emitted in network when a new turn starts
	 * 
	 * @return the event
	 */
	public IEvent1<IRoundState> getRoundStateUpdateEvent() {
		return roundStateUpdateEvent;
	}

	/**
	 * End the round
	 * 
	 * @return endOfRoundEvent
	 */
	public IEvent1<Score> getEndOfRoundEvent() {
		return endOfRoundEvent;
	}

	/**
	 * Begin the round
	 */
	public void startRound() {
		if (roundState != null) {
			deal();
		}

		continueRound();
	}

	/**
	 * Continue a saved round after loading
	 */
	public void continueRound() {

		connections.add(view.getStartTurnEvent().add(new IListener() {
			@Override
			public void handle() {
				startTurn();
			}
		}));

		connections.add(view.getAcknowledgeInvalidEvent().add(new IListener() {
			@Override
			public void handle() {
				nextPlayer();
			}
		}));

		if (roundState != null) {
			prepareTurn();
		}
	}

	/**
	 * Abort round if a new one is started or a saved one is loaded
	 */
	public void abortRound() {
		removeListeners();
		if (turnControl != null) {
			turnControl.abortTurn();
			turnControl = null;
		}
	}

	protected void setRoundState(IRoundState state) {
		roundState = state;
		roundStateUpdateEvent.emit(state);
	}

	protected void prepareTurn() {
		doPrepareTurn();

		boolean isHuman = roundState.getActivePlayer().getPlayerSettings()
				.getType() == HUMAN;
		boolean oneHuman = roundState.getGameSettings().oneHuman();
		boolean isAI = (turnControl instanceof AIControl);

		if (isAI || (isHuman && oneHuman)) {
			startTurn();
		}
	}

	protected void doPrepareTurn() {
		updateSidePanel();

		boolean isHuman = roundState.getActivePlayer().getPlayerSettings()
				.getType() == HUMAN;
		boolean oneHuman = roundState.getGameSettings().oneHuman();

		if (isHuman && !oneHuman) {
			view.setBottomPanel(BottomPanelType.START_TURN_PANEL);
		}

		view.getTablePanel().setStoneSets(roundState.getTable().clone());
		view.setCurrentPlayerName(roundState.getActivePlayer().getPlayerSettings()
				.getName());
		view.setCurrentPlayerColor(roundState.getActivePlayer().getPlayerSettings()
				.getColor());
		view.setCurrentPlayerHasLaidOut(roundState.getActivePlayer().getLaidOut());

		turnControl = createTurnControl(roundState.getActivePlayer()
				.getPlayerSettings().getType());
	}

	protected void startTurn() {
		if (turnControl == null) {
			doPrepareTurn();
		}

		boolean isHuman = roundState.getActivePlayer().getPlayerSettings()
				.getType() == HUMAN;

		view.setBottomPanel(isHuman ? BottomPanelType.HUMAN_HAND_PANEL
				: BottomPanelType.NONHUMAN_HAND_PANEL);

		TurnMode turnMode = TurnMode.NORMAL_TURN;

		view.getTablePanel().setStoneSets(roundState.getTable().clone());

		clonedTable = (ITable) roundState.getTable().clone();
		clonedHand = (IHand) roundState.getActivePlayer().getHand().clone();

		if (roundState.getTurnNumber() < 1) {
			view.setStoneCollectionHidden(true);
			turnMode = TurnMode.INSPECT_ONLY;
			if (clonedHand.getIdenticalStoneCount() >= 3) {
				turnMode = TurnMode.MAY_REDEAL;
			}
		}

		if (isHuman) {
			view.getPlayerPanel().setEndTurnMode(turnMode);
		}

		turnControl.setup(new ITurnControl.TurnInfo(clonedTable, clonedHand,
				roundState.getActivePlayer().getLaidOut(), turnMode), roundState
				.getGameSettings(), view);
		turnControl.getEndOfTurnEvent().add(
				new IListener3<IHand, ITable, ITable>() {
					@Override
					public void handle(IHand oldHand, ITable oldTable, ITable newTable) {
						if (oldHand == null) {
							oldHand = roundState.getActivePlayer().getHand();
						}
						if (oldTable == null) {
							oldTable = roundState.getTable();
						}

						endOfTurn(oldHand, oldTable, newTable);
					}
				});
		turnControl.getRedealEvent().add(new IListener() {
			@Override
			public void handle() {
				redeal();
			}
		});
		addTurnControlListeners(turnControl);

		turnControl.startTurn();
	}

	private void updateSidePanel() {
		view.showSidePanel(true);
		view.getSidePanel().setGameSettings(roundState.getGameSettings());
		List<IPlayer> players = new ArrayList<IPlayer>();
		for (int i = 1; i < roundState.getPlayerCount(); i++) {
			players.add(roundState.getNthNextPlayer(-i));
		}
		view.getSidePanel().setPlayers(players);
		view.getSidePanel().setHeapCapacity(
				roundState.getGameSettings().getTotalStones());
		view.getSidePanel().setHeapSize(roundState.getGameHeap().getSize());
	}

	/** Override this */
	protected void addTurnControlListeners(ITurnControl turnControl) {
	}

	protected ITurnControl createTurnControl(Type type) {
		return TurnControlFactory.getFactory(type).create();
	}

	void deal() {
		for (int i = 0; i < roundState.getPlayerCount(); i++) {
			IHand hand = roundState.getNthNextPlayer(i).getHand();
			for (int j = 0; j < roundState.getGameSettings().getNumberOfStonesDealt(); j++) {
				hand.drop(roundState.getGameHeap().drawStone(), new Position(0, 0));
			}
		}

		view.getSidePanel().setHeapSize(roundState.getGameHeap().getSize());
	}

	private boolean laidOutValidPoints(ITable oldTable, ITable newTable) {
		List<StoneSet> newSets = tableSetDifference(oldTable, newTable);

		int totalValue = 0;
		for (StoneSet set : newSets) {
			totalValue += set.classify(roundState.getGameSettings()).getSecond();
		}

		return totalValue == 0
				|| totalValue >= roundState.getGameSettings().getInitialMeldThreshold();
	}

	protected void endOfTurn(IHand oldHand, ITable oldTable, ITable newTable) {
		boolean wasHuman = (turnControl instanceof HumanTurnControl);
		boolean wasAI = (turnControl instanceof AIControl);

		view.setBottomPanel(BottomPanelType.NONHUMAN_HAND_PANEL);

		turnControl = null;

		roundState.getActivePlayer().setLastTurnInvalid(false);
		roundState.getActivePlayer()
				.setLastTurnStoneCount(
						roundState.getActivePlayer().getHand().getSize()
								- clonedHand.getSize());

		roundState.getActivePlayer().setHand(clonedHand);
		boolean goToNextPlayer = true;
		lastTurnNotEnoughPoints = false;
		lastTurnMeldError = false;
		if (roundState.getTurnNumber() >= 1) {
			goToNextPlayer = checkTurn(oldTable, newTable);
		}
		if (goToNextPlayer || wasAI) {
			nextPlayer();
		} else {
			if (wasHuman) {
				view.setBottomPanel(BottomPanelType.INVALID_TURN_PANEL);
			}

			if (lastTurnNotEnoughPoints) {
				view.setInitialMeldError(roundState.getGameSettings()
						.getInitialMeldThreshold());
				view.getTablePanel().setStoneSets(newTable);
				view.setInvalidStoneSets(tableSetDifference(oldTable, newTable));
			} else if (lastTurnMeldError) {
				view.setInitialMeldFirstError();
				view.getTablePanel().setStoneSets(newTable);
				view.setInvalidStoneSets(touchedStoneSets(oldHand, oldTable, newTable));
			} else {
				view.setStoneCollectionHidden(true);
				view.getTablePanel().setStoneSets(newTable);
				view.setInvalidStoneSets(invalidStoneSets(newTable));
			}
		}
	}

	private List<StoneSet> invalidStoneSets(ITable newTable) {
		List<StoneSet> invalidSets = new ArrayList<StoneSet>();
		for (Pair<StoneSet, Position> set : newTable) {
			if (set.getFirst().isValid(roundState.getGameSettings())) {
				continue;
			}
			invalidSets.add(set.getFirst());
		}
		return invalidSets;
	}

	private List<StoneSet> touchedStoneSets(IHand oldHand, ITable oldTable,
			ITable newTable) {
		List<StoneSet> touchedSets = new ArrayList<StoneSet>();
		for (StoneSet set : tableSetDifference(oldTable, newTable)) {
			for (Stone stone : set) {
				if (!oldHand.contains(stone)) {
					touchedSets.add(set);
					break;
				}
			}
		}

		return touchedSets;
	}

	private void nextPlayer() {
		view.setSelectedStones(Collections.<Stone> emptyList());
		view.setInvalidStoneSets(Collections.<StoneSet> emptyList());
		view.setStoneCollectionHidden(false);
		if (roundState.getLastPlayer() == null) {
			if (roundState.getGameHeap().getSize() == 0) {
				roundState.setLastPlayer(roundState.getNthNextPlayer(0));
				roundState.nextPlayer();
				roundState.nextTurn();
			} else {
				roundState.nextPlayer();
				roundState.nextTurn();
			}
		} else {
			if (roundState.getActivePlayer() == roundState.getLastPlayer()) {
				endOfRound();
			} else {
				roundState.nextPlayer();
				roundState.nextTurn();
			}
		}
		if (!roundFinished) {
			prepareTurn();
		}
	}

	private boolean checkTurn(ITable oldTable, ITable newTable) {
		if (!newTable.isValid()) {
			rejectMove(oldTable, newTable);
			return false;
		}
		if (!roundState.getActivePlayer().getLaidOut()) {
			// Player touched forbidden stones
			if (!tableSetDifference(newTable, oldTable).isEmpty()) {
				rejectMove(oldTable, newTable);
				lastTurnMeldError = true;
				return false;
			}
			if (!laidOutValidPoints(oldTable, newTable)) {
				rejectMove(oldTable, newTable);
				lastTurnNotEnoughPoints = true;
				return false;
			}
		}
		Set<Stone> tableDiff = tableDifference(oldTable, newTable);

		roundState.setTable(newTable);

		if (tableDiff.isEmpty()) {
			// Player hasn't made a move
			dealStone();
		} else {
			roundState.getActivePlayer().setLaidOut(true);
			if (roundState.getActivePlayer().getHand().getSize() == 0) {
				endOfRound();
			}
		}
		return true;
	}

	private void rejectMove(ITable oldTable, ITable newTable) {
		Set<Stone> tableDiff = tableDifference(oldTable, newTable);
		// deal penalty, reset
		roundState.getGameHeap().putBack(tableDiff);
		roundState.getActivePlayer().setLastTurnInvalid(true);
		dealPenalty(tableDiff.size());

	}

	static Set<Stone> tableDifference(ITable oldTable, ITable newTable) {
		Set<Stone> ret = new HashSet<Stone>();

		for (Pair<StoneSet, Position> entry : newTable) {
			for (Stone stone : entry.getFirst()) {
				ret.add(stone);
			}
		}
		for (Pair<StoneSet, Position> entry : oldTable) {
			for (Stone stone : entry.getFirst()) {
				ret.remove(stone);
			}
		}
		return ret;
	}

	static List<StoneSet> tableSetDifference(ITable oldTable, ITable newTable) {
		List<StoneSet> ret = new ArrayList<StoneSet>();

		for (Pair<StoneSet, Position> entry : newTable) {
			ret.add(entry.getFirst());
		}
		for (Pair<StoneSet, Position> entry : oldTable) {
			ret.remove(entry.getFirst());
		}
		return ret;
	}

	void dealStones(int count) {
		IHand hand = roundState.getActivePlayer().getHand();
		int rowCount = hand.getRowCount();

		for (int i = 0; i < count; ++i) {
			if (hand.getFreeRowSpace(rowCount - 1) == 0) {
				rowCount++;
			}

			hand.drop(roundState.getGameHeap().drawStone(), new Position(
					Hand.WIDTH - 1, rowCount - 1));
		}

	}

	private void dealStone() {
		dealStones(1);
	}

	private void dealPenalty(int count) {
		dealStones(count + 3);
	}

	void endOfRound() {
		removeListeners();
		Score roundScore = score();
		endOfRoundEvent.emit(roundScore);
		roundFinished = true;
	}

	private void removeListeners() {
		for (Connection c : connections) {
			c.remove();
		}
	}

	private Score score() {
		List<Boolean> winners = new ArrayList<Boolean>();
		List<Integer> points = new ArrayList<Integer>();

		boolean foundRegularWinner = false;
		int winnerPlayerNumber = 0;
		Pair<Integer, Integer> bestScore = new Pair<Integer, Integer>(
				Integer.MIN_VALUE, Integer.MAX_VALUE);
		int pointSum = 0;
		for (int i = 0; i < roundState.getPlayerCount(); i++) {
			IPlayer player = roundState.getNthPlayer(i);
			IHand playerHand = player.getHand();
			boolean isWinner = playerHand.getSize() == 0;
			winners.add(isWinner);
			if (isWinner) {
				foundRegularWinner = true;
				winnerPlayerNumber = i;
			}
			int stonePoints = 0;

			if (!player.getLaidOut()) {
				stonePoints = playerHand.isInitialMeldPossible(roundState
						.getGameSettings()) ? 200 : 100;
			} else {
				stonePoints = playerHand.getStonePoints(roundState.getGameSettings());
			}

			bestScore = updateBestScore(bestScore, -stonePoints, playerHand.getSize());

			points.add(-stonePoints);
			pointSum += stonePoints;
		}

		if (foundRegularWinner) {
			points.set(winnerPlayerNumber, pointSum);
		} else {
			for (int i = 0; i < roundState.getPlayerCount(); i++) {
				if (bestScore.equals(new Pair<Integer, Integer>(points.get(i),
						roundState.getNthPlayer(i).getHand().getSize()))) {
					winners.set(i, true);
				}
			}
		}
		return new Score(winners, points);
	}

	private static Pair<Integer, Integer> updateBestScore(
			Pair<Integer, Integer> bestScore, int stonePoints, int size) {
		if (bestScore.getFirst() == stonePoints) {
			return new Pair<Integer, Integer>(stonePoints, Math.min(
					bestScore.getSecond(), size));
		} else if (bestScore.getFirst() < stonePoints) {
			return new Pair<Integer, Integer>(stonePoints, size);
		}
		return bestScore;
	}

	/**
	 * Emitted when the round is aborted and needs to be restarted
	 * 
	 * @return the event
	 */
	public IEvent getRestartRoundEvent() {
		return restartRoundEvent;
	}

	private void redeal() {
		turnControl = null;
		for (Connection c : new ArrayList<Connection>(connections)) {
			c.remove();
		}
		restartRoundEvent.emit();
	}
}
