/** Game class */

import java.awt.*;
import java.applet.Applet;

import java.util.Random;
import java.util.Date;
import java.util.Hashtable;
import java.util.Enumeration;



public class Kabale extends Applet implements Runnable {
    // Game data
    Image backImage;		// back side of cards
    Image[] cardImages;		// front side of cards

    Card[] cards;		// 52 cards - they are all found here

    // Configuation stuff
    final int autoPilotDelay = 333;
    final int autoPilotFastDelay = -1;
    final int moveStackDelay = 50;

    // The actual game works mostly like an animation
    Thread gameEngine = null;



    // METHODS USED BY THE APPLET THREAD

    // GUI data
    Button newButton;
    Button sameButton;
    Button finishButton;
    Button helpButton;
    Button dumpButton;
    Button quitButton;

    Label statusLabel;

    int numberOfImages = 0;
    MediaTracker mt = null;
    String[] image_names = null;


    String imageFileName(String s) {
	return "gfx/" + s + ".gif";
    }

    Image getImage(String s) {
	Image img = getImage(getDocumentBase(), imageFileName(s));
	image_names[numberOfImages] = s;
	mt.addImage(img, numberOfImages++);
	return img;
    }

    Image getCardImage(int i) {
	StringBuffer s = new StringBuffer("card");
	s.append((char)('0'+(i/10)));
	s.append((char)('0'+(i%10)));
	return getImage(new String(s));
    }

    public void init() {
	int i;

	cards = null;
	hands = -1;
	randomSeed = 0;
	helpMode = false;

	numberOfImages = 0;

	// Load all images

	mt = new MediaTracker(this);
	image_names = new String[52+32];

	backImage = getImage("back");

	cardImages = new Image[52];
	for (byte s = 0; s <= 3; s++) {
	    String name = Card.suitNames[s];

	    for (byte v = Card.ACE; v <= Card.KING; v++) {
		i = s*13+v;
		cardImages[i] = getCardImage(i);
	    }
	}

	statusLabel = new Label("The Lazy Peoples Solitaire Game");
	statusLabel.setAlignment(Label.CENTER);

	status = new GameStatusIndicator(statusLabel);
	status.addImage("help-yes", getImage("help-yes"),
			"There are still possible moves");
	status.addImage("help-no", getImage("help-no"),
			"There are no possible moves");
	status.addImage("no-move", getImage("no-move"),
			"There is no place to move that card");
	status.addImage("ambiguous", getImage("ambiguous"),
			"Ambigous move - please click on the destination.");
	status.addImage("game-won", getImage("game-won"),
			"Congratulations - you won");
	status.addImage("game-lost", getImage("game-lost"),
			"You've lost - again");
	/*
	status.addImage("game-auto", getImage("game-auto"),
			"Auto-pilot");
	*/

	// Create GUI components

	Font f = new Font("TimesRoman", Font.PLAIN, 14);

	this.removeAll();
	this.requestFocus();

	this.setFont(f);
	this.setBackground(Color.green);
	this.setForeground(Color.black);

	Panel gameBoard = new Panel();

	gameBoard.setLayout(new GridLayout(1,9,0,5));

	// Buttons to the top left
	newButton = new Button("New");
	sameButton = new Button("Same");
	finishButton = new Button("Finish");
	helpButton = new Button("Help");
	dumpButton = new Button("Dump");
	quitButton = new Button("Quit");

	Panel panelLeftTop = new Panel();
	panelLeftTop.setLayout(new StackLayout(0));
	panelLeftTop.add(newButton);
	panelLeftTop.add(sameButton);
	panelLeftTop.add(finishButton);
	panelLeftTop.add(helpButton);
	panelLeftTop.add(dumpButton);
	// XXX panelLeftTop.add(quitButton);

	// Cards to the button left
	hand = new HandCards("hand", backImage);
	flipped = new FlippedCards("flip", backImage);

	Panel panelLeftButtom = new Panel();
	panelLeftButtom.setLayout(new StackLayout(0));
	panelLeftButtom.add(status);
	panelLeftButtom.add(hand);
	panelLeftButtom.add(flipped);

	// The left button/card column
	Panel panelLeft = new Panel();
	panelLeft.setLayout(new BorderLayout());
	panelLeft.add("North", panelLeftTop);
	panelLeft.add("South", panelLeftButtom);

	gameBoard.add(panelLeft);

	// Seven stack columns
	stacks = new StackColumn[7];
	for (i = 0; i < stacks.length; i++) {
	    stacks[i] = new StackColumn("stk"+i, backImage);
	    gameBoard.add(stacks[i]);
	    stacks[i].requestFocus();
	}

	// Aces column
	aces = new AceColumn("aces", backImage);
	gameBoard.add(aces);

	this.setLayout(new BorderLayout());
	this.add("Center", gameBoard);
	this.add("North", statusLabel);


	// Assure alle images are properly loaded before proceeding

	// showStatus("Loading images ...");
	try {
	    boolean done = false;
	    while (!done) {
		if (!(done = mt.checkAll(true))) {
		    int count = 0;
		    int error = 0;
		    for (i = numberOfImages; --i >= 0; ) {
			if (mt.checkID(i,true)) {
			    count++;
			}
			if (mt.isErrorID(i)) {
			    error++;
			}
		    }
		    showStatus("Loading images ... " 
			       + (count*100/numberOfImages)
			       + "%"
			       + (error == 0 ? "" : ", errors " + error)
			);
		    mt.waitForAll(1000);
		}
	    }
	} catch (InterruptedException e) {
	    showStatus(e.getMessage());
	}

	showStatus("Loading images ... done");

	for (i = numberOfImages; --i >= 0; ) {
	    if (mt.isErrorID(i)) {
		showStatus("Error loading image ... "+image_names[i]);
	    }
	}

	mt = null;
	image_names = null;
    }

