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

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

// The main client class.
class Club extends Frame implements Runnable {
  public static final String TITLE = "World Xiangqi League";
  private static final String version = "V3.2.5 11apr99";
  public static String name() { return TITLE+" "+version; }

  public static final Color WINDOW_COLOR   = new Color(0xc7c36d); // tan
  public static final Color BOARD_COLOR    = new Color(0xc9b49a); // brown
  public static final Color CONTROLS_COLOR = new Color(0x000066); // blue
  public static final Color RED_TITLE      = new Color(0x990000); // dark red
  public static final Color BLUE_TITLE     = new Color(0x000099); // dark blue
  
  public String table = "Lobby";
  public String username;
  private String password;
  private String rating;
  private String country;
  
  public static boolean debug = false;
  public static boolean verbose = false;
  // See also ChatPanel.MOTD
  private static final String LINESEP = System.getProperty("line.separator");

  private String hostname;
  private int port;
  public BootApplet boot;
  public Applet applet;
  public ThreadGroup threadGroup;

  private long connectTime;
  public static boolean disconnecting = false;
  public static boolean disconnected = false;
  private static Connection connection = null; /// Problems re: static
  private static PrintStream ps = null;
  private Socket socket = null;
  private DataInputStream dis = null;
  private Thread thread;
  private boolean inApplet;

  private FormLayout layout = null;
  public MenuBar clubMenuBar = null;
  public SliderCanvas sliderCanvas = null;
  public GamePanel gamePanel = null;
  public PlayerCanvas redPlayer, bluePlayer;
  public ChatPanel chatPanel = null;
  private SponsorCanvas sponsorCanvas = null;

  // -----------------------------------------------------------------
  // 1. Login instantiates the main club.
  Club(String username, String password,
       String hostname, int port,
       boolean inApplet, BootApplet boot,
       ThreadGroup threadGroup,
       boolean debug, boolean verbose) {
    this.username = username;
    this.password = password;
    this.hostname = hostname;
    this.port = port;
    this.inApplet = inApplet;
    this.boot = boot;
    this.applet = (Applet)boot;
    this.threadGroup = threadGroup;
    this.debug = debug;
    this.verbose = verbose;

    setBackground(new Color(0xc79f6d));
    // Replace tired coffee cup with a cool xiangqi icon!
    setIconImage(ImageButton.load("clubicon.gif",this,false));
  }

  // 2. Login requests a connection to the server.
  private int connectionID = 0;
  public boolean connect() {
    disconnecting = disconnected = false;
    try { 
      socket = new Socket(hostname,port);
      dis = new DataInputStream(socket.getInputStream());
      ps = new PrintStream(socket.getOutputStream());
      // Initial server output looks something like:
      // server Game Page Network Server V3.0-A2 06apr98 411082834
      String line = dis.readLine();
      int n = line.lastIndexOf(" ");
      connectionID = Club.atoi(line.substring(n+1,line.length()));
    } catch (Exception e) {
      Club.warning(e,"Could not connect to "+hostname+" on port "+port);
      disconnect();
      return false;
    }
    // Server connection management.
    if (connection!=null)	// loginAgain
      connection.shutdown();
    connection = new Connection(dis,ps,threadGroup);
    connection.start();

    // Club server communications manager.
    if (thread!=null)		// loginAgain
      thread.stop();		// BUG?
    thread = new Thread(threadGroup, this, "Club");
    thread.start();
    return true;
  }      

  // 3. Login tries to login the user.
  public void login(boolean force) {
    int id = connectionID;
    for (int i=0; i<username.length(); i++) {
      id ^= ((int)username.charAt(i))<<(i%3);
    }
    send("login "+username+" "+password+" "+id+
	 ((force)?" !":""));
  }

  public void loginAgain(String command) {
    chatPanel.clearInput();
    if (connection!=null && connection.isAlive() && !disconnected) {
      state("Already connected.");
      return;
    }
    state("Login again, please wait...");
    boot.bootTime = System.currentTimeMillis(); // hack!
    if (!connect()) {
      state("Could not reconnect.");
      return;
    }
    StringTokenizer st = new StringTokenizer(command);
    if (st.countTokens()==3) {
      st.nextToken();		// /login
      username = st.nextToken();
      password = st.nextToken();
    }
    login(true);
    table = "Lobby";
    redPlayer.setupPlayer(null);
    bluePlayer.setupPlayer(null);
    gamePanel.setupGame(null,null,true);
    chatPanel.clear();
    //gamePanel.gotoLobby();
    title();
  }

