/** (C) Game Page Network, Inc., Confidential, All Rights Reserved */
// Table.java
// --pme, 24jul97

import java.io.*;
import java.util.*;

/** Manage players and observers at a game table. */
public class Table {

  // Class Fields
  private static final int MAXTABLES = 1000;
  public static final int P1 = 1; // top priority message
  public static final int P2 = 2; // medium priority message
  public static final int P3 = 3; // low priority message
  private static Hashtable tables = new Hashtable(100); // name, value Table
  private static BitSet tableNumbers = new BitSet(MAXTABLES); // in use

  // Instance Fields
  private String name;          // name of this table
  public boolean playable;
  private Member player1, player2;
  private Vector observers = new Vector(10,10);
  private Game game;

  public Enumeration getObservers() {
    return observers.elements();
  }

  // Constructor
  public Table(String name, boolean playable) {
    this.name = name;
    this.playable = playable;
    player1 = player2 = null;
    observers.removeAllElements();
    game      = null;
    tables.put(name,this);
  }

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

  public String tag() { return "@"+name; }
  public String toString()              { return name; }  
  public Game getGame()                 { return game; }

  public boolean isActive() {
    return (game!=null) && game.isActive();
  }
  public boolean isOpen() {
    return player1==null || player2==null;
  }

  public static Table find(String name) {
    if (name==null) return null;
    if (name.startsWith("@"))
      name = name.substring(1);
    return (Table)tables.get(name);
  }
  public static Table findIt(String name) {
    Table table = Table.find(name);
    if (table==null) {
      Member member = Member.findMember(name);
      if (member!=null)
	table = member.getTable();
    }
    return table;
  }

  public Member getOpponent(Member player) {
    if (player==player1)
      return player2;
    else if (player==player2)
      return player1;
    else
      return null;
  }

  public String setupInfo() {
    String p1name = "*", p1rating = "*", p1country = "*";
    String p2name = "*", p2rating = "*", p2country = "*";
    if (game!=null) {
      if (player1!=null) {
	p1name = player1.toString();
	p1rating = player1.getRating(false);
	p1country = player1.getCountry();
      }
      if (player2!=null) {
	p2name = player2.toString();
	p2rating = player2.getRating(false);
	p2country = player2.getCountry();
      }
    }
    return this +" "+
      p1name+" "+p1rating+" "+p1country+" "+
      p2name+" "+p2rating+" "+p2country;
  }

  // -----------------------------------------------------------------
  // Hello.
  
  public void login(Member member) {
    observe(member,false);
    tell("login " +member +" "+
	 member.getRating(false)+" "+member.getCountry(),P2);
  }
  
  /** Create a new open table for MEMBER as 1st player.
   * Called by OpenCommand.process().
   */
  public static void open(Member member) {
    int tableNumber = 0;
    synchronized(tableNumbers) {
      for ( int n = 1; n<MAXTABLES; n++ ) {
	if ( ! tableNumbers.get(n) ) { // found an available table number
	  tableNumbers.set(n); // now in use
	  tableNumber = n;
	  break;
	}
      }
    }
    if (tableNumber==0) {
      Club.warning("Table.open failed!");
      Client c = member.getClient();
      c.send("! open server error");
      return;
    }
		   
    Table table = new Table(""+tableNumber,true);
    Table oldTable = member.getTable();
    oldTable.leave(member,null); // we'll tell below
    member.gotoTable(table,0);
    
    table.game = new Game(table,member);
    table.player1 = member;
    // Optimize. Just tell current table and other observers.
    tellAll("open @" +table +" " +member+
	    " "+member.getRating(false)+" "+member.getCountry(),P3);
  }

  /** Have MEMBER join table as the next player. */
  public void join(Member member) {
    Table oldTable = member.getTable();
    oldTable.leave(member,this);	// force resign if necessary
    observers.removeElement(member);	// in case switching role
    int role = 0;
    if (player1==null) {
      player1 = member;
    } else {
      player2 = member;
      role = 1;
    }
    member.gotoTable(this,role);
    this.game.join(member);
    tell("setup @"+setupInfo(),P1);
    if (numPeople()>2)
      showWho(member.getClient());
  }

  public void start(Client c) {
    if (game!=null && game.start())
      tell("start @"+this,P1);
    else
      c.send("! start "+this);
  }