    public void start() {
	if (gameEngine == null)
	    gameEngine = new Thread(this, "Kabale");

	if (!gameEngine.isAlive())
	    gameEngine.start();

	this.requestFocus();
    }

    public void destroy() { 
	// Free images for GC
	for (int i = 0; i < cardImages.length; i++)
	    cardImages[i].flush();
	backImage.flush();

	gameEngine.stop();
	gameEngine = null;
	stop();
    }

    // Event handling

    public boolean mouseDown(Event e, int x, int y) {
	if ((e.modifiers & Event.META_MASK) != 0) // right button
	    sendCommand(' ');
	else if (e.target == hand)
	    sendCommand(' ');
	else if (e.target == flipped)
	    sendCommand('0');
	else if (e.target == aces)
	    sendCommand('a');
	else {
	    int i = stacks.length;
	    while (--i >= 0 && e.target != stacks[i])
		;
	    if (i >= 0)
		sendCommand((char)('1'+i));
	}
	return true;
    }

    public boolean keyDown(Event e, int key) {
	sendCommand(key);
	return true;
    }

    public boolean action(Event e, Object arg) {
	if (e.target == newButton) {
	    sendCommand('n');
	} else if (e.target == sameButton) {
	    sendCommand('s');
	} else if (e.target == finishButton) {
	    sendCommand('f');
	} else if (e.target == helpButton) {
	    sendCommand('h');
	} else if (e.target == dumpButton) {
	    sendCommand('D');
	} else if (e.target == quitButton) {
	    sendCommand('q');
	}
	return true;
    }




    // INTER-THREAD COMMUNICATION

    int gameCommand = 0;

    synchronized void sendCommand(int key) {
	if (gameCommand != 0) {	// wait for game engine
	    // showStatus("sender waiting");
	    try { this.wait(); }
	    catch (InterruptedException e) {}
	    // showStatus("sender awoken");
	}

	gameCommand = key;
	this.notify();	// wakeup the game engine
    }

    synchronized int recvCommand() {
	int command = 0;

	if (gameCommand == 0) {	// wait for applet thread
	    // showStatus("reader waiting");
	    try { this.wait(); }
	    catch (InterruptedException e) {}
	    // showStatus("reader awoken");
	}

	command = gameCommand;
	gameCommand = 0;

	this.notify();		// wakeup the applet thread
	return command;
    }






