/** (C) 1999 World Xiangqi League, Confidential, All Rights Reserved */

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

class GameCanvas extends Canvas implements Runnable {
  public char gameState = 'b';	// Begin else Run else End
  public boolean practice = false;
  
  private int role;		// 0=red, 1=blue, 2+=observer
  private boolean ready;

  private Thread thread;

  private Club club;
  private GamePanel gamePanel;

  private String boardPos;	// current board position (0th=a1)
  private boolean a1Bottom = true; // board orientation, a1 at bottom?
  private boolean isMyTurn = false;
  private boolean isMoving;
  private int updateRequests;	// number of explicit updates in queue
  private int inCheck;		// role of person in check now
  private double ww,hh;		// half a board square 
  private int movingPos;	// board index position of moving piece
  private int moveClickPos;	// two-click move in progress?
  public boolean hints = false;	// show move hints?
  private int xLoc,yLoc;	// screen coordinate
  private int lastMovedPiece;
  private int positionShown;	// which move are we showing (for VCR buttons)
  private Vector position = new Vector(50,50);	// list of moves

  // -----------------------------------------------------------------
  // Game Specific Settings:
  public static final String GAMENAME = "xiangqi";       // not used yet
  private static final String DEFAULTARMY = "Beijing";
  private static final int NROWS = 10;            // 1-10, index 0-9
  private static final int NCOLS = 9;             // a-i, index 0-8
  private static final int NPOS = NROWS*NCOLS;
  private static final Color ARMY0COLOR = Color.red; // uppercase goes first
  private static final Color ARMY1COLOR = Color.blue; // lowercase goes second
  private static final String ARMYCOLORS = "rb"; // red and blue

  // PIECECHARS: chaRiot, Horse, Elephant, Advisor, King, Cannon, Pawn:
  private final static String PIECECHARS = "RHEAKCPrheakcp";
  private final static String[] PIECENAMES = {
    "chaRiot (ju)", "Horse (ma)", "Elephant (xiang)",
    "Advisor (shi)", "King (shuai)", "Cannon (pao)", "Pawn (bing)",
    "chaRiot (ju)", "Horse (ma)", "Elephant (xiang)",
    "Advisor (shi)", "King (jiang)", "Cannon (pao)", "Pawn (zu)",
  };
  private final static int NPIECES = PIECECHARS.length(); // total

  /** Initial board setup */ 
  private final String boardPos0 =
  // abcdefghi   
    "RHEAKAEHR"+ // row 1a-1i (pos 0-8)   RED
    "         "+ // row 2a-2i (pos 9-17)
    " C     C "+ // row 3a-3i (pos 18-26)
    "P P P P P"+ // row 4a-4i (pos 27-35)
    "         "+ // row 5a-5i (pos 36-44)
    "         "+ // row 6a-6i (pos 45-53)
    "p p p p p"+ // row 6a-6i (pos 54-62)
    " c     c "+ // row 7a-7i (pos 63-71)
    "         "+ // row 8a-8i (pos 72-80)
    "rheakaehr"; // row 9a-9i (pos 81-89) BLACK(blue)
  // abcdefghi     

  // ----------------------------------------------------------------
  public GameCanvas(GamePanel gamePanel, Club club) {
    super();
    this.gamePanel = gamePanel;
    this.club      = club;
    initState(2,true);
  }

  /** @return number of moves made so far.
   */
  public int setReady() {
    ready = true;
    return position.size();
  }

  /** Called at game canvas construct and Game->Reset menu */
  public void initState(int role, boolean ready) {
    this.role = role;
    this.ready = ready;
    practice = false;
    gameState = 'b'; // begin
    inCheck = -1; // nobody
    boardImage = null; // force draw
    clipClear();
    Dimension d = size();
    //if (d.width<1) resize(preferredSize());
    boardPos = boardPos0;
    position.removeAllElements();
    position.addElement(boardPos); // initialize history
    positionShown = 0;
    clearHints();
    isMyTurn = (role==0); // red player
    a1Bottom = (role!=1); // show a1 at bottom unless we are blue
    lastMovedPiece = 100;
    moveClickPos = -1;
    isMoving = false;
    updateRequests = 0;
    if (role!=0)
      isMyTurn = false;
    paintImage("initState");
  }

  public void flip() {
    a1Bottom = !a1Bottom;
    boardImage = null;		// redraw the row and column labels
    paintImage("flip");
  }

