import java.io.File;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;

import javax.swing.AbstractAction;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JOptionPane;
import javax.swing.JFileChooser;

import java.util.ArrayList;


/**
 * Simple Sudoku game UI panel implementation. Handles keyboard navigation and
 * game state rendering. 
 * This is a small modification of a panel by org.quinn.sudoku.ui
 */
public class SudokuPanel extends JPanel {

  /** The directory to start browsing for files in. */
  public String rootDirName = (new File("./data")).getAbsolutePath();

  /** Width margin for Sudoku panel */
  public static final int MW = 5;
  /** Height margin for Sudoku panel */
  public static final int MH = 5;

  /** Prefered cell width. */
  public static final int PCW = 32;
  /** Prefered cell height. */
  public static final int PCH = 32;

  /** Current puzzle state. */
  SudokuControl cntrl;

  /** Pixel coordinate of grid origin (x position) */
  int origx;
  /** Pixel coordinate of grid origin (x position) */
  int origy;
  /** Pixel width of a cell */
  int cellw;
  /** Pixel height of a cell */
  int cellh;

  /** Build a new SudokuPanel. */
  public SudokuPanel() {
    setFocusable(true);
    // Deal with mouse clicks.
    addMouseListener(new MouseAdapter() {
        public void mousePressed(MouseEvent e) {
          Point mouse = e.getPoint();
          Index2D c = new Index2D((mouse.y-origy)/cellh,
                                  (mouse.x-origx)/cellw);
          cntrl.setCursor(c);
          invalidate();
          repaint();
        }
      });

    // Deal with keyboard presses.
    addKeyListener(new KeyAdapter() {
        public void keyPressed(KeyEvent e) {
          Index2D cursor = cntrl.getCursor();
          switch (e.getKeyCode()) {
          case KeyEvent.VK_LEFT: 
            if (!cntrl.undoMove()) {
              Toolkit.getDefaultToolkit().beep(); 
            }
            break;
          case KeyEvent.VK_RIGHT:
            if (!cntrl.redoMove())
              Toolkit.getDefaultToolkit().beep(); 
            break;
          case KeyEvent.VK_UP:
          case KeyEvent.VK_DOWN: 
          case KeyEvent.VK_DELETE:
          case KeyEvent.VK_BACK_SPACE:
          case KeyEvent.VK_0: 
            if (!cntrl.isFrozen(cursor.row, cursor.col)) {
              cntrl.makeMove(cursor, 0);
            }
            break;
          case KeyEvent.VK_1:
          case KeyEvent.VK_2:
          case KeyEvent.VK_3:
          case KeyEvent.VK_4:
          case KeyEvent.VK_5:
          case KeyEvent.VK_6:
          case KeyEvent.VK_7:
          case KeyEvent.VK_8:
          case KeyEvent.VK_9:
            if (!cntrl.isFrozen(cursor.row, cursor.col)) {
              cntrl.makeMove(cursor, e.getKeyCode() - KeyEvent.VK_1 + 1);
            }
          }

          invalidate();
          repaint();
        }
      });
    // Set up a new puzzle.
    if (cntrl == null)
      cntrl = new SudokuControl("easy0.txt");
    else
      newPuzzle();
  }

  /** Set up a new puzzle. */
  void newPuzzle() {
    // Browse for a sudoku text file
    String fileName = "[easy|med|hard|evil|sud]#.txt";
    File sudFile = browseForFile(fileName);
    // Read the Sudoku file, build the
    // inital board.
    try {
      cntrl = new SudokuControl(sudFile.getName()); 
    } catch (Exception ex) {
      System.out.print(ex);
      cntrl = new SudokuControl("easy0.txt");
    }
    invalidate();
    repaint();
  }

  /** Determine the preferred (default) size of the panel. */
  public Dimension getPreferredSize() {
    return new Dimension(9*PCW+2*MW+3, 9*PCW+2*MH+3);
  }

