/* @(#)ConcentrationGame.java    26 September 2007 */

import edu.neu.ccs.*;
import edu.neu.ccs.gui.*;
import edu.neu.ccs.util.*;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * <p>The class for the Concentration game.</p>
 * 
 * <p>Uses Java Power Tools 2.6.0.</p>
 * 
 * <p>Copyright, Richard Rasala, 2007.</p>
 * 
 * @author Richard Rasala
 */
class ConcentrationGame
    extends DisplayPanel
{
    /** The minimum tile size: 50. */
    public static final int MINIMUM_TILE_SIZE = 50;
    
    
    /** The minimum grid size: 3. */
    public static final int MINIMUM_GRID_SIZE = 3;
    
    
    /** The maximum grid size: 10. */
    public static final int MAXIMUM_GRID_SIZE = 10;
    
    
    /** The paintables for the current game. */
    protected Paintable[] paintables = null;
    
    
    /** The length of the current paintables array. */
    protected int length = 0;
    
    
    /** The tile size in pixels. */
    protected int tileSize = 0;
    
    
    /** The game grid size. */
    protected int gridSize = 0;
    
    
    /** The middle index to skip if the grid size is odd. */
    protected int gridSkip = -1;
    
    
    /**
     * <p>The count of active tiles in the game grid.<p>
     * 
     * <p>If <code>gridSize</code> is even,
     * this is <code>gridSize*gridSize</code>.</p>
     * 
     * <p>If <code>gridSize</code> is odd,
     * this is <code>gridSize*gridSize-1</code>.</p>
     */
    protected int tileCount = 0;
    
    
    /**
     * <p>The tile boxes in the grid.</p>
     *
     * <p>The array size should be <code>tileCount</code>.
     * The tiles should inserted in the paintable sequence
     * by rows.  If <code>gridSize</code> is odd, then the
     * center tile position should be skipped.</p>
     */
    protected TileBox[] tiles = null;
    
    
    /**
     * <p>The paintable contents corresponding to the tile boxes.</p>
     * 
     * <p>The array size should be <code>tileCount</code>.</p>
     * 
     * <p>The paintable <code>contents[i]</code> should correspond
     * to the tile <code>tiles[i]</code>.</p>
     */
    protected Paintable[] contents = null;
    
    
    /** The paintable sequence for the tile boxes. */
    protected PaintableSequence sequence = new PaintableSequence();
    
    
    /** The game board. */
    protected PaintableComponent board = new PaintableComponent(sequence);
    
    
    /** The mouse action adapter for the game board. */
    protected MouseActionAdapter adapter = board.getMouseActionAdapter();
    
    
    /** The blank paintable. */
    protected Paintable blank = null;
    
    
    /** The number of guesses. */
    protected int guesses = 0;
    
    
    /** The number of matches. */
    protected int matches = 0;
    
    
    /** Selected index #1. */
    protected int index1 = -1;
    
    
    /** Selected index #2. */
    protected int index2 = -1;
    
    
    /** The tile background color. */
    protected Color tileBackground = Colors.burlywood;
    
    
    /** The game background color. */
    protected Color gameBackground = Colors.cornflowerblue;
    
    
    /** The panel background color. */
    protected Color panelBackground = Colors.white;
    
    
    /** The button background color. */
    protected Color buttonBackground = Colors.yellow;
    
    
    /** The action for showing or hiding tile pairs. */
    protected MouseAction clickAction =
        new MouseAction("Click Action") {
            public void mouseActionPerformed(MouseEvent mevt) {
                click(mevt);
            }
        };
    
    
    /** The action to launch a new game. */
    protected SimpleAction newGameAction =
        new SimpleAction("New Game") {
            public void perform() {
                newGame();
            }
        };
    
    
    /** The action to reveal all tiles. */
    protected SimpleAction revealAllAction =
        new SimpleAction("Reveal All") {
            public void perform() {
                revealAll();
            }
        };
    
    
    /** The tile and table panel gap. */
    protected int gap = 10;
        
        
    /** The progress message with guesses and matches. */
    protected Annotation message = new Annotation(" ");
    
    
    /** The button to launch a new game. */
    protected JButton newGameButton = new JButton(newGameAction);
    
    
    /** The button to reveal the solution to an impatient user. */
    protected JButton revealAllButton = new JButton(revealAllAction);
    
    
    /** The default constructor. */
    public ConcentrationGame() {
        this(null, 0, 0);
    }
    
    
    /**
     * <p>The constructor that supplies the paintables,
     * the tile size, and the grid size.</p>
     * 
     * <p>If the paintables are <code>null</code> then
     * a default set of shape paintables will be used,
     * and the tile size and grid size will be adjusted.</p>
     * 
     * <p>The tile size will be forced to be at least
     * MINIMUM_TILE_SIZE.</p>
     * 
     * <p>The grid size will be forced to be between
     * MINIMUM_GRID_SIZE and MAXIMUM_GRID_SIZE.</p>
     */
    public ConcentrationGame
        (Paintable[] paintables, int tileSize, int gridSize)
    {
        if ((paintables == null) || (paintables.length < 1)) {
            GamePaintables gamePaintables = new GamePaintables();
            
            paintables = gamePaintables.getShapePaintables();
            tileSize = gamePaintables.getTileSize();
            gridSize = 6;
        }
        
        tileSize = (tileSize > MINIMUM_TILE_SIZE) ? tileSize : MINIMUM_TILE_SIZE;
        gridSize = (gridSize > MAXIMUM_GRID_SIZE) ? MAXIMUM_GRID_SIZE
                 : (gridSize < MINIMUM_GRID_SIZE) ? MINIMUM_GRID_SIZE
                 : gridSize;
        
        this.paintables = paintables;
        this.tileSize = tileSize;
        this.gridSize = gridSize;
        
        length = paintables.length;
        
        initializeGame();
    }
    
    
    /** The initializer. */
    protected void initializeGame() {
        makeTiles();
        makeGUI();
        newGame();
    }
    
    
    /**
     * Makes the tile boxes for the game
     * and related objects.
     */
    protected void makeTiles() {
        tileCount = gridSize * gridSize;
        
        gridSkip = -1;
        
        if ((gridSize % 2) != 0) {
            tileCount --;
            
            gridSkip = gridSize / 2;
        }
        
        tiles = new TileBox[tileCount];
        contents = new Paintable[tileCount];
        
        int spacing = tileSize + gap;
        
        XRect bounds = new XRect(0, 0, tileSize, tileSize);
        
        blank = new ShapePaintable();
        blank.setDefaultOriginalBounds2D(bounds);
        
        int index = 0;
        
        for (int row = 0; row < gridSize; row++) {
            int y = gap + row * spacing;
            
            for (int col = 0; col < gridSize; col++) {
                if ((row == gridSkip) && (col == gridSkip))
                    continue;
                
                int x = gap + col * spacing;
                
                TileBox tile = tiles[index] = new TileBox(blank);
                
                tile.setDefaultOriginalBounds2D(bounds);
                tile.moveCornerTo(x, y);
                tile.setBackgroundPaint(tileBackground);
                
                sequence.appendPaintable(tile);
                
                index++;
            }
        }
        
        int pixels = gap + gridSize * spacing;
        
        XRect sequenceBounds = new XRect(0, 0, pixels, pixels);
        
        sequence.setDefaultBounds2D(sequenceBounds);
    }
    
    
    /**
     * Make the game GUI and add decorations.
     */
    protected void makeGUI() {
        board.lineBorder(Colors.black, 2);
        board.setBackground(gameBackground);
        board.setOpaque(true);
        
        newGameButton.setBackground(buttonBackground);
        revealAllButton.setBackground(buttonBackground);
        
        Object[] buttonStuff =
            { newGameButton, revealAllButton };
        
        HTable buttonTable =
            new HTable(buttonStuff, 2 * gap, 2 * gap, CENTER);
        
        buttonTable.setBackground(panelBackground);
        
        Object[] mainStuff =
            { board, message, buttonTable };
        
        VTable mainTable =
            new VTable(mainStuff, gap, gap, CENTER);
        
        mainTable.emptyBorder(gap);
        mainTable.setBackground(panelBackground);
        
        addObject(mainTable);
        
        adapter.addMouseClickedAction(clickAction);
    }
    
    
    /** Launch a new Concentration game. */
    protected synchronized void newGame() {
        for (int i = 0; i < tileCount; i++) {
            tiles[i].setPaintable(blank);
        }
        
        guesses = 0;
        matches = 0;
        
        setMessage();
        
        int[] indexList = selectRandomizedIndexList();
        
        for (int i = 0; i < tileCount; i++) {
            int k = indexList[i];
            contents[i] = paintables[k];
        }
        
        index1 = -1;
        index2 = -1;
    }
    
    
    /**
     * <p>Return a list of paintable indices
     * that have been selected randomly,
     * then duplicated to produce index pairs,
     * and then randomized again.</p>
     * 
     * <p>The size of the array returned is
     * <code>tileCount</code>.</p>
     */
    protected int[] selectRandomizedIndexList() {
        int[] indexList = null;
        
        // since each paintable must appear twice in the game grid
        // randomly select tileCount/2 paintable indices
        int paintableCount = tileCount / 2;
        
        if (length >= paintableCount)
            indexList =
                ProbStatTools.selectWithNoRepetition(length, paintableCount);
        else
            indexList =
                ProbStatTools.selectWithRepetition(length, paintableCount);
        
        // duplicate each index
        indexList = ProbStatTools.repeatData(indexList, 2);
        
        // randomize the index list
        indexList = ProbStatTools.randomPermutation(indexList);
        
        return indexList;
    }
    
    
    /** The method to reveal the solution. */
    protected synchronized void revealAll() {
        for (int i = 0; i < tileCount; i++)
            tiles[i].setPaintable(contents[i]);
        
        index1 = -1;
        index2 = -1;
    }
    
    
    /** The click action method. */
    protected synchronized void click(MouseEvent mevt) {
        // check for an earlier invalid match
        if ((index1 >= 0) && (index2 >= 0)) {
            tiles[index1].setPaintable(blank);
            tiles[index2].setPaintable(blank);
            
            index1 = -1;
            index2 = -1;
        }
        
        int x = mevt.getX();
        int y = mevt.getY();
        
        int index = sequence.hitsItemAtIndex(x, y);
        
        if (index < 0)
            return;
        
        Tile tile = tiles[index];
        Paintable paintable = tile.getPaintable();
        
        if (paintable != blank)
            return;
        
        if (index1 < 0) {
            index1 = index;
            tile.setPaintable(contents[index]);
            
            return;
        }
        
        if (index2 < 0) {
            index2 = index;
            tile.setPaintable(contents[index]);
            
            guesses++;
            
            if (contents[index1] == contents[index2]) {
                matches++;
                
                // ensure that match is considered valid
                index1 = -1;
                index2 = -1;
            }
            
            setMessage();
        }
    }
    
    
    /** Set the message with guesses and matches. */
    protected void setMessage() {
        String text =
            "Guesses = " + guesses + "     Matches = " + matches;
        
        if (guesses > 0) {
            double ratio = (100.0 * matches) / guesses;
            
            text += "     Ratio = " + ((int) ratio) + "%"; 
        }
        
        message.setText(text);
    }
    
    
    /**
     * <p>This main method opens a new <code>ConcentrationOptions</code>
     * panel in a frame.  From that panel, one or more instances of a
     * <code>ConcentrationGame</code> may be opened.</p>
     * 
     * <p>If args are supplied, they are passed to the constructor
     * of the <code>ConcentrationOptions</code> panel.</p>
     */
    public static void main(String[] args) {
        new ConcentrationOptions(args).frame("Options", WEST);
    }
    
}

