/** (C) Game Page Network, Inc., Confidential, All Rights Reserved */
// Client.java
// --paul@gamepage.net, 22jul97

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

/** Thread to manage a client session socket
 */
class Client extends Thread {
  // Class Fields
  public static Vector clients = new Vector(200);
  public static Enumeration elements() { return clients.elements(); }
  public static int size() { return clients.size(); }

  // Instance Fields
  private Thread             thread;
  private int		     sessionID;// client crypt to Member.login
  private Socket             socket;
  private InetAddress        address;
  private long               connected;
  public long		     access; // timestamp most recent request
  private long               receiveCount;
  private long               sendCount;
  private BufferedReader     in;
  private OutputStreamWriter out;
  private Member             member;
  private boolean	     active = true; // until user Quits
  private boolean            debug;

  // Field Access Methods
  public InetAddress getAddress()    { return this.address; }
  public OutputStreamWriter getWriter() { return this.out; }
  public Member getMember()             { return this.member; }
  public void setMember(Member member)  { this.member = member; } // login
  public void setDebug(boolean value) { this.debug = value; }
  
  // -----------------------------------------------------------------
  /** Constructor for client session thread.
   * Saves info about client socket, address, connect time, and opens streams.
   */
  public Client(Socket socket) {
    // Cf ORA JNP p.168; Disable Nagle's small packet buffering:
    try { socket.setTcpNoDelay(true); }
    catch (IOException e) { warning(e,"setTcpNoDelay"); }
    
    this.socket = socket;
    address     = socket.getInetAddress();
    connected   = System.currentTimeMillis();
    member      = null;

    // Semi-random session ID for Member.login:
    //sessionID = (int)(connected&0xffff) ^ (address.hashCode()&0xffff);
    sessionID = Club.random();
    if (sessionID<0) sessionID = -sessionID;
    if (sessionID<1000) sessionID += 1000;

    Runtime r = Runtime.getRuntime();
    log(address.getHostName()+" connect "+clients.size()+" "+
	(r.freeMemory()/1000) + "/" + (r.totalMemory()/1000));
	
    clients.addElement(this); // record instance with class
    
    try {
      InputStream is = socket.getInputStream();
      in  = new BufferedReader(new InputStreamReader(is));
      out = new OutputStreamWriter(socket.getOutputStream());
      Club.sleep(1);		// yield hack
    } catch (IOException err) {
      warning(err,"Client.open()!");
    }
  }

  // ----------------------------------------------------------------------
  /** Called by Thread start(), which is invoked by Club main().
   */
  public void run() {
    thread = Thread.currentThread();
    thread.setName("Client: "+
		   ((address!=null) ?
		    address.getHostName() : "??"));
    try {
      send("server "+Club.name()+" "+sessionID());

      while (active) {
        String request = receive(); // blocks
        if ( !active || request==null ) { // end of stream, shutdown thread
          // finally clause below will logout member
          return;
        } else if ( request.equals("") ) { // blank input line, try again
          send("?");
          continue;
        }
	access = System.currentTimeMillis(); // timestamp last request
        
        StringTokenizer args = new StringTokenizer(request);
        String name = args.nextToken();
	int argc = args.countTokens();

        // 1. Lookup the command:
        Object obj = Command.lookup(name, argc, this);
	if (obj instanceof String) {
	  send("! "+(String)obj); // informative error message
	  log((String)obj);
          continue;
        }
	if (!(obj instanceof Command)) {
	  log("command error: "+request);
	  continue;
	}
	Command command = (Command)obj;

	// 2. Run the command process method:
	try { command.process(args,this); }
	catch (Exception e) {
	  warning(e,"Client command.process error: "+request);
	  Club.sleep(1);	// yield hack
	  send("! exception "+request);
	  continue;
	}
	if (debug) {
	  long ms = System.currentTimeMillis() - access;
	  log("> "+request+": "+ms+"ms");
	}

	long seconds = (System.currentTimeMillis() - connected)/1000 + 1;
	if ((receiveCount/seconds) > 1)
	  Club.sleep(2000);	// slow down bots a little?
      } // end while(true) main receive-process-respond loop
      
    } catch(IOException e) {
      log("Client.run() IOexception");

    } finally {
      log("logout I"+receiveCount+" O"+sendCount);
      Table.logout(member,"disconnect "+member);
      close(false);		// normal healthy shutdown
    }
  }

  // -----------------------------------------------------------------
  // Utilities

  /** Has this client been idle for MINUTES or more? */
  public boolean isIdle(long now, int minutes) {
    long recent = now - 60*1000 * minutes;
    return(access < recent);	// last access before MINUTES ago
  }
  public String idleMinutes() {
    long now = System.currentTimeMillis();
    long minutes = (now - this.access) / 60000;
    long hours = minutes / 60;
    minutes = minutes % 60;
    return (hours + ":" + ((minutes<10)?"0":"") + minutes);
  }

  public void debug(String message) {
    if (debug)
      log(message);
  }