  // 4. Club processes login response.
  // Possible replies:
  // last paul day time host -- login ok
  // ! login invalid -- invalid username/password given
  // ! login paul: Active host date -- possible zombie still there
  // ! login paul: Already logged in: paul
  // ? login -- syntax error
  private void processLogin(String response) {
    inform(response);
    if (!response.startsWith("last")) {
      //disconnect();	// stop server bleeding sockets!
      return;
    }
    inform("Starting "+TITLE+" client...");
    buildGUI();
    //bar.tick("GUI ready.");
    Login.hasClient=true;
    show();
    inform(TITLE+" startup time "+secondsSince(boot.bootTime));
  }

  // -----------------------------------------------------------------
  // User Interface.

  // 5. Login ok, ready to build club GUI.
  public void buildGUI() {
    if (sliderCanvas!=null) return; // loginAgain
    msgLanguage(Login.language);
    
    ImageButton.loadToolbar(this);
    PlayerCanvas.loadFlags(this);

    sliderCanvas = new SliderCanvas(this); // game/chat resize button
    redPlayer = new PlayerCanvas(RED_TITLE,this);
    bluePlayer = new PlayerCanvas(BLUE_TITLE,this);
    gamePanel = new GamePanel(this);
    chatPanel = new ChatPanel(this);
    Panel sponsorPanel = sponsorPanel();

    Tables.initialize(this,gamePanel);

    Menu clubMenu = buildClubMenu();
    Menu gameMenu = gamePanel.buildGameMenu();
    Menu optionsMenu = gamePanel.buildOptionsMenu();
    Menu memberMenu = chatPanel.buildMemberMenu();
    Menu chatMenu = chatPanel.buildChatMenu();
    Menu helpMenu = new Menu(msg("gHelp")); {
      for (int i = 0; i<helpMenuItems.length; i++) {
	MenuItem menu = new MenuItem(msg(helpMenuItems[i]));
	if (helpMenuPages[i]==null)
	  menu.disable();
	helpMenu.add(menu);
      }
      helpMenu.addSeparator();
      helpMenu.add(new MenuItem(msg("mConfiguration")));
      helpMenu.add(new MenuItem(msg("mFeedback")));
      helpMenu.addSeparator();
      helpMenu.add(new MenuItem("About "+TITLE));
    }
    clubMenuBar = new MenuBar(); {
      setMenuBar(clubMenuBar);
      clubMenuBar.add(clubMenu);
      clubMenuBar.add(gameMenu);
      clubMenuBar.add(optionsMenu);
      clubMenuBar.add(memberMenu);
      clubMenuBar.add(chatMenu);
      clubMenuBar.add(helpMenu);
      clubMenuBar.setFont(new Font("Helvetica",Font.PLAIN,16));
    }

    Toolkit tk = Toolkit.getDefaultToolkit();
    Dimension d = tk.getScreenSize();
    send("log "+version+" "+d.width+"x"+d.height+" C"
	 +tk.getColorModel().getPixelSize()+" "
	 +osName()+" "+getProperty("os.arch")+" "
	 +browser(true)+" T"+new Date().getTimezoneOffset());
    if (d.width>=800)
      d.width *= 0.9;
    d.height = (d.width * 3) / 4;
    resize(d);

    layout = new FormLayout(this,2,3);
    //         component,    pos, span,fill,stretch,anchor
    layout.add(sliderCanvas, 1,1, 2,1, 'H', 1.0,0.0, 'C');
    layout.add(gamePanel,    1,2, 1,2, 'B', 0.8,1.0, 'C');
    layout.add(chatPanel,    2,2, 1,1, 'B', 0.2,1.0, 'C');
    layout.add(sponsorPanel, 2,3, 1,1, 'H', 0.2,0.0, 'C');
    
    title();
    show();

    connectTime = System.currentTimeMillis();
  }

  private Menu buildClubMenu() {
    Menu menu = new Menu(msg("gClub")); {
      menu.add(new MenuItem(msg("mGoToLobby")));
      menu.add(new MenuItem(msg("mNewTable")));
      menu.add(new MenuItem(msg("mJoinTable")));
      menu.addSeparator();      
      menu.add(new MenuItem(msg("mExitClub")));
    }
    return menu;
  }

  private Panel sponsorPanel() {
    Panel panel = new Panel();
    sponsorCanvas = new SponsorCanvas(this);
    ImageButton nextButton =
      new ImageButton("next", msg("tSponsorNext"));
    ImageButton prevButton =
      new ImageButton("prev", msg("tSponsorPrev"));

    FormLayout layout = new FormLayout(panel,2,2);
    //         component,  pos,span,fill,stretch,anchor
    layout.add(sponsorCanvas, 1,1, 1,2, 'N', 0.0,0.0, 'E');
    layout.add(prevButton,    2,1, 1,1, 'N', 0.0,0.0, 'E');
    layout.add(nextButton,    2,2, 1,1, 'N', 0.0,0.0, 'E');
    return panel;
  }

  // -----------------------------------------------------------------
  // Translation of messages.

