/**
 * <p>
 * Materialien zu den zentralen NRW-Abiturpruefungen im Fach Informatik ab 2018
 * </p>
 * <p>
 * Klasse Server
 * </p>
 * <p>
 * Objekte von Unterklassen der abstrakten Klasse Server ermoeglichen das
 * Anbieten von Serverdiensten, so dass Clients Verbindungen zum Server mittels
 * TCP/IP-Protokoll aufbauen koennen. Zur Vereinfachung finden Nachrichtenversand
 * und -empfang zeilenweise statt, d. h., beim Senden einer Zeichenkette wird ein
 * Zeilentrenner ergaenzt und beim Empfang wird dieser entfernt.
 * Verbindungsannahme, Nachrichtenempfang und Verbindungsende geschehen
 * nebenlaeufig. Auf diese Ereignisse muss durch Ueberschreiben der entsprechenden
 * Ereignisbehandlungsmethoden reagiert werden. Es findet nur eine rudimentaere
 * Fehlerbehandlung statt, so dass z.B. Verbindungsabbrueche nicht zu einem
 * Programmabbruch fuehren. Einmal unterbrochene oder getrennte Verbindungen
 * koennen nicht reaktiviert werden.
  * </p>
 *
 * @author Qualitaets- und UnterstuetzungsAgentur - Landesinstitut fuer Schule
 * @version 30.08.2016
 */
import java.net.*;
import java.io.*;

public abstract class Server
{
    private NewConnectionHandler connectionHandler;
    private List<ClientMessageHandler> messageHandlers;

    private class NewConnectionHandler extends Thread
    {
        private ServerSocket serverSocket;
        private boolean active;

        public NewConnectionHandler(int pPort)
        {
            try
            {
                serverSocket = new ServerSocket(pPort);
                start();
                active = true;
            }
            catch (Exception e)
            {
                serverSocket = null;
                active = false;
            }
        }

        public void run()
        {
            while (active)
            {
                try
                {
                    //Warten auf Verbdinungsversuch durch Client:
                    Socket clientSocket = serverSocket.accept();
                    // Eingehende Nachrichten vom neu verbundenen Client werden
                    // in einem eigenen Thread empfangen:
                    addNewClientMessageHandler(clientSocket);
                    processNewConnection(clientSocket.getInetAddress().getHostAddress(),clientSocket.getPort());
                }

                catch (IOException e)
                {
                    /*
                     * Kann keine Verbindung zum anfragenden Client aufgebaut werden,
                     * geschieht nichts.
                     */
                }
            }
        }

        public void close()
        {
            active = false;
            if(serverSocket != null)
                try
                {
                    serverSocket.close();
                }
                catch (IOException e)
                {
                    /* 
                     * Befindet sich der ServerSocket im accept()-Wartezustand oder wurde
                     * er bereits geschlossen, geschieht nichts.
                     */
                }
        }
    }

    private class ClientMessageHandler extends Thread
    {
        private ClientSocketWrapper socketWrapper;
        private boolean active;

        private class ClientSocketWrapper
        {
            private Socket clientSocket;
            private BufferedReader fromClient;
            private PrintWriter toClient;

            public ClientSocketWrapper(Socket pSocket)
            {
                try
                {
                    clientSocket = pSocket;
                    toClient = new PrintWriter(clientSocket.getOutputStream(), true);
                    fromClient = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                }
                catch (IOException e)
                {
                    clientSocket = null;
                    toClient = null;
                    fromClient = null;
                }
            }

            public String receive()
            {
                if(fromClient != null)
                    try
                    {
                        return fromClient.readLine();
                    }
                    catch (IOException e)
                    {
                    }
                return(null);
            }

            public void send(String pMessage)
            {
                if(toClient != null)
                {
                    toClient.println(pMessage);
                }
            }

            public String getClientIP()
            {
                if(clientSocket != null)
                    return(clientSocket.getInetAddress().getHostAddress());
                else
                    return(null); //Gemaess Java-API Rueckgabe bei nicht-verbundenen Sockets
            }

            public int getClientPort()
            {
                if(clientSocket != null)
                    return(clientSocket.getPort());
                else
                    return(0); //Gemaess Java-API Rueckgabe bei nicht-verbundenen Sockets
            }

            public void close()
            {
                if(clientSocket != null)
                    try
                    {
                        clientSocket.close();
                    }
                    catch (IOException e)
                    {
                        /*
                         * Falls eine Verbindung getrennt werden soll, deren Endpunkt
                         * nicht mehr existiert bzw. ihrerseits bereits beendet worden ist,
                         * geschieht nichts.
                         */
                    }
            }
        }

