package minesweeper.solver;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import minesweeper.gamestate.GameStateModel;
import minesweeper.gamestate.MoveMethod;
import minesweeper.solver.BoardStateCache.AdjacentSquares;
import minesweeper.solver.BoardStateCache.Cache;
import minesweeper.solver.utility.Logger;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Action;
import minesweeper.structure.Area;
import minesweeper.structure.Location;

public class BoardState {

	private final static int[] DX = {0, 1, 1, 1, 0, -1, -1, -1};
	private final static int[] DY = {-1, -1, 0, 1, 1, 1, 0, -1};

	//private AdjacentSquares[][] adjacentLocations1;
	//private AdjacentSquares[][] adjacentLocations2;

	private int[][] board;
	private boolean[][] revealed;
	private boolean[][] confirmedMine;
	private boolean[][] flagOnBoard;
	
	//private int[][] clearAll;

	private int[][] adjFlagsConfirmed;
	private int[][] adjFlagsOnBoard;
	private int[][] adjUnrevealed;

	// this holds the actions made against each location and a list of actions generated this turn
	private Action[][] action;
	private List<Action> actionList = new ArrayList<Action>();

	private final GameStateModel myGame;
	private final Solver solver;
	private final int height;
	private final int width;

	private int totalFlags = 0;
	private int confirmedMinesTotal = 0;
	private int numOfHidden = 0;

	private int[] unplayedMoves;

	private int testMoveBalance = 0;
	
	private Set<Location> livingWitnesses = new HashSet<>();
	
	private final Cache cache;
	
	public BoardState(Solver solver) {

		this.solver = solver;
		this.myGame = solver.getGame();
		this.width = myGame.getWidth();
		this.height = myGame.getHeight();

		confirmedMine = new boolean[myGame.getWidth()][myGame.getHeight()];
		adjFlagsConfirmed =  new int[myGame.getWidth()][myGame.getHeight()];
		adjUnrevealed = new int[myGame.getWidth()][myGame.getHeight()];
		revealed = new boolean[myGame.getWidth()][myGame.getHeight()];
		board = new int[myGame.getWidth()][myGame.getHeight()];
		
		flagOnBoard = new boolean[myGame.getWidth()][myGame.getHeight()];
		adjFlagsOnBoard = new int[myGame.getWidth()][myGame.getHeight()];
		
		action = new Action[myGame.getWidth()][myGame.getHeight()];

		// look up the adjacent squares details
		cache = BoardStateCache.getInstance().getAdjacentSquares1(myGame.getWidth(), myGame.getHeight());
		//adjacentLocations1 = cache.adjacentLocations1;
		//adjacentLocations2 = cache.adjacentLocations2;
		

		final int bottom = myGame.getHeight() - 1;
		final int right =  myGame.getWidth() - 1;

		//  set up how many adjacent locations there are to each square - they are all unrevealed to start with
		for (int x=0; x < width; x++) {
			for (int y=0; y < height; y++) {

				int adjacent = 8;
				// corners
				if (x == 0 && y == 0 || x == 0 && y == bottom || x == right && y == 0 || x == right && y == bottom) {
					adjacent = 3; 
					// the edge
				} else if (x == 0 || y == 0 || x == right || y == bottom){
					adjacent = 5;
				}

				adjUnrevealed[x][y] = adjacent;
				
			}
		}


	}