  /** Key = String language name; Value = Properties.
   */
  private static Hashtable languages = new Hashtable(5,1);
  private static Properties messages = null; 
  
  public static void msgLanguage(String language) {
    if (!languages.containsKey(language)) {
      String path = Login.serverLocal+"/msg/"+
	language.toLowerCase()+".txt";
      Properties msgs = new Properties();
      try {
	URL url = new URL(path);
	msgs.load(url.openStream());
      } catch (IOException e) {
	System.err.println(path+": Could not load messages.");
	return;
      }
      languages.put(language,msgs);
      messages = msgs;
    }
  }
  /** @return translated message for KEY.
   */
  public static String msg(String key) {
    if (messages==null || key==null)
      return key;
    String translated = (String)messages.get(key);
    if (translated==null)
      return key;
    else
      return translated;
  }
  
  // -----------------------------------------------------------------
  // Table Management.

  private boolean savedGame;	// already saved?
  
  public boolean hasGame() {
    return table!=null && !table.equals("Lobby");
  }
  // Member has just left, if player, remove from playerCanvas.
  private void unplayTable(String table, String member) {
    if (myTable(table,null)) {
      if (redPlayer.unplay(member) || bluePlayer.unplay(member))
	title();
    }
  }

  public void requestOpen() {
    if (!gamePanel.canLeave())
      return;
    state("Requesting new table...");
    send("open");
  }
  public void processOpen(String table, String player,
			  String rating, String country) {
    if (!isMe(player))
      return;
    setTable(table);
    redPlayer.setupPlayer(player,rating,country);
    bluePlayer.setupPlayer(null);
    this.rating = rating;
    this.country = country;
    gamePanel.setupGame(player,null,true);
    savedGame = false;
    ImageButton.clearEdge(null);
  }

  public void processSetup(String table,
			   String player1, String rating1, String country1,
			   String player2, String rating2, String country2) {
    setTable(table);
    redPlayer.setupPlayer(player1,rating1,country1);
    bluePlayer.setupPlayer(player2,rating2,country2);
    gamePanel.setupGame(player1,player2,false);
    title();
    sound("reset");
  }

  // Called from Club->Lobby menu or toolbar button.
  public void gotoLobby() {
    if (table.equals("Lobby")) {
      state("You are already in the Lobby.");
      sound("cmderr");
    } else {
      gamePanel.gotoLobby();
    }
  }

  // Called by Members->Here and who toolbar button.
  public void who(boolean useSelection) {
    String name = useSelection ? chatPanel.selectedMember() : null;
    if (name!=null) {
      send("who "+name);
      state("Requesting profile for "+name);
    } else {
      send("who "+table);
      state("Requesting members @"+table);
    }
  }

  // Called Club->Join menu or goto toolbar button.
  public void tables(boolean useSelection) {
    String name = useSelection ? chatPanel.selectedMember() : null;
    if (name!=null) {
      send("join "+name);
      state("Requesting join "+name+"...");
    } else {
      Tables.dialog();	
    }
    ImageButton.clearEdge(null);      
  }

  /** A move has just been received from the server.
   * Confirmation of my move, or opponent move. Process it.
   */
  private synchronized void processMove(String table, String moveNum,
					String piece1, String pos1,
					String pos2, String piece2,
					String seconds) {
    if (!myTable(table,"move"))
      return;
    gamePanel.processMove(atoi(moveNum),
			  piece1,pos1,pos2,piece2,
			  atoi(seconds));
  }

  public boolean isMe(String username) {
    return this.username.equalsIgnoreCase(username);
  }
  private void setTable(String name) {
    if (name==null) return;
    if (name.startsWith("@"))
      this.table = name.substring(1);
    else
      this.table = name;
  }
  public boolean myTable(String name, String operation) {
    if (name.startsWith("@"))
      name = name.substring(1);
    if (!this.table.equals(name)) {
      if (operation!=null)
	Club.debug("Ignoring "+operation+" for "+name+", at "+this.table);
      return false;
    }
    return true;
  }
  
  // ----------------------------------------------------------------
  // Server Broadcast Consumer. Connection:run() is producer.
  