        private ClientMessageHandler(Socket pClientSocket)
        {
            socketWrapper = new ClientSocketWrapper(pClientSocket);
            if(pClientSocket!=null)
            {
                start();
                active = true;
            }
            else
            {
                active = false;
            }
        }

        public void run()
        {
            String message = null;
            while (active)
            {
                message = socketWrapper.receive();
                if (message != null)
                    processMessage(socketWrapper.getClientIP(), socketWrapper.getClientPort(), message);
                else
                {
                    ClientMessageHandler aMessageHandler = findClientMessageHandler(socketWrapper.getClientIP(), socketWrapper.getClientPort());
                    if (aMessageHandler != null)
                    {
                        aMessageHandler.close();
                        removeClientMessageHandler(aMessageHandler);
                        processClosingConnection(socketWrapper.getClientIP(), socketWrapper.getClientPort());
                    }
                }
            }
        }

        public void send(String pMessage)
        {
            if(active)
                socketWrapper.send(pMessage);
        }

        public void close()
        {
            if(active)
            {
                active=false;
                socketWrapper.close();
            }
        }

        public String getClientIP()
        {
            return(socketWrapper.getClientIP());
        }

        public int getClientPort()
        {
            return(socketWrapper.getClientPort());
        }

    }

    public Server(int pPort)
    {   
        connectionHandler = new NewConnectionHandler(pPort);
        messageHandlers = new List<ClientMessageHandler>();
    }
    
    public boolean isOpen()
    {
        return(connectionHandler.active);
    }
    
    public boolean isConnectedTo(String pClientIP, int pClientPort)
    {
         ClientMessageHandler aMessageHandler = findClientMessageHandler(pClientIP, pClientPort);
         if (aMessageHandler != null)
             return(aMessageHandler.active);
         else
             return(false);
    }

    public void send(String pClientIP, int pClientPort, String pMessage)
    {
        ClientMessageHandler aMessageHandler = this.findClientMessageHandler(pClientIP, pClientPort);
        if (aMessageHandler != null)
            aMessageHandler.send(pMessage);
    }

    public void sendToAll(String pMessage)
    {
        synchronized(messageHandlers)
        {
            messageHandlers.toFirst();
            while (messageHandlers.hasAccess())
            {
                messageHandlers.getContent().send(pMessage);
                messageHandlers.next();
            }
        }
    }
    
    public void closeConnection(String pClientIP, int pClientPort)
    {
        ClientMessageHandler aMessageHandler = findClientMessageHandler(pClientIP, pClientPort);
        if (aMessageHandler != null)
        {
            processClosingConnection(pClientIP, pClientPort);
            aMessageHandler.close();
            removeClientMessageHandler(aMessageHandler);
        }

    }

    public void close()
    {
        connectionHandler.close();
        
        synchronized(messageHandlers)
        {
            ClientMessageHandler aMessageHandler;
            messageHandlers.toFirst();
            while (messageHandlers.hasAccess())
            {
                aMessageHandler = messageHandlers.getContent();
                processClosingConnection(aMessageHandler.getClientIP(), aMessageHandler.getClientPort());
                aMessageHandler.close();
                messageHandlers.remove();
            }
        }

    }
    public abstract void processNewConnection(String pClientIP, int pClientPort);

    public abstract void processMessage(String pClientIP, int pClientPort, String pMessage);

    public abstract void processClosingConnection(String pClientIP, int pClientPort);
       
    private void addNewClientMessageHandler(Socket pClientSocket)
    {
        synchronized(messageHandlers)
        {
            messageHandlers.append(new Server.ClientMessageHandler(pClientSocket));
        }
    }

    private void removeClientMessageHandler(ClientMessageHandler pClientMessageHandler)
    {
        synchronized(messageHandlers)
        {
            messageHandlers.toFirst();
            while (messageHandlers.hasAccess())
            {
                if (pClientMessageHandler ==  messageHandlers.getContent())
                {
                    messageHandlers.remove();
                    return;
                }
                else
                    messageHandlers.next();
            }
        }
    }

    private ClientMessageHandler findClientMessageHandler(String pClientIP, int pClientPort)
    {
        synchronized(messageHandlers)
        {
            ClientMessageHandler aMessageHandler;
            messageHandlers.toFirst();

            while (messageHandlers.hasAccess())
            {
                aMessageHandler = messageHandlers.getContent();       
                if (aMessageHandler.getClientIP().equals(pClientIP) && aMessageHandler.getClientPort() == pClientPort)
                    return (aMessageHandler);
                messageHandlers.next();
            }
            return (null);
        }
    }

}