// Copyright 1996, Marimba Inc. All Rights Reserved.
// @(#)TileWidget.java, 1.12, 10/18/96

package samegame;

import java.awt.*;
import java.net.*;
import java.util.*;
import java.awt.image.*;
import java.io.*;

import marimba.persist.*;
import marimba.gui.*;
import marimba.io.*;

/**
 * Widget to draw tiles
 *
 * @author	Carl W. Haynes III
 * @version     1.12, 10/18/96
 */
public class TileWidget extends Widget implements Runnable{

    public final int TILESIZE = 30;

    public final static int TILES_WIDE = 20;
    public final static int TILES_HIGH = 10;

    int tiles[][] = new int[TILES_WIDE][TILES_HIGH];
    int savedTiles[][] = new int[TILES_WIDE][TILES_HIGH];
    boolean mask[][] = new boolean[TILES_WIDE][TILES_HIGH];

    /** there can only ever be a maximum of (total blocks / 2) moves */
    UndoInfo undo[] = new UndoInfo[(TILES_WIDE * TILES_HIGH) / 2];
    int	undoLevels = 0;
    int	curUndoLevel = 0;

    /** offscreen images */
    Image osImage = null;
    Graphics osGraphics = null;
    Image osMask = null;
    Graphics osMaskGraphics = null;

    boolean flashing = false;
    boolean flashState = false;
    Thread flashThread = null;

    public String tileFile;

    /** high scores */
    HighScoreManager highScores = null;

    final static Color	colors[] = {	Color.blue,
	Color.red,
	Color.cyan,
	Color.green,
	Color.yellow,
	Color.black};

    //for testing & and easy win
    //	 final static Color	colors[] = {	Color.blue,
    //						    Color.green,
    //						    Color.yellow,
    //						    Color.black};

    final static int BLANK = colors.length - 1;
    
    static final int NUMCOLORS = 5;
    Image tileImages[] = new Image[NUMCOLORS+1];

    int	furthestLeft = 0;
    int	furthestRight = 0;
    int	furthestTop = 0;
    int	furthestBottom = 0;
    
    int	curTargetColor = BLANK;
    int	totalMatches = 0;
    
    int score = 0;
    int tilesLeft = 200;
    
    /**
     * Default constructor.
     */
    public TileWidget() {
	super();
    }
    
    /**
     * Get the properties of this widget.
     */
    public void getProperties(PropertyList list) {
    	super.getProperties(list);
    	list.setString("tileFile", tileFile, null);
    }

    /**
     * Set the properties of this widget.
     */
    public void setProperties(PropertyList list) {
    	super.setProperties(list);
    	tileFile = list.getString("tileFile", tileFile);
    }

    /**
     * init
     */
    public void init() {
	reshape(10, 10, TILES_WIDE*TILESIZE, TILES_HIGH*TILESIZE);
	osImage = getPlayerPanel().createImage(TILES_WIDE * TILESIZE, TILES_HIGH * TILESIZE);
	osGraphics = osImage.getGraphics();
	osGraphics.setColor(Color.black);
	osGraphics.fillRect(0, 0, TILES_WIDE * TILESIZE, TILES_HIGH * TILESIZE);
	
	osMask = getPlayerPanel().createImage(TILES_WIDE * TILESIZE, TILES_HIGH * TILESIZE);
	osMaskGraphics = osMask.getGraphics();
	osMaskGraphics.setColor(Color.black);
	osMaskGraphics.fillRect(0, 0, TILES_WIDE * TILESIZE, TILES_HIGH * TILESIZE);
    }


    public void start() {
//	if (tileFile==null) {
	    createDefaultTiles();
//	}
//	else
//	    createTiles();

    }