  public static void debug(Client client) {
    StringBuffer buf = new StringBuffer(80*(clients.size()+1));
    buf.append("# Client.debug() - "+clients.size()+"\n");
    Enumeration e = clients.elements();
    while (e.hasMoreElements()) {
      Client c = (Client)e.nextElement();
      buf.append(c.id()+" "+
		 Club.dayTime(c.connected)+" "+
		 c.idleMinutes()+
		 " I"+c.receiveCount+
		 " O"+c.sendCount+"\n");
    }
    System.out.println(buf.toString());
  }

  // -----------------------------------------------------------------
  public int sessionID() { return sessionID; }
  
  public String id() {
    if (member!=null)
      return member.toString();
    if (address!=null)
      return address.getHostAddress();
    else
      return "*";
  }

  public String sessionMinutes() {
    long now = System.currentTimeMillis();
    long minutes = (now - connected) / 60000;
    long hours = minutes / 60;
    minutes = minutes % 60;
    return(hours + ":" + ((minutes<10)?"0":"") + minutes);
  }
  
  private String IPname() {
    if (address==null)
      return "*";
    String addr = address.getHostAddress();
    String name = address.getHostName();
    return addr + (addr.equals(name)?"":"="+name);
  }
  public void log(String message) {
    Club.log(id()+" "+message);
  }
  private void warning(String message) {
    warning(null,message);
  }
  private void warning(Exception e,String message) {
    Club.warning(e,id()+" "+message);
  }

  // -----------------------------------------------------------------
  // Termination Types:
  // 1. QuitCommand graceful: logout
  // 2. Disconnect via send/receive IOException: disconnect
  // 3. Reaper idle timeout: zombie
  // 4. Coach EjectCommand: eject
  // 5. ShutdownCommand.
  // 6. Force LoginCommand.

  public void prepareQuit() {		// called by QuitCommand
    this.active = false;
  }
  
  // Called by Club.shutdown.
  public static void shutdown(Client coach, String message) {
    coach.log("shutdown "+message);
    Enumeration e = clients.elements();
    while (e.hasMoreElements()) {
      Client c = (Client)e.nextElement();
      System.out.println("shutdown "+c.getMember());
      c.send("shutdown server now");
      if (c!=coach)
	c.close(true);
    }
    System.out.println("shutdown done");
  }
  
  // Called by a coach EjectCommand.
  public void eject(Member badguy) {
    for (int i=0; i<clients.size(); i++) {
      Client c = (Client)clients.elementAt(i);
      if (c.member==badguy) {
	log("eject "+badguy);	// this coach ejects badguy
	badguy.dump();
	Table.logout(badguy,"eject "+badguy);
	c.close(false);
	return;
      }
    }
    log("eject no "+badguy);
  }

  // Called by many different termination methods.
  public void close(boolean force) {
    //log("closing");
    active = false;
    member = null;
    clients.removeElement(this); // make class forget this instance
    // Cf ORA JNP p.168; terminate any lingering datagrams:
    if (force && socket!=null) {
      try { socket.setSoLinger(false,0); }
      catch (SocketException e) {}
    }
    if (in!=null) {
      //try { in.close(); }	// blocks
      //catch (IOException e1) {}
      in = null;
    }
    if (out!=null) {
      //try { out.flush(); out.close(); }	// blocks
      //catch (IOException e2) {}
      out = null;
    }
    if (socket!=null) {
      //try { socket.close(); }	// blocks
      //catch (IOException e3) {}
      socket = null;
    }
    if (Thread.currentThread()!=thread && thread!=null) {
      log("close:interrupt now");
      thread.interrupt();
      log("close.interrupt ok");
    }
    // http://java.sun.com/products/jdk/1.2/docs/guide/misc/threadPrimitiveDeprecation.html
    //stop();			// stop run method now
  }

  // -----------------------------------------------------------------
  // Client Communication
  
  public void send(StringBuffer buf) {
    if (buf==null || buf.length()<1) return;
    send(buf.toString());
  }
  public void send(String str) {
    sendCount++;
    try {
      if (str==null || out==null) return;
      //synchronized(out)
      //if (out==null) return;
      out.write(str+"\n",0,str.length()+1);
      out.flush();
    } catch (IOException e) {
      log("Client.send: IOException");
      active = false;		// will cause disconnect?
      Table.logout(member,"disconnect "+member);
      close(true);
    }
  }

  /** @return String read on this line, without line termination or 
   * leading/trailing whitespace chararacters,
   * returning null when at end of stream.
   */
  private String receive() throws IOException {
    Club.sleep(1);		// yield hack
    receiveCount++;
    String line = null;
    if (in==null) return null;	// race condition!
    //synchronized(in)
    //if (in==null) return null;
    line = in.readLine();	// blocks
    if (line==null) {		// connection was terminated    
      log("terminated");	// disconnect vs graceful quit
      return(null);
    }
    line = line.trim();		// eat leading and trailing whitespace
    return(line);
  }
}