  public void run() {
    String line;
    while (!disconnected) {
      try {
	if ((line = connection.get()) != null) {

	  StringTokenizer st = new StringTokenizer(line);
	  if (!st.hasMoreTokens())
	    continue;
	  
	  String command = st.nextToken().intern();
	  int argc = st.countTokens();
	  String arg1 = st.hasMoreTokens() ? st.nextToken() : null;
	  String arg2 = st.hasMoreTokens() ? st.nextToken() : null;
	  String arg3 = st.hasMoreTokens() ? st.nextToken() : null;
	  String arg4 = st.hasMoreTokens() ? st.nextToken() : null;
	  String arg5 = st.hasMoreTokens() ? st.nextToken() : null;
	  String arg6 = st.hasMoreTokens() ? st.nextToken() : null;
	  String arg7 = st.hasMoreTokens() ? st.nextToken() : null;
	  String arg8 = st.hasMoreTokens() ? st.nextToken() : null;
	  
	  if ((command.equals("last")) || line.startsWith("! login")) {
	    processLogin(line);
	    continue;
	  }
	  if (command.equals("move")) { // move @5 1 p e4 e5 P 395 comment
	    processMove(arg1,arg2,arg3,arg4,arg5,arg6,arg7);
	    continue;
	  }
	  if (command.equals("sound")) { // sound talker beep
	    sound(arg2,true);	// hehehe
	    continue;
	  }
	  if (chatPanel==null) {
	    System.out.println(line);
	    continue;
	  }
	  if (command.startsWith("@")) {
	    Tables.processTable(line.substring(1));
	    continue;
	  }
	  if (chatPanel.processChat(line)) {
	    continue;
	  }
	  
	  inform(line);		// explicitly output command to status

	  if (command.equals("ready")) {
	    gamePanel.processReady(arg1);
	    continue;
	  }
	  if (command.equals("practice")) { // practice @1 on
	    gamePanel.processPractice(arg1,arg2);
	    continue;
	  }
	  if (command.equals("disconnected") ||
	      command.equals("bye") ||
	      command.equals("shutdown")) {
	    if (!disconnecting) {
	      sound("disconnect");
	      sleep(1000);
	    }
	    disconnecting = disconnected = true;
	    title();
	    continue;
	  }
	  
	  if (command.equals("open")) { // open @1 paul 1500P US
	    processOpen(arg1,arg2,arg3,arg4);
	    unplayTable(arg1,arg2); // in case he's leaving
	    continue;
	  }
	  if (command.equals("setup")) { // setup @1 paul 1500 US karl 1600 HK
	    processSetup(arg1,arg2,arg3,arg4,arg5,arg6,arg7);
	    continue;
	  }
	  if (command.equals("timer")) { // timer @4 20 3 [1800 1800]
	    gamePanel.processTimer(arg1,arg2,arg3,arg4,arg5);
	    continue;
	  }
	  if (command.equals("start")) {
	    gamePanel.processStart(arg1);
	    continue;
	  }
	  
	  if (command.equals("draw")) {
	    // 1st broadcast to table: draw request @1 paul karl
	    // 2nd broadcast to all:   draw @1 karl paul
	    if (argc==3)
	      gamePanel.processEndGame(arg1);
	    continue;
	  }
	  if (command.equals("resign")) { // resign @1 paul to wxy
	    gamePanel.processEndGame(arg1); // table
	    continue;
	  }
	  if (command.equals("quit")) { // quit @1 paul to wxy
	    gamePanel.processEndGame(arg1); // table
	    continue;
	  }
	  if (command.equals("win")) { // win @1 paul wxy
	    gamePanel.processEndGame(arg1); // table
	    continue;
	  }
	  if (command.equals("rating")) { // rating paul 1560P karl 1834
	    redPlayer.newRating(arg1,arg2);
	    bluePlayer.newRating(arg1,arg2);
	    redPlayer.newRating(arg3,arg4);
	    bluePlayer.newRating(arg3,arg4);
	    continue;
	  }
	  
	  // Possible table leaves:
	  if (command.equals("observe")) { // observe table member rating country
	    unplayTable(arg1,arg2); // in case he was a player
	    continue;
	  }
	  if (command.equals("logout") || command.equals("zombie") ||
	      command.equals("disconnect")) {
	    unplayTable(table,arg1); // table member
	    continue;
	  }
	  if (command.equals("leave")) { // leave 1 karl
	    unplayTable(arg1,arg2); // table member
	    continue;
	  }
	  if (command.equals("eject")) { // eject paul
	    unplayTable(table,arg1);
	    if (isMe(arg1)) {
	      sound("eject");	// hehehe
	      sleep(2000);
	      exit();
	      return;
	    }
	    continue;
	  }
	  // Other possible commands which just got echoed:
	  // - login member rating country
	  // - win paul vs karl at 4
	  
	} else {
	  Club.sleep(100);
	}
      } catch (Exception e) {
	warning(e,"Club.run");
	boot.status("Program Error: see Java Console.");
	state("Program Error: see Java Console.");
      }
    }
    Club.verbose("Club end.");
  }

  // ----------------------------------------------------------------
  // Event Handler.
  
