package org.academiadecodigo.enuminatti.irc;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * MIT License
 * (c) 2017 Ricardo Constantino
 */

public class Server {
    private ServerSocket serverSocket;
    private ExecutorService threadPool;
    private CopyOnWriteArrayList<ServerWorker> clients;
    private int clientsConnected = 0;


    public static void main(String[] args) {
        Server server = new Server(getPort(args));
        server.listen();
    }

    public Server(int port) {
        try {
            this.serverSocket = new ServerSocket(port);
            System.out.println("Listening on port " + port);
            threadPool = Executors.newCachedThreadPool();
            clients = new CopyOnWriteArrayList<>();
        } catch (IOException e) {
            System.err.println("Couldn't bind to port " + port);
            e.printStackTrace();
        }
    }

    private void listen() {
        try {
            while (true) {
                ServerWorker client = new ServerWorker(serverSocket.accept());

                System.out.println(String.format("received new client <%s> %s", client, client.getHostMask()));

                broadcastToOthers(String.format("- %s \uD83D\uDE4C joined the server -", client), client);
                threadPool.submit(client);
                clients.add(client);
            }
        } catch (IOException e) {
            System.err.println("Couldn't connect to client.");
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }

    private void whisper(String message, ServerWorker from, String[] nicksToBroadcastTo) {
        System.out.println(String.format("whispering from <%s>:\n\t%s", from, message));

        for (ServerWorker to : nicksToWorkers(nicksToBroadcastTo)) {
            System.out.println("to <" + to + ">");
            to.sendMessage(String.format("%s: %s", from, message));
        }
    }

    private void broadcastToOthers(String message, ServerWorker from) {
        System.out.println(String.format("broadcasting from <%s> to everyone else:\n\t%s", from, message));

        for (ServerWorker client : clients) {
            if (from == client)
                continue;

            client.sendMessage(String.format("<%s> %s", from.getNick(), message));
        }
    }

    private void broadcastToAll(String message, ServerWorker from) {
        System.out.println(String.format("broadcasting from <%s> to everyone:\n\t%s", from, message));

        for (ServerWorker client : clients) {
            client.sendMessage(message);
        }
    }

    private static int getPort(String[] args) {
        int port = 0;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }
        return port != 0 ? port : 6667;
    }

    private boolean sendWhoIs(String nick, ServerWorker from) {
        ServerWorker req;
        if ((req = nickToWorker(nick)) == null) {
            req = from;
        }

        from.sendMessage(req.getWhoIs());
        return true;
    }

    private class ServerWorker implements Runnable {

        private Socket clientSocket;
        private BufferedReader in;
        private PrintWriter out;
        private String nick;

        public ServerWorker(Socket client) {
            clientSocket = client;
            nick = "Anonymous" + ++clientsConnected;
            try {
                in = new BufferedReader(new InputStreamReader(client.getInputStream()));
                out = new PrintWriter(new OutputStreamWriter(client.getOutputStream()), true);
                sendHelp();
            } catch (IOException e) {
                System.err.println("Couldn't open streams.");
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            try {
                String message = "";
                while ((message = in.readLine()) != null) {
                    handleMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                disconnectWorker(null);
            }
        }

        private void disconnectWorker(String quitMessage) {

            if (!clients.contains(this)) {
                return;
            }

            clients.remove(this);
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                broadcastToOthers(String.format("- %s \uD83D\uDC4B disconnected from server -%s",
                        nick, quitMessage != null ? " " + quitMessage : ""), this);
            }
        }

        private void handleMessage(String message) {
            if (message.length() < 1) {
                // discard empty messages
                return;
            }

            message = sanitizeText(message);
            if (!parseCommand(message)) {
                broadcastToOthers(message, this);
            }
        }

        private boolean parseCommand(String message) {

            String[] args = message.split(" +", 2);

            String cmd = args[0];
            String rest = args.length > 1 ? args[1] : null;

            switch (cmd) {
                case "NICK":
                    return changedNick(rest);
                case "QUIT":
                    disconnectWorker(rest);
                    return true;
                case "PRIVMSG":
                    return sentPrivateMsg(rest);
                case "HELP":
                    return sendHelp();
                case "NAMES":
                    return sendNickList();
                case "WHOIS":
                    return sendWhoIs(rest, this);
                default:
                    return false;
            }
        }

        private String getWhoIs() {
            ArrayList<String> whoIs = new ArrayList<>();
            whoIs.add(String.format("%s [%s:%d]", nick, clientSocket.getInetAddress(), clientSocket.getPort()));
            whoIs.add(" server : " + serverSocket.getInetAddress() + ":" + serverSocket.getLocalPort());
            whoIs.add("End of WHOIS");
            return String.join("\n", whoIs);
        }

        private boolean sendNickList() {
            ArrayList<String> names = new ArrayList<>();
            names.add(String.format("Clients connected (%d):", clients.size()));
            for (ServerWorker client : clients) {
                names.add("\t" + client);
            }
            sendMessage(String.join("\n", names));
            return true;
        }

        private boolean sendHelp() {
            StringBuilder helpMessage = new StringBuilder();
            helpMessage.append("NICK newNick - changes your nick to this one if available\n");
            helpMessage.append("QUIT [quit message] - disconnects your client with optional quit message.\n");
            helpMessage.append("PRIVMSG nick[,nick,...] message - sends private message to specified comma-separated list of nicks.\n");
            helpMessage.append("HELP - this message");
            sendMessage(helpMessage.toString());
            return true;
        }

        private boolean changedNick(String newNick) {
            if (newNick == null)
                return false;

            newNick = newNick.split("\\s+")[0];

            if (nickExists(newNick))
                return false;

            String oldNick = nick;
            setNick(newNick);
            broadcastToOthers(String.format("- %s changed nick to %s -", oldNick, nick), this);
            return true;
        }

        private boolean sentPrivateMsg(String rest) {
            if (rest == null) {
                return false;
            }
            String[] splitRest = rest.split(" ", 2);
            if (splitRest.length < 2) {
                return false;
            }
            String[] nicksTo = splitRest[0].split(",");

            whisper(splitRest[1], this, nicksTo);
            return true;
        }

        public void sendMessage(String message) {
            out.println(message);
        }

        private void setNick(String newNick) {
            nick = newNick;
            out.println("Nick changed to " + nick);
        }

        public String getNick() {
            return nick;
        }

        @Override
        public String toString() {
            return getNick();
        }

        public String getHostMask() {
            return "[" + clientSocket.getInetAddress() + ":" + clientSocket.getPort() + "]";
        }
    }

    private static String sanitizeText(String text) {
        return text.replaceAll("[^\\p{Print}]", "");
    }

    private ServerWorker nickToWorker(String nick) {
        if (nick == null) {
            return null;
        }

        nick = nick.toLowerCase();
        for (ServerWorker client : clients) {
            String clientNick = client.getNick().toLowerCase();
            if (clientNick.equals(nick)) {
                return client;
            }
        }
        return null;
    }

    private ServerWorker[] nicksToWorkers(String[] nicks) {
        Set<ServerWorker> workers = new HashSet<>();
        for (String nick : nicks) {
            workers.add(nickToWorker(nick));
        }
        return workers.toArray(new ServerWorker[]{});
    }

    private boolean nickExists(String newNick) {
        return nickToWorker(newNick) != null;
    }
}