  // -----------------------------------------------------------------
  /** Slide piece from POS1 to POS2.
   * POS1/POS2 are absolute board indices 0-89 (a1-i10).
   */
  private void slide(int pos1,int pos2) {
    int loc1 = pos2loc(pos1);	// screen relative
    int loc2 = pos2loc(pos2);	// screen relative
    int deltaX = distanceX(loc1,loc2); // pixels (pos or neg)
    int deltaY = distanceY(loc1,loc2); // pixels (pos or neg)

    int absX = abs(deltaX);
    int absY = abs(deltaY);
    //int delta = (int)Math.sqrt(deltaX*deltaX + deltaY*deltaY);
    int skips = (int)((absX>absY) ? (absX/ww*2) : (absY/hh*2));
    // skips now 0-9
    // 10*delay should be between 500ms (one skip) to 1000ms (10 skips)
    int delay = 50 + skips*5; // ms delay for each slide step
    int xLast = xLoc = (int)(positionX(loc1)); // left edge
    int yLast = yLoc = (int)(positionY(loc1) - hh); // top edge
    updateRequests = 0;		// global
    movingPos = pos1;		// global
    isMoving = true;		// global
    long t1=0,t2=0;
    for (int i=0; i<10; i++) {
      t1 = System.currentTimeMillis();      
      xLoc += deltaX/10;  yLoc += deltaY/10;
      clipClear();
      /*
      clipSet(min(xLoc,xLast)-(int)(1.25*ww),
	      min(yLoc,yLast)-(int)(1.25*hh),
	      absX/10+(int)(ww*4), absY/10+(int)(hh*4));
      */
      if (offScreenImage!=null)
	offScreenImage.flush();
      offScreenImage = null;
      repaint();
      xLast = xLoc;  yLast = yLoc;
      t2 = System.currentTimeMillis();
      Club.sleep(delay-(t2-t1));
    }
    isMoving = false;
  }

  /** Process a move just received from the server.
   * This could be my move confirmation or opponent move.
   * Called by GamePanel.processMove and by GamePanel.setup.
   */
  public void processMove(int moveNum,
			  String piece1,String pos1,String pos2,String piece2,
			  int seconds, boolean realtime) {
    if (!gamePanel.isVisible())
      realtime = false;
    if (gameState=='e') {
      club.state("Resetting game, incoming move...");
      Club.debug("GameCanvas.move reset");
      Club.sleep(600);
      initState(role,realtime);
      club.state(null);
    }
    if (!posCurrent()) {	// VCR showing other position!
      System.out.println("GameCanvas.move: VCR fix");
      posLast();		// correct it (bug 211 thang)
    }
    int justMoved = army(piece1); // who just made this move?
    if (realtime && gameState!='e') {
      // stop mover, but don't start other      
      timerClick(justMoved,seconds,false);
    }
    moveChess(posIndex(pos1),posIndex(pos2),realtime); // slide baby
    if (realtime && gameState!='e') {
      // stop mover and start other now
      timerClick(justMoved,-1,true);
    }
    if (role<2) isMyTurn=!isMyTurn;
    gameCursor();
  }
  /** Process move broadcast from the server */
  private void moveChess(int pos1, int pos2, boolean realtime) {
    gameState = 'r'; // if not started do so now!
    if ((!isMyTurn || role>=2) && realtime && ready)
      slide(pos1,pos2);
     
    boardPos = (String)position.elementAt(position.size()-1);
    StringBuffer sb = new StringBuffer(boardPos);
    char c = boardPos.charAt(pos2);
    sb.setCharAt(pos2,boardPos.charAt(pos1));
    sb.setCharAt(pos1,' ');
    boardPos = sb.toString();
    position.addElement(boardPos);
    positionShown = position.size()-1;
    lastMovedPiece=pos2;
    // if (!realtime) return;
    
    int count=GameRule.count(boardPos,pos2);
    inCheck = GameRule.inCheck(boardPos); // army index of king in check
    if (count<1) { // game over
      boolean isRed=Character.isLowerCase(boardPos.charAt(pos2)); // who win
      endGame();		// this will paintImage and sound gong
    } else if (realtime) {
      if (ready)
	club.sound("move");
      //paintPos("moveChess",pos1,pos2); // set clip and draw now
      paintImage("moveChess");
    }
  }

  // -----------------------------------------------------------------
  // VCR buttons for viewing board at any game position:
  private void positionShow(int moveNum) {
    club.state(null);
    clearHints();
    positionShown = moveNum;
    if (positionShown < 0)
      positionShown = 0;
    if (positionShown >= position.size())
      positionShown = position.size()-1;
    boardPos = (String)position.elementAt(positionShown);
    inCheck = GameRule.inCheck(boardPos);    
    paintImage("positionShow");
  }
  public void posFirst() { positionShow(0); }
  public void posNext()  { positionShow(positionShown+1); }
  public void posPrev()  { positionShow(positionShown-1); }
  public void posLast()  { positionShow(position.size()-1); }
  private boolean posCurrent() { return positionShown==(position.size()-1); }

  /** Called by moveChess and gamePanel:resign() */
  public void endGame() {
    boardImage = null; // force redraw
    //gameText = "Red Won, Game Over!";
    //if (!isRedWin) gameText = "Blue Won, Game Over!";
    isMyTurn = false;
    gameCursor();
    gameState = 'e'; //end
    gamePanel.endGame();
    paintImage("endGame");
    club.sound("endgame");
  }

