/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package minesweeper.solver;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import Asynchronous.Asynchronous;
import minesweeper.gamestate.GameStateModel;
import minesweeper.gamestate.MoveMethod;
import minesweeper.solver.RolloutGenerator.Adversarial;
import minesweeper.solver.coach.CoachModel;
import minesweeper.solver.coach.CoachSilent;
import minesweeper.solver.constructs.CandidateLocation;
import minesweeper.solver.constructs.EvaluatedLocation;
import minesweeper.solver.constructs.InformationLocation;
import minesweeper.solver.constructs.WitnessData;
import minesweeper.solver.iterator.Iterator;
import minesweeper.solver.iterator.SequentialIterator;
import minesweeper.solver.settings.PlayStyle;
import minesweeper.solver.settings.SolverSettings;
import minesweeper.solver.settings.SolverSettings.GuessMethod;
import minesweeper.solver.utility.Binomial;
import minesweeper.solver.utility.Logger;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.solver.utility.ProgressMonitor;
import minesweeper.structure.Action;
import minesweeper.structure.Area;
import minesweeper.structure.Location;

/**
 *
 * @author David
 */
public class Solver implements Asynchronous<Action[]> {


	public final static String VERSION = "1.05";


	// used to hold valid moves which are about to be passed out of the solver
	private class FinalMoves {

		Action[] result = new Action[0];
		int suppressedFlags = 0;    // number of place flag moves suppressed because of playing Flag Free
		boolean moveFound = false; // this is set to true if a move is found, even if it is suppressed

		private FinalMoves(Action...actions) {
			result = actions;
			moveFound = (actions.length > 0);
		}

	}

	private class LoopCheck implements Runnable {

		private boolean finished = false;

		@Override
		public void run() {

			int countDown = 100;

			while (countDown > 0 && !finished) {
				try {
					Thread.sleep(20);
				} catch (InterruptedException e) {
				}		    	
				countDown--;
			}

			if (!finished) {
				System.out.println(myGame.showGameKey() + " might be looping");
			}

		}

		public void finishedOkay() {
			finished = true;
		}

	}


	public final static int DP = 20;

