//ChatServer.java --- by tsaiwn@csie.nctu.edu.tw and jwwang@csie.nctu
////// http://www.csie.nctu.edu.tw/~tsaiwn/course/java/examples/network/
//--Demostrate using ServerSocket and Socket to build a chat server
// javac ChatServer.java
// java ChatServer 1234      (Default port 6789 is used if port == 0 )
////// 要進入聊天室的用 telnet host_name port_listening 測試即可
import java.io.*;
import java.net.*;
import java.util.*;

public class ChatServer
{
    private final static int DEFAULT_PORT = 6789;
    private ServerSocket serverSock;
    private int portNumber;
    private Socket clientSock;

    public ChatServer(int port) {  // constructor
        portNumber = port;
        try {
            serverSock = new ServerSocket( portNumber );
            System.out.println("ChatServer 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
                new ClientThread(clientSock).start();  //用thread 處理連線
                /// ClientThread guest = new ClientThread(clientSock);
                /// guest.start( );  // it will call run( )
            }
        } catch ( Exception e ) {
            System.err.println("Cannot startup ChatServer!");
        }
    } // ChatServer constructor

    public static void main(String[ ] argv) {
        if ( argv.length != 1 ) {
            System.out.println("Usage: java ChatServer [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 ChatServer( port );
    } // main
} // class ChatServer

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 就能看到以上常數

class ClientThread extends Thread implements CMD_Constant
{
    private Socket mySock;
    private String myName;
    private String remoteAddr;   // 連著我的對方 IP address
    private BufferedReader in;
    private PrintWriter out;
    private PrintStream tty = System.out;   // tty 只是簡寫而已

    static private Hashtable onlineUsers = new Hashtable( );  // MAP
    static private String lock = "This is a lock"; // static ensure one copy

    private Date now;

    public ClientThread(Socket skt) { mySock = skt; }    // constructor

    private String getNickname() { return myName; }
    private String getRemoteAddr() { return remoteAddr; }

    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_QUERY: doQuery(cmd.substring(6)); break;
                    case CMD_NICK:  doNick(msg);  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 doList( ) {
        for(Enumeration e = onlineUsers.elements(); e.hasMoreElements(); ) {
            ClientThread c = (ClientThread) e.nextElement();
            writeLine("CHAT *** [" + c.getNickname() +
                               "@" + c.getRemoteAddr() + "] ***");
        } // for
    } // doList

    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( ) {
        if(myName!=null && ! myName.equals(SECRET_NAME))
            doBcast("CHAT *** " + myName + " has left the chatroom ***");
        synchronized ( lock ) {
            try { onlineUsers.remove(myName); } catch ( Exception e ) { }
        } // synchronized critical section
        try {
            if ( in != null ) in.close();
            if ( out != null ) out.close();
            if ( mySock != null ) mySock.close();
        } catch ( Exception e ) { }
    } // close( ) 關閉此連線之 socket 及其 In/Out Stream

    private boolean stranger(String myName) { 
        //tty.println("=+= " + (myName.getBytes()[0])+" =" );
        //tty.println("=+= " + Character.isISOControl(myName.charAt(0)));
        if (Character.isISOControl(myName.charAt(0)) ) myName = "QUIT";
        if (myName.equals("QUIT")){ writeLine("! Thank you for coming!"); }
        if(myName == null || myName.equals("QUIT") ) { 
            tty.println(" --- from "+remoteAddr+" at "+now.toString());
            return true;
        } // "QUIT" is not allowed as a nick name
        if(myName.equals(SECRET_NAME)){
            tty.println("GiGi  from "+remoteAddr+" at "+now.toString());
            doList(); return true;
        }
        return false;   // a usual user name
    }

    private String askNickname() throws Exception {
        String name = null;
        boolean ok = false;
        writeLine("=> Enter Your Nickname: ");
       while(!ok) {
          name = in.readLine();
          while ( onlineUsers.containsKey(name) ) {
             writeLine("=> " + name + " exists! Re-enter your nickname: ");
             name = in.readLine();
          }
          ok = true;
          if (name.equalsIgnoreCase("yoshiki") ){
             System.out.println("!!! " + name + " is trying ... ");
             writeLine("? Yoshiki 你這日本的走狗滾開!");
             close(); return null;
          }
          if (name.endsWith("ki") || name.endsWith("kli") ||
             name.endsWith("KI") || name.endsWith("KLI") ){
             System.out.println("!!! " + name + " is trying ... ");
             writeLine("? No Japanese! Sorry! Re-enter your nickname: ");
             ok=false;
          }
          if (name.equals("tsaiwn") ){
             writeLine("? Do Not use God's name -- 別盜用別人 username");
             writeLine("=> Re-Enter Your Nickname: ");
             ok=false;
          }
          if (name.startsWith(" ") || name.startsWith("/") ){
             writeLine("? Do Not cheat me ! 別亂打!");
             writeLine("=> Re-Enter Your Nickname: ");
             ok=false;
          }
          if (name.equalsIgnoreCase("quit") ){ return "QUIT"; }
       }  // while(!ok) 
        if(! name.equals(SECRET_NAME)) setNickname(name);
        return name;
    }  // askNickname()

    private void setNickname(String name) {
        synchronized ( lock ) {
            onlineUsers.put(name, this);
        }
    }

    private void doMsg(String cmd) {
        cmd = cmd.trim();
        int pos = cmd.indexOf(' ');    // find tell whom
        if ( pos < 0 ) return;    // no message on the line
        String dst = cmd.substring(0, pos);
        String msg = cmd.substring(pos+1);
        msg = msg.trim(); if(msg==null) msg = " ";
        if ( onlineUsers.containsKey(dst) ) {
            ClientThread c = (ClientThread) onlineUsers.get(dst);
            c.writeLine("*** [" + myName + "] " + msg);
        } else {
            writeLine("CHAT *** " + dst + " not in the ROOM ***");
        }
    }

    private void doQuery(String cmd) {
        //if(cmd.charAt(0) == ' ') cmd = cmd.substring(1);
        cmd = cmd.trim();
        if ( onlineUsers.containsKey(cmd) ) {
            ClientThread c = (ClientThread) onlineUsers.get(cmd);
            writeLine("CHAT *** " + c.getNickname() + " is from " +
                                           c.getRemoteAddr() + " ***");
        } else {
            writeLine("CHAT *** " + cmd + " is UNKNOWN ***");
        }
    } // doQuery

    private void doNick(String cmd) {
        // not implemented yet
        writeLine("Not implemented yet to change nick name!");
    }

    private void doBcast(String msg) {
        for ( Enumeration e = onlineUsers.elements(); e.hasMoreElements(); ) {
            ClientThread c = (ClientThread) e.nextElement();
            if ( c != this ) c.writeLine(msg);
        }
    }

    private void doHelp() {
        writeLine("Available commands: /MSG /LIST /QUERY /NICK /HELP /QUIT");
    }

    private int parseCommand(String x) {
        if ( x.startsWith("/QUIT") ||  x.startsWith("/BYE") ) {
            return CMD_QUIT;
        } else if ( x.startsWith("/MSG") || x.startsWith("/TELL") ) {
            return CMD_MSG;
        } else if ( x.startsWith("/LIST") ) {
            return CMD_LIST;
        } else if ( x.startsWith("/QUERY") ) {
            return CMD_QUERY;
        } else if ( x.startsWith("/NICK") ) {
            return CMD_NICK;
        } else if ( x.startsWith("/HELP") || x.startsWith("/?") ) {
            return CMD_HELP;
        } else if ( x.startsWith("/") ) {
            return CMD_HELP;
        } else {   // 不是 "/"開始的都當作 message data
            return CMD_DATA;
        }
    } // parseCommand
}
