/***************************************************************************
 * Copyright (c) 2005, Northwest Summit. All Rights Reserved.
 * 
 * This software is the proprietary information of Northwest Summit.
 * Use is subject to license terms.
 ***************************************************************************/

package com.nwsummit.games.minesweeper;

import java.awt.*;
import java.awt.event.*;
import java.net.URL;
import java.util.List;
import java.util.Iterator;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.UIManager;

/**
 * A minesweeper game with Swing GUI.
 */
public class JMinesweeper
  implements MouseListener, MouseMotionListener, ActionListener, Runnable {

  private static final boolean DOWN = true;
  private static final boolean UP = false;

  public static final String CMD_NEWGAME = "cmd_newgame";
  public static final String CMD_BEGINNER = "cmd_beginner";
  public static final String CMD_INTERMEDIATE = "cmd_intermediate";
  public static final String CMD_EXPERT = "cmd_expert";
  public static final String CMD_EXIT = "cmd_exit";
  public static final String CMD_MARK_QUESTION = "cmd_markQuestion";

  private Minefield _minefield;
  private ImageIcon[] _icons, _digits, _faces;
  private JLabel[][] _tiles;
  private JLabel[] _time, _bombs;
  private JButton _btn;
  private JPanel _minePane;
  private Frame _frame;
  private int _iw, _ih; // width & height of the icons
  private int _mr, _mc; // where in the minefield mouse was pressed
  private boolean _mbtn1, _mbtn2, _mbtn3; // mouse's buttons
  private boolean _gameOver;

  // timer counting elapsed seconds
  private Thread _timer;
  private boolean _tmRun;
  private int _tmSec;

  /**
   * Create a minesweeper GUI. The GUI uses a number images to render itself.
   * By convention, the array of images will contain the following in order:
   * <table>
   * <tr><th>Index</th><th>Description</th></tr>
   * <tr><td>0</td><td>Empty open cell</td></tr>
   * <tr><td>1-8</td><td>Numbers 1-8</td></tr>
   * <tr><td>9</td><td>Coverred cell</td></tr>
   * <tr><td>10</td><td>Flag</td></tr>
   * <tr><td>11</td><td>Question mark</td></tr>
   * <tr><td>12</td><td>Bomb</td></tr>
   * <tr><td>13</td><td>Exploded bobm</td></tr>
   * <tr><td>14</td><td>Bomb wrongly flagged</td></tr>
   * <tr><td>15</td><td>Pressed question mark</td></tr>
   * <tr><td>16-25</td><td>Digit 0-9</td></tr>
   * <tr><td>26</td><td>Smiley face button</td></tr>
   * <tr><td>27</td><td>Shady smiley face for wining game</td></tr>
   * <tr><td>28</td><td>Smiley face when a cell is open</td></tr>
   * <tr><td>29</td><td>Frowny face for losing game</td></tr>
   * <tr><td>30</td><td>Pressed smiley face</td></tr>
   * </table>
   * @param images used by the GUI.
   */
  public JMinesweeper(ImageIcon[] images)
  {
    initialize(9, 9, 10);

    // icons for the minefield
    _icons = new ImageIcon[16];
    for (int i=0; i<_icons.length; i++)
      _icons[i] = images[i];

    // digits for time and bombs count
    _digits = new ImageIcon[10];
    for (int i=0; i<_digits.length; i++)
      _digits[i] = images[16 + i];

    // faces for new game button
    _faces = new ImageIcon[5];
    for (int i=0; i<_faces.length; i++) 
      _faces[i] = images[26 + i];

    _iw = _icons[0].getIconWidth();
    _ih = _icons[0].getIconHeight();
  }

  /** Set the window frame this game pane is part of. */
  public void setFrame(Frame frame) {
    _frame = frame;
  }

  /**
   * Return the frame this game pane is part of. <code>null</code> if this
   * game is not within a window frame.
   */
  public Frame getFrame() {
    return _frame;
  }

  /**
   * Initialize the game with the specified parameters. The parameters
   * represent the level of the game.
   * <p>
   * @param rows number of rows in the grid.
   * @param cols number of columns in the grid.
   * @param bombs number of bombs to sweep.
   */
  private void initialize(int rows, int cols, int bombs)
  {
    _minefield = new Minefield(rows, cols, bombs);
    _tiles = new JLabel[rows][cols];
  }

  /**
   * Create the game pane. The game pane is composed of the mines grid
   * (minefield), the bombs counter, timer clock and the new game button.
   * It's a replica of the Windows minesweeper.
   * <p>
   * @return the game pane.
   */
  public JPanel createComponents()
  {
    _btn = new JButton(_faces[0]);
    _btn.setPressedIcon(_faces[1]);
    _btn.setActionCommand(CMD_NEWGAME);
    _btn.setBorder(null);
    _btn.setFocusPainted(false);
    _btn.setMargin(new Insets(0,0,0,0));
    _btn.addActionListener(this);

    FlowLayout layout = new FlowLayout(FlowLayout.CENTER, 0, 0);
    JPanel countPane = new JPanel(layout);
    _bombs = new JLabel[3];
    for (int i=0; i<3; i++)
      countPane.add(_bombs[i] = new JLabel());
    setNumber(_bombs, _minefield.getBombs());

    JPanel timePane = new JPanel(layout);
    _time = new JLabel[3];
    for (int i=0; i<3; i++) {
      _time[i] = new JLabel(_digits[0]);
      timePane.add(_time[i]);
    }

    JPanel btnPane = new JPanel(layout);
    btnPane.add(_btn);

    JPanel topPane = new JPanel(new BorderLayout());
    topPane.add(countPane, BorderLayout.WEST);
    topPane.add(btnPane, BorderLayout.CENTER);
    topPane.add(timePane, BorderLayout.EAST);

    int rows = _minefield.getRows();
    int cols = _minefield.getColumns();
    _minePane = new JPanel(new GridLayout(rows, cols));
    _minePane.addMouseListener(this);
    _minePane.addMouseMotionListener(this);
    for (int r=0; r<rows; r++)
      for (int c=0; c<cols; c++) {
        _tiles[r][c] = new JLabel(_icons[toIndex(Minefield.M_COVER)]);
        _minePane.add(_tiles[r][c]);
      }

    JPanel mainPane = new JPanel();
    mainPane.setLayout(new BoxLayout(mainPane, BoxLayout.PAGE_AXIS));
    mainPane.add(topPane);
    mainPane.add(_minePane);

    return mainPane;
  }

  /**
   * Set the specified number with the specified value.
   * <p>
   * @param number array representing the 3-digit number.
   * @param val value to be displayed.
   */
  private void setNumber(JLabel[] number, int val)
  {
    number[0].setIcon(_digits[val / 100]);
    number[1].setIcon(_digits[(val / 10) % 10]);
    number[2].setIcon(_digits[val % 10]);
  }

  /** Convert the specified value to icons' index. */
  private int toIndex(int val)
  {
    int index;
    switch (val) {
      case Minefield.M_COVER:
        index = 9; break;
      case Minefield.M_FLAG:
        index = 10; break;
      case Minefield.M_DOUBT:
        index = 11; break;
      case Minefield.M_BOMB:
        index = 12; break;
      case Minefield.M_BAD:
        index = 13; break;
      case Minefield.M_WRONG:
        index = 14; break;
      default:
        index = val;
    }
    return index;
  }

  /**
   * Create a new game using the current parameters (same level).
   */
  public void newGame()
  {
    setNumber(_bombs, _minefield.getBombs());
    setNumber(_time, 0);

    // in case new game request takes place during a game
    if (_timer != null)
      stopTimer();
    _timer = null;

    _minefield.reset();
    _btn.setIcon(_faces[0]);
    _gameOver = false;

    int rows = _minefield.getRows();
    int cols = _minefield.getColumns();
    for (int r=0; r<rows; r++)
      for (int c=0; c<cols; c++)
        _tiles[r][c].setIcon(_icons[toIndex(Minefield.M_COVER)]);
  }

  /**
   * Create a new game with the specified parameters. The parameters represent
   * the level of the game.
   * <p>
   * @param rows number of rows in the grid.
   * @param cols number of columns in the grid.
   * @param bombs number of bombs.
   */
  public void newGame(int rows, int cols, int bombs)
  {
    if (rows != _minefield.getRows() ||
        cols != _minefield.getColumns() ||
        bombs != _minefield.getBombs()) {
      initialize(rows, cols, bombs);
      _minePane.removeAll();
      _minePane.setLayout(new GridLayout(rows, cols));
      for (int r=0; r<rows; r++)
        for (int c=0; c<cols; c++) {
          _tiles[r][c] = new JLabel();
          _minePane.add(_tiles[r][c]);
        }
    }

    newGame();
    if (_frame != null)
      _frame.pack();
  }

  /**
   * Update the GUI when the game is over.
   * <p>
   * @param win <code>true</code> if the game is won; <code>false</code>
   * otherwise.
   */
  public void gameOver(boolean win)
  {
    _gameOver = true;
    _mbtn1 = _mbtn2 = _mbtn3 = false;

    int[][] grid = _minefield.getGrid();
    List bombs = _minefield.makeFinalSolution(win);
    for (Iterator i=bombs.iterator(); i.hasNext(); ) {
      Minefield.Cell cell = (Minefield.Cell)i.next();
      int r = cell.getX();
      int c = cell.getY();
      _tiles[r][c].setIcon(_icons[toIndex(grid[r][c])]);
    }

    _btn.setIcon(_faces[win? 3 : 4]);
    stopTimer();
  }

  /**
   * Update the GUI with the pressing or release of the surrounding area of the
   * specified cell.
   * <p>
   * @param r row index of the cell.
   * @param c column index of the cell.
   * @param down <code>true</code> for pressed down event; <code>false</code>
   * otherwise.
   */
  private void suroundingArea(int r, int c, boolean down)
  {
    int r1 = Math.max(r-1, 0);
    int r2 = Math.min(r+1, _minefield.getRows()-1);
    int c1 = Math.max(c-1, 0);
    int c2 = Math.min(c+1, _minefield.getColumns()-1);
    int[][] grid = _minefield.getGrid();
    if (down) {
      for (int i=r1; i<=r2; i++)
        for (int j=c1; j<=c2; j++)
          if (grid[i][j] == Minefield.M_COVER)
            _tiles[i][j].setIcon(_icons[0]);
    }
    else {
      for (int i=r1; i<=r2; i++)
        for (int j=c1; j<=c2; j++)
          if (grid[i][j] == Minefield.M_COVER)
            _tiles[i][j].setIcon(_icons[toIndex(Minefield.M_COVER)]);
    }
  }

  /** Start the timer clock. */
  private void startTimer()
  {
    if (_timer != null)
      stopTimer();

    _tmSec = 0;
    _tmRun = true;
    _timer = new Thread(this);
    _timer.start();
  }

  /** Stop the timer clock. */
  private void stopTimer()
  {
    _tmRun = false;
    if (_timer != null && _timer.isAlive())
      _timer.interrupt();
  }

  /**
   * Quit the game. The timer thread is stopped and if the game is run within
   * a window, the window is disposed (closed).
   */
  public void quitGame()
  {
    stopTimer();
    if (_frame != null)
      _frame.dispose();
  }


  //--------------------------------------------------| MouseListener interface

  /** Process the pressing of a mouse button. */
  public void mousePressed(MouseEvent e)
  {
    if (_gameOver) return;

    _mr = e.getY() / _iw;
    _mc = e.getX() / _ih;

    // set mouse button presed
    boolean mouse3button = false;
    int btn = e.getButton();
    switch (btn) {
      case MouseEvent.BUTTON1: // left button
        _mbtn1 = true; break;
      case MouseEvent.BUTTON2: // middle button
        _mbtn2 = mouse3button = true; break;
      case MouseEvent.BUTTON3: // right button
        _mbtn3 = true; break;
      default: // unknown mouse button
        return;
    }
    _mbtn2 = mouse3button? _mbtn2 : (_mbtn1 && _mbtn3);

    // process accordingly
    int[][] grid = _minefield.getGrid();
    if (_mbtn2) {
      _btn.setIcon(_faces[2]);
      suroundingArea(_mr, _mc, DOWN);
    }
    else if (_mbtn1) {
      _btn.setIcon(_faces[2]);
      if (grid[_mr][_mc] == Minefield.M_COVER)
        _tiles[_mr][_mc].setIcon(_icons[0]);
    }
    else if (_mbtn3) {
      int val = _minefield.flag(_mr, _mc);
      _tiles[_mr][_mc].setIcon(_icons[toIndex(val)]);
      setNumber(_bombs, _minefield.getBombs() - _minefield.getFlags());
    }
  }

  /** Process the release of a mouse button. */
  public void mouseReleased(MouseEvent e)
  {
    if (_gameOver) return;

    int r = e.getY() / _iw;
    int c = e.getX() / _ih;
    int[][] grid = _minefield.getGrid();

    // process the release event first according to button(s) pressed
    List openCells = Minefield.EMPTY_LIST;
    if (_mbtn2) {
      _btn.setIcon(_faces[0]);
      suroundingArea(r, c, UP);
      openCells = _minefield.openRemaining(r, c);
    }
    else if (_mbtn1) {
      _btn.setIcon(_faces[0]);
      openCells = _minefield.open(r, c);

      if (_timer == null) startTimer();
    }

    if (openCells == null)
      gameOver(false);
    else {
      for (Iterator i=openCells.iterator(); i.hasNext(); ) {
        Minefield.Cell cell = (Minefield.Cell)i.next();
        int x = cell.getX();
        int y = cell.getY();
        _tiles[x][y].setIcon(_icons[toIndex(grid[x][y])]);
      }
      if (_minefield.getRemainingCells() == 0) {
        setNumber(_bombs, 0);
        gameOver(true);
      }
    }

    // reset button pressed
    boolean mouse3button = false;
    int btn = e.getButton();
    switch (btn) {
      case MouseEvent.BUTTON1:
        _mbtn1 = false; break;
      case MouseEvent.BUTTON2:
        _mbtn2 = false;
        mouse3button = true;
        break;
      case MouseEvent.BUTTON3:
        _mbtn3 = false; break;
    }
    _mbtn2 = mouse3button? _mbtn2 : (_mbtn1 && _mbtn3);
   }

  // not interested in these events
  public void mouseClicked(MouseEvent e) {}
  public void mouseEntered(MouseEvent e) {}
  public void mouseExited(MouseEvent e) {}

  //--------------------------------------------| MouseMotionListener interface

  /**
   * Update the GUI when the mouse is pressed and dragged. The update is done
   * when the mouse leaves a cell and enters a new one.
   */
  public void mouseDragged(MouseEvent e)
  {
    int r = e.getY() / _ih;
    int c = e.getX() / _iw;

    // has not moved to a nother cell => do nothing
    if (r == _mr && c == _mc) return;

    int[][] grid = _minefield.getGrid();
    if (_mbtn2) {
      suroundingArea(_mr, _mc, UP);
      suroundingArea(r, c, DOWN);
    }
    else if (_mbtn1) {
      _tiles[_mr][_mc].setIcon(_icons[toIndex(grid[_mr][_mc])]);
      if (grid[r][c] == Minefield.M_COVER)
        _tiles[r][c].setIcon(_icons[0]);
    }
    _mr = r;
    _mc = c;
  }

  // not interested in these events
  public void mouseMoved(MouseEvent e) {}

  //-------------------------------------------------| ActionListener interface

  /**
   * Process actions fired by the GUI interface. Expected actions are defined
   * by <code>CMD_XXX</code> constants.
   */
  public void actionPerformed(ActionEvent e)
  {
    String cmd = e.getActionCommand();
    if (CMD_NEWGAME.equals(cmd))
      newGame();
    else if (CMD_BEGINNER.equals(cmd))
      newGame(9, 9, 10);
    else if (CMD_INTERMEDIATE.equals(cmd))
      newGame(16, 16, 40);
    else if (CMD_EXPERT.equals(cmd))
      newGame(16, 30, 99);
    else if (CMD_EXIT.equals(cmd))
      quitGame();
    else if (CMD_MARK_QUESTION.equals(cmd))
      _minefield.doQuestionMark(!_minefield.doQuestionMark());
  }

  //-------------------------------------------------------| Runnable interface

  /**
   * Timer clock thread. Update the time panel every second.
   */
  public void run()
  {
    do {
      setNumber(_time, Math.min(999, _tmSec++));
      try {Thread.sleep(1000);}
      catch (InterruptedException ignore) {}
    }
    while (_tmRun);
  }

  //---------------------------------------------------------------------------

  /**
   * Construct a window GUI for the specified game. The window has a menubar
   * allowing the user to select different game levels.
   * <p>
   * NOTE it is up to the caller to call <code>pack()</code> and
   * <code>setVisible()</code> on the returned window.
   * <p>
   * @param game for which the window is built.
   * @return a window for the specified game.
   */
  public static JFrame createGameWindow(JMinesweeper game)
  {
    //Create and set up the window.
    JFrame frame = new JFrame("JMinesweeper");
    game.setFrame(frame);

    JMenuBar mbar = new JMenuBar();
    JMenu menu = new JMenu("Game");
    frame.setJMenuBar(mbar);
    mbar.add(menu);

    ButtonGroup group = new ButtonGroup();
    JMenuItem item = new JMenuItem("New Game");
    item.setActionCommand(CMD_NEWGAME);
    item.addActionListener(game);
    menu.add(item);
    menu.addSeparator();

    item = new JRadioButtonMenuItem("Beginner", true);
    item.setActionCommand(CMD_BEGINNER);
    item.addActionListener(game);
    menu.add(item);
    group.add(item);

    item = new JRadioButtonMenuItem("Intermidate");
    item.setActionCommand(CMD_INTERMEDIATE);
    item.addActionListener(game);
    group.add(item);
    menu.add(item);

    item = new JRadioButtonMenuItem("Expert");
    item.setActionCommand(CMD_EXPERT);
    item.addActionListener(game);
    group.add(item);
    menu.add(item);
    menu.addSeparator();

    item = new JRadioButtonMenuItem("Mark (?)");
    item.setActionCommand(CMD_MARK_QUESTION);
    item.addActionListener(game);
    menu.add(item);
    menu.addSeparator();

    item = new JMenuItem("Exit");
    item.setActionCommand(CMD_EXIT);
    item.addActionListener(game);
    menu.add(item);

    Component contents = game.createComponents();
    frame.getContentPane().add(contents, BorderLayout.CENTER);
    frame.setResizable(false);

    return frame;
  }

  /**
   * Standalone Swing minesweeper game.
   */
  public static class Game {
    public static void main(String[] args)
    {
      try {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
      }
      catch (Exception ignore) {}

      ImageIcon[] images = new ImageIcon[31];
      // loading icons
      for (int i=0; i<16; i++) {
        URL imgUrl = JMinesweeper.class.getResource("images/" + i + ".gif");
        images[i] = new ImageIcon(imgUrl);
      }
      // loading digits
      for (int i=0; i<10; i++) {
        URL imgUrl = JMinesweeper.class.getResource("images/d" + i + ".gif");
        images[16+i] = new ImageIcon(imgUrl);
      }
      // loading faces for new game button
      for (int i=0; i<5; i++) {
        URL imgUrl = JMinesweeper.class.getResource("images/f" + i + ".gif");
        images[26+i] = new ImageIcon(imgUrl);
      }

      JMinesweeper game = new JMinesweeper(images);
      JFrame frame = JMinesweeper.createGameWindow(game);
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.pack();
      frame.setVisible(true);
    }
  }
}