    // GAME FUNCTIONALITY - USED BY THE GAME ENGINE

    int hands;			// number of times hand can be flipped
				// hands = 0 means auto-finish mode
				// hands < 0 means game over
    boolean helpMode = false;

    // Game and GUI data
    HandCards hand;		// cards on hand, showing back
    FlippedCards flipped;	// cards flipped, top card showing
    StackColumn[] stacks;	// 7 columns of cards, some showing
    AceColumn aces;		// 1 column with aces, top card showing

    CardStack savedSrc;		// source for ambiguous moves
    long randomSeed;		// seed for last random generator

    GameStatusIndicator status;

    int autoDelay = 0;		// delay when auto-finishing game

    public void run() {
	gameEngine.setPriority(Thread.MIN_PRIORITY);

	newGame(0);

	int key;
	while ((key = recvCommand()) != 'q')
	    handleCommand(key);
    }

    void handleCommand(int key) {
	CardStack src = null;
	CardStack dest = null;

	switch (key) {
	case 'n':
	    newGame(0);
	    break;
	case 's':
	    newGame(randomSeed);
	    break;
	case 'f':
	    finishGame();
	    break;
	case 'F':
	    finishGame();
	    finishGame();
	    break;
	case 'h':
	    toggleHelpMode();
	    break;
	case 'D':
	    dumpDebugOutput();
	    break;
	case ' ':
	case '\n':
	    src = hand;
	    break;
	case '0':
	    src = flipped;
	    break;
	case 'a':
	case '8':
	    src = aces;
	    break;
	case '1': case '2': case '3': case '4': 
	case '5': case '6': case '7':
	    src = stacks[key-'1'];
	    if (src.peek() == null)
		src = null;
	    break;
	default:
	    return;
	}

	// automated finish is running
	if (autoDelay != 0)	
	    return;

	// no game
	if (hands <= 0 || src == null)
	    return;

	if (savedSrc != null) {
	    setStatusIndicator(null);
	    dest = src;
	    src = savedSrc;
	    savedSrc = null;

	    if (dest == flipped || dest == hand)
		return;

	    if ((dest == aces && canMoveToAce(src.peek()))
		|| (canMoveToStack(src.firstVisible(), dest.peek()))) {
		// showStatus("Handling saved move.");
		moveCards(src, dest);
	    }
	} else {
	    MoveInfo info = findUniqueMove(src);
	    if (info.count == 0) {
		setStatusIndicator("no-move");
	    } else if (info.count > 1) {
		setStatusIndicator("ambiguous");
		savedSrc = src;
	    } else {
		// showStatus("Automatic move "+info);
		moveCards(src, info.dest);
		if (hands > 0)
		    setStatusIndicator(null);
	    }
	}

	setHelpIndicator();
	checkGameOver();
    }

    void pause(int msecs) {
	try { Thread.sleep(msecs); } 
	catch (InterruptedException e) {}
    }

    void newGame(long seed) {
	int i, j, c;		// temporaries

	cards = new Card[52];
	for (i = 0; i < 4; i++) {
	    for (j = Card.ACE; j <= Card.KING; j++) {
		c = i*13+j;
		cards[c] = new Card((byte)i, (byte)j, cardImages[c]);
	    }
	}

	if (seed == 0) {
	    Date d = new Date();
	    randomSeed = d.getTime();
	}
	Random rand = new Random(randomSeed);

	for (i = cards.length; --i > 0; ) {
	    j = Math.abs(rand.nextInt()) % (i+1);
	    Card tmp = cards[i];
	    cards[i] = cards[j];
	    cards[j] = tmp;
	}

	hand.clear();
	flipped.clear();
	aces.clear();
	for (i = stacks.length; --i >= 0; )
	    stacks[i].clear();

	c = 0;
	for (i = 0; i < stacks.length; i++) {
	    for (j = 0; j <= i; j++)
		stacks[i].push(cards[c++]);
	    stacks[i].setHidden(i);
	}

	while (c < cards.length)
	    hand.push(cards[c++]);

	flipCards();

	hand.setTurn(hands = 3);

	hand.repaint();
	flipped.repaint();
	aces.repaint();
	for (i = 0; i <  stacks.length; i++)
	    stacks[i].repaint();

	setStatusIndicator(null);
	setHelpIndicator();

	autoDelay = 0;
    }