	public void process() {

		totalFlags = 0;
		confirmedMinesTotal = 0;
		numOfHidden = 0;

		// clear down this array, which is a lot faster then defining it fresh
		for (int i=0; i < width; i++) {
			for (int j=0; j < height; j++) {
				adjFlagsOnBoard[i][j] = 0;
			}
		}

		// clear down the moves we collected last turn
		actionList.clear();
		
		// load up what we can see on the board
		for (int i=0; i < width; i++) {
			for (int j=0; j < height; j++) {
				
				Location location = getLocation(i, j);
				
				flagOnBoard[i][j] = false; // until proven otherwise
				int info = myGame.query(location);

				Action act = action[i][j];

				// if the move isn't a certainty then don't bother with it. The opening book is a certainty on the first move, but isn't really if the player plays somewhere else.
				if (act != null && (!act.isCertainty() || act.getMoveMethod() == MoveMethod.BOOK)) {
					action[i][j] = null;
					act = null;
				}

				if (info != GameStateModel.HIDDEN) {
					if (info == GameStateModel.FLAG) {
						totalFlags++;
						flagOnBoard[i][j] = true;
						
						// inform its neighbours they have a flag on the board
						for (int k=0; k < DX.length; k++) {
							if (i + DX[k] >= 0 && i + DX[k] < width && j + DY[k] >= 0 && j + DY[k] < height) {
								adjFlagsOnBoard[i + DX[k]][j + DY[k]]++;
							}
						}    							

						if (confirmedMine[i][j]) {    // mine found by solver
							confirmedMinesTotal++;
						} else {
							numOfHidden++;           // flag on the board but we can't confirm it
						}

						// if the board is a flag, but we are 100% sure its a clear then remove the flag
						// then clear the square
						if (act != null && act.getAction() == Action.CLEAR && act.isCertainty()) {
							actionList.add(new Action(act, Action.FLAG, MoveMethod.CORRECTION, "Remove flag", BigDecimal.ONE, 0));
							actionList.add(act);
						}

					} else {

						// if this is a new unrevealed location then set it up and inform it's neighbours they have one less unrevealed adjacent location
						if (!revealed[i][j]) {

							livingWitnesses.add(location);  // add this to living witnesses
							//display("Location (" + i + "," + j + ") is revealed");
							
							revealed[i][j] = true;
							board[i][j] = info;            

							for (int k=0; k < DX.length; k++) {
								if (i + DX[k] >= 0 && i + DX[k] < width && j + DY[k] >= 0 && j + DY[k] < height) {
									adjUnrevealed[i + DX[k]][j + DY[k]]--;
								}
							}    	

						}

					}
				} else {
					if ((solver.getPlayStyle().flagless || solver.getPlayStyle().efficiency) && confirmedMine[i][j]) {  // if we are playing flags free then all confirmed mines are consider to be flagged
						confirmedMinesTotal++;   
						totalFlags++;
					} else {
						numOfHidden++;
					}

					// if we have an action against this location which we are 100% sure about then do it
					if (act != null && act.isCertainty()) {
						if ((solver.getPlayStyle().flagless || solver.getPlayStyle().efficiency) && act.getAction() == Action.FLAG) {
							// unless the we are playing flag free and it's a flag
						} else {
							actionList.add(act);
						}

					}
				}

			}
		}
		
		List<Location> toRemove = new ArrayList<>();
		for (Location wit: livingWitnesses) {
			if (this.countAdjacentUnrevealed(wit) == 0) {
				//display("Location " + wit.display() + " is now a dead witness");
				toRemove.add(wit);
			}
		}
		livingWitnesses.removeAll(toRemove);
		
		
		// this sorts the moves by when they were discovered
		Collections.sort(actionList, Action.SORT_BY_MOVE_NUMBER);

		unplayedMoves = new int[MoveMethod.values().length];

		// accumulate how many unplayed moves there are by method
		for (Action a: actionList) {
			unplayedMoves[a.getMoveMethod().ordinal()]++;
		}

		getLogger().log(Level.INFO, "Moves left to play is %d", actionList.size());
		for (int i=0; i < unplayedMoves.length; i++) {
			if (unplayedMoves[i] != 0) {
				getLogger().log(Level.INFO, "   %s has %d moves unplayed",MoveMethod.values()[i], unplayedMoves[i]);
			}
		}


	}

	protected int getGameWidth() {
		return width;
	}

	protected int getGameHeight() {
		return height;
	}

	protected int getMines() {
		return this.myGame.getMines();
	}

	
	//public void setAction(Action a) {
	//	setAction(a, true);
	//}

