import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Pattern;

public class ChatServer {
    public static final int HOST_PORT = 8080;
    
    public static void main(String[] args) {
        Server server = new Server();
        new Thread(server).start();
        server.runServer();
    }
    
    private static class OutboundMessage {
        private Client recipient;
        private String message;
        
        OutboundMessage(Client recipient, String message) {
            this.recipient = recipient;
            this.message = message;
        }
    }
    
    private static class Server implements Runnable {
        private ServerModel model;
        private LinkedBlockingQueue<OutboundMessage> outbound = new LinkedBlockingQueue<OutboundMessage>();
        
        private Server() {
            model = new ServerModel(this);
        }
        
        public void runServer() {
            ServerSocket server;
            try {
                server = new ServerSocket(HOST_PORT);
            } catch (IOException e) {
                System.err.printf("Server: Could not start: %s\n", e);
                return;
            }
            
            System.out.printf("Server started listening on port %d\n", HOST_PORT);
            
            while (true) {
                Socket connection;
                try {
                    connection = server.accept();
                } catch (IOException e) {
                    System.err.printf("Server: Error accepting connection\n", e);
                    return;
                }
                
                Client client = new Client(model, connection);
                model.addClient(client);
                new Thread(client).start();
            }
        }
        
        public void run() {
            while (true) {
                try {
                    OutboundMessage out = outbound.take();
                    out.recipient.send(out.message);
                } catch (InterruptedException e) {
                    break;
                }
            }
        }
        
        public void queueOutbound(Client recipient, String arg0, String... args) {
            StringBuilder buffer = new StringBuilder(arg0);
            for (String arg : args) {
                buffer.append(" ");
                buffer.append(arg);
            }
            try {
                outbound.put(new OutboundMessage(recipient, buffer.toString()));
            } catch (InterruptedException e) {
                throw new RuntimeException("put interrupted");
            }
        }
    }

    private static class ServerModel {
        private Server server;
        private Object lock = new Object();
        private ArrayList<Client> clients = new ArrayList<Client>();
        private ArrayList<ClientModel.Message> messages = new ArrayList<ClientModel.Message>();
        
        public ServerModel(Server server) {
            this.server = server;
        }
        
        public void send(Client recipient, String arg0, String... args) {
            server.queueOutbound(recipient, arg0, args);
        }
        
        public void addClient(Client toAdd) {
            synchronized (lock) {
                clients.add(toAdd);
            }
        }
        
        public void post(Client source, String message) {
            String userId = source.getUserId();
            synchronized (lock) {
                messages.add(new ClientModel.Message(source.getUserId(), message));
                while (messages.size() > 30) {
                    messages.remove(0);
                }
                for (Client to : clients) {
                    server.queueOutbound(to, "post", userId, message);
                }
            }
        }
        
        public boolean isUserIdTaken(String userid) {
            synchronized (lock) {
                for (Client client : clients) {
                    String id = client.getUserId();
                    if (id != null && id.equals(userid)) {
                        return true;
                    }
                }
                return false;
            }
        }
        
        public void handleUserEnter(Client source) {
            synchronized (lock) {
                String userId = source.getUserId();
                for (Client user : clients) {
                    String other = user.getUserId();
                    if (other != null) {
                        server.queueOutbound(source, "useradd", other);
                    }
                }
                for (ClientModel.Message msg : messages) {
                    server.queueOutbound(source, "post", msg.getSender(), msg.getMessage());
                }
                for (Client to : clients) {
                    if (to != source) {
                        server.queueOutbound(to, "useradd", userId);
                    }
                }
            }
        }

        public void announceUserExit(Client source) {
            String userId = source.getUserId();
            if (userId == null) return;
            synchronized (lock) {
                clients.remove(source);
                for (Client to : clients) {
                    if (to != source) {
                        server.queueOutbound(to, "userdel", userId);
                    }
                }
            }
        }
    }

    private static class Client implements Runnable {
        private ServerModel server;
        private Socket connection;
        private PrintWriter out;
        private String userid = null;
        private Object lock = new Object();
        
        public Client(ServerModel server, Socket connection) {
            this.server = server;
            this.connection = connection;
            try {
                out = new PrintWriter(connection.getOutputStream());
            } catch (IOException e) {
                System.err.printf("Client: no output stream: %s\n", e);
            }
        }
        
        public String getUserId() {
            return userid;
        }
        
        public void send(String message) {
            synchronized (lock) {
                if (out != null) {
                    out.println(message);
                    out.flush();
                    if (out.checkError()) {
                        logoutUser();
                    }
                }
            }
        }
        
        private void logoutUser() {
            boolean alreadyOut;
            synchronized (lock) {
                alreadyOut = out == null;
                out = null;
            }
            if (!alreadyOut) {
                System.out.printf("%s: logout\n", userid == null ? "??" : userid);
                try {
                    Socket conn = connection;
                    connection = null;
                    conn.close();
                } catch (IOException e) { }
                server.announceUserExit(this);
            }
        }
        
        public void run() {
            InputStream istream;
            try {
                istream = connection.getInputStream();
            } catch (IOException e) {
                System.err.printf("Client: no input stream: %s\n", e);
                return;
            }
            
            BufferedReader in = new BufferedReader(new InputStreamReader(istream));
            while (true) {
                String line;
                try {
                    line = in.readLine();
                } catch (IOException e) {
                    System.err.printf("Client: read error: %s\n", e);
                    break;
                }
                if (line == null) break;
                
                String[] parts = line.split("\\s+", 3);
                String msgId = parts.length >= 1 ? parts[0] : "";
                String op = parts.length >= 2 ? parts[1] : "";
                String arg = parts.length >= 3 ? parts[2] : "";
                
                if (op.equals("login")) {
                    System.out.printf("%s: login\n", arg);
                    if (!Pattern.matches("^[a-zA-Z0-9_]+$", arg)) {
                        server.send(this, "err", msgId, "User ID can contain only alphanumeric characters");
                        break;
                    } else if (arg.equals(userid)) {
                        server.send(this, "err", msgId, "You cannot log in a second time");
                    } else if (server.isUserIdTaken(arg)) {
                        server.send(this, "err", msgId, "User ID is already taken by another user");
                        break;
                    } else {
                        userid = arg;
                        server.send(this, "ok", msgId);
                        server.handleUserEnter(this);
                    }
                } else if (op.equals("logout")) {
                    server.send(this, "ok", msgId);
                    break;
                } else if (op.equals("post")) {
                    System.out.printf("%s: post \"%s\"\n", userid, arg);
                    if (userid == null) {
                        server.send(this, "err", msgId, "Cannot post before logging in");
                    } else if (arg.equals("")) {
                        server.send(this, "err", msgId, "cannot post empty message");
                    } else if (!Pattern.matches("^[a-zA-Z0-9 .,?!'();:\"-]+$", arg)) {
                        server.send(this, "err", msgId, "Message contains invalid characters");
                    } else {
                        server.post(this, arg);
                        server.send(this, "ok", msgId);
                    }
                } else {
                    System.out.printf("%s: unknown op \"%s\"\n", userid, op);
                    server.send(this, "err", msgId, "unrecognized operation");
                }
            }
            try { Thread.sleep(100;) } catch (InterruptedException e) { }
            logoutUser();
        }
    }
}