    void finishGame() {
	if (autoDelay == 0 && hands >= 0) {
	    showStatus("Finishing game on auto pilot");

	    autoDelay = autoPilotDelay;

	    while (hands >= 0) {
		MoveInfo move = findMove();
		// showStatus("Finish move " + move);
		moveCards(move.src, move.dest);
		setHelpIndicator();
		checkGameOver();

		// Continue receiving commands
		if (gameCommand != 0) {
		    handleCommand(recvCommand());
		    if (autoDelay == 0)
			break;
		}

		Thread.yield();
		if (autoDelay > 0) {
		    pause(autoDelay);
		}
	    }
	} else
	    autoDelay = autoPilotFastDelay;
    }

    void checkGameOver() {
	if (aces.count() == 52)
	    gameOver();
    }

    void gameOver() {
	hand.setTurn(hands = -1);
	setStatusIndicator((aces.count() == 52) ? "game-won" : "game-lost");
	autoDelay = 0;
    }

    void toggleHelpMode() {
	helpMode = !helpMode;
	showStatus("Help mode " + (helpMode ? "enabled" : "disabled"));
	setHelpIndicator();
    }


    // SUPPORT METHODS

    boolean canMoveToStack(Card src, Card dest) {
	return ((src != null) 
		&& (((dest == null) 
		     && (src.value == Card.KING))
		    || ((dest != null) 
			&& (!src.sameColor(dest)) 
			&& (src.value == dest.value-1))));
    }

    boolean canMoveToAce(Card src) {
	if (src == null) 
	    return false;
	if (aces.top(src.suit) == null)
	    return src.value == Card.ACE;
	return (src.value == aces.top(src.suit).value+1);
    }

    MoveInfo findMove() {
	CardStack src = null;
	CardStack dest = null;
	boolean ok = false;
	int sn;

	// check FLIPPED to ACE
	if (canMoveToAce(flipped.peek()))
	    return new MoveInfo(flipped, aces, 1);

	// check STACK to ACE
	for (sn = 0; sn < stacks.length; sn++) {
	    if (canMoveToAce(stacks[sn].peek()))
		return new MoveInfo(stacks[sn], aces, 1);
	}

	// check STACK to STACK
	for (sn = 0; sn < stacks.length; sn++) {
	    if (stacks[sn].count() == 0)
		continue;
	    if (stacks[sn].firstVisible().value == Card.KING
		&& stacks[sn].getHidden() == 0)
		continue;

	    for (int sn2 = 0; sn2 < stacks.length; sn2++ ) {
		if (sn != sn2
		    && canMoveToStack(stacks[sn].firstVisible(),
				      stacks[sn2].peek())) {
		    return new MoveInfo(stacks[sn], stacks[sn2], 1);
		}
	    }
	}

	/* check FLIPPED to STACK */
	for (sn = 0; sn < stacks.length; sn++) {
	    if (canMoveToStack(flipped.peek(), stacks[sn].peek())) {
		return new MoveInfo(flipped, stacks[sn], 1);
	    }
	}

	// Flip new cards from hand
	return new MoveInfo(hand, flipped, 1);
    }

    /** findUniqueMove
     */
    MoveInfo findUniqueMove(CardStack src) {
	MoveInfo info = findBestMove(src, true);
	return info;
    }