  public void observe(Member member, boolean inform) {
    Table oldTable = member.getTable();
    if (oldTable!=null)
      oldTable.leave(member,this);
    if (!observers.contains(member))
      observers.addElement(member);
    member.gotoTable(this,2);
    if (inform && member!=null) {	// login has separate message
      tell("observe " +this +" " +member+" "+
	   member.getRating(false)+" "+member.getCountry(),P2);
    }
    if (numPeople() > (playable ? 2 : 1))
      showWho(member.getClient());
    if (game!=null)
      game.sendHistory(member); // send all setup info!
  }

  private int numPeople() {
    int n = 0;
    if (player1!=null) n++;
    if (player2!=null) n++;
    return n + observers.size();
  }
  
  // -----------------------------------------------------------------
  // Bye Bye.
  
  public void leave(Member member, Table newTable) {
    if (member==null) return;
    boolean isPlayer = member.isPlaying(this);
    if (isPlayer && this.isActive())
      resign(member);

    observers.removeElement(member);
    if (player1==member) player1 = null;
    if (player2==member) player2 = null;

    if (game!=null)
      game.leave(member);

    if (this!=newTable && newTable!=null) {
      tell("leave " +this +" " +member+" to "+newTable,
	   (isPlayer ? P1 : P2));
    }
    
    // Maybe close:
    if (this==Club.lobby)
      return;

    if (player1==null && player2==null && observers.isEmpty()) {
      tables.remove(this.name);
      int n = Club.atoi(this.name);
      if (n>0)
        tableNumbers.clear(n);
    }
  }

  public static void logout(Member member, String message) {
    if (member==null) return;
    Table oldTable = member.getTable();
    int role = member.getRole();
    if (oldTable!=null)
      oldTable.leave(member,null);
    if (member.logout()) {	// valid first logout
      if (oldTable!=null) {
	oldTable.tell(((message==null) ? "logout "+member : message),
		      (role<2 ? P1 : P2));
      }
    } else {
      Club.log("logout again "+member);
    }
  }
   
  // -----------------------------------------------------------------
  // Game Methods.
  
  public void resign(Member member) {
    // why should be m(enu), d(isconnect) or t(imeout).
    Club.assert(member.isPlayer(this));
    Member opponent = getOpponent(member);
    if (game!=null) {
      if (game.resign(member))	// if a true resign, real game was active
	record(member,'r',opponent);
      else			// leaving game near begin
	tell("quit @"+this+" "+member+" to "+opponent,P1);
    }
  }
  public boolean draw(Member member) {
    Club.assert(member.isPlayer(this));
    Member opponent = getOpponent(member);
    if (game==null || opponent==null)
      return false;		// error
    if (game.draw(member)) // game over
      record(member,'d',opponent); // will also broadcast
    else
      tell("draw request @"+this+" "+member+" "+opponent,P1);
    return true;		// no error
  }

  public boolean practice(Member member, boolean state) {
    Club.assert(member.isPlayer(this));
    if (game==null)
      return false;		// error
    if (game.practice(member,state)) {
      tell("practice @"+this+
	   (state ? " on" : " off"),P1);
      return true;		// no error
    }
    return false;
  }

  // ----------------------------------------------------------------------
  // Game records.
  public void record(Member p1, char result, Member p2) {
    if (p1==null || p2==null) return;
    boolean forReal = game!=null && !game.practice;
    if (forReal) {
      String command;
      if (result=='r') {		// resign
	command = "rating "+p2+" w "+p1;
      } else {
	command = "rating "+p1+" "+result+" "+p2;
      }
      String results = Club.execReturn(command);
      // results is now something like "paul 1564 1\nkarl 1532 2\n"
      if (results!=null) {
	StringTokenizer st = new StringTokenizer(results,"\n");
	Member.updateRating(st.nextToken());
	Member.updateRating(st.nextToken());
      } else {
	Club.debug("Table.record null "+p1+" "+result+" "+p2);
      }
    }

    String log = p1+" "+result+" "+p2;
    String msg = log;
    if (result=='w') {		// win paul vs. karl at 2
      log = "win "+p1+" vs. "+p2+" at "+this;
      msg = "win @"+this+" "+p1+" "+p2;
    } else if ( result=='d') {	// draw paul vs. wxy at 3
      log = "draw "+p1+" vs. "+p2+" at "+this;
      msg = "draw @"+this+" "+p1+" "+p2;
    } else if ( result=='r' ) {	// resign paul to wxy at 4
      log = "resign "+p1+" to "+p2+" at "+this;
      msg = "resign @"+this+" "+p1+" "+p2;
    }
    // 1998-2-10 18:23.28 record: win playboy vs. paul at 1; T30-3-2; M1
    if (game!=null)
      log += " "+game.stats();	// e.g., T30.3-4-2 M1
    if (forReal && p1!=null && p2!=null) {
      Club.log("record: "+log);
      Table.tellAll(msg,P3);
    } else {
      tell(msg,P3);
    }
    if (p1==null || p2==null) return;
    String ratings = "rating "+p1+" "+p1.getRating(false)+" "
      +p2+" "+p2.getRating(false);
    if (forReal)
      tell(ratings,P2);
  }
  