  public boolean handleEvent(Event event) {
    // Event fields: id, arg, target
    try {
      if (event.id==Event.KEY_ACTION || event.id==Event.KEY_PRESS)
	return handleKey(event);
    
      if (event.id==Event.WINDOW_DESTROY) {
	if (socket!=null && ps!=null) {
	  inform("Disconnecting...");
	  disconnecting = true;
	  send("quit !");
	}
	if (inApplet)
	  dispose();
	exit();
      }
      if (event.id!=Event.ACTION_EVENT)
	return false;

      if (event.arg=="next")
	return sponsorCanvas.next();
      else if (event.arg=="prev")
	return sponsorCanvas.prev();
      else if (event.target==sponsorCanvas)
	return sponsorCanvas.click();

      if (handleClubMenu(event))
	return true;
      if (gamePanel.handleMenu(event))
	return true;
      if (chatPanel.handleMenu(event))
	return true;
      if (handleHelpMenu(event))
	return true;
    } catch (Exception e) {
      debugEvent("Club.handleEvent error: ",event);
      warning(e,"Club.handleEvent");
    }
    return super.handleEvent(event);
  }

  public boolean handleKey(Event event) {
    return chatPanel.handleKey(event);
  }

  
  // -----------------------------------------------------------------
  // Help.
  
  public boolean handleClubMenu(Event event) {
    // Club: Go to Lobby, New Table, Join Table..., Exit Club
    //Club.debugEvent("Club.handleClubMenu",event);
    if (event.arg==msg("mGoToLobby")) {
      gotoLobby();
      return true;
    }
    if (event.arg==msg("mNewTable")) {
      requestOpen();
      return true;
    }
    if (event.arg==msg("mJoinTable")) {
      tables(false);		// don't use selection
      return true;
    }
    if (event.arg==msg("mExitClub")) {
      if (disconnected) {
	Club.debug("Exiting Club immediately.");
	exit();
	return true;
      }
      if (!gamePanel.canLeave()) {
	sound("cmderr");
	return true;
      }
      // Fix bugno 215 for macleod (al.fang@mci2000.com) on 31jan98:
      // New fix: should be bConfirmExitYes and bConfirmExitNo 
      //   --mpassell 3/5/01
      if (!Stickup.yesno(this, msg("hExitClub"), msg("pConfirmExit"),
			 msg("bConfirmExitYes"), msg("bConfirmExitNo")))
	return true;
      disconnecting = true;
      send("quit !");
      exit();
      return true;
    }
    return false;
  }
  
  private boolean handleHelpMenu(Event event) {
    //Club.debugEvent("handleHelpMenu",event);
    if (event.id!=Event.ACTION_EVENT ||	(event.arg instanceof String)==false)
      return false;
    if (event.arg==msg("mConfiguration")) {
      Stickup.info(this,msg("hConfiguration"),configuration());
      return true;
    }
    if (event.arg==msg("mFeedback")) {
      feedback();
      return true;
    }
    if (event.arg=="About "+TITLE) {
      Stickup.info(this,"About "+TITLE,
   "The World Xiangqi League (WXL) is the largest\n"+
   "  Chinese Chess network in the world, with\n"+
   "  over 50,000 online members. Accounts are\n"+
   "  free and allow you to play at any server.\n\n"+
   "Members get a free xiangqi.com email address\n"+
   "  and Personal Game Page home page.\n\n"+
   "We welcome players of any age, skill and culture.\n\n"+
   "(C) 1998 World Xiangqi League (tm)\n"+
   "(C) 1999 World Xiangqi League (tm)\n\n"+"Client "+version);
      return true;
    }
    // Try to display help file for chosen menu item:
    String topic = (String)event.arg;
    for (int i=0; i<helpMenuItems.length; i++ ) {
      // Fixed bug that made help menu items do nothing.  Used to check
      // topic.equals(helpMenuItems[i]), but topic is a string that
      // has been passed through msg. --mpassell 3/5/01
      if (topic.equals(msg(helpMenuItems[i]))) {
	boolean retval = showDoc(helpMenuPages[i]);
	if (retval)
	  chatPanel.state("Browser open \""+topic+"\".");
	else
	  chatPanel.state("Could not open \""+topic+"\".");
	return retval;
      }
    }
    return false;
  }
  
  private static final String[] helpMenuItems = {
    "mIntroduction", "mWhatsNew", "mProblems",
    "mRatings", "mTimers", "mRules", "mResize", "mCache", 
    "mEmail", "mGamePages", "mPerformance", "mWebsite"
  };
  private static final String[] helpMenuPages = {
    "help", "new", "problems",
    "ratings", "timers", "rules", "resize", "cache",
    "email", "http://members.xiangqi.com", "cgi-bin/perf.cgi", "site"
  };
  