    /** findBestMove
     */
    MoveInfo findBestMove(CardStack src, boolean unique) {
	int movecnt = 0;
	CardStack dest = null;
	Card c = null;

	if (src == aces)	// impossible moves
	    dest = null;
	else if (src == hand) {	// flip cards src hand
	    dest = flipped;
	    movecnt++;
	    
	} else if (src == flipped) { // move from FLIPPED
	    c = src.peek();

	    if (c != null) {
		// check FLIPPED to STACK
		if (c.value != Card.ACE) {
		    for (int s = 0; s < stacks.length; s++) {
			if (canMoveToStack(c, stacks[s].peek())) {
			    movecnt++;
			    dest = stacks[s];
			}
		    }
		}
	    }
	} else {		// src is one of stacks[?]
	    c = src.firstVisible();

	    if (c != null) {
		// check STACK to STACK 
		if ((c.value != Card.ACE)
		    && !(c.value == Card.KING
			 && ((StackColumn)src).getHidden() == 0)) {
		    for (int s = 0; s < stacks.length; s++) {
			if ((src != stacks[s])
			    && canMoveToStack(c, stacks[s].peek())) {
			    dest = stacks[s];
			    movecnt++;
			}
		    }
		}
	    }
	}

	// Common check for src = flipped || src = stack[?]
	if (src != hand && src != aces) {
	    // special check for kings
	    if (unique && (c.value == Card.KING) && (movecnt > 1)) 
		movecnt = 1;

	    // check src to ACE
	    if (src.peek() != null && canMoveToAce(src.peek())) {
		// special check for twos
		if (unique && src.peek().value == Card.TWO)
		    movecnt = 0;
		movecnt++;
		dest = aces;
	    }
	}

	return new MoveInfo(src, dest, movecnt);
    }

    void flipCards() {
	int i = 3;
	while (--i >= 0 && hand.peek() != null) {
	    flipped.push(hand.pop());
	}

	if (hand.peek() == null) {
	    String ord = "???";
	    switch (3-hands) {
	    case 0:
		ord = "first";
		break;
	    case 1:
		ord = "second";
		break;
	    default:
		ord = "last";
		break;
	    }
	    showStatus("Finished your " + ord + " hand");
	}
	
	hand.repaint();
	flipped.repaint();
    }

    void moveCards(CardStack src, CardStack dest) {
	if (dest == null) {
	    showStatus("No unique move");
	} else if (src == hand && dest == flipped) {
	    if (src.peek() == null) {
		hand.setTurn(--hands);
		if (hands > 0) {
		    while (flipped.peek() != null)
			hand.push(flipped.pop());
		    flipCards();
		} else if (hands == 0)  {
		    finishGame();
		} else
		    gameOver();
	    } else {
		flipCards();
	    }
	} else if (dest == aces) { // dest == aces
	    Card tmp = src.pop();
	    src.repaint();

	    dest.push(tmp);
	    dest.repaint();
	} else {		// dest == stacks[?]
	    CardStack stk = new CardStack("tmp", 12, null);
	    Card last = src.firstVisible();
	    Card tmp;

	    do {
		stk.push(tmp = src.pop());
		src.repaint();
		pause(moveStackDelay);
	    } while (tmp != last);

	    while((tmp = stk.pop()) != null) {
		dest.push(tmp);
		dest.repaint();
		pause(moveStackDelay);
	    }
	}
    }


    // VARIOUS SUPPORT METHODS FOR GAME ENGINE

    public void showStatus(String msg) {
	super.showStatus("Solitaire: " + msg);
	System.out.println(msg);
    }

    void setStatusIndicator(String s) {
	if (s == null)
	    status.clearImage();
	else {
	    status.setImage(s);
	    super.showStatus(status.getMessage());
	}

	status.repaint();
//	statusLabel.repaint();
    }

    void setHelpIndicator() {
	if (status.getName() == null
	    || status.getName().startsWith("help-")) {
	    if (helpMode) {
		MoveInfo info = findMove();
		// showStatus("Help can move "+info);
		setStatusIndicator(info.src == hand ? "help-no" : "help-yes");
	    } else
		setStatusIndicator(null);
	}
    }

