package jrummikub.control;

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

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import jrummikub.control.turn.ITurnControl;
import jrummikub.control.turn.TurnControlFactory;
import jrummikub.control.turn.TurnMode;
import jrummikub.model.IHand;
import jrummikub.model.IPlayer;
import jrummikub.model.IRoundState;
import jrummikub.model.ITable;
import jrummikub.model.PlayerSettings;
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.Event1;
import jrummikub.util.IEvent1;
import jrummikub.util.IListener;
import jrummikub.util.IListener2;
import jrummikub.util.Pair;
import jrummikub.view.IView;
import jrummikub.view.IView.BottomPanelType;

/**
 * Controller that manages a single round of rummikub
 */
public class RoundControl {
	/**
	 * Enum summarizing the different types of invalid turns to set the correct
	 * panel message
	 */
	public enum InvalidTurnType {
		/** There are invalid set(s) on the table */
		INVALID_SETS,
		/**
		 * The player tried to modify the table without providing the initial meld
		 * threshold first
		 */
		INITIAL_MELD_ERROR,
		/**
		 * The player didn't provide enough points for the initial meld threshold
		 */
		NOT_ENOUGH_POINTS
	}

	/**
	 * Table, stone sets and type of an invalid turn to allow a user to track his
	 * own errors
	 */
	public static class InvalidTurnInfo implements Serializable {
		private static final long serialVersionUID = -3591000741414366776L;

		private ITable table;
		private InvalidTurnType type;
		private ArrayList<StoneSet> invalidSets;

		/**
		 * Creates new InvalidTurnInfo
		 * 
		 * @param table
		 *          the table after the turn
		 * @param type
		 *          the type of the invalid turn
		 * @param invalidSets
		 *          the sets causing the turn to be invalid
		 */
		public InvalidTurnInfo(ITable table, InvalidTurnType type,
				Collection<StoneSet> invalidSets) {
			this.table = (ITable) table.clone();
			this.type = type;
			this.invalidSets = new ArrayList<StoneSet>(invalidSets);
		}

		/**
		 * Getter for table
		 * 
		 * @return the table
		 */
		public ITable getTable() {
			return table;
		}

		/**
		 * Getter for invalid turn type
		 * 
		 * @return the type
		 */
		public InvalidTurnType getType() {
			return type;
		}

		/**
		 * Getter for the invalid sets
		 * 
		 * @return invalid sets
		 */
		public List<StoneSet> getInvalidSets() {
			return invalidSets;
		}
	}

	private ITurnControl turnControl;
	protected IRoundState roundState;
	private IView view;
	private Event1<PlayerSettings> restartRoundEvent = new Event1<PlayerSettings>();
	private Event1<IRoundState> roundStateUpdateEvent = new Event1<IRoundState>();
	private Event1<Score> endOfRoundEvent = new Event1<Score>();
	protected List<Connection> connections = new ArrayList<Connection>();
	private boolean mayPause;

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

	/**
	 * Create a new RoundControl using the given roundState and view
	 * 
	 * @param roundState
	 *          initial round state
	 * @param view
	 *          view used for user interaction
	 * @param mayPause
	 *          true when players are allowed to pause
	 */
	protected RoundControl(IRoundState roundState, IView view, boolean mayPause) {
		this.roundState = roundState;
		this.view = view;
		this.mayPause = mayPause;
	}

	/**
	 * 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;
		}
	}

	/**
	 * Sets the current round state
	 * 
	 * @param state
	 *          to be set
	 */
	protected void setRoundState(IRoundState state) {
		roundState = state;
		roundStateUpdateEvent.emit(state);
	}

	protected BottomPanelType showStartTurnPanel() {
		boolean isHuman = roundState.getActivePlayer().getPlayerSettings()
				.getType() == HUMAN;
		boolean oneHuman = roundState.getGameSettings().oneHuman();

		if (!isHuman) {
			return null;
		}

		if (roundState.getLastPlayer() != null) {
			view.setLastStonePlayerName(roundState.getLastPlayer()
					.getPlayerSettings().getName());
			return BottomPanelType.START_LAST_TURN_PANEL;
		}

		if (roundState.getTurnNumber() < 1
				&& roundState.getGameState().getLastPlayerRedealed() != null) {
			view.setRedealedPlayerName(roundState.getGameState()
					.getLastPlayerRedealed().getName());
			return BottomPanelType.START_REDEAL_TURN_PANEL;
		}

		if (oneHuman) {
			return null;
		}

		return BottomPanelType.START_TURN_PANEL;
	}

	/**
	 * Prepare a player's turn by checking the player types and setting the
	 * correct turn control
	 */
	protected void prepareTurn() {
		doPrepareTurn();

		if (showStartTurnPanel() == null) {
			startTurn();
		}
	}