    public void createTiles() {
	
	Image img = getImage(tileFile);
	if (Toolkit.getDefaultToolkit().prepareImage(img, -1, -1, this)) {
	}

	if ((Toolkit.getDefaultToolkit().checkImage(img, -1, -1, this) & ImageObserver.ERROR) != 0) {
	    createDefaultTiles();
	    return;
	}

	while ((Toolkit.getDefaultToolkit().checkImage(img, -1, -1, this) & ImageObserver.ALLBITS) == 0) {
	}


	for (int i = 0 ; i < NUMCOLORS ; i++) {
	    tileImages[i] = getPlayerPanel().createImage(TILESIZE, TILESIZE);
	    Graphics g = tileImages[i].getGraphics();
	    g.setColor(Color.black);
	    g.fillRect(0, 0, TILESIZE, TILESIZE);
	    g.drawImage(img, -(i*TILESIZE), 0, null);
	    g.dispose();
	}

	tileImages[NUMCOLORS] = getPlayerPanel().createImage(TILESIZE, TILESIZE);
	Graphics g = tileImages[NUMCOLORS].getGraphics();
	g.setColor(Color.black);
	g.fillRect(0, 0, TILESIZE, TILESIZE);
	g.dispose();
    }

    public void createDefaultTiles() {
	
	for (int i = 0 ; i < NUMCOLORS ; i++) {
	    tileImages[i] = getPlayerPanel().createImage(TILESIZE, TILESIZE);
	    Graphics g = tileImages[i].getGraphics();
	    g.setColor(colors[i]);
	    g.fillRect(0, 0, TILESIZE, TILESIZE);
	    g.dispose();
	}

	tileImages[NUMCOLORS] = getPlayerPanel().createImage(TILESIZE, TILESIZE);
	Graphics g = tileImages[NUMCOLORS].getGraphics();
	g.setColor(Color.black);
	g.fillRect(0, 0, TILESIZE, TILESIZE);
	g.dispose();
    }

    /**
     * reset all
     */
    public void newGame() {
	if (flashing == true) {
	    flashing = false;
	    if (flashThread != null) {
		((SameGameWidget)getParent()).stopFlashing();
		flashThread.stop();
	    }
	}
	
	for (int x = 0 ; x < TILES_WIDE ; x++) {
	    for (int y = 0 ; y < TILES_HIGH ; y++) {
		tiles[x][y] = random(colors.length - 1);
		savedTiles[x][y] = tiles[x][y];
		mask[x][y] = false;
	    }
	}
	
	score = 0;
	tilesLeft = 200;
	
	undoLevels = 0;
	curUndoLevel = 0;
	undo[0] = new UndoInfo(tiles, score, tilesLeft);
	
	((SameGameWidget)getParent()).setUndoButtonState(false);
	((SameGameWidget)getParent()).setRedoButtonState(false);
	((SameGameWidget)getParent()).setScore(score);
	((SameGameWidget)getParent()).setTilesLeft(tilesLeft);
	
	updateImage(false);
	repaint();
    }
    
    /**
     * updates offscreen image
     */
    public void updateImage() {
	updateImage(true);
    }
    
    /**
     * updates offscreen image
     */
    public void updateImage(boolean checkSavedTiles) {
	int curColor = -1;
	for (int x = 0 ; x < TILES_WIDE ; x++) {
	    for (int y = 0 ; y < TILES_HIGH ; y++) {
		if ((checkSavedTiles==false) || ((checkSavedTiles==true) && (savedTiles[x][y] != tiles[x][y]))) {
		    if (tiles[x][y] != curColor) {
			osGraphics.setColor(colors[tiles[x][y]]);
			curColor = tiles[x][y];
		    }
		    
		    osGraphics.drawImage(tileImages[curColor], x * TILESIZE, y * TILESIZE, null);
//		    osGraphics.fillRect(x * TILESIZE, y * TILESIZE, TILESIZE, TILESIZE);
		}
		savedTiles[x][y] = tiles[x][y];
	    }
	}
	osGraphics.setColor(Color.black);
	for (int x = 1 ; x < TILES_WIDE ; x++)
	    osGraphics.drawLine(x * TILESIZE, 0, x * TILESIZE, TILES_HIGH * TILESIZE);
	for (int y = 1 ; y < TILES_HIGH ; y++)
	    osGraphics.drawLine(0, y * TILESIZE, TILES_WIDE * TILESIZE, y * TILESIZE);
	osGraphics.drawRect(0, 0, (TILES_WIDE * TILESIZE)-1, (TILES_HIGH * TILESIZE)-1);
    }
    