  private String configuration() {
    if (debug)
      threadGroup.list();
    StringBuffer buf = new StringBuffer();
    Toolkit tk = Toolkit.getDefaultToolkit();
    Dimension d = tk.getScreenSize();
    // 640x480, 800x600, 1024x768, 1152x864, 1280x1024
    int dpi = tk.getScreenResolution(); // dpi
    int bpp = tk.getColorModel().getPixelSize(); //bits per pixel
    buf.append("Member: "+username+
	       ((gamePanel.role<2) ? " playing at " : " observing ")+
	       table+"\n");
    buf.append("Session time: "+timeSince(connectTime)+"\n");
    buf.append("Client version: "+version+"\n");
    buf.append("Display: "+d.width+"x"+d.height+", "
	       +dpi+" dpi, " +bpp+" bits per pixel.\n");

    Runtime r = Runtime.getRuntime();
    buf.append("Memory: "+(r.freeMemory()/1000)+"kb free of "+
	       (r.totalMemory()/1000)+"kb total.\n");
    buf.append("Java: "+getProperty("java.version")+
	       " from "+getProperty("java.vendor.url")+"\n");
    buf.append("OS: "+getProperty("os.name")+
	       ", "+getProperty("os.arch")+
	       ", "+getProperty("os.version")+"\n\n");
    return buf.toString();
  }
  private void feedback() {
    showDoc("cgi-bin/feedback.cgi/form?"+
	    "login="+username+
	    "&email="+username+"@xiangqi.com"+
	    "&browser="+browser(false));
  }
  private String osName() {
    String os = getProperty("os.name");
    if (os.equals("Windows 95"))
      return "Win95";
    else if (os.equals("Windows NT"))
      return "WinNT";
    else if (os.equals("Mac OS"))
      return "MacOS";
    else
      return os;		// HP-UX, Linux, OSF1, SunOS, etc
  }
  public boolean browserMSIE3() {
    String browser = browser(true);
    return (browser.startsWith("IE*1.0"));
  }
  private String browser(boolean concise) {
    String browser = getProperty("java.vendor.url");
    String javaVersion = getProperty("java.version");
    if (browser.indexOf("netscape")>=0) {
      browser=(concise ? "N*" : "Netscape+");
    } else if (browser.indexOf("microsoft")>=0) {
      browser=(concise ? "IE*" : "Internet+Explorer+");
    } else {
      if (!concise)
	browser = "Other";
    }
    return browser + (concise ? javaVersion :
		      (javaVersion.startsWith("1.0") ? "3" : "4"));
  }
  public String getProperty(String key) {
    String value = null;
    try { value = System.getProperty(key); }
    catch (SecurityException e) {};
    return value;
  }

  // -----------------------------------------------------------------
  // User Interface Utilities.

  private MenuItem menuItemDisabled(String label) {
    MenuItem menu = new MenuItem(label);
    menu.disable();
    return menu;
  }

  public static int fontSize() {
    Toolkit tk = Toolkit.getDefaultToolkit();
    Dimension d = tk.getScreenSize();
    return (d.width<800)? 12 : 14;
  }
  public static Font font(int face) {
    return new Font("Helvetica",face,fontSize());
  }
  public static Font font(int face,int delta) {
    return new Font("Helvetica",face,fontSize()+delta);
  }
  public void title() {
    String title = TITLE+" - "+table;
    if (hasGame()) {
      String player1 = redPlayer.getName();
      String player2 = bluePlayer.getName();
      title += ". "+player1+" "+player2;
    }
    if (disconnected)
      title += " [disconnected]";
    setTitle(title);
    validate();
  }

  // -----------------------------------------------------------------
  // Context-Sensitive Mouse Cursor.
  
  private int cursor = Frame.DEFAULT_CURSOR;
  // CROSSHAIR, DEFAULT, HAND, MOVE, TEXT, WAIT, x_RESIZE
  public void cursor(String caller, int newCursor) {
    //if (verbose)
    //state("Cursor "+caller+" "+newCursor+" "+cursor);
    if (newCursor!=cursor) {
      setCursor(cursor = newCursor);
    }
  }
  public void cursorReset(String caller) {
    //if (verbose)
    //state("Cursor reset "+caller+" "+cursor);
    if (cursor!=Frame.DEFAULT_CURSOR) {
      setCursor(cursor = Frame.DEFAULT_CURSOR);
    }
  }
  public boolean mouseEnter(Event event, int x, int y) {
    cursor("Club",Frame.DEFAULT_CURSOR);
    return true;
  }
  
  // -----------------------------------------------------------------
  /* Called by SliderCanvas. Param x is requested split position.
   * If called with 0, just return where cursor should be.
   */
  public int slider(int x) {
    if (x==0) {
      int cursor = gamePanel.slider(x);
      //debug("Club.slider "+size().width+" ("+gamePanel.size().width+
      //"+"+chatPanel.size().width+") = "+cursor);
      return cursor;
    } else {
      Dimension d = size();
      x = gamePanel.slider(x);
      chatPanel.slider(d.width - x);
      validate();
      layout();
      return x;
    }
  }