  // -----------------------------------------------------------------
  // Fields for double buffer image processing:
  private Image offScreenImage;
  private Dimension offScreenSize;
  private Graphics offScreenGraphics;
  private Dimension minimumSize = new Dimension(120,132);
  public Dimension minimumSize() { return minimumSize;  }

  // -----------------------------------------------------------------
  // Animation Thread
  private void startAnimation(String caller) {
    if (thread==null) {
      thread = new Thread(club.threadGroup, this, "GameCanvas "+caller);
      thread.start(); // will cause run() method to be invoked
    } else {
      Club.debug("GameCanvas.startAnimation("+caller+") "+thread);
    }
  }
  private void stopAnimation(String caller) {
    if (thread!=null) {
      //thread.stop();
      thread = null;
    }
  }
  public void run() {		// animation loop
    while (thread!=null) {
      if (clipActive) {
	try {
	  prepareImage();		// redraw offScreenImage
	  Graphics g = getGraphics();
	  clipNow(g);
	  g.drawImage(offScreenImage,0,0,this); // blast to screen
	  g.dispose();
	  clipClear();
	} catch (Exception e) {
	  if (Club.debug)
	    Club.warning(e,"GameCanvas.run animation loop");
	}
      }
      Club.sleep(50);
    }
  }

  // -----------------------------------------------------------------
  // Clipping
  private Rectangle clipRect = new Rectangle();
  private boolean clipActive;
  private synchronized void clipClear() {
    clipActive = false;
    Dimension d = size();
    clipRect.x = 0; clipRect.y = 0;
    clipRect.width = d.width; clipRect.height = d.height;
  }
  private synchronized void clipSet(int x, int y, int width, int height) {
    clipActive = true;
    clipRect.x = x; clipRect.y = y;
    clipRect.width = width; clipRect.height = height;
  }
  private Rectangle clipMore = new Rectangle();
  /** Update clipRect to contain the game piece centered at x,y.
   * Callers: move events (mouseDrag, piece slide).
   * User: run() animation loop
   */
  private synchronized void clipMore(int x, int y) {
    clipActive = true;
    clipMore.x = x; clipMore.y = y;
    clipMore.width = (int)(ww*1.25);
    clipMore.height = (int)(hh*1.25);
    clipRect.add(clipMore);
  }
  private void clipNow(Graphics g) {
    if (clipActive)
      g.clipRect(clipRect.x,clipRect.y, clipRect.width,clipRect.height);
  }
  private void clipDebug(String caller) {
    Club.debugRect(caller+" "+(clipActive?"":"!")+"clipActive", clipRect);
  }

  // -----------------------------------------------------------------
  /** Called by window manager on unobscure or after explicit repaint() */
  public void paint(Graphics g) { update(g); }
  private long lastUpdate = 0;
  public void update(Graphics g) {
    try {
      long t0 = System.currentTimeMillis();
      Dimension d = size();
      boolean doClip = (offScreenImage==null && thread!=null);
      if (offScreenImage==null || imageErrors>0 ||
	  offScreenSize.width!=d.width || offScreenSize.height!=d.height) {
	prepareImage();
      }
      if (offScreenImage!=null) {
	if (doClip)
	  clipNow(g);
	g.drawImage(offScreenImage,0,0,this); // blast to screen
	lastUpdate = System.currentTimeMillis();
      }
    } catch (Exception e) {
      if (Club.debug)
	Club.warning(e,"GameCanvas.update");
    }
    // Code 25nov97 takes my P100 about 170ms to repaint--uggh!
    //Club.debug("updated "+updateRequests+" "+(lastUpdate-t0)+"ms");
  }

  /** Set the clip and draw now.
   * A single move just happened, animation loop is off.
   */
  private void paintPos(String caller, int pos1, int pos2) {
    pos1 = pos2loc(pos1);
    pos2 = pos2loc(pos2);
    int x1 = min( posX(pos1), posX(pos2) ) - (int)(1.25*ww);
    int x2 = max( posX(pos1), posX(pos2) ) + (int)(1.25*ww);
    int y1 = min( posY(pos1), posY(pos2) ) - (int)(1.25*hh);
    int y2 = max( posY(pos1), posY(pos2) ) + (int)(1.25*hh);

    clipSet(x1,y1,x2-x1,y2-y1);
    //clipClear();
    clipDebug("paintPos:"+caller);
    if (offScreenImage!=null)
      offScreenImage.flush();
    offScreenImage = null;
    repaint();
  }
  /** Add POS to clip and draw now.
   */
  private void paintPos(String caller, int pos) {
    int loc = pos2loc(pos);
    clipMore(posX(loc),posY(loc));
    if (offScreenImage!=null)
      offScreenImage.flush();
    offScreenImage = null;
    repaint();
  }