    /**
     * paint
     */
    public void paint(Graphics g) {
	
	if (flashing == true) {
	    if (flashState == true) {
		if (osMask != null)
		    g.drawImage(osMask, 0, 0, null);
	    }
	    else {
		if (osImage != null)
		    g.drawImage(osImage, 0, 0, null);
	    }
	}
	else {
	    if (osImage != null)
		g.drawImage(osImage, 0, 0, null);
	}
    }
    
    /**
     * returns random number between 0 and max-1
     */
    private int random(int max) {
	return (int)Math.floor(Math.random() * max);
    }
    
    
    
    /**
     * paint the mask into an offscreen buffer
     */
    public void drawMask() {
	osMaskGraphics.drawImage(osImage, 0, 0, null);
	osMaskGraphics.setColor(Color.black);
	for (int x = furthestLeft ; x <= furthestRight ; x++) {
	    for (int y =  furthestTop; y <= furthestBottom ; y++)
		if (mask[x][y] == true)
		    osMaskGraphics.fillRect(x * TILESIZE, y * TILESIZE, TILESIZE, TILESIZE);
	}
    }
    
    
    /**
     * returns if there are any matching blocks
     */
    boolean findMatchingBlocks(int x, int y) {
	boolean match = false;
	
	curTargetColor = tiles[x][y];
	
	tiles[x][y] = BLANK;
	
	// check left
	if (x != 0) {
	    if (tiles[x - 1][y] == curTargetColor) {
		if (findMatchingBlocks(x - 1, y)){}
		match = true;
		totalMatches++;
	    }
	}
	
	// check right
	if (x != TILES_WIDE - 1) {
	    if (tiles[x + 1][y] == curTargetColor){
		if (findMatchingBlocks(x + 1, y)){}
		match = true;
		totalMatches++;
	    }
	}
	
	// check top
	if (y != 0) {
	    if (tiles[x][y - 1] == curTargetColor) {
		if (findMatchingBlocks(x, y - 1)) {}
		match = true;
		totalMatches++;
	    }
	}
	
	// check bottom
	if (y != TILES_HIGH - 1) {
	    if (tiles[x][y + 1] == curTargetColor) {
		if (findMatchingBlocks(x, y + 1)){}
		match = true;
		totalMatches++;
	    }
	}
	
	if (x > furthestRight)
	    furthestRight = x;
	if (x < furthestLeft)
	    furthestLeft = x;
	if (y > furthestBottom)
	    furthestBottom = y;
	if (y < furthestTop)
	    furthestTop = y;
	
	tiles[x][y] = curTargetColor;
	mask[x][y] = true;
	if (match == true)
	    return(true);
	
	return(false);
    }
    
    /**
     * moves blocks to the right if necessary
     */
    boolean shiftBlocks() {
	boolean shifted;
	boolean shiftedleft = false;
	int i, j, k;
	
	// shift down
	do {
	    shifted = false;
	    for (j = 0 ; j < TILES_HIGH - 1 ; j++) {
		for ( i = 0 ; i < TILES_WIDE ; i++) {
		    if (tiles[i][j] != BLANK && tiles[i][j + 1] == BLANK) {
			tiles[i][j + 1] = tiles[i][j];
			tiles[i][j] = BLANK;
			shifted = true;
		    }
		}
	    }
	}while (shifted == true);
	
	// shift across
	
	boolean seenNonBlankRow =  false;
	int	lastBlankRow = -1;
	for ( i = TILES_WIDE - 1 ; i >= 0  ; i--) {
	    boolean nonblankfound = false;
	    
	    for (j = 0 ; j < TILES_HIGH ; j++) {
		if (tiles[i][j] != BLANK) {
		    nonblankfound = true;
		    if (seenNonBlankRow == false) {
			seenNonBlankRow = true;
			lastBlankRow = i+1;
		    }
		}
	    }
	    
	    if (nonblankfound == false && seenNonBlankRow == true) {
		for ( k = i ; k < TILES_WIDE - 1 ; k++)
		    for (j = 0 ; j < TILES_HIGH ; j++)
			tiles[k][j] = tiles[k+1][j];
		
		for (j = 0 ; j < TILES_HIGH ; j++) {
		    if (lastBlankRow == TILES_WIDE)
			tiles[TILES_WIDE - 1][j] = BLANK;
		    else
			tiles[lastBlankRow][j] = BLANK;
		}
		
		shiftedleft = true;
	    }
	}
	
	return(shiftedleft);
    }
    
