/* @(#)TicTacToeTester.java 1.0  12 October 2004 */

import edu.neu.ccs.*;
import edu.neu.ccs.gui.*;
import edu.neu.ccs.jpf.*;
import edu.neu.ccs.util.*;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.font.*;
import javax.swing.*;

/** The tester for tic-tac-toe. */
public class TicTacToeTester extends JPF 
{
    public static void main(String[] args) { 
        // LookAndFeelTools.showSelectLookAndFeelDialog();
        LookAndFeelTools.adjustAllDefaultFontSizes(12);
        
        new TicTacToeTester();
    }
    
    
    public void TicTacToe() {
        TicTacToe.newGame();
    }
    
    
    public void testBlankTile() {
        window.clearPanel();
        Paintable p = TileFactory.getBlankTile();
        p.paint(window.getBufferGraphics());
        window.repaint();
    }
    
    
    public void testXTile() {
        window.clearPanel();
        Paintable p = TileFactory.getXTile();
        p.paint(window.getBufferGraphics());
        window.repaint();
    }
    
    
    public void testOTile() {
        window.clearPanel();
        Paintable p = TileFactory.getOTile();
        p.paint(window.getBufferGraphics());
        window.repaint();
    }
}


/**
 * <p>Class TileFactory constructs as singleton objects the three tiles
 * that are needed for the tic-tac-toe game.</p>
 *
 * <p>The three tiles are the blank tile plus the X and O tiles.</p>
 */
class TileFactory {
    // The three tiles
    private static ShapePaintable blank = null;
    private static ShapePaintable X = null;
    private static ShapePaintable O = null;
    
    // Dimensions
    private static int tileSize = 100;
    private static int tileInset = 10;
    private static int strokeSize = 10;
    
    // Common objects
    private static Stroke stroke =
        new BasicStroke(strokeSize, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
    
    private static Rectangle2D bounds =
        new Rectangle2D.Double(0, 0, tileSize, tileSize);
    
    private static Color shapeColor = Colors.black;
    
    
    /** Prevent instantiation of a TileFactory object. */
    private TileFactory() { }
    
    
    /** Returns the blank tile. */
    public static ShapePaintable getBlankTile() {
        if (blank != null)
            return blank;
        
        blank = new ShapePaintable(new GeneralPath());
        blank.setDefaultBounds2D(bounds);
        
        return blank;
    }
    
    
    /** Returns the X tile. */
    public static ShapePaintable getXTile() {
        if (X != null)
            return X;
        
        int x1 = tileInset;
        int y1 = tileInset;
        
        int x2 = tileSize - tileInset;
        int y2 = tileSize - tileInset;
        
        Line2D line1 = new Line2D.Double(x1, y1, x2, y2);
        Line2D line2 = new Line2D.Double(x1, y2, x2, y1);
        
        GeneralPath cross = Path.append(null, new Shape[] { line1, line2 }, false);
        
        X = new ShapePaintable
            (cross, PaintMode.DRAW, shapeColor, shapeColor, stroke);
        
        X.setDefaultBounds2D(bounds);
        
        return X;
    }
    
    
    /** Returns the O tile. */
    public static ShapePaintable getOTile() {
        if (O != null)
            return O;
        
        int x1 = tileInset;
        int y1 = tileInset;
        
        int w1 = tileSize - 2 * tileInset;
        int h1 = tileSize - 2 * tileInset;
        
        Ellipse2D circle =
            new Ellipse2D.Double(x1, y1, w1, h1);
        
        O = new ShapePaintable
            (circle, PaintMode.DRAW, shapeColor, shapeColor, stroke);
        
        O.setDefaultBounds2D(bounds);
        
        return O;
    }
}


/** The class for the tic-tac-toe game. */
class TicTacToe implements JPTConstants
{
    /** The frame object for this game; needed to permit auto-closure. */
    private JPTFrame frame;
    