  /** Call when we need to redisplay the entire canvas */
  private void paintImage(String caller) {
    clipClear();
    if (offScreenImage!=null)
      offScreenImage.flush();
    offScreenImage = null; // invalidate so update will call prepareImage
    repaint(); // will generate an update() event
  }
  /** Draw the offScreenImage, to prepare for actual paint. */
  private int imageErrors = 0;
  private void prepareImage() {
    Dimension d = size();
    boolean newSize = (offScreenSize==null)
      || (d.width != offScreenSize.width);
    boolean doClip = !newSize && thread!=null;

    // Set local globals for (half) board square size:
    ww = d.width/20.0;
    hh = d.height/22.0;
    
    //Club.debugDim("prepareImage() "+(int)ww+"x"+(int)hh, d);
    if (d.width<1) {
      Club.debug("prepareImage() width=0 thus return");
      return;
    }
    if (boardImage==null ||
	offScreenSize.width!=d.width ||	offScreenSize.height!=d.height) {
      prepareBoard();
      doClip = false;
    }
    if (offScreenImage==null ||
	offScreenSize.width!=d.width ||	offScreenSize.height!=d.height) {
      //Club.debugDim("prepareImage() init offScreen data ",d);
      setBackground((gameState=='e')?Color.black:Club.BOARD_COLOR);
      if (offScreenImage!=null)
	offScreenImage.flush();
      offScreenImage = createImage(d.width, d.height);
      offScreenSize = d;
    }
    offScreenGraphics = offScreenImage.getGraphics();
    Graphics g = offScreenGraphics;
    if (doClip)
      clipNow(g);
    //clipDebug("update");

    // 1. Blast cached board to offScreenImage:
    g.drawImage(boardImage,0,0,this);

    if (club.table==null || club.table.equals("Lobby")) {
      g.dispose();
      return;			// no pieces in Lobby
    }

    // 2. Now draw the pieces in current position:
    imageErrors = 0;
    // Proportioned even size pieces will scale better/faster;
    int pieceW = (int)(2*ww);
    int pieceH = (int)(2*hh);
    if (pieceW<pieceH) pieceH=pieceW; else pieceW=pieceH;
    pieceW &= ~0x1; pieceH &= ~0x1;
    //if (newSize)
    //Club.debug("WH="+ww+ "x"+hh+", piece="+pieceW+"x"+pieceH);

    for (int i=0; i<NPOS; i++) {
      int loc = pos2loc(i);
      int row = row(loc);	// row 0-9 (1-10)
      int col = column(loc);	// col 0-8 (a-i)
      int pieceX = (int)((col+1)*2*ww)-(pieceW/2);
      int pieceY = (int)((NROWS-row)*2*hh)-(pieceH/2);

      if (boardPos.charAt(i) != ' ') {
	if (isMoving && i==movingPos) {
	  // Just drop a circle to show where we come from:
	  g.setColor(Color.lightGray);
	  g.fillOval(pieceX,pieceY, pieceW,pieceH);
	  g.setColor(Color.black);
        } else {
          if (!drawPiece(i, pieceX,pieceY, pieceW,pieceH))
	    imageErrors++;
        }
      }
      if (isMyTurn && hints && moveHints.get(i) && movingPos>=0) {
	g.setColor(myColor());
	g.drawOval(pieceX-1,pieceY-1, pieceW+2,pieceH+2);
	//g.drawOval(pieceX-2,pieceY-2, pieceW+3,pieceH+3);
      }
    }
    if (isMoving) {
      // Draw this moving piece last so it will be on "top":
      if (!drawPiece(movingPos, xLoc-pieceW/2, yLoc-pieceH/2,
		     pieceW,pieceH))
	imageErrors++;
    }
    g.dispose();
    //clipDebug("imageErrors="+imageErrors);
  }

  // ----------------------------------------------------------------
  private Image boardImage;
  private Image backgrImage = null; // defaults to "Plain", no texture

  // Called (only) via user invoking Options->Board menu:
  public void loadBackground(String name) {
    Image image = null;		// for "Plain", no texture
    if (!name.equals("Plain")) {
      club.state("Loading "+name+" background...");
      image = ImageButton.load("backgr/"+name.toLowerCase()+".gif",
			       this,true);
      if (image==null) {
	club.state("Could not load background: "+name);
	return;
      }
    }
    backgrImage = image;
    if (boardImage!=null)
      boardImage.flush();
    boardImage = null; // force redraw
    if (offScreenImage!=null)
      offScreenImage.flush();
    offScreenImage = null; // force redraw
    repaint();
    club.state("OK");
  }

  /** List of intersections which should be drawn with ornate markers */
  private static int[] ornatePositions = {
    19,25,27,29,31,33,35,
    54,56,58,60,62,64,70
  };
  private void drawLine(Graphics g, double x1,double y1, double x2,double y2) {
    g.drawLine((int)x1,(int)y1, (int)x2,(int)y2);
  }
  