	/**
	 * Register the action against the location(x,y);
	 * Optionally add the action to the list of actions to play this turn
	 */
	public void setAction(Action a) {

		//display("Setting action at " + a.display());
		
		if (action[a.x][a.y] != null) {
			return;
		}

		action[a.x][a.y] = a;

		if (a.getAction() == Action.FLAG) {
			setMineFound(a);
		}
		
		if ((solver.getPlayStyle().flagless || solver.getPlayStyle().efficiency)  && a.getAction() == Action.FLAG) {
			// if it is flag free or efficiency and we have discovered a mine then don't flag it
		} else if (isFlagOnBoard(a) && a.getAction() == Action.FLAG) {
			// if the flag is already on the board then nothing to do
		} else if (isFlagOnBoard(a) && a.getAction() == Action.CLEAR) {
			// if a flag is blocking the clear move then remove the flag first
			actionList.add(new Action(a, Action.FLAG, MoveMethod.CORRECTION, "Remove flag", BigDecimal.ONE, 0));
			actionList.add(a);			
		} else {
			actionList.add(a);
		}


	}

	protected boolean alreadyActioned(Location l) {
		return alreadyActioned(l.x, l.y);
	}

	protected boolean alreadyActioned(int x, int y) {
		return (action[x][y] != null);
	}


	protected List<Action> getActions() {
		return this.actionList;
	}

	/**
	 * This will consider chords when returning the moves to play
	 */
	/*
	protected List<Action> getActionsWithChords() {

		// if we aren't using chords or none are available then skip all this expensive processing
		if (chordLocations.isEmpty() || !solver.isPlayChords()) {
			return getActions();
		}
		
		List<Action> actions = new ArrayList<>();
		boolean[][] processed = new boolean[myGame.getWidth()][myGame.getHeight()];
		
		// sort the most beneficial chords to the top
		Collections.sort(chordLocations, ChordLocation.SORT_BY_BENEFIT_DESC);
		
		List<ChordLocation> toDelete = new ArrayList<>();
		
		for (ChordLocation cl: chordLocations) {
			
			int benefit = 0;
			int cost = 0;
			
			for (Location l: getAdjacentSquaresIterable(cl)) {
				// flag not yet on board
				if (!processed[l.x][l.y] && isConfirmedFlag(l) && !isFlagOnBoard(l)) {
					cost++;
				}
				if (!processed[l.x][l.y] && isUnrevealed(l)) {
					benefit++;
				}
			}			
			
			
			if (benefit - cost > 1) {

				for (Location l: getAdjacentSquaresIterable(cl)) {
					// flag not yet on board
					if (!processed[l.x][l.y] && isConfirmedFlag(l) && !isFlagOnBoard(l)) {
						actions.add(new Action(l, Action.FLAG, MoveMethod.TRIVIAL, "Place flag", BigDecimal.ONE, 0));
					}
					// flag on board in error
					if (!processed[l.x][l.y] && !isConfirmedFlag(l) && isFlagOnBoard(l)) {
						actions.add(new Action(l, Action.FLAG, MoveMethod.CORRECTION, "Remove flag", BigDecimal.ONE, 0));
					}
					processed[l.x][l.y] = true;
				}
				// now add the clear all
				actions.add(new Action(cl, Action.CLEARALL, MoveMethod.TRIVIAL, "Clear All", BigDecimal.ONE, 1));	
				
			} else {
				//toDelete.add(cl);
			}
		}
		
		chordLocations.removeAll(toDelete);

		// now add the the actions that haven't been resolved by a chord play
		for (Action act: actionList) {
			if (!processed[act.x][act.y]) {
				processed[act.x][act.y] = true;
			}
			actions.add(act);
		}

		return actions;
	}
	*/
	/**
	 * Get the probability of a mine being in this square (based upon the actions still pending)
	 */
	protected BigDecimal getProbability(int x, int y) {
		
		for (Action act: actionList) {
			if (act.x == x && act.y== y) {
				if (act.getAction() == Action.FLAG) {
					return BigDecimal.ZERO;
				} else if (act.getAction() == Action.CLEAR) {
					return act.getBigProb();
				}
				
			}
		}		
		
		
		return null;
		
	}
	
	
	//protected int getActionsCount() {
	//	return this.actionList.size();
	//}
	
	/**
	 * Add a isolated dead tile
	 */
	//protected void addIsolatedDeadTile(Location loc) {
	//	isolatedDeadTiles.add(loc);
	//}
	
	//public int getIsolatedDeadTileCount() {
	//	return this.isolatedDeadTiles.size();
	//}
	
