package minesweeper.solver;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Area;
import minesweeper.structure.Location;

public class LongTermRiskHelperOld {

	private boolean[][] PATTERNS = new boolean[][] {{true, false, false, true}, {false, true, true, false}};  // 2 ways to make a 50/50 in a box
	
	private class Risk5050 {
		
		private Location poi;
		private List<Location> area;
		private List<Location> livingArea = new ArrayList<>();
		
		private Risk5050(Location poi, Location... locs) {
			this.poi = poi;
			this.area = Arrays.asList(locs);
			
			for (Location loc: locs) {
				if (!deadLocations.contains(loc)) {
					livingArea.add(loc);
				}
			}
			
		}
		
	}
	
	
	private final static BigDecimal HALF = new BigDecimal("0.5");
	
	private final BoardState board;
	private final WitnessWeb wholeEdge;
	private final ProbabilityEngineModel currentPe;
	private final Area deadLocations;
	private final List<Location> fifty;
	
	private BigDecimal currentLongTermSafety;
	
	private Risk5050 worstBox5050;
	private BigDecimal box5050Safety = BigDecimal.ONE;
	
	private BigDecimal twoTileSafety;
	
	private List<Risk5050> risk5050s = new ArrayList<>();
	
	public LongTermRiskHelperOld(BoardState board, WitnessWeb wholeEdge, ProbabilityEngineModel pe)  {
		
		this.board = board;
		this.wholeEdge = wholeEdge;
		this.currentPe = pe;
		this.deadLocations = pe.getDeadLocations();
		this.fifty = currentPe.getFiftyPercenters();
		
		// sort into location order
		fifty.sort(null);
		
	}
	
	public void findRisks() {
		
		checkFor2Tile5050();
		
		//checkForBox5050();
		
		this.currentLongTermSafety = this.box5050Safety.multiply(this.twoTileSafety);
		
	}

	public void checkFor2Tile5050() {
		
		BigDecimal longTermSafety = BigDecimal.ONE;
		
		for (int i=0; i < fifty.size(); i++) {
			
			Location tile1 = fifty.get(i);
			Location tile2 = null;
			
			Location info = null;
			Risk5050 risk = null;
			
			for (int j=i+1; j < fifty.size(); j++) {
				tile2 = fifty.get(j);
				
				// tile2 is below tile1
				if (tile1.x == tile2.x && tile1.y == tile2.y - 1) {
					info = checkVerticalInfo(tile1, tile2);
					
					if (info == null)  { // try extending it
						
						Location tile3 = getFifty(tile2.x, tile2.y + 2);
						Location tile4 = getFifty(tile2.x, tile2.y + 3);

						if (tile3 != null && tile4 != null) {
							info = checkVerticalInfo(tile1, tile4);
							if (info != null) {
								risk = new Risk5050(info, tile1, tile2, tile3, tile4);
							}
						}

					} else {
						risk = new Risk5050(info, tile1, tile2);
					}

					break;
				}
				
				// tile 2 is right of tile1
				if (tile1.x == tile2.x - 1 && tile1.y == tile2.y) {
					info = checkHorizontalInfo(tile1, tile2);
					
					if (info == null) { // try extending it
						
						Location tile3 = getFifty(tile2.x + 2, tile2.y);
						Location tile4 = getFifty(tile2.x + 3, tile2.y);
						
						if (tile3 != null && tile4 != null) {
							info = checkHorizontalInfo(tile1, tile4);
							if (info != null) {
								risk = new Risk5050(info, tile1, tile2, tile3, tile4);
							}
						}
					
					}  else {
						risk = new Risk5050(info, tile1, tile2);
					}
					
					break;
				}
				
			}
			
			// if the 2 fifties form a pair with only 1 remaining source of information
			if (risk != null) {
				risk5050s.add(risk);  // store the positions of interest
				
				BigDecimal safety = BigDecimal.ONE.subtract(BigDecimal.ONE.subtract(currentPe.getProbability(info)).multiply(HALF));
				board.getLogger().log(Level.INFO, "Seed %d - %s %s has 1 remaining source of information - tile %s  %f", board.getSolver().getGame().getSeed(), tile1, tile2, info, safety);
				longTermSafety = longTermSafety.multiply(safety);
			}
			
		}
		
		if (longTermSafety.compareTo(BigDecimal.ONE) != 0) {
			board.getLogger().log(Level.INFO, "Seed %d - Total long term safety %f", board.getSolver().getGame().getSeed(), longTermSafety);
		}

		
		this.twoTileSafety = longTermSafety;
		
	}
	
	private boolean isFifty(int x, int y) {
		
		return (getFifty(x, y) != null);
		
	}
	
	private Location getFifty(int x, int y) {
		
		for (Location loc: fifty) {
			if (loc.x == x && loc.y == y) {
				return loc;
			}
		}
		
		return null;
		
	}
	