  /** Called by prepareImage to create offScreenBoard;
   * Save time by only doing this when window size changes,
   * instead of having to draw boardImage each time a piece moves.
   */
  private void prepareBoard() {
    Dimension d = size();
    //Club.debugDim("prepareBoard",d);
    if (boardImage!=null)
      boardImage.flush();
    boardImage = createImage(d.width,d.height);
    Graphics g = boardImage.getGraphics();

    // 1. Prepare the boardImage:
    if (backgrImage!=null && gameState!='e') {
      for (int y=0; y<d.height; y+=backgrImage.getHeight(null))
	for (int x=0; x<d.width; x+=backgrImage.getWidth(null))
	  g.drawImage(backgrImage, x,y, this);
    } else {
      g.setColor((gameState=='e')?Color.black:Club.BOARD_COLOR);
      g.fillRect(0, 0, d.width, d.height);
    } 
    g.setColor(Club.CONTROLS_COLOR);
    g.draw3DRect(0,0,d.width-1,d.height-1,true);

    // 2. Draw all the grid lines on the board:
    g.setColor((gameState=='e') ? Color.white : Color.black);
    for (int i=0; i<NROWS; i++) { // horizontal row lines
      drawLine(g, 2*ww,(2*i+2)*hh, 18*ww,(2*i+2)*hh);
    }
    for (int i=0; i<NCOLS; i++) { // vertical lines above/below river
      drawLine(g, (2*i+2)*ww,2*hh,  (2*i+2)*ww,10*hh);
      drawLine(g, (2*i+2)*ww,12*hh, (2*i+2)*ww,20*hh);
    }
    // Vertical lines left/right of river: 
    drawLine(g, 2*ww,10*hh,  2*ww,12*hh);
    drawLine(g, 18*ww,10*hh, 18*ww,12*hh);
    
    // Diagonals for palaces:
    drawLine(g, 8*ww,2*hh,  12*ww,6*hh);
    drawLine(g, 8*ww,6*hh,  12*ww,2*hh);
    drawLine(g, 8*ww,16*hh, 12*ww,20*hh);
    drawLine(g, 8*ww,20*hh, 12*ww,16*hh);

    // 3. Draw the ornate intersections:
    int xo = (int)(ww/3);	// x offset
    int yo = (int)(hh/3);	// y offset
    for (int i=0; i<ornatePositions.length; i++) {
      int pos=ornatePositions[i];
      int col=column(pos);	// 0-8 (a-i)
      int x = posX(pos);
      int y = posY(pos);
      if (col>0) {
	drawLine(g, x-xo, y-yo,  x-2*xo, y-yo); //top left
	drawLine(g, x-xo, y-yo,  x-xo, y-2*yo); //left up
	drawLine(g, x-xo, y+yo,  x-2*xo, y+yo); //bottom left
	drawLine(g, x-xo, y+yo,  x-xo, y+2*yo); //left down
      }
      if (col<8) {
	x++;
	drawLine(g, x+xo, y-yo,  x+2*xo, y-yo); //top right
	drawLine(g, x+xo, y-yo,  x+xo, y-2*yo); //right up
	drawLine(g, x+xo, y+yo,  x+2*xo, y+yo); //bottom right
	drawLine(g, x+xo, y+yo,  x+xo, y+2*yo); //right down
      }
    }
    
    // 4. Label the rows numbers along the left side of the board:
    /// BUG: watch carefully to see drawn in one font and then another.
    setFont(Club.font(Font.PLAIN,-3));
    for (int i=0; i<NROWS; i++) {
      int rowNum = a1Bottom ? NROWS-i : i+1;
      g.drawString( Integer.toString(rowNum),
                    ((rowNum==NROWS)? 2 : 8), (int)(2*(i+1)*hh+7));
    }

    // Label the columns below the board:
    for (int i=0; i<NCOLS; i++) {
      int colChar = a1Bottom ? 'a'+i : 'i'-i;
      // BUG: slow, get rid of "new" object here:
      Character ch = new Character((char)colChar);
      g.drawString( ch.toString(),
                    (int)(2*(i+1)*ww-4), (int)(21.8*hh) );
    }

    // Print Lobby label in the river:
    if (club.table==null || club.table.equals("Lobby"))
      riverMessage(g,"You are in the Lobby");

    g.dispose();
  }

  private void riverMessage(Graphics g, String msg) {
    FontMetrics fm = g.getFontMetrics();
    int w = fm.stringWidth(msg);
    g.drawString( msg, (int)(20*ww - w)/2, (int)(11.5*hh));
  }
  
  // ----------------------------------------------------------------
  /** @return number of moves that had been made so far.
   * Called by GamePanel.sync().
   */
  public int gameReset() {
    int retval = position.size();
    initState(role,false);	// flush/reset canvas & position, reset timers
    loadPieceImages(null);
    return retval;
  }

  // -----------------------------------------------------------------
  // Piece images, one per file (slow but works):
  private static Image[] pieces = new Image[14];
  