    /** The array of 3 by 3 tic-tac-toe cells. */
    private PaintableComponent[][] cells;
    
    /** The panel for the 3 by 3 tic-tac-toe cells. */
    private TablePanel tilePanel;
    
    /** The button panel. */
    private TablePanel buttonPanel;
    
    /** The main enclosure panel. */
    private TablePanel mainPanel;
    
    /** The next tile to be placed in the tic-tac-toe game. */
    private ShapePaintable nextTile = TileFactory.getXTile();
    
    /** The common background color for the tiles. */
    private Color tileBackground = Colors.burlywood;
    
    /** The common highlight color for the winning tile sequence. */
    private Color highlightBackground = Colors.yellow;
    
    /** The common panel background color. */
    private Color panelBackground = Colors.dimgray;
    
    /** The flag that signifies that the game is over. */
    private boolean gameOver = false;
    
    /** The number of tiles clicked so far. */
    private int clickCount = 0;
    
    /** The large font for dialog box messages. */
    private Font largeFont = new Font("serif", Font.ITALIC, 72);
    
    /** The text part of the announcement of a winner. */
    private TextPaintable winnerText  = new TextPaintable("Wins!", largeFont);
    
    /** The text part of the announcement of tie game. */
    private TextPaintable tieGameText = new TextPaintable("Tie Game", largeFont);
    
    /** The action to handle mouse clicks on the tiles. */
    private MouseAction clickAction =
        new MouseAction("TicTacToe Click Action") {
            public void mouseActionPerformed(MouseEvent mevt) {
                click(mevt);
            }
        };
    
    /** The action to start a new game and end the current game. */
    private SimpleAction newGameAction =
        new SimpleAction("New Game") {
            public void perform() {
                endGame();
                TicTacToe.newGame();
            }
        };
    