	public final static BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100);


	final static BigDecimal OFF_EDGE_TOLERENCE = new BigDecimal("0.95");  // was 0.98 --- consider off edge tiles which if they are above the threshold of the best on edge tile
	final static boolean PRUNE_BF_ANALYSIS = true;
	final static boolean CONSIDER_HIGH_DENSITY_STRATEGY = false;

	public final static BigDecimal PROGRESS_VALUE = new BigDecimal("0.20");  // how much 100% Progress is worth as a proportion of Safety
	final static BigDecimal PROB_ENGINE_HARD_TOLERENCE = new BigDecimal("0.90"); // consider tiles on the edge with a threshold of this from the best value
	final static BigDecimal PROGRESS_MULTIPLIER = BigDecimal.ONE.add(PROGRESS_VALUE);
	//final static BigDecimal OFF_EDGE_TOLERENCE = BigDecimal.ONE.subtract(PROGRESS_VALUE); // consider off edge tiles which if they are above the threshold of the best on edge tile

	final static BigDecimal PROB_ENGINE_TOLERENCE = BigDecimal.ONE.subtract(PROGRESS_VALUE).max(PROB_ENGINE_HARD_TOLERENCE);
	//final static BigDecimal PROB_ENGINE_TOLERENCE = new BigDecimal("0.85");  // for experimental tiebreak

	// won't play the book opening on start if false
	//protected final static boolean PLAY_OPENING = true;

	/**
	 * If the number of iterations is less than this then process sequential else go parallel
	 */
	final static BigInteger PARALLEL_MINIMUM = new BigInteger("10000");

	final static int CORES = Runtime.getRuntime().availableProcessors();


	// a binomial coefficient generator which allows up to (choose n from 1000000) and builds a cache of everything up to (choose n from 100) 
	static Binomial binomialEngine = new Binomial(1000000, 500); 


	protected final SolverSettings preferences;
	protected final Logger logger;

	// the class that knows the real board layout, which squares have been revealed and where the flags are
	private final GameStateModel myGame;

	// a class which can be used to display the summary information generated by the solver
	private final CoachModel coachDisplay;

	// a class which holds the solves current view of the board
	private final BoardState boardState;
	private ProbabilityEngineModel pe;
	private BruteForce bf;

	private BruteForceAnalysisModel bruteForceAnalysis;
	private LocationEvaluator evaluateLocations;

	private BigDecimal offEdgeProb;

	private List<Location> bfdaStartLocations = null;

	private List<Location> allWitnesses;
	private Area allWitnessedSquares;
	private Area deadLocations;


	// work areas 
	private boolean[] workRestNotFlags;
	private boolean[] workRestNotClear;


	private Location overriddenStartLocation;

	private final boolean interactive;

	private FinalMoves answer;

	private PlayStyle playStyle = PlayStyle.FLAGGED;

	// used to indicate that the solver shouldn't bother placing flags on the board
	// this is considered expert tactics because it reduces the number of mouse actions.
	//private boolean flagFree = false;

	// playing chords will make the solver run slower, but should result in less moves
	// it is suggested to play chords if playing an external boarding using mouse controller and you wish to look impressive
	//private boolean playChords = false;

	// Shows the best tree in sysout from Brute Force Deep analysis
	private boolean showProbabilityTree = false;

	// won't play the book opening on start if false
	private boolean playOpening = true;

	private boolean early5050Check = false;
	
	// If we are only interested in the win rate we can cheat when we encounter isolated edges
	// if there is x chance of surviving the edge then 
	private boolean winRateOnly = false;
	private BigDecimal winValue = BigDecimal.ONE;

	/**
	 * Start the solver without a coach display
	 */
	public Solver(GameStateModel myGame, SolverSettings preferences, boolean interactive) {
		this(myGame, preferences, new CoachSilent(), interactive);
	}

	/**
	 * Start the solver with a coach display
	 * @param myGame
	 * @param preferences
	 * @param interactive
	 */
	public Solver(GameStateModel myGame, SolverSettings preferences, CoachModel coachDisplay, boolean interactive) {

		this.myGame = myGame;
		this.interactive = interactive;
		this.preferences = preferences.lockSettings();

		if (this.interactive) {
			this.logger = new Logger(Level.INFO, "Solver");
		} else {
			this.logger = new Logger(Level.ERROR, "Solver");
		}

		this.overriddenStartLocation = preferences.getStartLocation();

		this.boardState = new BoardState(this);
		boardState.process();

		logger.log(Level.INFO, "Running with %d Cores", CORES);
		logger.log(Level.INFO, "Max memory available to JVM %d", Runtime.getRuntime().maxMemory());
		logger.log(Level.INFO, "Free Memory available to JVM %d", Runtime.getRuntime().freeMemory());
		logger.log(Level.INFO, "Solving game %s", myGame.showGameKey());

		this.coachDisplay = coachDisplay;

		List<Location> witnesses = new ArrayList<>(500);
		for (int x=0; x < myGame.getWidth(); x++) {
			for (int y=0; y < myGame.getHeight(); y++) {
				Location l = boardState.getLocation(x,y);
				if (myGame.query(l) != GameStateModel.FLAG && myGame.query(l) != GameStateModel.HIDDEN) {
					witnesses.add(l);
				}
			}
		}
		logger.log(Level.DEBUG, "Found %d witnesses already in the game", witnesses.size());

	}


	// Start of Asynchronous methods 
	@Override
	public void start() {

		LoopCheck check = new LoopCheck();

		Thread checkThread  = new Thread(check);
		checkThread.start();

		int loopSafe = 0;

		answer = newProcess();
		while (answer.moveFound && answer.result.length == 0) {
			if (loopSafe++ >= 5) {
				this.logger.log(Level.WARN, "LOOPSAFE check!! - exiting the processing after %d iterations", loopSafe);
				break;
			}
			logger.log(Level.DEBUG, "There are no moves provided ( %d have been supressed) - rerunning the solver", answer.suppressedFlags );
			answer = newProcess();
		}

		check.finishedOkay();

	}

	@Override
	public void requestStop() {
	}

	@Override
	public Action[] getResult() {
		return answer.result;
	}    
	// end of Asynchronous methods

	public BigDecimal getWinValue() {
		return this.winValue;
	}
	
	/**
	 * Return a list of Tiles which were considered when picking a guess
	 * @return
	 */
	public List<EvaluatedLocation> getEvaluatedLocations() {
		if (evaluateLocations == null) {
			return null;
		} else {
			return evaluateLocations.getEvaluatedLocations();
		}
	}

	/**
	 * Return an Area containing locations which are determined to be dead
	 * @return
	 */
	public Area getDeadLocations() {
		if (deadLocations == null) {
			return null;
		} else {
			return deadLocations;
		}
	}

	/**
	 * True indicates the solver shouldn't place flags
	 * @param flagFree
	 */

	//public void setFlagFree(boolean flagFree) {
	//	this.flagFree = flagFree;
	//}

	//public boolean isFlagFree() {
	//	return this.flagFree;
	//}

	/**
	 * True indicates the solver should play the opening move at the start
	 * @param flagFree
	 */
	public void setPlayOpening(boolean playOpening) {
		this.playOpening = playOpening;
	}

	/**
	 * Use this to override the default start location (which depends on the game type being played)
	 * @param startLocation
	 */
	//public void setStartLocation(Location startLocation) {
	//	overriddenStartLocation = startLocation;
	//}

	/**
	 * Sets the solver play style
	 */
	public void setPlayStyle(PlayStyle playStyle) {
		this.playStyle = playStyle;
	}

	public PlayStyle getPlayStyle() {
		return this.playStyle;
	}


	public void setShowProbabilityTree(boolean showTree) {
		this.showProbabilityTree = showTree;
	}

	public boolean isShowProbabilityTree() {
		return this.showProbabilityTree;
	}

	public void setBFDAStartLocations(List<Location> start) {
		this.bfdaStartLocations = start;
	}

	List<Location> bfdaStartLocations() {
		return bfdaStartLocations;
	}

	private FinalMoves newProcess() {

		FinalMoves fm = doNewProcess();

		if (fm.result.length > 0) {
			newLine("---------- Recommended Move ----------");
			newLine(fm.result[0].toString());
			newLine("----------  Analysis Ended -----------");
		}

		if (boardState.getTestMoveBalance() != 0) {
			this.logger.log(Level.ERROR, "Test moves are not being set and reset in pairs!! Balance = %d", boardState.getTestMoveBalance());

		}

		return fm;
	}


	private FinalMoves doNewProcess() {

		this.logger.log(Level.INFO, "--- Starting Analysis ---");

		Action[] result = null;

		FinalMoves fm = new FinalMoves();

		long time1 = System.currentTimeMillis();

		coachDisplay.clearScreen();
		pe = null;
		bf = null;
		evaluateLocations = null;
		deadLocations = null;

		if (myGame.getGameState() == GameStateModel.LOST) {
			topLine("The game has been lost, so no further analysis is possible");
			if (myGame.supports3BV()) {
				newLine("3BV value " + myGame.getTotal3BV());
				newLine("3BV solved " + myGame.getCleared3BV());
				newLine("Action Count " + myGame.getActionCount());
				double eff = ((10000 * myGame.getCleared3BV()) / myGame.getActionCount()) / 100d;
				newLine("Efficiency is " + eff + "%");
			}
			return fm;
		}

		if (myGame.getGameState() == GameStateModel.WON) {
			topLine("The game has been won, so no further analysis is required");
			if (myGame.supports3BV()) {
				newLine("3BV value " + myGame.getTotal3BV());
				newLine("3BV solved " + myGame.getCleared3BV());
				newLine("Action Count " + myGame.getActionCount());
				double eff = ((10000 * myGame.getTotal3BV()) / myGame.getActionCount()) / 100d;
				newLine("Efficiency is " + eff + "%");
			}
			return fm;
		}        

		// query the game State object to get the current board position
		boardState.process();

		// being asked to start the game
		if (myGame.getGameState() == GameStateModel.NOT_STARTED && playOpening) {

			if (myGame.safeOpening()) {
				offEdgeProb = BigDecimal.ONE;   
			} else {
				offEdgeProb = BigDecimal.ONE.subtract(BigDecimal.valueOf(myGame.getMinesLeft()).divide(BigDecimal.valueOf(myGame.getHidden()), Solver.DP, RoundingMode.HALF_UP));
			}

			fm = guess(null);

			newLine("This is the first move");
			newLine("Note: if you aren't accepting guesses nothing will happen!");
			newLine("---------- Recommended Move ----------");
			newLine(fm.result[0].toString());
			newLine("----------  Analysis Ended -----------");        

			return fm;
		}                

		// are we walking down a brute force deep analysis tree?
		if (bruteForceAnalysis != null) {
			Location expectedMove = bruteForceAnalysis.getExpectedMove();
			if (bruteForceAnalysis.isShallow() || expectedMove == null) {  // if the analysis was shallow then don't rely on it
				bruteForceAnalysis = null;
			} else {
				if (expectedMove != null && !boardState.isRevealed(expectedMove)) {  // we haven't played the recommended move - so the analysis is probably useless
					this.logger.log(Level.INFO, "The expected Brute Force Analysis move %s wasn't played", expectedMove );
					bruteForceAnalysis = null;
				} else {
					if (myGame.query(expectedMove) != 0) {
						Action move = bruteForceAnalysis.getNextMove(boardState);
						if (move != null) {
							this.logger.log(Level.INFO, "Brute Force Deep Analysis move is %s", move);
							newLine("-------- Brute Force Deep Analysis Tree --------");
							newLine(move.toString());
							newLine("--------  Brute Force Deep Analysis Tree---------");
							return new FinalMoves(move);
						}        		            			
					} else {
						this.logger.log(Level.INFO, "After a zero the board can be in an unexpected state, so cancelling Brute Force Analysis moves");
						bruteForceAnalysis = null;
					}

				}       		
			}
		}

		int unrevealed = boardState.getTotalUnrevealedCount();

		allWitnesses = boardState.getAllLivingWitnesses();
		allWitnessedSquares = boardState.getUnrevealedArea(allWitnesses);


		newLine("----------- Game Situation -----------");
		newLine("There are " + allWitnesses.size() + " witness(es)");  
		newLine("There are " + allWitnessedSquares.size() + " square(s) witnessed, out of " + unrevealed);

		if (unrevealed == 0) {
			newLine("Nothing to analyse!");
			//return fm;
		}

		// are the flags in the correct place?
		if (coachDisplay.analyseFlags() && boardState.getTotalFlagCount() > 0) {
			//newLine("----------  Flag Analysis -----------");
			if (boardState.getConfirmedMineCount() == boardState.getTotalFlagCount()) {
				coachDisplay.setOkay();
				newLine("All " + boardState.getTotalFlagCount() + " flags have been confirmed as correct");
			} else { 
				newLine((boardState.getTotalFlagCount() - boardState.getConfirmedMineCount()) + " flags can not be confirmed as correct");
				if (boardState.validateData()) {
					coachDisplay.setWarn();
				} else {
					newLine("At least 1 flag is definitely wrong!");
					coachDisplay.setError();
				}
			}
		} else {
			coachDisplay.setOkay();
		}

		int totalMinesConfirmed = boardState.getConfirmedMineCount();

		// Build a web of all the witnesses still useful and all the un-revealed tiles adjacent to them
		WitnessWeb wholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations());

		int obvious = 0;
		int lessObvious = 0;

		obvious = findTrivialActions(wholeEdge.getPrunedWitnesses());

		long time2 = System.currentTimeMillis();

		lessObvious = findLocalActions(wholeEdge.getPrunedWitnesses());

		long time3 = System.currentTimeMillis();

		// output some text describing the results

		int displayObvious = obvious + boardState.getUnplayedMoves(MoveMethod.TRIVIAL);
		int displayLessObvious = lessObvious + boardState.getUnplayedMoves(MoveMethod.LOCAL);

		newLine("----------- Basic Analysis -----------");
		newLine("There are " + displayObvious + " trivial moves found in " + (time2 - time1) + " milliseconds");        
		newLine("There are " + displayLessObvious + " locally certain moves found in " + (time3 - time2) + " milliseconds");

		this.logger.log(Level.INFO, "There are %d trivial / locally discoverable certain moves", (displayObvious + displayLessObvious));

		if (this.playStyle.efficiency || early5050Check) {

			fm = new FinalMoves(new Action[0]); 

			// we can't do the probability engine if extra mines have been found ... so return empty and try again
			if (boardState.getConfirmedMineCount() != totalMinesConfirmed) {
				fm.moveFound = true;
			} else {
				fm.moveFound = false;   // otherwise push on to the probability engine
			}

		} else {
			fm = new FinalMoves(boardState.getActions().toArray(new Action[0]));

			if (obvious + lessObvious > 0) {  // in flag free mode we can find moves which we don't play 
				fm.moveFound = true;
			}
		}


		int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount();

		if (interactive) {  // can be expensive to do this, so only if we are actually going to display it
			BigInteger comb = combination(minesLeft, unrevealed);
			this.logger.log(Level.INFO, "Combinations: choose %d from %d gives %d", minesLeft, unrevealed, comb);        	
		}

		// leave at this point if we have got something to do
		if (fm.moveFound) {
			return fm;
		}

		// If no trivial, local, or unavoidable guess then use the probability engine


		// find (some) dead locations on the board - these can be ignored when looking for a good guess
		deadLocations = Area.EMPTY_AREA;

		this.logger.log(Level.INFO, "----- Starting probability engine -----");

		pe = new ProbabilityEngineFast(boardState, wholeEdge, unrevealed, minesLeft);
		pe.process();

		// get the new deadLocations with any found by the probability engine 
		deadLocations = pe.getDeadLocations();

		offEdgeProb = pe.getOffEdgeProb();

		if (offEdgeProb.compareTo(BigDecimal.ONE) > 0) {
			this.logger.log(Level.ERROR, "Game %s has probability off edge of %f", myGame.showGameKey(), offEdgeProb);
		} else {
			this.logger.log(Level.INFO, "Probability off edge is %f", offEdgeProb);
		}

		this.logger.log(Level.INFO, "All Dead %b and dead locations %d", pe.allDead(), deadLocations.size() );
		// if all the locations are dead then just use any one (unless there is only one solution)
		if (pe.allDead() && deadLocations.size() != 0) {
			if (pe.getSolutionCount().compareTo(BigInteger.ONE) == 0) {
				this.logger.log(Level.INFO, "Only one solution left");
			} else {
				this.logger.log(Level.INFO, "All locations are dead");
			}
			
			if (winRateOnly) {
				BigDecimal chance = BigDecimal.ONE.divide(new BigDecimal(pe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
				fm = identifyLocations(deadLocations.getLocations(), chance);
				if (fm != null) {
					return fm;
				}
			}
			
			// if there are no squares next to a witness then just guess
			if (allWitnessedSquares.getLocations().isEmpty()) {
				return guess(wholeEdge);
			}

			// pick any tile since they're all dead
			Location picked = null;
			for (Location tile: allWitnessedSquares.getLocations()) {  // get any tile
				if (pe.getProbability(tile).signum() != 0) {
					picked = tile;
					break;
				}
			}


			CandidateLocation cl = new CandidateLocation(picked.x, picked.y, pe.getProbability(picked), 0, 0); 
			Action a = cl.buildAction(MoveMethod.GUESS);
			// let the boardState decide what to do with this action
			boardState.setAction(a);

			result = boardState.getActions().toArray(new Action[0]);

			fm = new FinalMoves(result);
			return fm;
		}

		// fetch the best candidates from the edge.  If high density only get the best tiles
		List<CandidateLocation> bestCandidates;
		if (boardState.isHighDensity()) {
			bestCandidates = pe.getBestCandidates(BigDecimal.ONE, true);
			//} else if (preferences.isExperimentalScoring()) {
			//	bestCandidates = pe.getBestCandidates(BigDecimal.valueOf(0.8d), true);
		} else {
			bestCandidates = pe.getBestCandidates( PROB_ENGINE_TOLERENCE, true);
		}

		List<Location> allUnrevealedSquares = null;

		BigDecimal offEdgeCutoff = pe.getBestOnEdgeProb().multiply(Solver.OFF_EDGE_TOLERENCE);

		this.logger.log(Level.INFO, "Off edge threshold is %f", offEdgeCutoff);

		// are clears off the edge within the permitted cut-off?
		boolean addOffEdgeOptions = (offEdgeProb.compareTo(offEdgeCutoff) > 0);

		this.logger.log(Level.INFO, "Probability Engine processing took %d milliseconds", pe.getDuration());
		this.logger.log(Level.INFO, "----- Probability engine finished -----");

		newLine("------ Probability Engine Analysis ------");
		newLine("There are " + pe.getIndependentGroups() + " independent edges on the board");
		newLine("Probability Engine processing took " + pe.getDuration() + " milliseconds");

		if (pe.getSolutionCount().bitLength() < 40) {
			newLine("There are " + pe.getSolutionCount() + " candidate solutions remaining");
		}

		boolean certainClearFound = pe.foundCertainty();

		// look for unavoidable 50/50 here if we are doing early checks
		// -7317529077410525620
		if (early5050Check) {
	        FiftyFiftyHelper fiftyFiftyHelper = null;
             if (preferences.isDo5050Check()) {
            	fiftyFiftyHelper = new FiftyFiftyHelper(boardState, wholeEdge, deadLocations);
            	Location findFifty = fiftyFiftyHelper.findUnavoidable5050(pe.getMines());

            	if (findFifty != null) {
    				Action a = new Action(findFifty, Action.CLEAR, MoveMethod.UNAVOIDABLE_GUESS, "Fifty-Fifty",  pe.getProbability(findFifty));  
    				fm = new FinalMoves(a);

            		newLine("--------- Unavoidable Guess ---------");
            		newLine("An unavoidable guess has been found - playing now to save time");
            		this.logger.log(Level.DEBUG, "Fifty/Fifty found in game %s : %s", myGame.showGameKey(), fm.result[0] );
            		return fm;
            	}
            }
		}


		// if there are no certain moves then process any Isolated non-dead edges we have found
		if (!certainClearFound && !pe.getIsolatedEdges().isEmpty()) {
			this.logger.log(Level.INFO, "Processing an Isolated edge");
			newLine("--------- Isolated Area ---------");
			newLine("An isolated area has been found which can be processed separately");

			// get the smallest isolated area to solve
			BruteForce cruncher = null;
			for (BruteForce c: pe.getIsolatedEdges()) {
				if (cruncher == null || c.getTileCount() < cruncher.getTileCount()) {
					cruncher = c;
				}
			}

			// determine all possible solutions
			cruncher.process();

			if (cruncher.hasRun()) {

				// determine best way to solver them
				BruteForceAnalysisModel bfa = cruncher.getBruteForceAnalysis();
				if (bfa != null) {
					bfa.process();

					// if after trying to process the data we can't complete then abandon it
					if (!bfa.isComplete()) {
						this.logger.log(Level.INFO, "%s Abandoned the Brute Force Analysis after %d steps", myGame.showGameKey(), bfa.getNodeCount() );
						bfa = null;

					} else { // otherwise try and get the best long term move

						//TODO
						if (winRateOnly) {
							fm = identifyLocations(bfa.getLocations(), bfa.getSolveChance());
							if (fm != null) {
								return fm;
							}
						}
						
						bruteForceAnalysis = bfa;  // by setting this we will walk the tree until completed in subsequent solver calls

						newLine("Built probability tree from " + bruteForceAnalysis.getSolutionCount() + " solutions in " + bruteForceAnalysis.getNodeCount() + " steps");
						Action move = bruteForceAnalysis.getNextMove(boardState);
						if (move != null) {
							this.logger.log(Level.INFO, "%s Brute Force Analysis: %s", myGame.showGameKey(), move);
							//newLine("Brute Force Analysis move is " + move.asString());
							fm = new FinalMoves(move);
							return fm;
						} else {
							if (bruteForceAnalysis.allDead()) {
								this.logger.log(Level.INFO, "All Brute Force Analysis moves are dead");

								// otherwise pick one of the ones on the edge
								Location picked = getLowest(bruteForceAnalysis.getDeadLocations().getLocations(), Area.EMPTY_AREA);
								
								if (picked != null) {
									fm = new FinalMoves(new Action(picked, Action.CLEAR, MoveMethod.GUESS, "", pe.getProbability(picked)));
									return fm;
								}

							}
							this.logger.log(Level.WARN, "Game %s Brute Force Analysis: no move found!", myGame.showGameKey());
						}
					}            	
				} else {
					this.logger.log(Level.WARN, "Game %s Brute Force analysis class is null", myGame.showGameKey());
				}

			} else {
				this.logger.log(Level.INFO, "Game %s Brute Force did not run", myGame.showGameKey());
			}
		}        


		if (bestCandidates.isEmpty()) {
			newLine("The probability engine found no candidate moves on the edge");
			newLine("Probability off the edge is " + Action.FORMAT_2DP.format(offEdgeProb.multiply(ONE_HUNDRED)) + "%");
		} else {
			newLine("The probability engine found " + bestCandidates.size() + " candidate moves on the edge");
		}

		BigDecimal safeDensity = BigDecimal.valueOf( (double) (unrevealed - minesLeft) / (double) unrevealed);
		this.logger.log(Level.INFO, "Safe density %f", safeDensity);
		BigDecimal safeDensity3 = new BigDecimal(pe.getSolutionCount()).multiply(safeDensity).multiply(safeDensity).multiply(safeDensity);
		this.logger.log(Level.INFO, "BFDA Solution value %f", safeDensity3);

		// do brute force if the number of candidate solutions is not greater than the allowable maximum
		//boolean doBruteForce = (pe.getSolutionCount().compareTo(BigInteger.valueOf(preferences.getBruteForceMaxSolutions())) <= 0);
		boolean doBruteForce = (pe.getSolutionCount().compareTo(BigInteger.valueOf(preferences.getBruteForceMaxSolutions())) <= 0) 
				|| (safeDensity3.compareTo(BigDecimal.valueOf(preferences.getBruteForceVariableSolutions())) <= 0);

		//boolean certainFlagFound = !pe.getMines().isEmpty();

		BruteForceAnalysisModel incompleteBFA = null;  // this is used to carry forward an analysis run which didn't complete

		// Probability engine says there are few enough candidate solutions to do a Brute force deep analysis - so lets try
		if (doBruteForce && !certainClearFound) {
			this.logger.log(Level.INFO, "----- Brute Force starting -----");
			newLine("----------- Brute Force Analysis -----------");

			allUnrevealedSquares = boardState.getAllUnrevealedSquares();

			WitnessWeb wholeBoard = new WitnessWeb(boardState, wholeEdge.getPrunedWitnesses(), allUnrevealedSquares);

			bf = new BruteForce(this, boardState, wholeBoard, minesLeft, preferences.getBruteForceMaxIterations(), pe.getSolutionCount().intValue(), "Game");

			bf.process();

			if (bf.hasRun()) {
				newLine("Found " + bf.getSolutionCount() + " candidate solutions from " + bf.getIterations() + " iterations");

				// Interpret the brute force data if we have some
				this.bruteForceAnalysis = bf.getBruteForceAnalysis();
				if (!bf.hasCertainClear() && bruteForceAnalysis != null) {  // if we haven't found some 100% clears and we can do a deeper analysis

					bruteForceAnalysis.process();

					// if all the locations are dead then just use any one
					if (bruteForceAnalysis.allDead()) {
						this.logger.log(Level.INFO, "Brute force deep analysis has detected that all locations are dead");
						// if there are no squares next to a witness then just guess
						if (allWitnessedSquares.getLocations().isEmpty()) {
							return guess(wholeEdge);
						}

						// otherwise pick one of the ones on the edge
						Location picked = null;
						for (Location l: allWitnessedSquares.getLocations()) {
							if (pe.getProbability(l).signum() != 0) { // pick a tile which isn't a mine
								picked = l;
								break;
							}
						}
						if (picked != null) {
							CandidateLocation cl = new CandidateLocation(picked.x, picked.y, pe.getProbability(picked), 0, 0); 
							Action a = cl.buildAction(MoveMethod.GUESS);
							// let the boardState decide what to do with this action
							boardState.setAction(a);

							result = boardState.getActions().toArray(new Action[0]);

							fm = new FinalMoves(result);
							return fm;                    		
						}

					}


					// if after trying to process the data we can't complete then abandon it
					if (!bruteForceAnalysis.isComplete()) {
						this.logger.log(Level.INFO, "Game %s Abandoned the Brute Force Analysis after %d steps, %d of %d moves analysed", 
								myGame.showGameKey(), bruteForceAnalysis.getNodeCount(),  bruteForceAnalysis.getMovesProcessed(),  bruteForceAnalysis.getMovesToProcess());
						incompleteBFA = bruteForceAnalysis;  // remember the incomplete analysis
						bruteForceAnalysis = null;

					} else { // otherwise try and get the best long term move

						deadLocations = bruteForceAnalysis.getDeadLocations();

						// do win rate only
						if (winRateOnly) {
							fm = identifyLocations(bruteForceAnalysis.getLocations(), bruteForceAnalysis.getSolveChance());
							if (fm != null) {
								return fm;
							}
						}
						
						newLine("Built probability tree from " + bruteForceAnalysis.getSolutionCount() + " solutions in " + bruteForceAnalysis.getNodeCount() + " steps");
						Action move = bruteForceAnalysis.getNextMove(boardState);
						if (move != null) {
							this.logger.log(Level.DEBUG, "Brute Force Analysis move: %s", move);
							fm = new FinalMoves(move);
						} else {
							this.logger.log(Level.WARN, "Game %s Brute Force Analysis: no move found!", myGame.showGameKey());
						}
					}
				} 

				// if we didn't find a BFDA move (too many solutions or too many nodes searched)
				if (!fm.moveFound) {

					if (bestCandidates.isEmpty()) {
						newLine("Brute Force didn't find any moves...?");
					} else if (bestCandidates.get(0).getProbability().compareTo(BigDecimal.ONE) == 0) {
						newLine("There are " + bestCandidates.size() + " certain moves");
					} else {
						newLine("There are no certain moves, so use the best guess");
					}   
				}

			} else { 
				newLine("Brute Force rejected - too many iterations to analyse");
			}
			this.logger.log(Level.INFO, "----- Brute Force finished -----");        	
		}

		// look for unavoidable 50/50
		FiftyFiftyHelper fiftyFiftyHelper = null;
		if (!certainClearFound && !fm.moveFound && !early5050Check) {
			if (preferences.isDo5050Check()) {
				fiftyFiftyHelper = new FiftyFiftyHelper(boardState, wholeEdge, deadLocations);
				Location findFifty = fiftyFiftyHelper.findUnavoidable5050(pe.getMines());

				if (findFifty != null) {
					Action a = new Action(findFifty, Action.CLEAR, MoveMethod.UNAVOIDABLE_GUESS, "Fifty-Fifty",  pe.getProbability(findFifty));  
					fm = new FinalMoves(a);

					newLine("--------- Unavoidable Guess ---------");
					newLine("An unavoidable guess has been found - playing now to save time");
					this.logger.log(Level.DEBUG, "Fifty/Fifty found in game %s : %s", myGame.showGameKey(), fm.result[0] );
					return fm;
				}
			}
		}

		// look for pseudo 50-50 guess 
		LongTermRiskHelper ltr = null;
		if (!certainClearFound && !fm.moveFound) {
			ltr = new LongTermRiskHelper(boardState, wholeEdge, pe);
			if (preferences.isDo5050Check()) {
				//Location findFifty = fiftyFiftyHelper.process(pe);

				Location findFifty = ltr.findInfluence();

				if (findFifty != null) {
					Action a = new Action(findFifty, Action.CLEAR, MoveMethod.UNAVOIDABLE_GUESS, "Fifty-Fifty",  pe.getProbability(findFifty));  
					fm = new FinalMoves(a);

					newLine("--------- Unavoidable Guess ---------");
					newLine("An unavoidable guess has been found - playing now to save time");
					this.logger.log(Level.DEBUG, "Fifty Fifty %s : %s", myGame.showGameKey(), fm.result[0]);
					return fm;
				}
			}
		}

		//  evaluate positions
		if (preferences.getGuessMethod() == GuessMethod.SECONDARY_SAFETY_PROGRESS) {
			evaluateLocations = new SecondarySafetyEvaluator(this, boardState, wholeEdge, pe, incompleteBFA, ltr);
		} else {
			evaluateLocations = new ProgressEvaluator(this, boardState, wholeEdge, pe);
		}

		// if we have few enough solutions do an adversarial rollout
		if (!fm.moveFound && !certainClearFound && !pe.isBestGuessOffEdge() && pe.getSolutionCount().compareTo(BigInteger.valueOf(preferences.getRolloutSolutions())) < 0) {

			this.logger.log(Level.INFO, "Doing adversarial rollout");

			long nanoStart = System.nanoTime();
			WitnessWeb arWholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations());

			RolloutGenerator rolloutGenerator = new RolloutGenerator(boardState, arWholeEdge, unrevealed, minesLeft);
			rolloutGenerator.process();

			List<Adversarial<CandidateLocation>> rolloutResult = rolloutGenerator.adversarial(bestCandidates);

			fm = new FinalMoves(rolloutResult.get(0).original.buildAction(MoveMethod.ROLLOUT));

			long nanoEnd = System.nanoTime();

			this.logger.log(Level.INFO, "Adversarial rollout took %f milli seconds", + (nanoEnd - nanoStart) / 1000000 );

		}

		// if we haven't got a move from the BFDA
		if (!fm.moveFound) {

			// no certain moves and we aren't doing tiebreaks
			if (!certainClearFound && !preferences.isDoTiebreak()) {

				// if off edge is better than on edge
				if (pe.isBestGuessOffEdge()) {
					fm = guess(wholeEdge);
				} else {
					// take the first move
					if (bestCandidates.size() != 0) {
						for (CandidateLocation cl: bestCandidates) {
							Action move = cl.buildAction(MoveMethod.PROBABILITY_ENGINE);
							// let the boardState decide what to do with this action
							boardState.setAction(move);   
							break;
						}
					} else {
						for (CandidateLocation cl: pe.getBestCandidates(BigDecimal.ZERO, false)) {  // get the best guess even if dead
							Action move = cl.buildAction(MoveMethod.PROBABILITY_ENGINE);
							// let the boardState decide what to do with this action
							boardState.setAction(move);   
							break;
						}

					}

					Action[] moves = boardState.getActions().toArray(new Action[0]);
					fm = new FinalMoves(moves);
				}
				return fm;
			} else if (addOffEdgeOptions && !certainClearFound) { // evaluate the off edge moves
				this.logger.log(Level.INFO, "Adding the off edge super locations to the candidate moves");

				if (allUnrevealedSquares == null) {   // defer this until we need it, can be expensive
					allUnrevealedSquares = boardState.getAllUnrevealedSquares();
				}

				this.logger.log(Level.DEBUG, "About to evaluate best candidates -->");
				evaluateLocations.addLocations(bestCandidates);
				evaluateLocations.evaluateOffEdgeCandidates(allUnrevealedSquares);
				evaluateLocations.evaluateLocations();

				this.logger.log(Level.DEBUG, "<-- Done");  

				evaluateLocations.showResults();

				Action[] moves = evaluateLocations.bestMove();
				fm = new FinalMoves(moves);

			} else if (certainClearFound) { // if there is only one solution or the solutions are certainties
				// bestCandidates.size() == 1 || certainClearFound
				// register all the moves
				for (CandidateLocation cl: bestCandidates) {
					Action move = cl.buildAction(MoveMethod.PROBABILITY_ENGINE);
					// let the boardState decide what to do with this action
					boardState.setAction(move);        			

				}

				// if all the off edge tiles are safe add them into the board state
				if (pe.getOffEdgeProb().compareTo(BigDecimal.ONE) == 0) {
					int mineCountClears = 0;
					for (Location loc: boardState.getAllUnrevealedSquares()) {
						if (!allWitnessedSquares.contains(loc)) {
							boardState.setAction(new Action(loc, Action.CLEAR, MoveMethod.PROBABILITY_ENGINE, "",  BigDecimal.ONE));  
							mineCountClears++;
						}
					}
					//if (mineCountClears > 0 && boardState.getMines() - boardState.getConfirmedFlagCount() > 8) {
					//	this.logger.log(Level.ALWAYS, "Seed %s has large mine count", myGame.getSeed());
					//}
				}

				// if we have a certain clear then also register all the mines
				if (certainClearFound) {
					this.logger.log(Level.INFO, "Found %d mines using the probability engine", pe.getMines().size());
					for (Location loc: pe.getMines()) {
						// let the boardState decide what to do with this action
						boardState.setAction(new Action(loc, Action.FLAG, MoveMethod.PROBABILITY_ENGINE, "",  BigDecimal.ONE));     			
					}

				}

				if (this.playStyle.efficiency) {
					EfficiencyHelper eff = new EfficiencyHelper(boardState, wholeEdge, boardState.getActions(), pe);
					if (this.playStyle.flagless) {
						fm = new FinalMoves(eff.processNF().toArray(new Action[0]));
					} else {
						fm = new FinalMoves(eff.process().toArray(new Action[0]));
					}

				} else {
					fm = new FinalMoves(boardState.getActions().toArray(new Action[0]));
				}


			} else {    // evaluate which of the best candidates to choose
				this.logger.log(Level.DEBUG, "About to evaluate best candidates -->");
				evaluateLocations.addLocations(bestCandidates);
				evaluateLocations.evaluateLocations();
				this.logger.log(Level.DEBUG, "<-- Done");     

				evaluateLocations.showResults();

				Action[] moves = evaluateLocations.bestMove();
				fm = new FinalMoves(moves);

			} 

			// if still no move then guess
			if (!fm.moveFound) {
				newLine("No certain, or high probability moves found, guess away from a witness");
				fm = guess(wholeEdge);
			} 
		}


		return fm;

	}

	/**
	 * Identify which tiles are safe and which are mines by cheating
	 */
	private FinalMoves identifyLocations(Collection<? extends Location> locs, BigDecimal value) {
		
		FinalMoves fm;
		
		if (locs.isEmpty()) {
			System.err.println("Nothing to identify");
		}
		
		List<Action> acts = this.myGame.identifyLocations(locs);
		if (acts != null) {
			for (Action a: acts) {
				this.boardState.setAction(a);
			}
			
			this.winValue = this.winValue.multiply(value);
			
			//fm = new FinalMoves(acts.toArray(new Action[0]));
			fm = new FinalMoves(this.boardState.getActions().toArray(new Action[0]));
		} else {
			fm = null;
		}
		
		return fm;
	}
	
	/**
	 * Returns the tile with the lowest hash code.  This results in a consistent tile being returned.
	 */
	protected <T extends Location> T getLowest(Collection<T> targets, Area deadLocations) {

		T lowest = null;
		T lowestLiving = null;

		for (T tile: targets) {
			if (lowest == null || lowest.hashCode() > tile.hashCode()) {
				lowest = tile;
			}
			if (deadLocations != null && !deadLocations.contains(tile)) {
				if (lowestLiving == null || lowestLiving.hashCode() > tile.hashCode()) {
					lowestLiving = tile;
				}
			}
		}

		if (lowestLiving != null) {
			return lowestLiving;
		} else {
			return lowest;
		}


	}


	/**
	 * This method will find the number of solutions which satisfy the constraints on the board
	 */
	public BigInteger getSolutionCount() throws Exception {

		// query the game State object to get the current board position
		boardState.process();

		int unrevealed = boardState.getTotalUnrevealedCount();

		List<Location> allWitnesses = boardState.getAllLivingWitnesses();
		Area allWitnessedSquares = boardState.getUnrevealedArea(allWitnesses);

		// Build a web of all the witnesses still useful and all the un-revealed tiles adjacent to them
		WitnessWeb wholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations());

		if (!wholeEdge.isWebValid()) {
			this.logger.log(Level.WARN, "Web is invalid");
			throw new Exception("Board is invalid");
		}

		int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount();

		this.logger.log(Level.INFO, "Mines left=%d, Unrevealed=%d, Witnesses=%d, Witnessed tiles=%d", minesLeft, unrevealed, allWitnesses.size(), allWitnessedSquares.size());

		SolutionCounter counter = new SolutionCounter(boardState, wholeEdge, unrevealed, minesLeft);
		counter.process(Area.EMPTY_AREA);

		return counter.getSolutionCount();

	}

	/**
	 * This method will rollout generator which can be used to construct random boards from this position
	 */
	public RolloutGenerator getRolloutGenerator() throws Exception {

		// query the game State object to get the current board position
		boardState.process();

		int unrevealed = boardState.getTotalUnrevealedCount();

		List<Location> allWitnesses = boardState.getAllLivingWitnesses();
		Area allWitnessedSquares = boardState.getUnrevealedArea(allWitnesses);

		// Build a web of all the witnesses still useful and all the un-revealed tiles adjacent to them
		WitnessWeb wholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations());

		if (!wholeEdge.isWebValid()) {
			this.logger.log(Level.WARN, "Web is invalid");
			throw new Exception("Board is invalid");
		}

		int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount();

		this.logger.log(Level.INFO, "Mines left=%d, Unrevealed=%d, Witnesses=%d, Witnessed tiles=%d", minesLeft, unrevealed, allWitnesses.size(), allWitnessedSquares.size());

		RolloutGenerator generator = new RolloutGenerator(boardState, wholeEdge, unrevealed, minesLeft);
		generator.process();

		return generator;

	}

	/**
	 * This method will find use the probability engine to get all the unrevealed tiles chance of being a mine
	 */
	public Map<Location, InformationLocation> runTileAnalysis(ProgressMonitor pm) throws Exception {

		Map<Location, InformationLocation> result = new HashMap<>();

		// query the game State object to get the current board position
		boardState.process();

		int unrevealed = boardState.getTotalUnrevealedCount();

		List<Location> allWitnesses = boardState.getAllLivingWitnesses();
		Area allWitnessedSquares = boardState.getUnrevealedArea(allWitnesses);

		// Build a web of all the witnesses still useful and all the un-revealed tiles adjacent to them
		WitnessWeb wholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations(), Logger.NO_LOGGING);

		if (!wholeEdge.isWebValid()) {
			this.logger.log(Level.WARN, "Web is invalid");
			throw new Exception("Board is invalid");
		}

		int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount();

		this.logger.log(Level.INFO, "Mines left=%d, Unrevealed=%d, Witnesses=%d, Witnessed tiles=%d", minesLeft, unrevealed, allWitnesses.size(), allWitnessedSquares.size());

		int maxProgress = boardState.getGameWidth() * boardState.getGameHeight() + 1;
		pm.SetMaxProgress("Processing", maxProgress);
		int progress = 0;

		ProbabilityEngineModel pe = new ProbabilityEngineFast(boardState, wholeEdge, unrevealed, minesLeft, Logger.NO_LOGGING);
		pe.process();

		// if the tile is flagged and we agree there is a mine there then mark it as discovered
		for (Location mine: pe.getMines()) {
			//if (boardState.isFlagOnBoard(mine)) {
				boardState.setMineFound(mine);
				
				InformationLocation il = new InformationLocation(mine.x,mine.y);
				il.setSafety(BigDecimal.ZERO);
				il.calculate();
				result.put(il, il);
			//}
		}
		
		System.out.println(pe.getMines().size());
		//this.logger.log(Level.INFO, "Mines found=%d", pe.getMines().size()); 

		// if we found some mines then recalculate the witness web
		if (pe.getMines().size() > 0) {
			boardState.process();
			
			unrevealed = boardState.getTotalUnrevealedCount();

			allWitnesses = boardState.getAllLivingWitnesses();
			allWitnessedSquares = boardState.getUnrevealedArea(allWitnesses);

			minesLeft = myGame.getMines() - boardState.getConfirmedMineCount();
			
			// Build a web of all the witnesses still useful and all the un-revealed tiles adjacent to them
			wholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations(), Logger.NO_LOGGING);
			
			//pe = new ProbabilityEngineFast(boardState, wholeEdge, unrevealed, minesLeft, Logger.NO_LOGGING);
			//pe.process();
		}
		
		
		pm.setProgress(++progress);

		LongTermRiskHelperOld ltr = new LongTermRiskHelperOld(boardState, wholeEdge, pe);
		ltr.findRisks();

		if (pe.getSolutionCount().signum() == 0) {
			throw new Exception("This board has no solutions");
		} else {
			for (int i=0; i < boardState.getGameWidth(); i++) {
				for (int j=0; j < boardState.getGameHeight(); j++) {
					if (boardState.isUnrevealed(i,j)) {

						InformationLocation il = new InformationLocation(i,j);

						il.setSafety(pe.getProbability(il));

						doFullEvaluateTile(wholeEdge, pe, il, ltr);

						il.calculate();

						result.put(il, il);
					}
					pm.setProgress(++progress);
				}
			}                        	   		 
		}

		return result;

	}

	private void doFullEvaluateTile(WitnessWeb wholeEdge, ProbabilityEngineModel probEngine, InformationLocation tile, LongTermRiskHelperOld ltr) {

		List<Location> superset = boardState.getAdjacentUnrevealedSquares(tile);
		int minesGot = boardState.countAdjacentConfirmedFlags(tile);

		int minMines = minesGot;
		int maxMines = minesGot + superset.size();

		//boardState.getLogger().log(Level.ALWAYS, tile + " ==> " + minMines + " - " + maxMines);
		
		// expected number of clears if we clear here to start with is 1 x our own probability
		//BigDecimal expectedClears = tile.getProbability(); 

		//boardState.display(tile.display() + " has " + linkedTiles + " linked tiles");

		BigDecimal progressProb = BigDecimal.ZERO;
		BigDecimal secondarySafety = BigDecimal.ZERO;
		BigDecimal longTermSafety = BigDecimal.ZERO;

		for (int i = minMines; i <= maxMines; i++) {

			//SolutionCounter counter = validateLocationUsingSolutionCounter(wholeEdge, tile, i, probEngine.getDeadLocations());
			ProbabilityEngineModel counter = runProbabilityEngine(wholeEdge, tile, i).pe;

			BigInteger sol = counter.getSolutionCount();
			int clears = counter.getLivingClearCount();

			if (sol.signum() != 0) {

				BigDecimal prob = new BigDecimal(sol).divide(new BigDecimal(probEngine.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);

				List<CandidateLocation> bestCandidates = counter.getBestCandidates(BigDecimal.ONE, true);

				BigDecimal safety;
				if (bestCandidates.size() == 0 ) { 
					safety = counter.getOffEdgeProb();
				} else {
					safety = bestCandidates.get(0).getProbability();
				}

				this.logger.log(Level.INFO, "Tile %s value %d has %d living clears with probability %f and secondary safety %f", tile, i, clears, prob, safety);

				longTermSafety = longTermSafety.add(prob.multiply(ltr.getLongTermSafety(tile, counter)));  // add all the weighted long term safety values together

				secondarySafety = secondarySafety.add(prob.multiply(safety));

				if (clears != 0) {
					progressProb = progressProb.add(prob);
				}

				// store the information
				tile.setByValue(i, clears, prob);

			} else {
				this.logger.log(Level.INFO, "Tile %s value %d with probability zero", tile, i);
			}

		}

		tile.setSecondarySafety(secondarySafety);

		if (tile.getSafety().signum() != 0) {
			longTermSafety = longTermSafety.divide(tile.getSafety(), Solver.DP, RoundingMode.HALF_UP);
		} else {
			longTermSafety = BigDecimal.ZERO;
		}

		tile.setLongTermSafety(longTermSafety);

	}



	private int findTrivialActions(List<? extends Location> witnesses) {

		int count = 0;

		for (Location loc: witnesses) {

			if (isObviousClear(loc)) {
				//boolean accepted = boardState.setChordLocation(loc);

				for (Location l: boardState.getAdjacentSquaresIterable(loc)) {
					if (boardState.isUnrevealed(l)) {
						if (!boardState.alreadyActioned(l)) {
							count++;
							boardState.setAction(new Action(l, Action.CLEAR, MoveMethod.TRIVIAL, "", BigDecimal.ONE));

						}

					}
				}

			} else if (isObviousFlag(loc)) {
				for (Location l: boardState.getAdjacentSquaresIterable(loc)) {
					if (boardState.isUnrevealed(l)) {
						if (!boardState.alreadyActioned(l)) {
							count++;

							boardState.setAction(new Action(l, Action.FLAG, MoveMethod.TRIVIAL, "",  BigDecimal.ONE));
							//boardState.setFlagConfirmed(l);
						}

					}
				}                		
			}
		}



		return count;

	}


	private boolean isObviousClear(Location loc) {

		//if (boardState.isRevealed(x,y) && boardState.getWitnessValue(x,y) != 0) {
		int flags = boardState.countAdjacentConfirmedFlags(loc);

		// if we have all the flags and there is something to clear
		if (boardState.getWitnessValue(loc) == flags && boardState.countAdjacentUnrevealed(loc) > 0) {
			return true;
		}
		//}      	

		return false;

	}


	private boolean isObviousFlag(Location loc) {

		//if (boardState.isRevealed(x,y) && boardState.getWitnessValue(x,y) != 0) {
		int flags = boardState.countAdjacentConfirmedFlags(loc);
		int free = boardState.countAdjacentUnrevealed(loc); 

		// if we only have space for the flags and there is some space
		if (boardState.getWitnessValue(loc) == flags + free && free > 0) {
			return true;
		} 
		//}      	

		return false;

	}

	private int findLocalActions(List<? extends Location> witnesses) {

		int count = 0;

		List<Location> square;
		List<Location> witness;

		for (Location loc: witnesses) {

			int flags = boardState.countAdjacentConfirmedFlags(loc);
			int free = boardState.countAdjacentUnrevealed(loc); 

			// if there are still some flags to find and there are 
			// too many places for it to be obvious ...
			if (free > 0 && boardState.getWitnessValue(loc) > flags && boardState.getWitnessValue(loc) < flags + free) {

				// get the un-revealed squares
				square = boardState.getAdjacentUnrevealedSquares(loc);

				// now get the witnesses
				witness = boardState.getWitnesses(square);

				// and crunch the result
				if (witness.size() > 1) {

					CrunchResult output = crunch(square, witness, new SequentialIterator(boardState.getWitnessValue(loc) - flags, square.size()), false, null);
					count = count + checkBigTally(output, MoveMethod.LOCAL, "");
					count = count + checkWitnesses(output, MoveMethod.LOCAL, "");  

				}                            

			}                                        


		}

		return count;

	}


	/**
	 * Checks whether this location can have the value using a localised check. Returns number squares which can be cleared. -1 means impossible situation
	 */
	/*
     protected int validateLocationUsingLocalCheck(Location superLocation, int value) {

    	 int clearCount = 0;

		 int minesToFit = value - boardState.countAdjacentConfirmedFlags(superLocation);

		 if (minesToFit == 0) {
			 return boardState.countAdjacentUnrevealed(superLocation);
		 	 //return true;
		 } else if (minesToFit < 0) { 
			 return -1;
			 //return false;
		 }

    	 // make the move
		 boardState.setWitnessValue(superLocation, value);

		 // get the un-revealed squares
		 List<Location> square = boardState.getAdjacentUnrevealedSquares(superLocation);

		 // now get the witnesses
		 List<Location> witnesses = boardState.getWitnesses(square);

		 // and crunch the result
		 try {
			 if (witnesses.size() > 1) {

				 //display(i + " " + j + " board " + board[i][j] + " flags = " + flags + " free = " + free);

				 CrunchResult output = crunch(square, witnesses, new SequentialIterator(boardState.getWitnessValue(superLocation) - boardState.countAdjacentConfirmedFlags(superLocation), square.size()), false, null);


				 if (output.bigGoodCandidates.signum() > 0) {
					 for (int i=0; i < output.bigTally.length; i++) {
						 if (output.bigTally[i].signum() == 0) {
							 Location l = output.getSquare().get(i);
							 if (!boardState.alreadyActioned(l)) {
								 clearCount++;
							 }
						 } 

					 }     
					 display(superLocation.display() + " Clear count = " + clearCount);
					 return clearCount;
					 //return true;
				 } else {
					 return -1;
					 //return false;
				 }

			 } else {         
				 return -1;
				 //return false;
			 }			 
		 } finally {
			 boardState.clearWitness(superLocation);
		 }


     }
	 */

	/**
	 * Checks whether this location can have the value using the solution counter
	 */
	protected SolutionCounter validateLocationUsingSolutionCounter(WitnessWeb wholeEdge, Location superLocation, int value, Area deadLocations) {

		// make the move
		boardState.setWitnessValue(superLocation, value);

		// create a new list of witnesses
		List<Location> witnesses = new ArrayList<>(wholeEdge.getPrunedWitnesses().size() + 1);
		witnesses.addAll(wholeEdge.getPrunedWitnesses());
		witnesses.add(superLocation);

		Area witnessed = boardState.getUnrevealedArea(witnesses);

		WitnessWeb edge = new WitnessWeb(boardState, witnesses, witnessed.getLocations(), Logger.NO_LOGGING);

		int unrevealed = boardState.getTotalUnrevealedCount() - 1;  // this is one less, because we have added a witness

		int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount();

		SolutionCounter counter = new SolutionCounter(boardState, edge, unrevealed, minesLeft);
		counter.process(deadLocations);

		// undo the move
		boardState.clearWitness(superLocation);

		return counter;

	}

	/**
	 * Checks whether this board state is valid
	 */
	protected SolutionCounter validatePosition(WitnessWeb wholeEdge, List<Location> mines, List<Location> noMines, Area deadLocations) {

		// add the mines
		for (Location mine: mines) {
			boardState.setMineFound(mine);
		}

		Area witnessed = boardState.getUnrevealedArea(wholeEdge.getPrunedWitnesses());

		WitnessWeb edge = new WitnessWeb(boardState, wholeEdge.getPrunedWitnesses(), witnessed.getLocations(), Logger.NO_LOGGING);

		int unrevealed = boardState.getTotalUnrevealedCount() - mines.size();  // this is less, because we have added some mines.

		int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount(); 

		SolutionCounter counter = new SolutionCounter(boardState, edge, unrevealed, minesLeft);

		//System.out.println("Unrevealed " + unrevealed + ", witnessed " + witnessed.size() + ", minesLeft " + minesLeft);

		// add the no mines
		if (noMines != null) {
			for (Location noMine: noMines) {
				if (!counter.setMustBeEmpty(noMine)) {
					this.logger.log(Level.WARN, "%s failed to set Must Be Empty", noMine);
				}
			}   	 
		}


		counter.process(deadLocations);

		// remove the mines
		for (Location mine: mines) {
			boardState.unsetMineFound(mine);
		}

		return counter;

	}


	class RunPeResult {
		ProbabilityEngineModel pe;
		boolean found5050 = false;
	}

	/**
	 * Runs the probability engine for a position with one extra witness than where we currently are
	 */
	protected RunPeResult runProbabilityEngine(WitnessWeb wholeEdge, Location location, int value) {

		// make the move
		boardState.setWitnessValue(location, value);

		// create a new list of witnesses
		List<Location> witnesses = new ArrayList<>(wholeEdge.getPrunedWitnesses().size() + 1);
		witnesses.addAll(wholeEdge.getPrunedWitnesses());
		witnesses.add(location);

		Area witnessed = boardState.getUnrevealedArea(witnesses);

		WitnessWeb edge = new WitnessWeb(boardState, witnesses, witnessed.getLocations(), Logger.NO_LOGGING);

		int unrevealed = boardState.getTotalUnrevealedCount() - 1;  // this is one less, because we have added a witness

		int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount();

		RunPeResult result = new RunPeResult();
		result.pe = new ProbabilityEngineFast(boardState, edge, unrevealed, minesLeft);

		result.pe.process();

		// looking for created 50/50s doesn't seem to help the win rate
		/*
    	 Location findFifty = new FiftyFiftyHelper(boardState, wholeEdge, deadLocations).findUnavoidable5050(result.pe.getMines());

    	 if (findFifty != null) {
    		 //this.logger.log(Level.WARN, "Fifty/Fifty created in game %s : %s", myGame.showGameKey(), findFifty );
    		 result.found5050 = true;
    	 }
		 */

		// undo the move
		boardState.clearWitness(location);

		return result;

	}

	protected CrunchResult crunch(final List<Location> square, final List<? extends Location> witness, Iterator iterator, boolean calculateDistribution, BruteForceAnalysisModel bfa) {

		this.logger.log(Level.DEBUG, "Crunching %d Mines in %d Tiles with %d Witnesses", iterator.getBalls(), square.size(), witness.size());

		// the distribution is the number of times a square reveals as the number 0-8
		BigInteger[][] bigDistribution = null;
		if (calculateDistribution) {
			bigDistribution = new BigInteger[square.size()][9];
			for (int i=0; i < square.size(); i++) {
				for (int j=0; j < 9; j++) {
					bigDistribution[i][j] = BigInteger.ZERO;
				}
			}
		}

		BigInteger bign = BigInteger.ZERO;


		// determine the witness type
		int[] witnessGood1 = generateWitnessType(witness, square);

		// encapsulate the witness data
		final WitnessData[] witnessData = new WitnessData[witness.size()];
		for (int i=0; i < witness.size(); i++) {
			WitnessData d = new WitnessData();
			d.location = witness.get(i);
			d.witnessGood = witnessGood1[i];
			d.witnessRestClear = true;
			d.witnessRestFlag = true;
			d.currentFlags = boardState.countAdjacentConfirmedFlags(d.location);
			d.alwaysSatisfied = iterator.witnessAlwaysSatisfied(d.location);
			witnessData[i] = d;
		}

		/*
        for (int i=0; i < square.length; i++) {
            display("Square " + i + " is " + square[i].display());
        }
		 */

		int[] sample = iterator.getSample();

		int[] tally = new int[square.size()];
		int candidates = 0;

		// define work areas
		workRestNotFlags = new boolean[witnessData.length];
		workRestNotClear = new boolean[witnessData.length];

		while (sample != null) {

			if (checkSample(sample, square, witnessData, bigDistribution, bfa)) {
				for (int i=0; i < sample.length; i++) {
					tally[sample[i]]++;
				}   
				candidates++;
			}

			sample = iterator.getSample();

		}

		BigInteger[] bigTally = new BigInteger[square.size()];
		for (int i = 0; i < bigTally.length; i++) {
			bigTally[i] = BigInteger.valueOf(tally[i]);
		}

		bign = BigInteger.valueOf(candidates);

		// store all the information we have gathered into this object for
		// later analysis
		CrunchResult output = new CrunchResult();
		output.setSquare(square);
		output.bigDistribution = bigDistribution;

		output.originalNumMines = iterator.getBalls();
		output.bigGoodCandidates = bign;
		output.bigTally = bigTally;

		// return data on the witnesses
		output.witness = new Location[witnessData.length];
		output.witnessGood = new int[witnessData.length];
		output.witnessRestClear = new boolean[witnessData.length];
		output.witnessRestFlags = new boolean[witnessData.length];

		for (int i=0; i < witnessData.length; i++) {
			output.witness[i] = witnessData[i].location;
			output.witnessGood[i] = witnessData[i].witnessGood;
			output.witnessRestClear[i] = witnessData[i].witnessRestClear;
			output.witnessRestFlags[i] = witnessData[i].witnessRestFlag;
		}

		return output;

	} 

	// this checks whether the positions of the mines are a valid candidate solution
	protected boolean checkSample(final int[] sample, final List<Location> square, WitnessData[] witnessData, BigInteger[][] bigDistribution, BruteForceAnalysisModel bfa) {

		/*
        String s= "";
        for (int i = 0; i < sample.length; i++) {
            s = s + " " + sample[i];
        }
        display(s);
		 */

		for (int i=0; i < witnessData.length; i++) {
			workRestNotFlags[i] = false;
			workRestNotClear[i] = false;
		}


		// get the location of the mines
		Location[] mine = new Location[sample.length];
		for (int i=0; i < sample.length; i++) {
			mine[i] = square.get(sample[i]);
		}

		for (int i=0; i < witnessData.length; i++) {

			if (!witnessData[i].alwaysSatisfied) {
				int flags1 = witnessData[i].currentFlags;
				int flags2 = 0;

				// count how many candidate mines are next to this witness
				for (int j = 0; j < mine.length; j++) {
					if (mine[j].isAdjacent(witnessData[i].location)) {
						flags2++;
					}
				}

				int flags3 = boardState.getWitnessValue(witnessData[i].location);

				// if the candidate solution puts more flags around the witness
				// than it says it has then the solution is invalid
				if (flags3 < flags1 + flags2) {
					WitnessData d = witnessData[0];
					witnessData[0] = witnessData[i];
					witnessData[i] = d;
					return false;
				}

				// if this is a 'good' witness and the number of flags around it
				// does not match with it exactly then the solution is invalid
				if (witnessData[i].witnessGood == 0 && flags3 != flags1 + flags2) {
					WitnessData d = witnessData[0];
					witnessData[0] = witnessData[i];
					witnessData[i] = d;
					return false;
				}

				if (flags3 != flags1 + flags2) {
					workRestNotClear[i] = true;
				}
				if (flags3 != flags1 + flags2 + witnessData[i].witnessGood) {
					workRestNotFlags[i] = true;
				}
			} else {
				// always satisfied means flag3 = flag1 + flag2, so the checks above can be simplified to
				if (witnessData[i].witnessGood != 0) {
					workRestNotFlags[i] = true;
				}                
			}

		}

		// if it is a good candidate solution then the witness information is valid
		for (int i=0; i < witnessData.length; i++) {
			if (workRestNotClear[i]) {
				witnessData[i].witnessRestClear = false;
			}
			if (workRestNotFlags[i]) {
				witnessData[i].witnessRestFlag = false;
			}
		}

		//if it is a good solution then calculate the distribution if required
		if (bfa != null && !bfa.tooMany()) {
			byte[] solution = new byte[square.size()];

			for (int i=0; i < square.size(); i++) {

				boolean isMine = false;
				for (int j=0; j < sample.length; j++) {
					if (i == sample[j]) {
						isMine = true;
						break;
					}
				}

				// if we are a mine then it doesn't matter how many mines surround us
				if (!isMine) {
					byte flags2 = (byte) boardState.countAdjacentConfirmedFlags(square.get(i));
					// count how many candidate mines are next to this square
					for (Location mine1 : mine) {
						if (mine1.isAdjacent(square.get(i))) {
							flags2++;
						}
					}            
					solution[i] = flags2;
					if (bigDistribution != null) {
						bigDistribution[i][flags2] = bigDistribution[i][flags2].add(BigInteger.ONE);
					}

				} else {
					solution[i] = GameStateModel.MINE;
				}

			}
			bfa.addSolution(solution);
		}

		return true;

	}


	// a witness is a 'good' witness if all its adjacent free squares are also
	// contained in the set of squares being analysed. A 'good' witness must
	// always be satisfied for the candidate solution to be valid.
	// this method returns the number of squares around the witness not being
	// analysed - a good witness has a value of zero
	protected int[] generateWitnessType(List<? extends Location> witness, List<Location> square) {

		int[] result = new int[witness.size()];

		for (int i=0; i < witness.size(); i++) {

			result[i] = 0;
			for (Location l: boardState.getAdjacentUnrevealedSquares(witness.get(i))) {
				boolean found = false;
				for (Location squ: square) {
					if (l.equals(squ)) {
						found = true;
						break;            			
					}
				}
				if (!found) {
					result[i]++; 
				}            	
			}

		}

		return result;

	}


	// do the tally check using the BigInteger values
	private int checkBigTally(CrunchResult output, MoveMethod method, String comment) {

		int result=0;

		// if there were no good candidates then there is nothing to check
		if (output.bigGoodCandidates.signum() == 0) {
			return 0;
		}

		// check the tally information to see if we have a square where a
		// mine is always present or never present
		for (int i=0; i < output.bigTally.length; i++) {

			if (output.bigTally[i].compareTo(output.bigGoodCandidates) == 0) {
				Location l = output.getSquare().get(i);

				if (!boardState.alreadyActioned(l)) {
					result++;

					boardState.setAction(new Action(l, Action.FLAG, method, comment, BigDecimal.ONE));

				}

			} else if (output.bigTally[i].signum() == 0) {
				Location l = output.getSquare().get(i);
				if (!boardState.alreadyActioned(l)) {
					result++;

					boardState.setAction(new Action(l, Action.CLEAR, method, comment, BigDecimal.ONE));
					//display("clear found at " + x + " " + y);
				}                
			}

		}        

		return result;

	}


	// in some cases we learn more about the other witnesses during the crunch
	// this only happens for local search. 
	private int checkWitnesses(CrunchResult output, MoveMethod method, String comment) {

		int result = 0;

		// check the witnesses to see if they have discovered something
		for (int i=0; i < output.witnessRestFlags.length; i++) {
			if (output.witnessGood[i] != 0) {
				if (output.witnessRestFlags[i]) {
					//display("**** CheckWitnesses has found a FLAG " + output.witness[i].display());
					result = result + restKnown(output.witness[i], output.getSquare(), Action.FLAG, method, comment);
				}                
				if (output.witnessRestClear[i]) {
					//display("**** CheckWitnesses has found a CLEAR " + output.witness[i].display());
					result = result + restKnown(output.witness[i], output.getSquare(), Action.CLEAR, method, comment);
				}                
			}
		} 

		return result;

	}



	private int restKnown(Location witness, List<? extends Location> square, int action, MoveMethod method, String comment) {

		int result=0;

		for (Location l: boardState.getAdjacentSquaresIterable(witness)) {

			// find all the unflagged and unrevealed squares
			if (!boardState.isRevealed(l) && !boardState.isConfirmedMine(l)) {

				boolean found = false;
				for (Location k: square) {
					if (l.equals(k)) {
						found = true;
						break;
					}
				}

				if (!found && !boardState.alreadyActioned(l)) {

					Action act;
					if (action == Action.FLAG) {
						act = new Action(l, Action.FLAG, method, comment, BigDecimal.ONE);
						//boardState.setFlagConfirmed(act);
					} else {
						act = new Action(l, Action.CLEAR, method, comment, BigDecimal.ONE);
					}
					result++;

					boardState.setAction(act);
					//display("Discovered witness information at " + x1 + " " + y1);
				}

			}

		}


		return result;

	} 

	/**
	 * Find the best guess off the edge
	 * @return
	 */
	private FinalMoves guess(WitnessWeb wholeEdge) {

		Action action = null;

		this.logger.log(Level.INFO, "Picking a guess");

		// get the starting move if we are at the start of the game
		if (myGame.getGameState() == GameStateModel.NOT_STARTED && playOpening) {
			if (overriddenStartLocation != null) {
				action = new Action(overriddenStartLocation, Action.CLEAR, MoveMethod.BOOK, "", offEdgeProb);
			} else { 
				action = new Action(myGame.getStartLocation(), Action.CLEAR, MoveMethod.BOOK, "", offEdgeProb);
			}
		}


		// if there is no book move then look for a guess off the edge
		if (action == null) {
			List<CandidateLocation> list = new ArrayList<>();


			for (int i=0; i < myGame.getWidth(); i++) {
				for (int j=0; j < myGame.getHeight(); j++) {
					// if we are an unrevealed square and we aren't on the contour
					// then store the location
					if (boardState.isUnrevealed(i,j)) {
						Location l = this.boardState.getLocation(i, j);
						// if we aren't on the edge and there are some adjacent squares 
						if ((wholeEdge == null || !wholeEdge.isOnWeb(l))) {
							list.add(new CandidateLocation(l.x, l.y, offEdgeProb, boardState.countAdjacentUnrevealed(l), boardState.countAdjacentConfirmedFlags(l)));
						}

					} 
				}
			}               

			// sort into most favourable order 
			//Collections.sort(list, CandidateLocation.SORT_BY_PROB_FLAG_FREE);
			Collections.sort(list, CandidateLocation.SORT_BY_PROB_FREE_FLAG);

			if (list.isEmpty()) {
				return new FinalMoves();
			}

			// ... and pick the first one
			action = list.get(0).buildAction(MoveMethod.GUESS);

		}

		// this will check there isn't a flag blocking the move 
		boardState.setAction(action);

		return new FinalMoves(boardState.getActions().toArray(new Action[0]));

	}

	public BigDecimal getProbability(int x, int y) {

		if (boardState.isConfirmedMine(x, y)) {
			return BigDecimal.ZERO;
		} else if (bf != null && bf.hasRun()) {
			return bf.getProbability(x, y);
		} else if (pe != null) {
			return pe.getProbability(this.boardState.getLocation(x,y));
		} else {
			return boardState.getProbability(x, y);
		}

	}

	protected void topLine(final String s) {

		coachDisplay.clearScreen();
		coachDisplay.writeLine(s);

	}

	protected void newLine(final String s) {

		coachDisplay.writeLine(s);

	}


	protected void display(String text) {

		if (interactive) {
			displayAlways(text);
		}

	}

	protected void displayAlways(String text) {

		System.out.println(text);

	}

	public void kill() {

		this.logger.log(Level.DEBUG, "Killing the Solver Object");

		coachDisplay.kill();

	}

	/**
	 * calculate the number of distinct ways mines can be placed in squares
	 */
	public static BigInteger combination(int mines, int squares) {

		try {
			return binomialEngine.generate(mines, squares);
		} catch (Exception e) {
			System.out.println("Error calculating the binomial coefficient");
			e.printStackTrace();
			//throw new RuntimeException("Error calculating the binomial coefficient", e);
			return BigInteger.ONE;
		}

	}    

	public GameStateModel getGame() {
		return myGame;
	}

}
