package minesweeper.model;

/** Trick: All matrices are initialized with +2 size and use their middle parts
 * only: M[1..rows][1..cols]. (Instead of M[0..rows+1][0..cols+1].) The +1 edge
 * all around is so I can avoid outOfBounds checks. This border is hidden from
 * view, and every cell in it is visited, thus can't be a bomb.
 *
 * I learned this from <http://www.techuser.net/minecascade.html>: "The solution
 * is quite clever. The visible Minesweeper board is the center of a larger
 * board. For example, the 9x9 board is the center of an 11x11 board. The extra
 * squares in the 11x11 board form a border around the 9x9 board. This allows
 * every square of the visible board to have 8 adjacent squares, and therefore
 * no special cases result. The cascade algorithm would break down if a non
 * visible square was put in the queue. This is avoided by initializing the
 * extra squares as non-mine and uncovered. Now, these squares can never be put
 * in the queue as step 5 of the algorithm only puts squares in the queue which
 * are covered."
 */
import java.io.*;
import java.util.Random;
import minesweeper.model.TileEvent.type;
//3ICE: Importing the enum directly ↑, so I don't have to type it in every time.

/**
 * Creates and manages a minesweeper game.
 * @author Daniel "3ICE" Berezvai
 */
public class MinesweeperLogic {

private int rows;
private int cols;
private int rows1, cols1, rows2, cols2;
private double bombProbability;
private boolean redsTurn;
/**
 * Offset order:
 * <pre>
 * 1 2 3
 * 4 _ 5
 * 6 7 8</pre>
 */
public static int[][] offsets = new int[][]{
  {-1, -1}, {0, -1}, {+1, -1},
  {-1, 0}, /* {0, 0}, */ {+1, 0},
  {-1, +1}, {0, +1}, {+1, +1}};
/**
 * Holds the number of adjacent bombs for each cell. (0..8)
 */
private int[][] count;
private boolean[][] bomb;
private boolean[][] visited;
private boolean[][] flag;
private int uncoveredTileCount;
private int flaggedTileCount;
private int tileCount;
private int bombCount;
private boolean gameOver;
private boolean firstClick;
MinesweeperEventListener minesweeperEventListener;
private Random rnd = new Random();

/**
 * I'll talk a bit about my overengineered concept of the difficulty parameter.
 *
 * First case: Difficulty is given in Bomb count. (above 1 but not approaching
 * the total number of tiles) We convert this to a percentage behind the scenes.
 *
 * Second case: Difficulty given in Bomb percent. (between 0.0 and 1.0) This is
 * good for us as it is.
 *
 * Fallback: Default to 10% if bad value (0, negative, or too big number)
 */
public MinesweeperLogic(int rows, int cols, double difficulty) {
  this.rows = rows;
  this.cols = cols;
  if ((rows - 1) * (cols - 1) > difficulty && difficulty > 1) {
    bombProbability = difficulty / (double) (rows * cols);
  } else if (difficulty > 0) {
    bombProbability = difficulty;
  } else {
    bombProbability = 0.1;
  }
  newGame();
}

/** Don't mind me, just refactoring out some reusable code...
 * @see #newGame
 * @see #loadGame */
private void initRows1Cols2andMatrices() {
  rows1 = rows + 1;
  cols1 = cols + 1;
  rows2 = rows + 2;
  cols2 = cols + 2;
  bomb = new boolean[rows2][cols2];
  visited = new boolean[rows2][cols2];
  flag = new boolean[rows2][cols2];
  count = new int[rows2][cols2];

}

/** Reset a bunch of variables and reinitialize the game. */
private void newGame() {
  redsTurn = true;
  gameOver = false;
  uncoveredTileCount = 0;
  flaggedTileCount = 0;
  tileCount = rows * cols;
  firstClick = true;
  initRows1Cols2andMatrices();
  initMiddleCells();
  initEgdes();
  //bombCounts(); //3ICE: So users can flag-spam before #handleFirstClickSeparately
  //3ICE: Never mind. It's better if users can't flag. Lets even make them start with 0 flags:
  bombCount = 0;
}

/** Initializes the following tiles for a 9×9 board (11×11 matrix):
 * <pre>(1st row uninitialized) (first and last columns also uninitialized)
 *       1 1    1 2    1 3    1 4    1 5    1 6    1 7    1 8    1 9    1 10
 *       2 1    2 2    2 3    2 4    2 5    2 6    2 7    2 8    2 9    2 10
 *       3 1    3 2    3 3    3 4    3 5    3 6    3 7    3 8    3 9    3 10
 *       4 1    4 2    4 3    4 4    4 5    4 6    4 7    4 8    4 9    4 10
 *       5 1    5 2    5 3    5 4    5 5    5 6    5 7    5 8    5 9    5 10
 *       6 1    6 2    6 3    6 4    6 5    6 6    6 7    6 8    6 9    6 10
 *       7 1    7 2    7 3    7 4    7 5    7 6    7 7    7 8    7 9    7 10
 *       8 1    8 2    8 3    8 4    8 5    8 6    8 7    8 8    8 9    8 10
 *       9 1    9 2    9 3    9 4    9 5    9 6    9 7    9 8    9 9    9 10
 *      10 1   10 2   10 3   10 4   10 5   10 6   10 7   10 8   10 9   10 10
 *(11th row uninitialized)</pre> */
private void initMiddleCells() {
  for (int i = 1; i <= rows; i++) {
    for (int j = 1; j <= cols; j++) {
//        System.out.println("middleCell " + i + " " + j);
      bomb[i][j] = (rnd.nextDouble() < bombProbability);
      visited[i][j] = false;
      flag[i][j] = false;
    }
  }
}

/** Initializes the following tiles for a 9×9 board (11×11 matrix):
 * <pre>
 * 0 0    0 1    0 2    0 3    0 4    0 5    0 6    0 7    0 8    0 9    0 10    0 11
 * 1 0                                                                           1 11
 * 2 0                                                                           2 11
 * 3 0                                                                           3 11
 * 4 0                                                                           4 11
 * 5 0                                                                           5 11
 * 6 0                                                                           6 11
 * 7 0                                                                           7 11
 * 8 0                                                                           8 11
 * 9 0                                                                           9 11
 *10 0                                                                          10 11
 *11 0   11 1   11 2   11 3   11 4   11 5   11 6   11 7   11 8   11 9   11 10   11 11
 * </pre> */
private void initEgdes() {
  for (int i = 0; i <= rows1; i++) {
//      System.out.println("First column: " + i + " 0");
    bomb[i][0] = false;
    visited[i][0] = true;
//      System.out.println("Last column: " + i + " " + cols1);
    bomb[i][cols1] = false;
    visited[i][cols1] = true;
  }
  for (int j = 1; j < cols1; j++) {
//      System.out.println("First row: 0 " + j);
    bomb[0][j] = false;
    visited[0][j] = true;
//      System.out.println("Last row: " + rows1 + " " + j);
    bomb[rows1][j] = false;
    visited[rows1][j] = true;
  }
}

/** Translates x and y to the tricky +2 size matrix representation. Handles
 * first click separately. Starts the cascade effect.
 * @return True if the operation was successful, false if the user tried to
 * uncover an already uncovered tile. */
public boolean uncoverTile(int x, int y) {
  ++x;
  ++y;
  if (firstClick) {
    handleFirstClickSeparately(x, y);
  }
  if (!visited[x][y] && !flag[x][y]) {
    redsTurn = !redsTurn;
    cascade(x, y);
    gameOver = bomb[x][y] || tieByUncovering();
    if (gameOver) {
      revealAllBombs();
    }
    return true;
  }
  return false;
}

/** Steps taken from TechUser.net. But then I wrote my own instead.
 *
 * <ol><li>If current square is a mine gameover, otherwise uncover square</li>
 *
 * <li>Count mines adjacent to current square</li>
 *
 * <li>If adjacent mine count is zero, uncover all adjacent covered squares and
 * make a recursive call for every one of them (steps 2-3)</li></ol>
 *
 * @see http://www.techuser.net/minecascade.html */
private void cascade(int x, int y) {
  if (!visited[x][y] && !flag[x][y]) {
    uncover(x, y);
    if (count[x][y] == 0) {
      for (int[] offset : offsets) {
        cascade(x + offset[0], y + offset[1]);
      }
    }
  }
}

private void uncover(int x, int y) {
  visited[x][y] = true;
  uncoveredTileCount++;
//    if (flag[x][y]) {
//      //3ICE: This used to be necessary for removing flags we've cascaded under.
//      //3ICE: But now I don't cascade under flags. (Standard Minesweeper behavior.)
//      flag[x][y] = false;
//      sendEvent(x, y, type.FLAG);
//    }
  sendEvent(x, y, type.EMPTY);
}

/**
 * One eventListener will be enough. Non-removable.
 */
public void setEventListener(MinesweeperEventListener l) {
  minesweeperEventListener = l;
}

/**
 * Place a flag on an unvisited tile. Flags stop cascading and prevent
 * uncovering of the tile until cleared.
 */
public boolean flagTile(int x, int y) {
  ++x;
  ++y;
  if (!visited[x][y]) {
    //3ICE: So players can still unflag at flag cap.
    if (flag[x][y] || flaggedTileCount < bombCount) {
      flag[x][y] = !flag[x][y];
      if (flag[x][y]) {
//3ICE: Can't win by flagging, see commented-out victoryByFlagging method below.
//        gameOver = victoryByFlagging();
//        if (gameOver) {
//          revealAllBombs();
//        }
        flaggedTileCount++;
      } else {
        flaggedTileCount--;
      }
      return true;
    }
  }
  return false;
}

/**
 * Original minesweeper: If a mine is uncovered on the first click, it is moved
 * to the upper-left corner of the board. Or as close to the corner as possible.
 *
 * My version: Mines uncovered on the first click are moved away randomly.
 *
 * Either way, because of this tricky first click behavior, we can only count
 * the bombs after the first click was handled separately.
 *
 * @see http://www.techuser.net/mineclick.html
 * @see #moveBomb */
private void handleFirstClickSeparately(int x, int y) {
  firstClick = false;
//  System.out.println("Removing bombs from around " + x + " " + y);
  if (bomb[x][y]) {
    moveBomb(x, y, x, y);
  }
  int ox, oy;
  for (int[] offset : offsets) {
    ox = x + offset[0];
    oy = y + offset[1];
    if (bomb[ox][oy]) {
      moveBomb(ox, oy, x, y);
    }
  }
  bombCounts();
}

/**
 * If the algorithm exceeds the arbitrary op_limit of 25, I simply delete the
 * bomb without moving it.
 *
 * bombCounts() is called only after all bomb moves are done, so it's safe to do
 * as we please with the bombs matrix here. Even removing a bomb won't break
 * anything.
 *
 * @see #bombCounts */
private void moveBomb(int x, int y, int ox, int oy) {
//  System.out.println("Moving bomb from " + x + " " + y);
  bomb[x][y] = false;
  int i, j;
  int op_limit = 0;
  do {
    op_limit++;
    i = rnd.nextInt(rows - 1) + 1;
    j = rnd.nextInt(cols - 1) + 1;
//    System.out.print("Trying " + i + " " + j + "... ");
    if (!bomb[i][j] && Math.abs(ox - i) > 1 && Math.abs(oy - j) > 1) {
//      System.out.println(" Success! (" + i + " " + j + ")");
      bomb[i][j] = true;
      return;
    }
  } while (op_limit < 25);
//  System.out.println(" Giving up.");
}

/**
 * Loop through all tiles (skipping edges) and check the 8 neighbors of each
 * cell for bombs.
 *
 * Sadly, <c>bombCounts()</c> has to be called from an unorthodox location.
 *
 * @see #handleFirstClickSeparately */
private void bombCounts() {
  for (int i = 1; i <= rows; i++) {
    for (int j = 1; j <= cols; j++) {
      if (bomb[i][j]) {
        count[i][j] = -1;
        bombCount++;
      } else {
        for (int[] offset : offsets) {
          if (bomb[i + offset[0]][j + offset[1]]) {
            count[i][j]++;
          }
        }
      }
    }
  }
}

/**
 * One player stepped on a bomb, the other wins. Or the game is won. Make sure
 * you prioritize the checking of that. (I did.) */
public boolean isGameOver() {
////3ICE: This is a hacky way to make sure we allow for victory by flagging:
//victoryByFlagging(); //Updates gameOver in the above event.
//3ICE: Never mind, victory by flagging can be used to cheat. See below.
  return gameOver;
}

/**
 * This would normally be a win, but in the specified multiplayer ruleset, it's
 * just a tie between the two players.
 * @note I have expanded the original rule ("Amennyiben sikerül minden nem akna
 * mezőt felfedni, akkor a játék döntetlen.") to also allow for victory by
 * flagging: Mark all bombs correctly and you win. (Or rather, "tie", to be
 * painstakingly specific.)
 * @note: Scratch that previous note, that was a bad idea. (Allows cheating.) */
public boolean tieByUncovering() {
  //Need to check for the race condition of blowing up on the last tile.
  for (int i = 1; i <= rows; i++) {
    for (int j = 1; j <= cols; j++) {
      if (visited[i][j] && bomb[i][j]) {
        return false;
      }
    }
  }
  if (uncoveredTileCount == tileCount - bombCount) {
    gameOver = true;
    return true;
  }
  return false;
}

//3ICE: Wow, so I just realized why correctly flagging all bombs shouldn't be a
//      victory condition... It can be used to cheat! Too bad.
///** Here are some, now redundant, comments because I split the isGameWon
// * function in two...
// *
// * //3ICE: I had a lot of problems with this function as you can see from the
// * wall of commented out debug messages.
// *
// * //3ICE: Using >= here is okay, although we only ever want to see == between
// * the two sides in case of a win. If the math is right, that is...
// *
// * Yeah, the math wasn't right. But now it is :)
// *
// * @see #victoryByFlagging
// * @see #tieByUncovering */
//public boolean victoryByFlagging() {
//  int incorrectlyFlagged = 0;
//  for (int i = 1; i <= rows; i++) {
//    for (int j = 1; j <= cols; j++) {
//      if (flag[i][j] && !bomb[i][j]) {
//        incorrectlyFlagged++;
//      }
//    }
//  }
//  if (bombCount - flaggedTileCount - incorrectlyFlagged == 0) {
//    gameOver = true;
//    return true;
//  }
//  return false;
//}
/** @return true if it's red's turn, false if it's blue's. */
public boolean redsTurn() {
  return redsTurn;
}

/** Dead code I didn't end up using. Maybe later... */
private boolean isBomb(int x, int y) {
  return bomb[x + 1][y + 1];
}

/** We have to make sure the padding edges are skipped. [x + 1][y + 1] does just
 * that. */
public int getNum(int x, int y) {
  return count[x + 1][y + 1];
}

public boolean isFlagged(int x, int y) {
  return flag[x + 1][y + 1];
}

/** @note Used to be called getBombsRemaining.
 * @return the number of bombs still undiscovered/flagged. Can and will be
 * inaccurate if the players flag the wrong (safe) tiles. (This is of course
 * intentional.) */
public int getFlagsRemaining() {
  return bombCount - flaggedTileCount;
}

/** Once the game is over, it is conventional to show all bombs. Not using
 * uncover instead of sendEvent because we can't be that lazy.
 * @see #uncover
 * @see #sendEvent */
private void revealAllBombs() {
  for (int i = 1; i <= rows; i++) {
    for (int j = 1; j <= cols; j++) {
      if (bomb[i][j] && !visited[i][j]) {
        sendEvent(i, j, type.EMPTY);
//          System.out.println("revealAllBombs: " + i + " " + j);
      }
      if (flag[i][j] && !bomb[i][j]) {
        sendEvent(i, j, type.EMPTY);
      }
    }
  }
}

/**
 * Saving was easy, I just coped savePageList from FacebookPageStats. Getting
 * loading to work, however, was a lot of back and forth struggle.
 *
 * @see #loadGame
 */
public boolean saveGame(String fileName) throws FileNotFoundException,
                                                IOException {
  if ("".equals(fileName)) {
    return false;
  }
  try (DataOutputStream o = new DataOutputStream(new FileOutputStream(fileName))) {
    o.writeInt(rows);
    o.writeInt(cols);
    for (int i = 0; i <= rows1; i++) {
      for (int j = 0; j <= cols1; j++) {
        o.writeBoolean(bomb[i][j]);
        o.writeBoolean(visited[i][j]);
        o.writeBoolean(flag[i][j]);
        o.writeInt(count[i][j]);
      }
    }
    o.writeBoolean(firstClick);
    o.writeBoolean(redsTurn);
    o.writeBoolean(gameOver);
    o.writeInt(flaggedTileCount);
    o.writeInt(tileCount);
    o.writeInt(bombCount);
    return true;
  }
}

/**
 * In this function, tons of NPE and then array out of bounds errors have been
 * dealt with. Also had to expand TileEvent to handle loading flagged tiles.
 */
public boolean loadGame(String fileName) throws FileNotFoundException,
                                                IOException {
  if ("".equals(fileName)) {
    return false;
  }
  try (DataInputStream in = new DataInputStream(new FileInputStream(fileName))) {
    rows = in.readInt();
    cols = in.readInt();
    initRows1Cols2andMatrices();
    for (int i = 0; i <= rows1; i++) {
      for (int j = 0; j <= cols1; j++) {
        bomb[i][j] = in.readBoolean();
        visited[i][j] = in.readBoolean();
        flag[i][j] = in.readBoolean();
        count[i][j] = in.readInt();
      }
    }
    firstClick = in.readBoolean();
    redsTurn = in.readBoolean();
    gameOver = in.readBoolean();
    flaggedTileCount = in.readInt();
    tileCount = in.readInt();
    bombCount = in.readInt();
    //3ICE: Cool bug I juts fixed: Reloading a game over and over kept ++'ing
    //uncoveredTileCount and I never reset it. Until now:
    uncoveredTileCount = 0;
    return true;
  }
}

/**
 * I have to send the tile events after the fact because the view isn't
 * initialized when the model is. Most importantly waiting for createButtons to
 * finish.
 *
 * @see #MinesweeperWindow.loadGameAction.actionPerformed
 */
public void sendEvents() {
  for (int i = 1; i <= rows; i++) {
    for (int j = 1; j <= cols; j++) {
      if (visited[i][j]) {
        uncover(i, j);
      }
      if (flag[i][j]) {
        sendEvent(i, j, type.FLAG);
      }
    }
  }
}

public int getRows() {
  return rows;
}

public int getCols() {
  return cols;
}

/**
 * @return The originally intended number of bombs would be:
 * <code>bombProbability * rows * cols</code>, but that's too confusing.
 */
public double getDifficulty() {
  return bombCount;
}

/**
 * All this trouble I've gone through just to write standard-compliant event
 * handling code...
 */
private void sendEvent(int x, int y, type t) {
  TileEvent e = new TileEvent(x - 1, y - 1, t);
  minesweeperEventListener.handleEvent(e);
}
}