	public List<Location> get5050Breakers() {
		List<Location> breakers = new ArrayList<>();
		
		if (board.getSolver().preferences.considerLongTermSafety()) {
			for (Risk5050 risk: risk5050s) {
				breakers.addAll(risk.livingArea);
				breakers.add(risk.poi);
			}
		}

		
		return breakers;
	}
	
	public BigDecimal getLongTermSafety() {
		return this.currentLongTermSafety;
	}
	
	public BigDecimal getLongTermSafety(Location candidate, ProbabilityEngineModel pe) {
		
		BigDecimal longTermSafety = null;
		
		if (board.getSolver().preferences.considerLongTermSafety()) {
			
			// if there is a possible box 50/50 then see if we are breaking it, otherwise use that as the start safety
			if (worstBox5050 != null) {
				
				if (worstBox5050.poi.equals(candidate) || pe.getProbability(worstBox5050.poi).compareTo(BigDecimal.ONE) == 0) {
					//board.getLogger().log(Level.INFO, "%s has broken 50/50", candidate);
					longTermSafety =  BigDecimal.ONE;					
				} else {
					for (Location loc: worstBox5050.area) {
						if (loc.equals(candidate)) {
							//board.getLogger().log(Level.INFO, "%s has broken 50/50", candidate);
							longTermSafety =  BigDecimal.ONE;
							break;
						}
					}					
				}
				

				if (longTermSafety == null) {
					longTermSafety = this.box5050Safety;
				}
				
			} else {
				longTermSafety = BigDecimal.ONE;
			}
			
			
			for (Risk5050 risk: this.risk5050s) {
				BigDecimal safety = null;
				
				// is the candidate part of the 50/50 - if so it is being broken
				for (Location loc: risk.area) {
					if (loc.equals(candidate)) {
						safety =  BigDecimal.ONE;
						break;
					}
				}
				
				if (safety == null) {
					if (risk.poi.equals(candidate)) {  
						safety = BigDecimal.ONE;
					} else {
						safety = BigDecimal.ONE.subtract(BigDecimal.ONE.subtract(pe.getProbability(risk.poi)).multiply(HALF));
					}				
				}
				
				longTermSafety = longTermSafety.multiply(safety);
			}			
		} else {
			longTermSafety = BigDecimal.ONE;
		}
		
		return longTermSafety;
		
	}
	
	
	// returns the location of the 1 tile which can still provide information, or null
	private Location checkVerticalInfo(Location tile1, Location tile2) {
		
		Location info = null; 
		
		final int top = tile1.y - 1;
		final int bottom = tile2.y + 1;
		
		final int left = tile1.x - 1;
		
		if (isPotentialInfo(left, top)) {
	    	if (board.isRevealed(left, top)) {
	    		return null;
	    	} else {
	    		info = new Location(left, top);
	    	}
		}
		
		if (isPotentialInfo(left + 1, top)) {
			if (board.isRevealed(left + 1, top)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(left + 1, top);
		}		
		
		if (isPotentialInfo(left + 2, top)) {
			if (board.isRevealed(left + 2, top)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(left + 2, top);
		}		
		
		if (isPotentialInfo(left, bottom)) {
			if (board.isRevealed(left, bottom)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(left, bottom);
		}		
		
		if (isPotentialInfo(left + 1, bottom)) {
			if (board.isRevealed(left + 1, bottom)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(left + 1, bottom);
		}		
		
		if (isPotentialInfo(left + 2, bottom)) {
			if (board.isRevealed(left + 2, bottom)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(left + 2, bottom);
		}		
		
		return info;
		
	}
	
	// returns the location of the 1 tile which can still provide information, or null
	private Location checkHorizontalInfo(Location tile1, Location tile2) {
		
		Location info = null; 
		
		final int top = tile1.y - 1;
		
		final int left = tile1.x - 1;
		final int right = tile2.x + 1;
		
		if (isPotentialInfo(left, top)) {
	    	if (board.isRevealed(left, top)) {
	    		return null;
	    	} else {
	    		info = new Location(left, top);
	    	}
		}
		
		if (isPotentialInfo(left, top + 1)) {
			if (board.isRevealed(left, top + 1)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(left, top + 1);
		}		
		
		if (isPotentialInfo(left, top + 2)) {
			if (board.isRevealed(left, top + 2)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(left, top + 2);
		}		
		
		if (isPotentialInfo(right, top)) {
			if (board.isRevealed(right, top)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(right, top);
		}		
		
		if (isPotentialInfo(right, top + 1)) {
			if (board.isRevealed(right, top + 1)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(right, top + 1);
		}		
		
		if (isPotentialInfo(right, top + 2)) {
			if (board.isRevealed(right, top + 2)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(right, top + 2);
		}		
		
		return info;
		
	}
	
	private void checkForBox5050() {
		
    	// box 2x2
		Location[] tiles = new Location[4];
		
		BigDecimal maxProbability = BigDecimal.ZERO;
		Risk5050 worst5050 = null;
		
		List<Location> mines = new ArrayList<>();
		List<Location> noMines = new ArrayList<>();
		for (int i=0; i < board.getGameWidth() - 1; i++) {
			for (int j=0; j < board.getGameHeight() - 1; j++) {
				
				// need 4 hidden tiles
				if (!board.isUnrevealed(i, j) || !board.isUnrevealed(i, j + 1) || !board.isUnrevealed(i + 1, j) || !board.isUnrevealed(i + 1, j + 1)) {
					continue;
				}
				
				tiles[0] = new Location(i, j);
				Location info = checkBoxInfo(tiles[0]);
				
				// need the corners to be flags or off the board
				if (info == null) {
					continue;  // this skips the rest of the logic below this in the for-loop 
				}

				tiles[1] = new Location(i + 1, j);
				tiles[2] = new Location(i, j + 1);
				tiles[3] = new Location(i + 1, j + 1);
				
				BigInteger solutions = BigInteger.ZERO;
				
				for (int k = 0; k < PATTERNS.length; k++) {
					
					mines.clear();
					noMines.clear();
					
					mines.add(info);  // the missing mine
					
					// allocate each position as a mine or noMine
					for (int l = 0; l < 4; l++) {
						if (PATTERNS[k][l]) {
							mines.add(tiles[l]);
						} else {
							noMines.add(tiles[l]);
						}
					}
					
					// see if the position is valid
					SolutionCounter counter = board.getSolver().validatePosition(wholeEdge, mines, noMines, Area.EMPTY_AREA);

					// if it is then mark each mine tile as risky
					if (counter.getSolutionCount().signum() != 0) {
						board.getLogger().log(Level.DEBUG, "Pattern %d is valid with %d solutions", k, counter.getSolutionCount());
						solutions = solutions.add(counter.getSolutionCount());
						
					} else {
						board.getLogger().log(Level.DEBUG, "Pattern %d is not valid", k);
					}
				}
				
				BigDecimal probability = new BigDecimal(solutions).divide(new BigDecimal(this.currentPe.getSolutionCount()), 6, RoundingMode.HALF_UP);
				
				board.getLogger().log(Level.INFO, "%s %s %s %s is box 50/50 %f of the time", tiles[0], tiles[1], tiles[2], tiles[3], probability);
				
				if (probability.compareTo(maxProbability) > 0) {
					maxProbability = probability;
					worst5050 = new Risk5050(info, tiles[0], tiles[1], tiles[2], tiles[3]);
					board.getLogger().log(Level.INFO, "%s %s %s %s is box 50/50 is new worst 50/50", tiles[0], tiles[1], tiles[2], tiles[3]);
				}
				
			}
		}
		
		this.worstBox5050 = worst5050;
		this.box5050Safety = BigDecimal.ONE.subtract(maxProbability.multiply(HALF));
		
	}
	
	// returns the location of the 1 tile which can still provide information for a 2x2 box, or null
	private Location checkBoxInfo(Location tileTopLeft) {
		
		Location info = null; 
		
		final int top = tileTopLeft.y - 1;
		final int left = tileTopLeft.x - 1;
		
		if (isPotentialInfo(left, top)) {
	    	if (board.isRevealed(left, top)) {
	    		return null;
	    	} else {
	    		info = new Location(left, top);
	    	}
		}
		
		if (isPotentialInfo(left, top - 3)) {
			if (board.isRevealed(left, top - 3)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(left, top - 3);
		}		
		
		if (isPotentialInfo(left + 3, top)) {
			if (board.isRevealed(left + 3, top)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(left + 3, top);
		}		
		
		if (isPotentialInfo(left + 3, top + 3)) {
			if (board.isRevealed(left + 3, top + 3)) {  // info is certain
				return null;
			} else {
				if (info != null) {  // more than 1 tile giving possible info
					return null;
				}
			}
			info = new Location(left + 3, top + 3);
		}		
		
		return info;
		
	}
	
    // returns whether there information to be had at this location; i.e. on the board and either unrevealed or revealed
    private boolean isPotentialInfo(int x, int y) {
    	
    	if (x < 0 || x >= board.getGameWidth() || y < 0 || y >= board.getGameHeight()) {
    		return false;
    	}
    	
    	if (board.isConfirmedMine(x, y) || isMineInPe(x, y)) {
    		return false;
    	} else {
    		return true;
    	}
    	
    }
	
	private boolean isMineInPe(int x, int y) {
		for (Location loc: this.currentPe.getMines()) {
			if (loc.x == x && loc.y == y) {
				return true;
			}
		}
		return false;
	}
}