    /** The action to quit. */
    private SimpleAction quitAction =
        new SimpleAction("Quit") {
            public void perform() {
                endGame();
            }
        };
    
    
    /** The constructor. */
    public TicTacToe() {
        initializeGame();
    }
    
    
    /** The initializer. */
    private void initializeGame() {
        // make cells, fill with blank tiles, place in panel
        cells = new PaintableComponent[3][3];
        
        for (int row = 0; row < 3; row++)
            for (int col = 0; col < 3; col++)
                cells[row][col] =
                    makeTileComponent
                        (TileFactory.getBlankTile(), true);
        
        tilePanel = new TablePanel(cells, 10, 10, CENTER);
        
        // place actions in panel to make buttons
        buttonPanel = new TablePanel(
            new Object[] { newGameAction, quitAction },
            HORIZONTAL, 10, 10, CENTER);
        
        // combine tiles and buttons into the main panel
        mainPanel = new TablePanel(
            new Object[] { tilePanel, buttonPanel },
            VERTICAL, 10, 10, CENTER);
        
        // adjust panel background colors
        tilePanel.setBackground(panelBackground);
        buttonPanel.setBackground(panelBackground);
        mainPanel.setBackground(panelBackground);
    }
    
    
    /**
     * Make a new PaintableComponent for the given tile and
     * make this component active if the given boolean is true. */
    private PaintableComponent makeTileComponent
        (ShapePaintable tile, boolean makeActive)
    {
        PaintableComponent cell
            = new PaintableComponent(tile);
        
        cell.setOpaque(true);
        cell.setBackground(tileBackground);
        
        if (makeActive)
            cell.getMouseActionAdapter().addMouseReleasedAction(clickAction);
        
        return cell;
    }
    
    
    /** Returns the current tile in the given row and col. */
    private ShapePaintable getTile(int row, int col) {
        return (ShapePaintable) cells[row][col].getPaintable();
    }
    
    
    /** Returns the next tile to be inserted in the game. */
    private ShapePaintable getNextTile() {
        if (nextTile == TileFactory.getXTile()) {
            // reset next tile to 0
            nextTile = TileFactory.getOTile();
            
            // return X
            return TileFactory.getXTile();
        }
        else {
            // reset next tile to X
            nextTile = TileFactory.getXTile();
            
            // return 0
            return TileFactory.getOTile();
        }
    }
    
    
    /**
     * If a win occurs using the cells at (row1, col1), (row2, col2), and (row3, col3),
     * then return the winning tile and highlight each cell; otherwise return
     * <code>null</code>.
     */
    private ShapePaintable checkCellsForWinner
        (int row1, int col1, int row2, int col2, int row3, int col3)
    {
        ShapePaintable tile1 = getTile(row1, col1);
        ShapePaintable tile2 = getTile(row2, col2);
        ShapePaintable tile3 = getTile(row3, col3);
        
        boolean isWinner = (tile1 != TileFactory.getBlankTile())
            && (tile1 == tile2) && (tile1 == tile3);
        
        if (isWinner) {
            cells[row1][col1].setBackground(highlightBackground);
            cells[row2][col2].setBackground(highlightBackground);
            cells[row3][col3].setBackground(highlightBackground);
            
            return tile1;
        }
        else
            return null;
    }
    
    
    /**
     * If a win occurs using one of the eight possible cell sequences,
     * then return the winning tile and highlight each cell in the sequence;
     * otherwise return <code>null</code>.
     */
    private ShapePaintable checkPanelForWinner() {
        ShapePaintable result;
        
        // check rows
        for (int row = 0; row < 3; row++) {
            result = checkCellsForWinner(row, 0, row, 1, row, 2);
            
            if (result != null)
                return result; 
        }
            
        // check cols
        for (int col = 0; col < 3; col++) {
            result = checkCellsForWinner(0, col, 1, col, 2, col);
            
            if (result != null)
                return result; 
        }
        
        // check one diagonal
        result = checkCellsForWinner(0, 0, 1, 1, 2, 2);
        
        if (result != null)
            return result; 
        
        // check other diagonal
        result = checkCellsForWinner(0, 2, 1, 1, 2, 0);
        
        if (result != null)
            return result; 
        
        // return null if no winner
        return null;
    }
    
    
    /**
     * If a winner exists then launch the winner dialog
     * and terminate current game when dialog closes.
     */
    private void checkForEndOfGame() {
        ShapePaintable tile = checkPanelForWinner();
        
        if (tile != null) {
            gameOver = true;
            winnerDialog(tile);
        }
        else {
            clickCount++;
            gameOver = (clickCount >= 9);
            
            if (gameOver)
                tieGameDialog();
        }
    }
    
    
    /** Show the winner dialog corresponding to the given tile. */
    private void winnerDialog(ShapePaintable tile) {
         PaintableComponent component = makeTileComponent(tile, false);
         
         TablePanel panel = new TablePanel(
             new Object[] { component, winnerText },
             HORIZONTAL, 10, 10, CENTER);
         
         GeneralDialog.showOKDialog(panel, "Winner");
    }
    
    
    /** Show the tie game dialog. */
    private void tieGameDialog() {
         GeneralDialog.showOKDialog(tieGameText, "Tie Game");
    }
    
    
    /** The click action method. */
    private void click(MouseEvent mevt) {
        PaintableComponent component = (PaintableComponent) mevt.getComponent();
        
        boolean doClick = !gameOver
            && component.getPaintable() == TileFactory.getBlankTile();
            
        if (doClick) {
            component.setPaintable(getNextTile());
            checkForEndOfGame();
        }
    }
    
    
    /** Launch a new tic-tac-toe game. */
    public static void newGame() {
        TicTacToe game = new TicTacToe();
        game.frame = JPTFrame.createQuickJPTFrame("Tic-Tac-Toe", game.mainPanel, NORTH);
    }
    
    
    /** End the current tic-tac-toe game. */
    private void endGame() {
         frame.dispose();
    }
}