    void dumpDebugOutput() {
	System.out.println("------ hand=" + hands
			   + ", game is " + 
			   (hands > 0 ? "manual" :
			    (hands == 0 ? "auto" : "over"))
	    );
	System.out.println(hand);
	System.out.println(flipped);
			   
	System.out.println(stacks[0]);
	System.out.println(stacks[1]);
	System.out.println(stacks[2]);
	System.out.println(stacks[3]);
	System.out.println(stacks[4]);
	System.out.println(stacks[5]);
	System.out.println(stacks[6]);
			   
	System.out.println(aces);
    }




    public String getAppletInfo() {
	return "Kabale 0.1 Copyright (C) 1998 Rene' Seindal";
    }

    public String[][] getParameterInfo() { return info; }
    private String[][] info = {
	{ "back" , "image URL", "Card back image" }
    };
}

class MoveInfo extends Object {
    public CardStack src;	// move from stack
    public CardStack dest;	// move to stack
    public int count;		// complete number of possible moves

    public MoveInfo(CardStack src, CardStack dest, int count) {
	this.src = src;
	this.dest = dest;
	this.count = count;
    }

    public String toString() {
	StringBuffer s = new StringBuffer(this.getClass().getName());
	s.append("[");

	s.append("src="+src.getName()+", ");
	s.append("dest="+dest.getName()+", ");
	s.append("count="+count);
	s.append("]");
	return new String(s);

    }
}



/**
 * GameStatusIndicator
 */

class GameStatusIndicator extends Canvas {
    Hashtable images = null;
    Hashtable messages = null;

    String key = null;
    Image current = null;
    Label message = null;
    String default_message = null;

    public GameStatusIndicator(Label message) {
	images = new Hashtable();
	messages = new Hashtable();

	key = null;
	current = null;
	this.message = message;
	default_message = message.getText();
    }

    public void paint(Graphics g) {
	if (current != null) {
	    g.drawImage(current, 0, 0, this);
	}
    }

    public void addImage(String key, Image img, String message) {
	images.put(key, img);
	messages.put(key, message);
    }

    public void clearImage() {
	key = null;
	current = null;
	message.setText(default_message);
    }

    public void setImage(String key) {
	this.key = key;
	current = (key == null) ? null : (Image)images.get(key);
	message.setText((String)messages.get(key));
    }

    public String getName() {
	return key;
    }

    public String getMessage() {
	return (key == null) ? null : (String)messages.get(key);
    }

    Dimension wantedSize() {
	Dimension maxSize = new Dimension(0, 0);

	for (Enumeration e = images.keys() ; e.hasMoreElements() ; ) {
	    String key = (String)e.nextElement();
	    Image img = (Image)images.get(key);

	    if (img.getWidth(null) > maxSize.width)
		maxSize.width = img.getWidth(null);
	    if (img.getHeight(null) > maxSize.height)
		maxSize.height = img.getHeight(null);	      
	}

	return maxSize;
    }

    public Dimension getPreferredSize() { return wantedSize(); }
    public Dimension getMinimumSize() { return wantedSize(); }
//  public Dimension getMaximumSize() { return wantedSize(); }

    public Dimension preferredSize() { return wantedSize(); }
    public Dimension minimumSize() { return wantedSize(); }
//  public Dimension maximumSize() { return wantedSize(); }
}


/** CardStack - a stack of cards
 *
 */
class CardStack extends Canvas {
    protected String name;
    protected Card[] cards;
    protected Image back;
    protected byte count;

    public CardStack(String name, int capacity, Image back) {
	this.name = name;
	this.cards = new Card[capacity];
	this.count = 0;
	this.back = back;
    }
    public CardStack(String name, Image back) { this(name, 52, back); }

    public void push(Card c) { 
	if (c != null) 
	    cards[count++] = c; 
    }

    public Card pop() {
	try { 
	    return cards[--count]; 
	}
	catch (ArrayIndexOutOfBoundsException e) {
	    count = 0; 
	    return null;
	}
    }

    public Card peek() {
	try { return cards[count-1]; }
	catch (ArrayIndexOutOfBoundsException e) { return null; }
    }

    public Card firstVisible() {
	return peek();
    }