    /**
     * handle an event
     */
    public boolean handleEvent(Event evt) {
	boolean handled = false;
	switch(evt.id) {
	  case Event.WINDOW_DESTROY:
	      break;
	    case Event.MOUSE_MOVE:
	      case Event.MOUSE_DRAG:
		case Event.MOUSE_ENTER:
		  case Event.MOUSE_EXIT:
		    case Event.MOUSE_UP:
			break;
	    case Event.MOUSE_DOWN:
		handleMouseDown(evt, evt.x, evt.y);
	      break;
	    case Event.ACTION_EVENT:
		break;
	    default:
		break;
	  }
	return((handled==false)?false:super.handleEvent(evt));
    }
    
    /**
     * user clicked
     */
    public boolean handleMouseDown(Event evt, int x, int y) {
	
	int tile_x = (x / TILESIZE);
	int tile_y = (y / TILESIZE);
	
	if (flashing == true) {
	    flashing = false;
	    if (flashThread != null) {
		((SameGameWidget)getParent()).stopFlashing();
		flashThread.stop();
	    }

	    int numCleared = 0;
	    if (mask[tile_x][tile_y] == true) {
		for (int k = 0 ; k < TILES_WIDE ; k++) {
		    for (int j = 0 ; j < TILES_HIGH ; j++) {
			if (mask[k][j] == true) {
			    numCleared++;
			    tiles[k][j] = BLANK;
			    mask[k][j] = false;
			}
		    }
		}
		
		numCleared -= 2;
		numCleared *= numCleared;
		score += numCleared;
		
		if (shiftBlocks() == true) {
		}
		
		updateImage();
		
		repaint();
		
		if (checkForWin() == true) {
		    score += 500;
		    
		    try {
			int place = -1;
			if ((place = highScores.place(score, HighScoreManager.LOCAL)) != -1) {
			    new HighScoreDialog(getFrame(),
						new URL(getPresentation().getBase(),"highscore.gui"),
						highScores,
						score,
						getPresentation()).show();
			}
			else {
			    new PlayerDialog(getFrame(),
					     new URL(getPresentation().getBase(),
						     "youwon.gui")).show();
			}

		    } catch (MalformedURLException e) {
			e.printStackTrace();
		    }
		}
		else if (checkForStuck() == true) {
		    score -= countTiles();
		    try {
			int place = -1;
			if ((place = highScores.place(score, HighScoreManager.LOCAL)) != -1) {
			    new HighScoreDialog(getFrame(),
						new URL(getPresentation().getBase(),"highscore.gui"),
						highScores,
						score,
						getPresentation()).show();
			}
			else {
//			    DebugStr("no high score" );
			    new PlayerDialog(getFrame(),
					     new URL(getPresentation().getBase(),
						     "nomoves.gui")).show();
			}

		    } catch (MalformedURLException e) {
			e.printStackTrace();
		    }
		}
		
		tilesLeft = countTiles();
		((SameGameWidget)getParent()).setTilesLeft(tilesLeft);
		((SameGameWidget)getParent()).setScore(score);
		
		setUndoInfo();
		
		return(false);
	    }
	    else {
		for (int k = 0 ; k < TILES_WIDE ; k++) {
		    for (int j = 0 ; j < TILES_HIGH ; j++) {
			if (mask[k][j] == true)
			    mask[k][j] = false;
		    }
		}
		repaint();
	    }
	}
	
	int colorIndex = tiles[tile_x][tile_y];
	if (colorIndex == BLANK) {
	    return(false);
	}
	
	// clear mask
	for (int i = 0 ; i < TILES_WIDE ; i++)
	    for (int j = 0 ;j < TILES_HIGH ; j++)
		mask[i][j] = false;
	
	if (findMatchingBlocks(tile_x, tile_y)) {
	    drawMask();
	    flashing = true;
	    flashThread = new Thread(this);
	    flashThread.start();
	    flashState = true;

	    int sel = 0;
	    for (int i = 0 ; i < TileWidget.TILES_WIDE ; i++) {
		for (int j = 0 ; j < TileWidget.TILES_HIGH ; j++) {
		    if (mask[i][j] == true)
			sel++;
		}
	    }

	    ((SameGameWidget)getParent()).startFlashing((sel-2)*(sel-2));
	}
	else {
	    for (int k = 0 ; k < TILES_WIDE ; k++) {
		for (int j = 0 ; j < TILES_HIGH ; j++) {
		    if (mask[k][j] == true)
			mask[k][j] = false;
		}
	    }
	}
	
	
	
	return(true);
    }
    
    
    /**
     * returns total number of tiles on board
     */
    private int countTiles() {
	int returnValue = 0;
	for (int i = 0 ; i < TILES_WIDE ; i++)
	    for (int j = 0 ; j < TILES_HIGH ; j++)
		if (tiles[i][j] != BLANK)
		    returnValue++;
	return(returnValue);
    }
    
    
    /**
     * did you win, yeah!
     */
    private boolean checkForWin() {
	for (int i = 0 ; i < TILES_WIDE ; i++) {
	    for (int j = 0 ; j < TILES_HIGH ; j++)
		if (tiles[i][j] != BLANK)
		    return(false);
	}
	return(true);
    }
    
    
    /**
     * any moves left?
     */
    private boolean checkForStuck() {
	for (int i = 0 ; i < TILES_WIDE ; i++) {
	    for (int j = 0 ; j < TILES_HIGH ; j++)
		if (tiles[i][j] != BLANK)
		    if (findMatchingBlocks(i, j))
			return(false);
	}
	return(true);
    }
    
