package jrummikub.control.turn;

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

import jrummikub.control.ITurnTimer;
import jrummikub.control.RoundControl.InvalidTurnInfo;
import jrummikub.control.RoundControl.InvalidTurnType;
import jrummikub.control.TurnTimer;
import jrummikub.model.GameSettings;
import jrummikub.model.Hand;
import jrummikub.model.IHand;
import jrummikub.model.IRoundState;
import jrummikub.model.ITable;
import jrummikub.model.Position;
import jrummikub.model.Stone;
import jrummikub.model.StoneSet;
import jrummikub.util.Connection;
import jrummikub.util.Event;
import jrummikub.util.Event1;
import jrummikub.util.Event2;
import jrummikub.util.IEvent;
import jrummikub.util.IEvent1;
import jrummikub.util.IEvent2;
import jrummikub.util.IListener;
import jrummikub.util.Pair;
import jrummikub.view.IView;

/**
 * Abstract base class for TurnControls
 */
public abstract class AbstractTurnControl implements ITurnControl {
	protected Event2<IRoundState, InvalidTurnInfo> endOfTurnEvent = new Event2<IRoundState, InvalidTurnInfo>();
	protected Event redealEvent = new Event();
	protected Event1<ITable> tableUpdateEvent = new Event1<ITable>();
	protected TurnInfo turnInfo;
	protected GameSettings settings;
	protected IView view;
	protected ITurnTimer timer;
	protected List<Connection> connections = new ArrayList<Connection>();
	private boolean started = false;

	@Override
	public IEvent2<IRoundState, InvalidTurnInfo> getEndOfTurnEvent() {
		return endOfTurnEvent;
	}

	@Override
	public IEvent getRedealEvent() {
		return redealEvent;
	}

	@Override
	public IEvent1<ITable> getTableUpdateEvent() {
		return tableUpdateEvent;
	}

	protected void pauseTurn() {
		if (!turnInfo.isMayPause()) {
			return;
		}

		timer.stopTimer();
		view.enablePauseMode(true);
	}

	protected void resumeTurn() {
		timer.startTimer();
		view.enablePauseMode(false);
	}

	@Override
	public void startTurn() {
		if (started) {
			return;
		}

		started = true;
		doStartTurn();
	}

	protected abstract void doStartTurn();

	protected abstract void timeOut();

	@Override
	public void setup(TurnInfo info, GameSettings settings, IView view) {
		turnInfo = info;
		this.settings = settings;
		this.view = view;
		if (timer == null) {
			timer = new TurnTimer(view, settings.getTotalTime());
		}
		connections.add(timer.getTimeRunOutEvent().add(new IListener() {
			@Override
			public void handle() {
				timeOut();
			}
		}));
		connections.add(view.getPauseEvent().add(new IListener() {
			@Override
			public void handle() {
				pauseTurn();
			}
		}));
		connections.add(view.getEndPauseEvent().add(new IListener() {
			@Override
			public void handle() {
				resumeTurn();
			}
		}));

		view.setMayPause(info.isMayPause());
	}

	protected void cleanUp() {
		if (timer != null) {
			timer.stopTimer();
		}
		started = true;

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

	public void abortTurn() {
		cleanUp();
	}

	protected InvalidTurnInfo checkTurn() {
		turnInfo.getRoundState().getActivePlayer().setLastTurnInvalid(false);
		turnInfo
				.getRoundState()
				.getActivePlayer()
				.setLastTurnStoneCount(
						turnInfo.getOldHand().getSize() - turnInfo.getHand().getSize());

		turnInfo.getRoundState().getActivePlayer().setHand(turnInfo.getHand());

		if (turnInfo.getRoundState().getTurnNumber() < 1) {
			return new InvalidTurnInfo(turnInfo.getTable(), null,
					Collections.<StoneSet> emptyList());
		}

		if (!turnInfo.getTable().isValid()) {
			rejectMove();
			return new InvalidTurnInfo(turnInfo.getTable(),
					InvalidTurnType.INVALID_SETS, invalidStoneSets());
		}
		if (!turnInfo.getLaidOut()) {
			// Player touched forbidden stones
			if (!tableSetDifference(turnInfo.getTable(), turnInfo.getOldTable())
					.isEmpty()) {
				rejectMove();
				return new InvalidTurnInfo(turnInfo.getTable(),
						InvalidTurnType.INITIAL_MELD_ERROR, touchedStoneSets());
			}
			if (!laidOutValidPoints()) {
				rejectMove();
				return new InvalidTurnInfo(turnInfo.getTable(),
						InvalidTurnType.NOT_ENOUGH_POINTS, tableSetDifference(
								turnInfo.getOldTable(), turnInfo.getTable()));
			}
		}
		Set<Stone> tableDiff = tableDifference(turnInfo.getOldTable(),
				turnInfo.getTable());

		turnInfo.getRoundState().setTable(turnInfo.getTable());

		if (tableDiff.isEmpty()) {
			// Player hasn't made a move
			dealStone();
		} else {
			turnInfo.getRoundState().getActivePlayer().setLaidOut(true);
		}
		return new InvalidTurnInfo(turnInfo.getTable(), null,
				Collections.<StoneSet> emptyList());
	}

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

	private List<StoneSet> touchedStoneSets() {
		List<StoneSet> touchedSets = new ArrayList<StoneSet>();
		for (StoneSet set : tableSetDifference(turnInfo.getOldTable(),
				turnInfo.getTable())) {
			for (Stone stone : set) {
				if (!turnInfo.getOldHand().contains(stone)) {
					touchedSets.add(set);
					break;
				}
			}
		}

		return touchedSets;
	}

	private boolean laidOutValidPoints() {
		List<StoneSet> newSets = tableSetDifference(turnInfo.getOldTable(),
				turnInfo.getTable());

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

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

	private void rejectMove() {
		Set<Stone> tableDiff = tableDifference(turnInfo.getOldTable(),
				turnInfo.getTable());
		// deal penalty, reset
		turnInfo.getRoundState().getStoneHeap().putBack(tableDiff);
		turnInfo.getRoundState().getActivePlayer().setLastTurnInvalid(true);
		dealPenalty(tableDiff.size());
	}

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

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

			hand.drop(turnInfo.getRoundState().getStoneHeap().drawStone(),
					new Position(Hand.WIDTH - 1, rowCount - 1));
		}

	}

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

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

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