    public Card cardAt(int i) {
	try { 
	    return cards[i]; 
	}
	catch (ArrayIndexOutOfBoundsException e) {
	    return null;
	}
    }

    public void clear() { count = 0; }
    public int count() { return (int)this.count; }

    private Image offScreenImage;
    private Dimension offScreenSize;
    private Graphics offScreenGraphics;

    public final synchronized void update(Graphics g) {
	Dimension d = size();
	if((offScreenImage == null) 
	   || (d.width != offScreenSize.width)
	   ||  (d.height != offScreenSize.height)) {
	    offScreenImage = createImage(d.width, d.height);
	    offScreenSize = d;
	    offScreenGraphics = offScreenImage.getGraphics();
	}
	offScreenGraphics.clearRect(0, 0, d.width, d.height);
	paint(offScreenGraphics);
	g.drawImage(offScreenImage, 0, 0, null);
    }

    Dimension wantedSize() {
	return new Dimension(0,0);
    }

    public Dimension getPreferredSize() { return wantedSize(); }
    public Dimension getMinimumSize() { return wantedSize(); }
//  public Dimension getMaximumSize() { return wantedSize(); }

    public Dimension preferredSize() { return wantedSize(); }
    public Dimension minimumSize() { return wantedSize(); }
//  public Dimension maximumSize() { return wantedSize(); }

    public String getName() {
	return name;
    }

    public String toString() {
	StringBuffer s = new StringBuffer(this.getClass().getName());
	s.append("/"+name+"[");

	s.append("count="+count+", ");
	for (int i = 0; i < count; i++)
	    s.append(cards[i].toString()+" ");
	s.append("]");
	return new String(s);
    }
}


/** Class FlippedCards - cards flipped on table, top showing
 */
class FlippedCards extends CardStack {

    public FlippedCards(String name, Image back) {
	super(name, 52-(1+2+3+4+5+6+7), back);
    }

    public void paint(Graphics g) {
	Dimension sz = size();

	Card c = peek();
	if (c != null) {
	    g.drawImage(c.getImage(), 0, 0, this);
	} else {
	    g.clearRect(0, 0, sz.width, sz.height);
	}
    }

    Dimension wantedSize() {
	return new Dimension(back.getWidth(null), back.getHeight(null));
    }
}


/** Class HandCards - cards held in hand, back showing
 */
class HandCards extends CardStack {
    int turn;

    public HandCards(String name, Image back) {
	super(name, 52-(1+2+3+4+5+6+7), back);
    }

    public void setTurn(int turn) {
	this.turn = (turn > 0) ? 4-turn : 0;
    }

    public void paint(Graphics g) {
	Dimension sz = size();

	Card c = peek();
	if (c != null) {
	    g.drawImage(back, 0, 0, this);
	}

	FontMetrics fm = g.getFontMetrics();
	int w = fm.stringWidth(""+turn);
	g.drawString(""+turn, (sz.width-w)/2, (sz.height-fm.getHeight())/2);
    }

    Dimension wantedSize() {
	return new Dimension(back.getWidth(null), back.getHeight(null));
    }
}


/** Class StackColumn - interactive deck 
 */
class StackColumn extends CardStack {
    private static int overlap = 5;
    protected byte hidden;	// number of hidden cards

    public StackColumn(String name, Image back) {
	super(name, 6+12, back); // 6 hidden cards, max 12 showing
	hidden = 0;
    }

    public byte getHidden() { return hidden; }
    public void setHidden(int hidden) { 
	if (hidden >= count)
	    hidden = count-1;
	if (hidden < 0)
	    hidden = 0;
	this.hidden = (byte)hidden; 
    }

    public Card pop() {
	Card c = super.pop();
	if (count > 0 && hidden == count)
	    hidden = (byte)(count-1);
	return c;
    }

    public Card firstVisible() {
	return count==0 ? null : cards[hidden];
    }