    /**
     * as an undo level
     */
    private void setUndoInfo() {
	undo[++curUndoLevel] = new UndoInfo(tiles, score, tilesLeft);
	undoLevels = curUndoLevel;
	
	((SameGameWidget)getParent()).setUndoButtonState(true);
	((SameGameWidget)getParent()).setRedoButtonState(false);
    }
    
    /**
     * user clicked on undo button
     */
    public void handleUndo() {
	if (curUndoLevel <= 0)
	    return;
	
	if (flashing == true) {
	    flashing = false;
	    if (flashThread != null) {
		((SameGameWidget)getParent()).stopFlashing();
		flashThread.stop();
	    }	
	}
	
	curUndoLevel--;
	
	for (int i = 0 ; i < TileWidget.TILES_WIDE ; i++)
	    for (int j = 0 ; j < TileWidget.TILES_HIGH ; j++)
		tiles[i][j] = undo[curUndoLevel].tiles[i][j];
	
	score = undo[curUndoLevel].score;
	tilesLeft = undo[curUndoLevel].tilesLeft;
	
	updateImage();
	repaint();
	((SameGameWidget)getParent()).setTilesLeft(tilesLeft);
	((SameGameWidget)getParent()).setScore(score);
	
	((SameGameWidget)getParent()).setUndoButtonState((curUndoLevel>0)?true:false);
	((SameGameWidget)getParent()).setRedoButtonState((curUndoLevel<undoLevels)?true:false);
    }
    
    /**
     * user clicked on redo button
     */
    public void handleRedo() {
	if (curUndoLevel >= undoLevels)
	    return;
	
	if (flashing == true) {
	    flashing = false;
	    if (flashThread != null) {
		((SameGameWidget)getParent()).stopFlashing();
		flashThread.stop();
	    }
	}
	
	curUndoLevel++;
	
	for (int i = 0 ; i < TileWidget.TILES_WIDE ; i++)
	    for (int j = 0 ; j < TileWidget.TILES_HIGH ; j++)
		tiles[i][j] = undo[curUndoLevel].tiles[i][j];
	
	score = undo[curUndoLevel].score;
	tilesLeft = undo[curUndoLevel].tilesLeft;
	
	updateImage();
	repaint();
	((SameGameWidget)getParent()).setTilesLeft(tilesLeft);
	((SameGameWidget)getParent()).setScore(score);
	
	((SameGameWidget)getParent()).setUndoButtonState((curUndoLevel>0)?true:false);
	((SameGameWidget)getParent()).setRedoButtonState((curUndoLevel<undoLevels)?true:false);
    }
    