  private boolean drawPiece(int pos, int x,int y, int w,int h) {
    if (pieces[0]==null)
      loadPieceImages("Chinese");
    if (offScreenImage==null) {
      //Club.debug("offScreenImage is null!");
      return false;
    }
    char piece = boardPos.charAt(pos);
    int i = PIECECHARS.indexOf(piece);
    if (pieces[i]==null) {
      Club.debug("drawPiece null "+i+" "+piece+" "+pos);
      return false;
    }

    Color ringColor = null;
    char upPiece = Character.toUpperCase(piece);
    int armyIndex=armyIndex(piece); // 0=uppercase/first or 1=lowercase/second    
    if (posCurrent() && pos==moveClickPos && !isMoving) {
      ringColor = Color.green;	// we are in a two-click move sequence
    } else if (upPiece=='K' && inCheck==armyIndex) { // this king in danger,
      ringColor = opponentColor(armyIndex); // ring him with opponent color
    } else if (posCurrent() && pos==lastMovedPiece) {
      ringColor = Color.yellow;
    }

    Graphics g = offScreenImage.getGraphics();
   
    if (ringColor!=null) {
      g.setColor(ringColor);
      g.drawOval(x-1,y-1, w+2,h+2);      
      g.drawOval(x-2,y-2, w+3,h+3);
    }

    boolean retval = g.drawImage(pieces[i], x,y, w,h, this);
    g.dispose();
    return retval;
  }

  /** Slow-- one http per piece! But loads transparent ok... */
  public void loadPieceImages(String version) {
    // Chinese, Inverted, Graphic, Roman
    // Beijing,Alternate...
    // Find under server images/pieces/VERSION.gif
    if (version==null)
      version = "Chinese";
    club.state("Loading "+version+" game pieces...");
    String dir="pieces/"+version.toLowerCase()+"/";
    //ImageButton.debug(true);
    for (int i=0; i<NPIECES; i++ ) {
      char piece = PIECECHARS.charAt(i);
      char army = (Character.isUpperCase(piece)) ? 'r' : 'b';
      piece = Character.toLowerCase(piece);
      //Club.debug("load "+i+" "+dir+army+piece);
      pieces[i] = ImageButton.load(dir+army+piece+".gif", this, false);
    }
    //ImageButton.debug(false);
    club.state("OK.");
    offScreenImage = null; // force redraw
    repaint();
  }
  

  // ----------------------------------------------------------------
  private int xLocSave, yLocSave;
  private int dragNumber;
  private static final String NOGAME = "Use the Club menu to start a game.";
  private long lastClickTime = 0;

  private void gameCursor() {
    club.cursor("GameCanvas",
		(isMyTurn ? Frame.MOVE_CURSOR : Frame.CROSSHAIR_CURSOR));
  }
  public boolean mouseEnter(Event event, int x, int y) {
    gameCursor();
    return true;
  }
  public boolean mouseExit(Event event, int x, int y) {
    //club.cursorReset("GameCanvas");
    return true;
  }

  public boolean mouseDown(Event event, int x, int y) {
    setReady();
    ImageButton.clearEdge(null);	// bogus hack
    gameCursor();
    //Club.debugEvent("GameCanvas.mouseDown",event);
    long lastClick = lastClickTime;
    long clickNow = System.currentTimeMillis();
    lastClickTime = clickNow;
    boolean doubleClick = (clickNow - lastClick) < 200;
    
    if (!practice && club.table==null || club.table.equals("Lobby")) {
      club.state(NOGAME);
      club.sound("moveerr");
      return true;		// ignore mouseDown
    }
    if (role>=2) {
      club.state("Observer, cannot move pieces.");
      club.sound("moveerr");
      return true;		// ignore mouseDown
    }
    xLocSave = xLoc;
    yLocSave = yLoc;
    if (gameState=='e') {
      club.state("Game is over; blue player must Reset for new.");
      club.sound("moveerr");
      return true;		// ignore mouseDown
    }
    int pos = mousePos(x,y);
    if (pos<0) return true;
    char piece = boardPos.charAt(pos);
    if (hints && piece!=' ') {
      int i = PIECECHARS.indexOf(piece);
      if (i>=0)
	club.state(PIECENAMES[i]);
      if (!practice &&
	  (!isMyTurn || (gameState=='b' && !gamePanel.bothPlayers())))
	return true;
    }
    if (!practice && !isMyTurn) {
      club.state("Not your turn to move!");
      club.sound("moveerr");
      return true;		// ignore mouseDown
    }
    if (gameState != 'r') {
      /*
      if (!gamePanel.defaultTimers()) {
	if (role==0)
	  club.state("Blue player must Game->Start when non-standard timer.");
	return true;
      }
      */
      if (practice) {
	club.send("start @"+club.table);
	startGame();
      }
      else
	return true;
    }
    if (isMoving) {
      club.state("Move in progress!");
      return true;		// ignore mouseDown
    }
    posLast();			// VCR to end
    if (pos<0) {
      club.state("No piece to move!");
      club.sound("moveerr");
      moveClickPos = -1;
      return true;		// ignore mouseDown
    }
      
    if (pos==moveClickPos) {	// undo the two click green ring
      moveClickPos = -1;
      isMoving = false;
      // BUG: need to force repaint now?
      return true;
    }
    if (moveClickPos>0) {	// already were in two-click move mode
      if (isMine(pos))
	moveClickPos = pos;	// switch to two-click mode for new piece
      else
	pos = moveClickPos;	// try to continue moving old piece
    } else if (!practice && !isMine(pos)) {
      club.state("Not your piece!");
      club.sound("moveerr");
      return true;		// ignore mouseDown
    }
    club.state(null);		// we've moving!
    isMoving = true;
    moveClickPos = -1;
    updateRequests = 0;
    dragNumber = 1;
    movingPos = pos;
    xLoc = x;
    yLoc = y;
    if (hints)
      updateHints(pos);
    startAnimation("mouseDown");
    return true;
  } // mouseDown