	/**
	 * Prepare turn by setting the view components
	 */
	protected void doPrepareTurn() {
		updateSidePanel();

		BottomPanelType startTurnPanel = showStartTurnPanel();
		if (startTurnPanel != null) {
			view.setBottomPanel(startTurnPanel);
		}

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

	/**
	 * Start a players turn with the correct turn control
	 */
	protected void startTurn() {
		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());

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

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

		turnControl = createTurnControl();
		turnControl.setup(
				new ITurnControl.TurnInfo(roundState, turnMode, mayPause),
				roundState.getGameSettings(), view);
		turnControl.getEndOfTurnEvent().add(
				new IListener2<IRoundState, InvalidTurnInfo>() {
					@Override
					public void handle(IRoundState newState,
							InvalidTurnInfo invalidTurnInfo) {
						setRoundState(newState);
						endOfTurn(invalidTurnInfo);
					}
				});
		turnControl.getRedealEvent().add(new IListener() {
			@Override
			public void handle() {
				redeal();
			}
		});
		addTurnControlListeners(turnControl);

		turnControl.startTurn();
	}

	/**
	 * Update the side panel to show correct player order and heap size
	 */
	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.getStoneHeap().getSize());
	}

	/**
	 * Override this
	 * 
	 * @param turnControl
	 *          current turn control
	 */
	protected void addTurnControlListeners(ITurnControl turnControl) {
	}

	/**
	 * Creates new turn control of the specified type
	 * 
	 * @param type
	 *          of the new turn control
	 * @return the new turn control
	 */
	protected ITurnControl createTurnControl() {
		return TurnControlFactory.getFactory(
				roundState.getActivePlayer().getPlayerSettings().getType()).create();
	}

	/**
	 * Deal each player the number of stones specified in the game settings
	 * (numberOfStonesDealt)
	 */
	protected 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.getStoneHeap().drawStone(), new Position(0, 0));
			}
		}

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

	/**
	 * End the players turn
	 * 
	 * @param invalidTurnInfo
	 *          info about the player's last turn
	 */
	protected void endOfTurn(InvalidTurnInfo invalidTurnInfo) {
		boolean isHuman = roundState.getActivePlayer().getPlayerSettings()
				.getType() == Type.HUMAN;

		turnControl = null;

		view.getTablePanel().setStoneSets(invalidTurnInfo.getTable());

		if (invalidTurnInfo.getType() != null) {
			if (isHuman) {
				view.setBottomPanel(BottomPanelType.INVALID_TURN_PANEL);
			}

			view.setInvalidStoneSets(invalidTurnInfo.getInvalidSets());

			switch (invalidTurnInfo.getType()) {
				case INITIAL_MELD_ERROR:
					view.setInitialMeldFirstError();
					break;
				case INVALID_SETS:
					view.setStoneCollectionHidden(true);
					break;
				case NOT_ENOUGH_POINTS:
					view.setInitialMeldError(roundState.getGameSettings()
							.getInitialMeldThreshold());
					break;
			}

			if (!isHuman) {
				nextPlayer();
			}
			return;
		}

		view.setBottomPanel(BottomPanelType.NONHUMAN_HAND_PANEL);
		view.getPlayerPanel().setTime(roundState.getGameSettings().getTotalTime(),
				roundState.getGameSettings().getTotalTime());

		nextPlayer();
	}

	/**
	 * Set the next player as active player if the round is not finished
	 */
	protected void nextPlayer() {
		view.setSelectedStones(Collections.<Stone> emptyList());
		view.setInvalidStoneSets(Collections.<StoneSet> emptyList());
		view.setStoneCollectionHidden(false);

		if (roundState.getActivePlayer().getHand().getSize() == 0) {
			endOfRound();
			return;
		}

		if (roundState.getLastPlayer() == null) {
			if (roundState.getStoneHeap().getSize() == 0) {
				roundState.setLastPlayer(roundState.getActivePlayer());
				roundState.nextPlayer();
				roundState.nextTurn();
			} else {
				roundState.nextPlayer();
				roundState.nextTurn();
			}
		} else {
			if (roundState.getActivePlayer() == roundState.getLastPlayer()) {
				endOfRound();
				return;
			} else {
				roundState.nextPlayer();
				roundState.nextTurn();
			}
		}

		prepareTurn();
	}

	/**
	 * Ends the current round and emits an event setting the score
	 */
	void endOfRound() {
		removeListeners();
		Score roundScore = score();
		endOfRoundEvent.emit(roundScore);
	}

	/**
	 * Removes all listeners form the connections
	 */
	private void removeListeners() {
		for (Connection c : connections) {
			c.remove();
		}
	}

	/**
	 * Calculate the score for the current round and the total game score
	 * 
	 * @return the new score
	 */
	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);
	}

	/**
	 * Update the best score to find the winner in case of special game end
	 * (everybody still has stones on hand)
	 * 
	 * @param bestScore
	 *          of previous rounds
	 * @param stonePoints
	 *          sum of points still left on hands
	 * @param size
	 *          number of players in game (= size of score list in columns)
	 * @return Pair of maximum points and hand size
	 */
	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 IEvent1<PlayerSettings> getRestartRoundEvent() {
		return restartRoundEvent;
	}

	/**
	 * Redeal stones and restart round if a player was allowed to redeal and chose
	 * to do so
	 */
	protected void redeal() {
		view.setBottomPanel(BottomPanelType.NONHUMAN_HAND_PANEL);
		view.getPlayerPanel().setTime(roundState.getGameSettings().getTotalTime(),
				roundState.getGameSettings().getTotalTime());

		turnControl = null;
		for (Connection c : new ArrayList<Connection>(connections)) {
			c.remove();
		}
		restartRoundEvent.emit(roundState.getActivePlayer().getPlayerSettings());
	}
}