	/**
	 * Returns and removes the first Isolated Dead Tile in the set
	 */
	//public Location getIsolatedDeadTile() {
	//	for (Location loc: isolatedDeadTiles) {
	//		//isolatedDeadTiles.remove(loc);
	//		return loc;
	//	}
	//	return null;
	//}

	protected List<Location> getWitnesses(Collection<? extends Location> square) {
		return new ArrayList<Location>(getWitnessesArea(square).getLocations());
	}
	
	/**
	 * From the given locations, generate all the revealed squares that can witness these locations
	 */
	protected Area getWitnessesArea(Collection<? extends Location> square) {

		Set<Location> work = new HashSet<>(10);

		for (Location loc: square) {

			for (Location adj: this.getAdjacentSquaresIterable(loc)) {
				// determine the number of distinct witnesses
				if (isRevealed(adj)) {
					work.add(adj);
				}            		
			}            
		}

		return new Area(work);

	}
	
	/**
	 * From the given locations, generate an area containing all the un-revealed squares around them
	 */
	protected Area getUnrevealedArea(List<? extends Location> witnesses) {
		return new Area(getUnrevealedSquaresDo(witnesses));
	}
	
	/**
	 * From the given locations, generate all the un-revealed squares around them
	 */
	protected List<Location> getUnrevealedSquares(List<? extends Location> witnesses) {
		return new ArrayList<Location>(getUnrevealedSquaresDo(witnesses));
	}
	
	
	private Set<Location> getUnrevealedSquaresDo(List<? extends Location> witnesses) {

		Set<Location> work = new HashSet<>(witnesses.size() * 3);       

		for (Location loc: witnesses) {
			
			for (Location adj: this.getAdjacentSquaresIterable(loc)) {

				if (isUnrevealed(adj)) {
					work.add(adj);
				}            		
			}
			
		}

		return work;
	}
	
	/**
	 * Return all the unrevealed Locations on the board
	 */
	protected List<Location> getAllUnrevealedSquares() {

		ArrayList<Location> work = new ArrayList<>(width * height);       

		for (int i=0; i < width; i++) {
			for (int j=0; j < height; j++) {
				if (isUnrevealed(i,j)) {
					work.add(getLocation(i,j));
				}
			}
		}                        

		return work;
	}

	protected List<Location> getAllLivingWitnesses() {
		return new ArrayList<>(livingWitnesses);
	}
	
	
	/**
	 * Return a list of Unrevealed Locations adjacent to this one
	 */
	protected List<Location> getAdjacentUnrevealedSquares(Location loc) {

		return getAdjacentUnrevealedSquares(loc, 1);
	}

	/**
	 * Return an Area of un-revealed Locations adjacent to this one
	 */
	protected Area getAdjacentUnrevealedArea(Location loc) {

		return new Area(getAdjacentUnrevealedSquares(loc, 1));
	}
	
	
	/**
	 * Return a list of Unrevealed Locations adjacent to this one
	 */
	protected List<Location> getAdjacentUnrevealedSquares(Location loc, int size ) {

		ArrayList<Location> work = new ArrayList<>();       

		for (Location a: getAdjacentSquaresIterable(loc, size)) {
			if (isUnrevealed(a)) {
				work.add(a);
			}
		}

		return work;
	}