  public boolean mouseUp(Event event, int x, int y) {
    dragNumber = 0;
    stopAnimation("mouseUp");
    if ((!practice && !isMyTurn) || (moveClickPos<0 && !isMoving)) {
      xLoc = xLocSave;
      yLoc = yLocSave;
      return true;
    }
    if (moveClickPos<0)
      isMoving = false;
    int pos = mousePos(x,y);
    if (pos==movingPos) {	// up where we started...
      moveClickPos = pos;	// ...start two click mode
      //paintImage("GameCanvas: set moveClickPos");
      paintPos("GameCanvas: set moveClickPos",pos);
      return true;
    }
    clearHints();
    isMoving = false;
    moveClickPos = -1;		// not two click attempt
    if (!practice && (pos<0 || !GameRule.isLegal(boardPos,movingPos,pos))) {
      if (pos>=0 && movingPos!=pos) {
	club.state("Invalid move.");
	club.sound("moveerr");
      }
      xLoc = xLocSave;
      yLoc = yLocSave;
      paintImage("GameCanvas: mouseUp()");
      return true;
    }

    // Draw the move and send it to the server
    int seconds = timerClick(role,-1,false); // stop mine, but don't start his
    //paintPos("mouseUp",movingPos,pos); // set clip and draw now
    paintImage("mouseUp");

    char piece1 = boardPos.charAt(movingPos);   
    char piece2 = boardPos.charAt(pos);
    if (piece2==' ') piece2='-'; // syntax for server move command blank piece
    String pos1str=posString(movingPos);
    String pos2str=posString(pos);
    String moveCommand="move "+club.table+ " "+(position.size())+" "+
      piece1+" "+pos1str+" "+pos2str+" "+piece2+" "+seconds;
    club.send(moveCommand);
    return false; //pass event to parent
  }

  public boolean mouseDrag(Event event,int x, int y) {
    if (gameState!='r' || (!practice && !isMyTurn) || !isMoving)
      return true;
    int xLast=xLoc;
    int yLast=yLoc;
    xLoc=x;
    yLoc=y;
    dragNumber++;
    clipMore(x,y);		// for animtation loop
    return true;
  } // mouseDrag

  private String pieceAt(int pos) {
    if (pos<0)
      return ""+pos;
    else
      return ""+boardPos.charAt(pos)+pos;
  }
  private void mouseDebug(String message,int pos) {
    Club.debug(message+" "+pieceAt(pos)+
	       " "+isMoving+" "+pieceAt(moveClickPos));
  }
  
  
  // -----------------------------------------------------------------
  private BitSet moveHints = new BitSet(NPOS);
  
  private void updateHints(int pos) {
    for (int i=0; i<NPOS; i++) {
      if (GameRule.isLegal(boardPos,pos,i))
	moveHints.set(i);
      else
	moveHints.clear(i);
    }
  }
  private void clearHints() {
    for (int i=0; i<NPOS; i++) {
      moveHints.clear(i);
    }
  }
  
  // -----------------------------------------------------------------
  public void startGame() {
    gameState = 'r';
    club.redPlayer.start();
  }
  /** Stop the timer for the guy who JUSTMOVED.
   * Force update his timer to STOPPED at seconds (unless -1).
   * Start other guy's timer if STARTNEXT is true.
   * Return num seconds left for guy who just moved.
   */
  private int timerClick(int justMoved,int stopped,boolean startNext) {
    int seconds = 0;		// seconds left
    if (gameState=='e')
      return 0;			// out of time baby
    if (justMoved==0) {		// red
      seconds = club.redPlayer.stop(stopped);
      if (startNext)
	club.bluePlayer.start();
    } else if (justMoved==1) {	// blue
      seconds = club.bluePlayer.stop(stopped);
      if (startNext)      
	club.redPlayer.start();
    }
    return seconds;
  }