  /** Draw the panel. */
  public void paintComponent(final Graphics g) {
    final Graphics2D graphics = (Graphics2D) g;
    final int panelw = getWidth();
    final int panelh = getHeight();
    Index2D cursor = cntrl.getCursor();

    // board dimensions
    final int boardw = (panelw - 3) / 9 * 9;
    final int boardh = (panelh - 3) / 9 * 9;
    origx = (panelw - boardw - 3) / 2;
    origy = (panelh - boardh - 3) / 2;

    // cell dimensions
    cellw = boardw / 9;
    cellh = boardh / 9;
    //System.out.println("Cell W="+cellw+" H="+cellh);

    // background rectangle
    graphics.setColor(Color.WHITE);
    graphics.fillRect(0, 0, panelw, panelh);

    // selection rectangle
    int sx = origx + cursor.col * cellw;
    int sy = origy + cursor.row * cellh;
    graphics.setColor(Color.YELLOW);
    graphics.fillRect(sx,sy,cellw,cellh);

    // grid lines
    graphics.setColor(Color.BLACK);
    for (int v = 0; v < 10; v++) {
      graphics.drawLine(origx+v*cellw,origy,origx+v*cellw,origy+boardh);
      if (v%3 == 0) {
        graphics.drawLine(origx+v*cellw+1,origy,
                          origx+v*cellw+1,origy+boardh+1);
        graphics.drawLine(origx+v*cellw+2,origy,
                          origx+v*cellw+2,origy+boardh+1);
      }
    }
    for (int h = 0; h < 10; h++) {
      graphics.drawLine(origx,origy+h*cellh,origx+boardw,origy+h*cellh);
      if (h%3 == 0) {
        graphics.drawLine(origx,origy+h*cellh+1,
                          origx+boardw+1,origy+h*cellh+1);
        graphics.drawLine(origx,origy+h*cellh+2,
                          origx+boardw+1,origy+h*cellh+2);
      }
    }

    // cell labels
    graphics.setFont(new Font("Arial", Font.BOLD, 
                              (Math.min(cellh,cellw)*8)/10));
    FontMetrics fm = graphics.getFontMetrics();
    int lh = fm.getHeight() - fm.getLeading() - fm.getDescent();

    // paint the cell
    for (int r = 0; r < 9; r++) {
      for (int c = 0; c < 9; c++) {
        int val = cntrl.getValue(r,c);
        ArrayList<Integer> ops = cntrl.getOptions(r, c);
        if (val > 0) {
          // Draw the digit, frozen or not.
          String label = Integer.toString(val);
          int lw = fm.stringWidth(label);
          int lx = origx + c*cellw + cellw/2 - lw/2;
          int ly = origy + r*cellh + cellh/2 + lh/2;
          Color tcolor;
          if (cntrl.isFrozen(r,c))
            tcolor = Color.BLACK;
          else if (!ops.contains(new Integer(val)))
            tcolor = Color.RED;
          else
            tcolor = Color.BLUE;
          graphics.setColor(tcolor);
          graphics.drawString(label, lx, ly);
        } else {
          // Draw the pencil marks.
          int bw = cellw/7;
          int bh = cellh/7;
          int x0 = origx + c*cellw+(cellw - 7*bw)/2; 
          int y0 = origy + r*cellh+(cellh - 7*bh)/2;
          int offseth = (cellw - 7*bw)/2;
          graphics.setColor(Color.BLUE);
          for (int k = 0; k < ops.size(); k++) {
            int mv = ops.get(k);
            int jy = (mv - 1) / SudokuBoard.CELL_SIZE;
            int jx = mv - 1 - SudokuBoard.CELL_SIZE * jy;
            int lx = x0 + (2*jx+1)*bw;
            int ly = y0 + (2*jy+1)*bh;
            graphics.fillRect(lx,ly, bw, bh);
          }
        }
      }
    }
    // When puzzle is solved, display a message. 
    if (cntrl.isSolution()) {
      Toolkit.getDefaultToolkit().beep(); 
      displayMessage(g, "Done!");
    }

  }
  
  /** Setup the File menu for the frame. */
  JMenu newFileMenu(final JFrame frame) {
    JMenu menu = new JMenu("File");
    menu.add(new JMenuItem(new AbstractAction("New") {
        public void actionPerformed(final ActionEvent e) {
          newPuzzle();
        }
      }));
    menu.add(new JMenuItem(new AbstractAction("Exit") {
        public void actionPerformed(final ActionEvent e) {
          System.out.println("Exiting...");
          frame.setVisible(false);
          frame.dispose();
        }
      }));
    return menu;
  }

  /** Set up the Game menu for the frame. */
  public JMenu newGameMenu() {
    JMenu gameMenu = new JMenu("History");
    gameMenu.add(new JMenuItem(new AbstractAction("Undo (L-Arrow)") {
        public void actionPerformed(final ActionEvent e) {
            if (!cntrl.undoMove())
              System.out.println("Can't undo.");
            else {
              invalidate();
              repaint();
            }
             
        }
      }));
    gameMenu.add(new JMenuItem(new AbstractAction("Redo (R-Arrow)") {
        public void actionPerformed(final ActionEvent e) {
          if (!cntrl.redoMove())
            System.out.println("Can't redo");
          else {
            invalidate();
            repaint();
          }
        }
      }));
    gameMenu.add(new JMenuItem(new AbstractAction("Restart") {
        public void actionPerformed(final ActionEvent e) {
          cntrl.clear();
          invalidate();
          repaint();
        }
      }));
    gameMenu.add(new JMenuItem(new AbstractAction("Solve 1") {
        public void actionPerformed(final ActionEvent e) {
          cntrl.solve(false);
          invalidate();
          repaint();
        }
      }));
    gameMenu.add(new JMenuItem(new AbstractAction("Solve 2") {
        public void actionPerformed(final ActionEvent e) {
          cntrl.solve(true);
          invalidate();
          repaint();
        }
      }));
    return gameMenu;
  }


  /** Open a file, using the file chooser if necessary. 
   *  @param fileName file name to be opened. 
   *  @return File object for the opened file. */
  public File browseForFile(String fileName) {  
    File f = new File(rootDirName, fileName);
    if (!f.isFile()) { 
      this.setVisible(true);
      JOptionPane.showMessageDialog(this, 
             "Please choose a Sudoku input file " + fileName,
             "Browse files...", JOptionPane.QUESTION_MESSAGE);
      
      JFileChooser chooser = new JFileChooser(rootDirName);
      int returnVal = chooser.showOpenDialog(this);
      if (returnVal == JFileChooser.APPROVE_OPTION) {
        f = chooser.getSelectedFile();
      }
    }
    return f;
  }

   /** Sets the display message in the screen graphics context g. 
   *  This is the helper method called by paintComponent. 
   *  @param g the displayed graphics frame.
   *  @param message the text message to be displayed. */
  private void displayMessage(Graphics g, String message) {
    
    Font font = new Font("Arial", Font.BOLD, (Math.min(cellh,cellw)*8)/5);
    Rectangle2D bbox = font.getStringBounds(message, 
                     ((Graphics2D) g).getFontRenderContext());
    double cx = bbox.getCenterX();
    double cy = bbox.getCenterY();
    g.setFont(font);
    // Pick the font colour.
    g.setColor(Color.RED);
    // Display the message string.
    ((Graphics2D) g).drawString(message, 
                                (float) (cellw*9 / 2.0 - cx), 
                                (float) (cellh*9 / 2.0 - cy));
  }

}