  // ----------------------------------------------------------------------
  // Table Who What Where

  public static void showTables(Client c) {
    long t0 = System.currentTimeMillis();
    Enumeration e = tables.elements();
    StringBuffer buf = new StringBuffer(80*(tables.size()+1));
    while (e.hasMoreElements()) {
      Table table = (Table)e.nextElement();
      buf.append("@"+table);
      if (table.playable) {
	table.showPlayer(table.player1,buf);
	buf.append(" -");
	table.showPlayer(table.player2,buf);
	buf.append(";");
      }
      for (int i=0; i<table.observers.size(); i++) {
	/// BUG: elementAt() fails when size() has changed!
	Member observer = (Member)table.observers.elementAt(i);
	if (!Member.zombie(observer))
	  buf.append(" "+observer);
      }
      buf.append("\n");
    }
    buf.append("@@");
    long t1 = System.currentTimeMillis();
    c.send(buf);
    long t2 = System.currentTimeMillis();
    c.debug("tables "+ (t1-t0) +" "+ (t2-t1));
  }
  private void showPlayer(Member player,StringBuffer buf) {
    if (player==null)
      buf.append(" *");
    else
      buf.append(" "+player+" "+player.getRating(false)+" "+
		 player.getCountry());
  }

  public void showWho(Client c) {
    StringBuffer buf = new StringBuffer(80*(observers.size()+2));
    if (playable) {
      Member.showMember(player1,buf,true);
      Member.showMember(player2,buf,true);
    }
    for (int i=0; i<observers.size(); i++) {
      /// BUG: elementAt() fails when size() has changed!      
      Member member = (Member)observers.elementAt(i);
      Member.showMember(member,buf,true);
    }
    buf.append("%%");
    if (c!=null)		// Table.login relogin error?
      c.send(buf);
  }

  
  
  //--------------------------------------------------------------------
  // Message Dispatch to Clients
  
  public void tell(String message, int priority) {
    tell(null,message,priority);
  }
  
  public void tell(Member talker, String message, int priority) {
    tellMember(talker,player1,message,priority);
    tellMember(talker,player2,message,priority);

    for (int i=0; i<observers.size(); i++) {
      Member member = (Member)observers.elementAt(i);
      tellMember(talker,member,message,priority);
    }
  }

  private static void tellAll(String message, int priority) {
    tellAll(null,message,priority);
  }
  public static void tellAll(Member talker, String message, int priority) {
    Enumeration e = Member.getMembers();
    while (e.hasMoreElements()) {
      Member member = (Member)e.nextElement();
      tellMember(talker,member,message,priority);
    }
  }

  public static void tellMembers(Member talker, Vector friends,
				 String message, int priority) {
    Club.debug(talker+") "+message);
    for (int i = 0; i<friends.size(); i++ ) {
      Member member = Member.findMember((String)friends.elementAt(i));
      tellMember(talker,member,message,priority);
    }
  }

  // Master Writer
  public static void tellMember(Member talker, Member member,
				String str, int priority) {
    if (member==null || !member.listensTo(talker) ||
	talker==member || str==null)
      return;
    OutputStreamWriter out = member.getWriter();
    if (out==null)
      return;	// player just logged out, no more writer
    Client c = member.getClient();
    if (c==null)
      return;

    long now = System.currentTimeMillis();

    // Possible further message prioritization handling:
    // - low priority messages just stuff in queue for each client
    //   to pickup, vs making this client wait to tell everyone
    // - give players higher priority than observers
    // - when server gets loaded, start to drop all P3 messages
    
    // P3 messages: report game result, or someone shouting
    if (priority==P3 && (Client.size()>75 || c.isIdle(now,5))) {
      return;			// not too important
    }
    // P2 messages: member observe/join/leave table
    if (priority==P2 && c.isIdle(now,10)) {
      return;
    }
    // P1 messages: game move, someone talk or whisper
    c.send(str);
  }
}
        