  // -----------------------------------------------------------------
  /** Return 0 if army at pos is red, else 1 for blue, else -1 if nobody. */
  private int army(String str) {
    char piece = str.charAt(0);
    if (piece==' ') return -1;    
    return (Character.isUpperCase(piece) ? 0 : 1);
  }
  private int army(int pos) {
    char piece=boardPos.charAt(pos);
    if (piece==' ') return -1;
    return (Character.isUpperCase(piece) ? 0 : 1);
  }
  private String armyName(int armyIndex) {
    return (armyIndex==0 ? "red" : "blue");
  }
  private String armyName(char piece) {
    return armyName(armyIndex(piece));
  }
  private boolean isMine(int pos) {
    if (pos<0)
      return false;
    char piece = boardPos.charAt(pos);
    if (piece==' ')
      return false;
    boolean isRed=Character.isUpperCase(piece);
    return (isRed==(role==0));    
  }
  private Color myColor() {
    if (role==0) return ARMY0COLOR;
    else if (role==1) return ARMY1COLOR;
    else return null;
  }
  private Color opponentColor(int armyIndex) {
    if (armyIndex==0) return ARMY1COLOR;
    if (armyIndex==1) return ARMY0COLOR;
    return null;
  }
  private int armyIndex(char piece) {
    return (Character.isUpperCase(piece) ? 0 : 1);
  }

  /** Convert position index int to String.
   * E.g., 0=a1, 8=i1, 9=a2, 17=i2,... 81=a9, 89=i9
   */
  private String posString(int pos) {
    int row=row(pos);		// 0-9 (1-10)
    int col=column(pos);	// 0-8 (a-i)
    return ""+ ((char)('a'+col)) + (row+1);
  }
  /** Convert position String to board position index.
   * a1=0, i1=8, a2=9, i2=17,... a9=81, i9=89
   */
  private int posIndex(String str) {
    int col = str.charAt(0)-'a'; // 0-8
    int row = Integer.parseInt(str.substring(1)) - 1; // 0-9
    return NCOLS*row + col;
  }
  /** Convert pixel position to absolute board position index 0-89.
   * Return -1 if not over any position.
   */
  private int mousePos(int x, int y) {
    int col = (int)((x+ww) / (ww*2)); // 1-9 (a-i) left to right
    if (col<1 || col>NCOLS) return -1;
    --col;	// column index 0-8 (a-i) left to right
    int row = (NROWS+1) - (int)((y+hh) / (hh*2)); // 1-10 bottom to top
    if (row<1 || row>NROWS) return -1;
    --row;			// row index 0-9 (1-10) bottom to top
    int loc = NCOLS*row + col;	// bottom left a1
    int pos = loc2pos(loc);	// absolute board position index
    return pos;
  }

  /** Convert absolute board POS index
   * to a screen position (a1 is bottom left).
   */
  private int pos2loc(int pos) {
    return a1Bottom ? pos : NPOS -1 -pos;
  }

  /** Convert a screen position (a1=bottom left)
   * to an absolute board POS index.
   */
  private int loc2pos(int loc) {
    return a1Bottom ? loc : NPOS -1 -loc;
  }

  /** Convert POS 0-89 to ROW index 0-9 (1-10) */
  private int row(int pos) { return pos/NCOLS; }
  /** Convert POS 0-89 to COLUMN index 0-8 (a-i) */
  //private int column(int pos) { return pos - 9*row(pos); }
  private int column(int pos) { return pos%NCOLS; }

  /** Return left edge of POS. */
  private int positionX(int pos) { // pos=0-89
    int col = column(pos);
    return (int)((2*col+1)*ww);
  }
  /** Return top edge of POS. */
  private int positionY(int pos) { // pos=0-89
    int row = NROWS - row(pos);		// board position index
    return (int)((2*row+1)*hh);
  }
  /** Return horizontal center of POS. */
  private int posX(int pos) { // pos=0-89
    int col = column(pos);
    return (int)((2*col+2)*ww);
  }
  /** Return vertical center of POS. */
  private int posY(int pos) { // pos=0-89
    int row = NROWS - row(pos);		// board position index
    return (int)((2*row)*hh);
  }
  private int distanceX(int pos1, int pos2) {
    return posX(pos2) - posX(pos1);
  }
  private int distanceY(int pos1, int pos2) {
    return posY(pos2) - posY(pos1);
  }
  private int inRange(int n, int floor, int ceiling) {
    if (n<floor) return floor;
    if (n>ceiling) return ceiling;
    return n;
  }
  private int max(int a, int b) { return ((a>b) ? a : b); }
  private int min(int a, int b) { return ((a<b) ? a : b); }
  private int delta(int a, int b) { return max(a,b) - min(a,b); }
  private int abs(int value) { return (value<0) ? -value : value; }
  private double abs(double value) { return (value<0) ? -value : value; }
}