  // ----------------------------------------------------------------
  // Bye Bye.
  
  public void disconnect() {
    if (disconnected) return;	// already disconnected
    try {
      if (ps!=null)
	ps.close();
      ps=null;
    }
    catch (Exception e) { Club.warning(e,"Club.close ps"); }
    try {
      if (dis!=null)
	dis.close();
      dis=null;
    }
    catch (IOException e) { Club.warning(e,"Club.close dis"); }
    try {
      if (socket!=null)
	socket.close();
      socket=null;
    }
    catch (Exception e) { Club.warning(e,"Club.close socket"); }
    disconnected = true;
  }
  
  public void exit() {
    sound("goodbye");
    sleep(1000);
    sounds = false;		// no disconnect cuckoo
    Login.hasClient=false;
    inform(username+" session "+timeSince(connectTime));
    disconnect();
    hide();
    dispose();
    threadGroup.stop();
    debug(TITLE+" client exit completed.");
    //IllegalThreadStateException on destroy:
    //threadGroup.destroy();
    if (!inApplet)
      System.exit(0);
  }
  

  // -----------------------------------------------------------------
  // Utilities.

  public void send(String command) {
    connection.send(command);
  }
  
  public String timeSince(long when) {
    long now = System.currentTimeMillis();
    long minutes = (now - when) / 60000;
    long hours = minutes / 60;
    minutes = minutes % 60;
    return(hours+":"+((minutes<10)?"0":"")+minutes);
  }

  public String secondsSince(long when) {
    long now = System.currentTimeMillis();
    long seconds = (now - when) / 1000;
    long minutes = seconds / 60;
    seconds %= 60;
    return(minutes+"."+((seconds<10)?"0":"")+seconds);
  }

  public void inform(String str) {
    boot.status(str);		// BUG: does System.out.println!
    status(str);
  }
  public void status(String message) {
    if (chatPanel!=null)
      chatPanel.status(message);
  }
  public void state(String message) {
    if (chatPanel!=null)
      chatPanel.state(message);
    else
      Club.debug(message);
  }
  public void appendLine(String message) {
    chatPanel.appendLine(message);
  }
  public String selectedMember() {
    return chatPanel.selectedMember();
  }

  // -----------------------------------------------------------------

  public void saveGame() {
    String sound = "cmderr";
    if (!hasGame()) {
      state("No Game to save at this table.");
    } else if (gamePanel.role>1) {
      state("Only players can save games.");
    } else if (!gamePanel.practice() && !gamePanel.bothPlayers()) {
      state("Both players must be here.");
    } else if (savedGame) {
      sound = null;
      state("OK, save requested.");	// already saved!
    } else {
      sound = null;
      state("Sending save request to server...");
      send("save "+table);
      savedGame = true;
      state("OK");
      Stickup.info(this,"Save Game to Home Page",

		   ((gamePanel.gameState()=='e') ?
		    "The server has saved the game.\n\n"
		    :
		    "The server will save the game upon completion.\n\n")+
		   
		   "Within one hour, this game will appear on your\n"+
		   "Xiangqi Personal Game Page, located at\n"+
		   "  http://members.xiangqi.com/"+username+"\n\n"+
		   "Click the web button to open your Game Page.\n");
    }
    sound(sound);
  }
  // We should also have a Member->Home Page menu item.
  // See also the F5 function key binding and PlayerCanvas button.
  public void webOpen() {
    String path = chatPanel.selectedURL(false);
    if (path==null) {		// no http:// or www. in selection
      String member = chatPanel.selectedMember(); // grab any selection
      path = "http://members.xiangqi.com/"
	+ ((member==null) ? username : member);
    }
    showDoc(path);
  }

  public boolean showDoc(String path) {
    if (path==null)
      return false;
    if (!path.startsWith("http://")) {
      path=Login.serverWeb+"/"+path+
	((path.indexOf('.')>0) ? "" : ".html");
    }
    if (inApplet) {
      try {
	URL file = new URL(path);
        Login.appletContext.showDocument(file,path);
	System.out.println("open "+path);
      } catch (MalformedURLException e) {
        Club.warning(e,"Error opening document: "+path);
	return false;
      }
    } else { Club.debug("Not in applet, can not open document "+path); }
    return true;
  }
  public void requestProfile(String name) {
    if (name==null) name = username;
    send("who "+name);
  }
  public boolean showMemberPage(String member) {
    if (inApplet) {
      try {
	if (member==null || member.equals("*")) {
	  URL file = new URL("http://members.xiangqi.com");
	  Login.appletContext.showDocument(file,"WXL Game Pages");
	  System.out.println("open "+file);
	} else {
	  member = member.toLowerCase();
	  URL file = new URL("http://members.xiangqi.com/"+member);
	  Login.appletContext.showDocument(file,member);
	  System.out.println("open "+file);
	}
      } catch (MalformedURLException e) {
        Club.warning(e,"Error opening member home page: "+member);
	return false;
      }
    } else {
      Club.debug("Not in applet, can not open member home page.");
      return false;
    }
    return true;
  }