	/**
	 * Return the Location for this position, avoids having to instantiate one
	 */
	protected Location getLocation(int x, int y) {
		if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
			return null;
		} else {
			return cache.getLocation(x, y);
		}

	}
	
	
	/**
	 * This returns the adjacent squares to a depth of size away. So (l,2) returns the 24 squares 2 deep surrounding l.
	 */
	protected Iterable<Location> getAdjacentSquaresIterable(Location loc, int size) {
		if (size == 1) {
			return getAdjacentSquaresIterable(loc);
		} else {
			return cache.adjacentLocations2[loc.x][loc.y];
		}
		
	}

	protected Iterable<Location> getAdjacentSquaresIterable(Location loc) {
		return cache.adjacentLocations1[loc.x][loc.y];
	}


	/**
	 * Done as part of validating Locations, must be undone using clearWitness()
	 */
	protected void setWitnessValue(Location l, int value) {    	 
		board[l.x][l.y] = value;
		revealed[l.x][l.y] = true;
		
		testMoveBalance++;
		
	}

	/**
	 * Done as part of validating Locations
	 */
	protected void clearWitness(Location l) {    	 
		board[l.x][l.y] = 0;
		revealed[l.x][l.y] = false;
		
		testMoveBalance--;
		
	}

	/**
	 * Method to read our own array of reveal values.
	 */
	protected int getWitnessValue(Location l) {
		return getWitnessValue(l.x, l.y);
	}

	/**
	 * Method to read our own array of reveal values.
	 */
	protected int getWitnessValue(int x, int y) {
		if (isUnrevealed(x,y)) {
			throw new RuntimeException("Trying to get a witness value for an unrevealed square");
		}
		return board[x][y];
	}    

	/**
	 * indicates a flag is on the board, but the solver can't confirm it
	 */
	protected boolean isUnconfirmedFlag(Location l) {
		return isUnconfirmedFlag(l.x, l.y);
	}

	/**
	 * indicates a flag is on the board, but the solver can't confirm it
	 */
	protected boolean isUnconfirmedFlag(int x, int y) {
		return flagOnBoard[x][y] && !confirmedMine[x][y];
	}        

	/**
	 * indicates a flag is on the board
	 */
	protected boolean isFlagOnBoard(Location l) {
		return isFlagOnBoard(l.x, l.y);
	}

	/**
	 * indicates a flag is on the board
	 */
	protected boolean isFlagOnBoard(int x, int y) {
		return flagOnBoard[x][y];
	}        


	protected void setFlagOnBoard(Location l) {
		setFlagOnBoard(l.x, l.y);
	}

	protected void setFlagOnBoard(int x, int y) {
		flagOnBoard[x][y] = true;
	}

	protected void setMineFound(Location loc) {

		if (isConfirmedMine(loc)) {
			return;
		}

		confirmedMinesTotal++;
		confirmedMine[loc.x][loc.y] = true;

		// if the flag isn't already on the board then this is also another on the total of all flags
		if (!flagOnBoard[loc.x][loc.y]) {
			totalFlags++;
		}

		// let all the adjacent squares know they have one more flag next to them and one less unrevealed location
		for (Location a: getAdjacentSquaresIterable(loc)) {
			adjFlagsConfirmed[a.x][a.y]++;
			adjUnrevealed[a.x][a.y]--;
		}
		
	}

	protected void unsetMineFound(Location loc) {

		if (!isConfirmedMine(loc)) {
			return;
		}

		confirmedMinesTotal--;
		confirmedMine[loc.x][loc.y] = false;


		// if the flag isn't already on the board then this is also another on the total of all flags
		if (!flagOnBoard[loc.x][loc.y]) {
			totalFlags--;
		}

		// let all the adjacent squares know they have one less mine next to them and one more unrevealed location
		for (Location a: getAdjacentSquaresIterable(loc)) {
			adjFlagsConfirmed[a.x][a.y]--;
			adjUnrevealed[a.x][a.y]++;
		}
		
	}
	
	/**
	 * Since Flag Free is a thing, we can't rely on the GameState to tell us where the flags are,
	 * so this replaces that.
	 */
	protected boolean isConfirmedMine(Location l) {
		return isConfirmedMine(l.x, l.y);
	}

	/**
	 * Since Flag Free is a thing, we can't rely on the GameState to tell us where the flags are,
	 * so this replaces that.
	 */
	protected boolean isConfirmedMine(int x, int y) {
		return confirmedMine[x][y];
	}    

	/**
	 * Returns whether the location is revealed
	 */
	protected boolean isRevealed(Location l) {
		return isRevealed(l.x, l.y);
	}    

	/**
	 * Returns whether the location is revealed
	 */
	protected boolean isRevealed(int x, int y) {
		return revealed[x][y];
	}    

	/**
	 * Returns whether the location is unrevealed (neither revealed nor a confirmed flag)
	 */
	protected boolean isUnrevealed(Location l) {
		return isUnrevealed(l.x, l.y);
	}    

	/**
	 * Returns whether the location is unrevealed (neither revealed nor a confirmed flag)
	 */
	protected boolean isUnrevealed(int x, int y) {
		return !confirmedMine[x][y] && !revealed[x][y];
	}    

	/**
	 * count how many confirmed flags are adjacent to this square
	 */
	protected int countAdjacentConfirmedFlags(Location l) {
		return countAdjacentConfirmedFlags(l.x, l.y);
	}

	/**
	 * count how many confirmed flags are adjacent to this square
	 */
	protected int countAdjacentConfirmedFlags(int x, int y) {

		return adjFlagsConfirmed[x][y];

	}

	/**
	 * count how many flags are adjacent to this square on the board
	 */
	protected int countAdjacentFlagsOnBoard(Location l) {
		return countAdjacentFlagsOnBoard(l.x, l.y);
	}

	/**
	 * count how many flags are adjacent to this square on the board
	 */
	protected int countAdjacentFlagsOnBoard(int x, int y) {

		return adjFlagsOnBoard[x][y];

	}
	
	
	/**
	 * count how many confirmed and unconfirmed flags are adjacent to this square
	 */
	protected int countAllFlags(int x, int y) {

		int result = 0;

		for (int i=0; i < DX.length; i++) {
			if (x + DX[i] >= 0 && x + DX[i] < width && y + DY[i] >= 0 && y + DY[i] < height) {
				if (confirmedMine[x + DX[i]][y + DY[i]] || flagOnBoard[x + DX[i]][y + DY[i]]) {
					result++;
				}
			}
		}

		return result;

	}

	/**
	 * count how many adjacent squares are neither flags nor revealed
	 */
	protected int countAdjacentUnrevealed(Location l) {

		return countAdjacentUnrevealed(l.x, l.y);

	}

	/**
	 * count how many adjacent squares are neither confirmed flags nor revealed
	 */
	protected int countAdjacentUnrevealed(int x, int y) {

		return adjUnrevealed[x][y];

	}    

	/**
	 * count how many squares are neither confirmed flags nor revealed
	 */    
	protected int getTotalUnrevealedCount() {

		return numOfHidden;

	}

	/**
	 * Returns the number of mines the solver knows the location of
	 */
	protected int getConfirmedMineCount() {

		return confirmedMinesTotal;

	}

	/**
	 * Number of flags on the board both confirmed and unconfirmed
	 */
	protected int getTotalFlagCount() {
		return this.totalFlags;
	}

	protected int getUnplayedMoves(MoveMethod method) {
		return unplayedMoves[method.ordinal()];
	}


	// check for flags which can be determined to be wrong
	protected boolean validateData() {

		for (int i=0; i < width; i++) {
			for (int j=0; j < height; j++) {

				// if there is an unconfirmed flag on the board but the solver
				// thinks it is clear then the flag is wrong
				if (isUnconfirmedFlag(i,j) && action[i][j] != null && action[i][j].getAction() == Action.CLEAR) {
					getLogger().log(Level.INFO, "Flag in Error at (%d, %d) confirmed CLEAR", i, j);
					return false;
				}

				if (isRevealed(i,j) && getWitnessValue(i,j) != 0) {
					int flags = countAllFlags(i,j);

					// if we have too many flags by a revealed square then a mistake has been made
					if (getWitnessValue(i,j) < flags) {
						getLogger().log(Level.INFO, "Flag in Error at witness (%d, %d) Overloads: Flags %d" ,i, j, flags);
						return false;
					}
				}

			}        

		}

		return true;

	}

	/**
	 * Returns true if the board is consider high mine density
	 * @return
	 */
	public boolean isHighDensity() {
		
		int minesLeft = getMines() - getConfirmedMineCount();
		int tilesLeft = getTotalUnrevealedCount();
		
		return (minesLeft * 5 > tilesLeft * 2) && Solver.CONSIDER_HIGH_DENSITY_STRATEGY;
	}
	

	public int getTestMoveBalance() {
		return this.testMoveBalance;
	}
	
	protected Logger getLogger() {
		return solver.logger;
	}
	
	//protected void display(String text) {
	//	solver.display(text);
	//}

	protected Solver getSolver() {
		return solver;
	}

}