    /**
     * size we'd like to be
     */
    public Dimension preferredSize() {
	return(new Dimension(TILES_WIDE * TILESIZE, TILES_HIGH * TILESIZE));
    }
    
    
    /**
     * thread to blink blocks
     */
    public void run() {
	boolean running = true;
	while (running) {
	    repaint();
	    try {
		Thread.sleep(650);
	    }
	    catch (InterruptedException e) {
		running = false;
	    }
	    flashState = (flashState==true)?false:true;
	}
	
	
	((SameGameWidget)getParent()).stopFlashing();
	flashing = false;
	flashThread = null;
    }
    
    
    /**
      * set the high score manager
      */
    public void setHighScoreManager(HighScoreManager hsm) {
	this.highScores = hsm;
    }


    public void writeState(FastOutputStream fos) {
//	try {
	    //	    raf.seek(0);
	    fos.writeInt(score);
	    fos.writeInt(tilesLeft);
	    fos.writeInt(undoLevels);
	    fos.writeInt(curUndoLevel);

	    for (int i = 0 ; i < TileWidget.TILES_WIDE ; i++)
		for (int j = 0 ; j < TileWidget.TILES_HIGH ; j++)
		    fos.writeInt(tiles[i][j]);

	    for (int i = 0 ; i <= undoLevels ; i++) {
		UndoInfo undoInfo = undo[i]; 
		fos.writeInt(undoInfo.tilesLeft);
		fos.writeInt(undoInfo.score);
		for (int ii = 0 ; ii < TileWidget.TILES_WIDE ; ii++)
		    for (int jj = 0 ; jj < TileWidget.TILES_HIGH ; jj++)
			fos.writeInt(undoInfo.tiles[ii][jj]);
	    }

//	}
//	catch(IOException e) {
//	    e.printStackTrace();
//	}
    }


    public void readState(FastInputStream fis) {
	int score = 0;
	int tilesLeft = 0;
	int undoLevels;
	int curUndoLevel;
	int tiles[][] = new int[TILES_WIDE][TILES_HIGH];
	//try {
	    //raf.seek(0);
	    score = fis.readInt();
	    tilesLeft = fis.readInt();
	    undoLevels = fis.readInt();
	    curUndoLevel = fis.readInt();

	    for (int i = 0 ; i < TileWidget.TILES_WIDE ; i++)
		for (int j = 0 ; j < TileWidget.TILES_HIGH ; j++)
		    tiles[i][j] = fis.readInt();

	    for (int i = 0 ; i <= undoLevels  ; i++) {
		int tl = fis.readInt();
		int sc = fis.readInt();
		int t[][] = new int[TILES_WIDE][TILES_HIGH];
		for (int ii = 0 ; ii < TileWidget.TILES_WIDE ; ii++)
		    for (int jj = 0 ; jj < TileWidget.TILES_HIGH ; jj++)
			t[ii][jj] = fis.readInt();
		undo[i] = new UndoInfo(t, sc, tl);
	    }
//	}
//	catch(IOException e) {
//	    e.printStackTrace();
//	    newGame();
//	    return;
//	}
	
	this.score = score;
	this.tilesLeft = tilesLeft;
	this.undoLevels = undoLevels;
	this.curUndoLevel = curUndoLevel;

	((SameGameWidget)getParent()).setScore(score);
	((SameGameWidget)getParent()).setTilesLeft(tilesLeft);
	((SameGameWidget)getParent()).setUndoButtonState((curUndoLevel>0)?true:false);
	((SameGameWidget)getParent()).setRedoButtonState((curUndoLevel<undoLevels)?true:false);

	for (int i = 0 ; i < TileWidget.TILES_WIDE ; i++)
	    for (int j = 0 ; j < TileWidget.TILES_HIGH ; j++)
		this.tiles[i][j] = tiles[i][j];

	updateImage(false);
	repaint();
    }


    /**
     * debugging
     */
    public void DebugStr(String s) {
	System.out.println("[" + (new Date()).toString() + "] " + "TileWidget: " + s);
	System.out.flush();
    }
    
    /**
     * loops around until it gets to a frame
     */
    private Frame getFrame() {
	Component p = (Component)getPlayerPanel();
	while (!(p instanceof Frame))
	    p = p.getParent();
	return((Frame)p);
    }
    
}