  // -----------------------------------------------------------------
  // Sound Effects
  public boolean sounds = false;
  public Hashtable soundClips = new Hashtable(32);
  /** Play sound file NAME, first loading if necessary. */
  public void sound(String name) { sound(name,false); }
  public synchronized void sound(String name,boolean literal) {
    if (sounds==false || name==null) return;
    Object obj = soundClips.get(name);
    if (obj==null) {
      state("Loading sound clip for "+name+"...");
      soundClips.put(name,name); // mark as attempted
      String path = Login.serverLocal+"/sounds/"+
	(literal? "" : "@") + name+".au";
      URL url = newURL(path);
      if (url==null) {
	warning(null,"Club.sound path error: "+path);
	state(null);
	return;
      }
      try {
	obj = Login.appletContext.getAudioClip(url);
	soundClips.put(name,obj);
	state(null);
      }
      catch (Exception e) {
	warning(e,"Club.sound load error: "+path);
	state(null);
	return;
      }
    }
    // Test for low memory:
    /*
    Runtime r = Runtime.getRuntime();
    if ((r.freeMemory()/r.totalMemory())<0.2) {
      debug("sound gc for "+name);
      r.gc();
    }
    if ((r.freeMemory()/r.totalMemory())<0.2) {    
      debug("sound lowmem "+name);
      return;			// hack!
    }
    */
    if (obj instanceof AudioClip) {
      AudioClip clip = (AudioClip)obj;
      clip.play();
    }
  }

  // -------------------------------------------------------------------
  // Misc. public utils:

  public static URL newURL(String path) {
    if (!path.startsWith("http"))
      path = "http://"+path;
    URL url = null;
    try { url = new URL(path); }
    catch (MalformedURLException e) { Club.warning(e,"Club.newURL "+path); }
    return url;
  }

  /** Sleep for MILLISECONDS */
  public static void sleep(long ms) {
    if (ms<=0) return;
    try { Thread.currentThread().sleep(ms); }
    catch (InterruptedException e) {Club.warning(e,"Interrupted sleep.");}
  }
  /** Return a random float between 0 and 1 */
  public static float random() {
    Random generator = new Random();
    return generator.nextFloat();
  }
  /** Convert String to int, return 0 if any errors. */
  public static int atoi(String str) {
    int retval = 0;
    try { retval = Integer.parseInt(str); }
    catch (NumberFormatException e) {};
    return retval;
  }
  /** Debug print of Rectangle */
  public static void debugRect(String caller, Rectangle r) {
    debug(caller+" "+r.x+"x"+r.y+", "+r.width+":"+r.height);
  }
  
  /** Debug print of event */
  public static void debugEvent(String caller, Event event) {
    // event.x and event.y
    debug(caller+" event: "+event.id+ ", arg="+event.arg+
	  ", target="+event.target+ ", key="+event.key);
  }
  /** Debug print of image */
  public static void debugImage(String caller, Image image) {
    if (image==null)
      debug(caller+" null image");
    else
      debug(caller+" "+image.getWidth(null)+"x"+image.getHeight(null));
  }
  /** Debug print of Dimension */
  public static void debugDim(String caller, Dimension d) {
    if (d==null)
      debug(caller+" null dimension");
    else
      debug(caller+" "+d.width+"x"+d.height);
  }
  /** Print MESSAGE and stack trace */
  public static void stacktrace(String message) {
    System.err.print(message+LINESEP);
    try { throw new Exception(); }
    catch (Exception e) { e.printStackTrace(System.err); }
  }
  /** Assert TRUTH else issue warning */
  public static void assert(boolean truth) {
    if (!truth) {
      try { throw new Exception(); }
      catch (Exception e) { warning(e,"Assertion Failure"); }
    }
  }
  /** For EXCEPTION, print MESSAGE to stderr then stack trace. */
  public static void warning(String message) {
    warning(null,message);
  }
  public static void warning(Exception e, String message) {
    System.err.print("Warning: "+message+LINESEP);
    if (e != null)    // programmer wants stack dump
      e.printStackTrace(System.err);
    System.err.flush();
  }
  /** Print MESSAGE to system out if we are in debug mode. */
  public static void debug(String str) {
    if (debug) {
      System.out.print(str+LINESEP);
      System.out.flush();
    }
  }
  /** Print MESSAGE to system out if we are in verbose mode. */
  public static void verbose(String str) {
    if (verbose) {
      System.out.print(str+LINESEP);
      System.out.flush();
    }
  }
}

