//cs3.java --- by tsaiwn@csie.nctu.edu.tw 
// javac cs3.java
// java cs3 3388      (Default port 5566 is used if port == 0 )
////// 要進入聊天室的用 telnet host_name port_listening 測試即可
/// For example,   telnet 127.0.0.1 3388
import java.io.*;
import java.net.*;
import java.util.*;
interface CMD_Constant { // 把所有共用常數集中在一個 interface
    public final int CMD_DATA  = -1;
    public final int CMD_QUIT  = 0;
    public final int CMD_MSG   = 1, CMD_LIST = 2;
    public final int CMD_QUERY = 3, CMD_NICK = 4;
    public final int CMD_HELP  = 999;
    public final String SECRET_NAME = "IloveGiGi IwantGiGi";
} /// 之後各class 只要 implements CMD_Constant 就能看到以上常數
public class cs3 {
    private final static int DEFAULT_PORT = 5566;
    private ServerSocket serverSock;
    private int portNumber;
    private Socket clientSock;
    public cs3 (int port) {  // constructor
        portNumber = port;
        try {
            serverSock = new ServerSocket( portNumber );
            System.out.println("cs3 started on port " +
                  portNumber + " at " + new Date().toString() );
            // then, wait for a client to connect into
            while ( true ) {
                clientSock = serverSock.accept();
                // after accept a connection, fork a thread to handle it
                ClientThread guest = new ClientThread(clientSock);
                guest.start( );  // it will call run( ) in ClientThread
            }
        } catch ( Exception e ) {
            System.err.println("Cannot startup chat server!");
        }
    } // cs3 constructor
    public static void main(String[ ] argv) {
        if ( argv.length != 1 ) {
            System.out.println("Usage: java cs3 [port number]");
            System.exit(0);
        }
        int port = 0;
        try {
            port = Integer.parseInt( argv[0] );
        } catch ( Exception e ) {
            System.err.println("Invalid port number");  System.exit(1);
        }
        if(port == 0) port = DEFAULT_PORT;
        new cs3( port );  // will call constructor
    } // main
} // class cs3  
//////////////////////////////////////////////////////////////
class ClientThread extends Thread implements CMD_Constant {
    //static private Hashtable onlineUsers = new Hashtable( );  // MAP
    //以上是舊版寫法, JDK 1.5 (5.0) 之後 support template 寫法  
    static private Hashtable<String, ClientThread> onlineUsers = 
            new Hashtable<String, ClientThread>( );  // MAP
    static private String lock = "a Lock"; // static ensure one copy

    public ClientThread(Socket skt) { mySock = skt; }   // constructor
    private void setNickname(String name) {
        synchronized ( lock ) { onlineUsers.put(name, this); }
    } // setNickname
    private void doBcast(String msg) {
       for (Enumeration e=onlineUsers.elements();e.hasMoreElements();){
           ClientThread c = (ClientThread) e.nextElement();
           if ( c != this ) c.writeLine(msg); // 不必寫給自己 ! 
       }
    } // doBcast
    private void doList( ) {
       for(Enumeration e=onlineUsers.elements();e.hasMoreElements();) {
           ClientThread c = (ClientThread) e.nextElement();
           writeLine("CHAT *** [" + c.getNickname() +
                              "@" + c.getRemoteAddr() + "] ***");
       } // for
    } // doList

    private Socket mySock;  private String myName;
    private String remoteAddr;   // 連著我的對方 IP address
    private BufferedReader in;  private PrintWriter out;
    private PrintStream tty = System.out;   // tty 只是簡寫而已
    private Date now;
    private String getRemoteAddr() { return remoteAddr; }
    private String getNickname() { return myName; }

    public void run() {
        now = new Date();
        try {
            remoteAddr = mySock.getInetAddress().getHostAddress();
             // 然後取得InputStream並包成 BufferedReader 方便 readLine()
            in = new BufferedReader(
                  new InputStreamReader(mySock.getInputStream()) );
             // 再取得 OutputStream 並包成 PrintWriter
            out = new PrintWriter(
                new OutputStreamWriter(mySock.getOutputStream()), true );
             // 接著, 要求連線者輸入 nickname
            myName = askNickname();
             // 若輸入怪異的nickname例如Control_C 則終止連線
            if (stranger(myName) ){ close(); return; }
             // 廣播給所有聊天室的人
            doBcast("CHAT *** " + myName + " is coming in ***");
             // 並在 console 上顯示 (tty == System.out)
            tty.println(myName + "@"+remoteAddr+" enters the Chat Room "
               + now.toString() );
             // writeLine(msg) 會把 msg 寫到目前連線者終端機
            writeLine("CHAT *** Welcome 歡迎 "+myName+" 進入聊天室 ***");
            writeLine("You can type '/help' for help");
            String cmd, msg; int mode;
           FOO:
            while ( (cmd = in.readLine()) != null ) {
                StringTokenizer stkn = new StringTokenizer(cmd, " \t");
                String command = " ";
                if(stkn.countTokens( ) >= 1) command = stkn.nextToken();
                msg = " ";
                if(stkn.hasMoreTokens()) msg = stkn.nextToken("\n");
                mode = parseCommand(command.toUpperCase());
                switch ( mode ) {
                    case CMD_MSG:   doMsg(msg);   break;
                    case CMD_LIST:  doList();     break;
                    case CMD_HELP:  doHelp();    break;
                    case CMD_QUIT:   now = new Date();
                         tty.println(myName + "  said BYE   at   " +
                                      now.toString() );
                         doBcast("["+myName + " saied Bye Bye! ]");
                                    break FOO;
                    case CMD_DATA:  doBcast("["+myName + "] " + cmd);
                         tty.println(myName + ": " + cmd);
                                    break;
                } // switch
            } // while  FOO:
        } catch ( Exception e ) { tty.println(e.toString()); }
        now = new Date( );
        tty.println(now.toString() + "  one thread stop");
        close();
    }
    private void writeLine(String msg) {
        try { out.println(msg+"\r");     // with carriage-return
        } catch ( Exception e ) { }   // simply ignore the Exception
    } // writeLine(msg) 把 msg 由此連線的 out 寫到連線者的 InputStream

    private void close( ) {
        try { onlineUsers.remove(myName); 
           if ( mySock != null ) mySock.close();
        } catch ( Exception e ) { }
    } // close( ) 關閉此連線之 socket 及其 In/Out Stream

    static int numUsers = 0;  // should be static (one copy for all)
    String genName( ) { return "pp" +  ++numUsers; }
    private String askNickname() throws Exception {
        String name =  genName( );
        writeLine("[now is " + new Date()+"\nYour Nickname is " + name);
        setNickname(name); return name;
    }  // askNickname()
    private boolean stranger(String myName) { return false; }
    private void doMsg(String cmd) { writeLine("not Implemented yet!");}
    void doHelp() { writeLine("commands: /MSG /LIST /HELP /QUIT"); }
    private int parseCommand(String x) {
        if ( x.startsWith("/QUIT") ||  x.startsWith("/BYE") ) 
            return CMD_QUIT;
        if ( x.startsWith("/LIST") ) return CMD_LIST;
        if (x.startsWith("/MSG")||x.startsWith("/TELL"))return CMD_MSG;
        if (x.startsWith("/HELP")||x.startsWith("/?")) return CMD_HELP;
        if ( x.startsWith("/") ) return CMD_HELP;  // error cmd
        return CMD_DATA;   // 不是 "/"開始的都當作 message data
    } // parseCommand
} // class