    public void paint(Graphics g) {
	int y = 0;
	int yoff = back.getHeight(null)/overlap;

	if (count > 0) {
	    for (int i = 0; i < count; i++) {
		Image img = ((i >= hidden) ? cards[i].getImage() : back);
		g.drawImage(img, 0, y, this);
		y += yoff;
	    }
	} else {
	    Dimension d = size();
	    g.clearRect(0, 0, d.width, d.height);
	}
    }

    public Dimension wantedSize() {
	int w = back.getWidth(null);
	int h = back.getHeight(null);
	return new Dimension(w, (h + h*17/overlap));
    }


    public String toString() {
	StringBuffer s = new StringBuffer(this.getClass().getName());
	s.append("/"+name+"[");

	s.append("count="+count+", ");
	s.append("hidden="+hidden+", ");
	for (int i = 0; i < count; i++)
	    s.append(cards[i].toString()+" ");
	s.append("]");
	return new String(s);
    }
}


/** Class AceColumn - Canvas for interactive ace stacks */
class AceColumn extends CardStack {
    protected Card[] top;
    protected int[] position;
    protected int positions_used;

    public AceColumn(String name, Image back) {
	super(name, back);
	top = new Card[4];
	position = new int[4];
	clear();
    }

    public void push(Card c) {
	super.push(c);

	if (c.value == Card.ACE)
	    position[positions_used++] = c.suit;
	top[c.suit] = c;
    }

    public void clear() {
	super.clear();
	for(int i = 0; i < 4; i++) {
	    top[i] = null;
	}
	positions_used = 0;
    }

    public Card top(int suit) {
	return this.top[suit];
    }

    public void paint(Graphics g) {
	int yoff = size().height/4;
	int y = (yoff - back.getHeight(null))/2;

	for(int i = 0; i < positions_used; i++) {
	    g.drawImage(top[position[i]].getImage(), 0, y, this);
	    y += yoff;
	}
    }

    Dimension wantedSize() {
	return new Dimension(back.getWidth(null), back.getHeight(null) * 4);
    }

    public String toString() {
	StringBuffer s = new StringBuffer(this.getClass().getName());
	s.append("/"+name+"[");

	s.append("top=");
	for (int i = 0; i < top.length; i++)
	    if (top[i] != null)
		s.append(" " + top[i].toString());
	    else
		s.append(" null");
	s.append("]");
	return new String(s);
    }
}


/** Class Card - a single card in a deck. */
class Card extends Object {
    public byte suit;		// Card suite 0..3 
    public byte value;		// Card valeur 0..12
    public Image face;		// Face image

    public static final byte ACE = 0;
    public static final byte TWO = 1;
    public static final byte THREE = 2;
    public static final byte FOUR = 3;
    public static final byte FIVE = 4;
    public static final byte SIX = 5;
    public static final byte SEVEN = 6;
    public static final byte EIGHT = 7;
    public static final byte NINE = 8;
    public static final byte TEN = 9;
    public static final byte JACK = 10;
    public static final byte QUEEN = 11;
    public static final byte KING = 12;

    public static final byte HEARTS = 0;
    public static final byte SPADES = 1;
    public static final byte DIAMONDS = 2;
    public static final byte CLUBS = 3;

    public static String[] suitNames = {
	"heart", "spade", "diamond", "club"
    };

    public Card(byte suit, byte value, Image face) {
	this.suit = suit;
	this.value = value;
	this.face = face;
    }

    public boolean isBlack() { return (suit == SPADES || suit == CLUBS); }
    public boolean isRed() { return (suit == HEARTS || suit == DIAMONDS); }

    public boolean sameColor(Card c) {
	return
	    (((this.suit == DIAMONDS) || (this.suit == HEARTS)) &&
	     ((c.suit == DIAMONDS) || (c.suit == HEARTS))) ||
	    (((this.suit == SPADES) || (this.suit == CLUBS)) &&
	     ((c.suit == SPADES) || (c.suit == CLUBS)));
    }

    public Image getImage() { return face; }

    public int width() { return face.getWidth(null); }
    public int height() { return face.getHeight(null); }

    public String toString() {
	int i = value+1;
	return suitNames[suit].substring(0,1) + ((i < 10) ? "0"+i : ""+i);
    }
}

