package com.hypefiend.javagamebook.chatterbox;

import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;
import java.net.*;
import java.io.*;

import org.apache.log4j.*;

/**
 * ChatterServer,java
 *
 * A basic example of a multi-user chat application using the JDK 1.4 NIO libraries
 * 
 * @author <a href="mailto:bret@hypefiend.com">bret barker</a>
 * @version 1.0
 */
public class ChatterServer extends Thread {
    private static final int BUFFER_SIZE = 255;
    private static final long CHANNEL_WRITE_SLEEP = 10L;
    private static final int PORT = 10997;

    private Logger log = Logger.getLogger(ChatterServer.class);
    private ServerSocketChannel sSockChan;
    private Selector acceptSelector;
    private Selector readSelector;
    private SelectionKey selectKey;
    private boolean running;
    private LinkedList clients;
    private ByteBuffer readBuffer;
    private ByteBuffer writeBuffer;
    private CharsetDecoder asciiDecoder;
  
    public static void main(String args[]) {
    // konfiguracja log4j
    BasicConfigurator.configure();
    
    // tworzenie i uruchamianie ChatterServer 
    ChatterServer cs = new ChatterServer();
    cs.start();
    }
  
    public ChatterServer() {
    clients = new LinkedList();
    readBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
    writeBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
    asciiDecoder = Charset.forName( "ISO-8859-2").newDecoder();
    }

    private void initServerSocket() {
    try {
        // otwarcie nieblokującego kanału gniazda serwera
        sSockChan = ServerSocketChannel.open();
        sSockChan.configureBlocking(false);

        // łączenie z localhost na wybranym porcie
        InetAddress addr = InetAddress.getLocalHost();
        sSockChan.socket().bind(new InetSocketAddress(addr, PORT));

        // get a selector for multiplexing the client channels
        readSelector = Selector.open();
    }
    catch (Exception e) {
        log.error("błąd inicjalizacji serwera", e);
    }
    }

    public void run() {
    initServerSocket();

    log.info("ChatterServer działa");
    running = true;
    int numReady = 0;

    // block while we wait for a client to connect
    while (running) {
        // check for new client connections
        acceptNewConnections();
        
        // check for incoming mesgs
        readIncomingMessages();
        
        // sleep a bit
        try {
        Thread.sleep(100);
        }
        catch (InterruptedException ie) {
        }
    }
    }
    
    private void acceptNewConnections() {
    try {
        SocketChannel clientChannel;
        // since sSockChan is non-blocking, this will return immediately 
        // regardless of whether there is a connection available
        while ((clientChannel = sSockChan.accept()) != null) {
        addNewClient(clientChannel);
        log.info("połączenie z adresu: " + clientChannel.socket().getInetAddress()); 
        sendBroadcastMessage("logowanie z adresu: " + clientChannel.socket().getInetAddress(), clientChannel);
        sendMessage(clientChannel, "\n\nWitamy w programie ChatterBox, podłączonych jest " + 
                clients.size() + " użytkowników.\n");
        sendMessage(clientChannel, "Napisz 'quit' aby zakończyć.\n");
        }        
    }
    catch (IOException ioe) {
        log.warn("błąd przy wywołaniu accept(): ", ioe);
    }
    catch (Exception e) {
        log.error("wyjątek w acceptNewConnections()", e);
    }
    }

    private void readIncomingMessages() {
    try {
        // nieblokująca operacja select, natychmiast kończy 
        // pracę niezależnie od ilości gotowych kluczy
        readSelector.selectNow();
        
        // odczytanie kluczy
        Set readyKeys = readSelector.selectedKeys();
        
        // przeglądanie i przetwarzanie kluczy
        Iterator i = readyKeys.iterator();
        while (i.hasNext()) {
        SelectionKey key = (SelectionKey) i.next();
        i.remove();
        SocketChannel channel = (SocketChannel) key.channel();
        readBuffer.clear();
        
        // odczyt danych z kanału do bufora
        long nbytes = channel.read(readBuffer);
        
        // sprawdzenie końca strumienia
        if (nbytes == -1) { 
            log.info("rozłączenie: " + channel.socket().getInetAddress() + ", koniec strumienia");
            channel.close();
            clients.remove(channel);
            sendBroadcastMessage("wylogowanie: " + channel.socket().getInetAddress() , channel);
        }
        else {
            // pobranie StringBuffer zapisanego jako załącznik
            StringBuffer sb = (StringBuffer)key.attachment();
            
            // użycie CharsetDecoder do zamiany tych bajtów na ciąg
            // i dołączenie do naszego StringBuffer
            readBuffer.flip( );
            String str = asciiDecoder.decode( readBuffer).toString( );
            readBuffer.clear( );
            sb.append( str);
            
            // sprawdzenie kompletności wiersza
            String line = sb.toString();
            if ((line.indexOf("\n") != -1) || (line.indexOf("\r") != -1)) {
            line = line.trim();
            if (line.startsWith("quit")) {
                // klient kończy pracę, zamknięie kanału, usunięcie z listy i powiadomienie pozostałych klientów
                log.info("odebrane polecenie quit, zamykam kanał dla: " + channel.socket().getInetAddress());
                channel.close();
                clients.remove(channel);
                sendBroadcastMessage("wylogowanie: " + channel.socket().getInetAddress(), channel);
            }
            else {
                // mamy wiersz, wysyłamy go do wszystkich klientów
                log.info("rozgłaszanie: " + line);
                sendBroadcastMessage(channel.socket().getInetAddress() + ": " + line, channel);
                sb.delete(0,sb.length());
            }
            }
        }
        
        }        
    }
    catch (IOException ioe) {
        log.warn("błąd przy wykonaniu select(): ", ioe);
    }
    catch (Exception e) {
        log.error("wyjatek w run()", e);
    }
    
    }
    
    private void addNewClient(SocketChannel chan) {
    // add to our list
    clients.add(chan);
    
    // register the channel with the selector 
    // store a new StringBuffer as the Key's attachment for holding partially read messages
    try {
        chan.configureBlocking( false);
        SelectionKey readKey = chan.register(readSelector, SelectionKey.OP_READ, new StringBuffer());
    }
    catch (ClosedChannelException cce) {
    }
    catch (IOException ioe) {
    }
    }
    
    private void sendMessage(SocketChannel channel, String mesg) {
    prepWriteBuffer(mesg);
    channelWrite(channel, writeBuffer);
    }
    
    private void sendBroadcastMessage(String mesg, SocketChannel from) {
    prepWriteBuffer(mesg);
    Iterator i = clients.iterator();
    while (i.hasNext()) {
        SocketChannel channel = (SocketChannel)i.next();
        if (channel != from) 
        channelWrite(channel, writeBuffer);
    }
    }
    
    private void prepWriteBuffer(String mesg) {
    // fills the buffer from the given string
    // and prepares it for a channel write
    writeBuffer.clear();
    writeBuffer.put(mesg.getBytes());
    writeBuffer.putChar('\n');
    writeBuffer.flip();
    }
    
    private void channelWrite(SocketChannel channel, ByteBuffer writeBuffer) {
    long nbytes = 0;
    long toWrite = writeBuffer.remaining();

    // loop on the channel.write() call since it will not necessarily
    // write all bytes in one shot
    try {
        while (nbytes != toWrite) {
        nbytes += channel.write(writeBuffer);
        
        try {
            Thread.sleep(CHANNEL_WRITE_SLEEP);
        }
        catch (InterruptedException e) {}
        }
    }
    catch (ClosedChannelException cce) {
    }
    catch (Exception e) {
    } 
    
    // get ready for another write if needed
    writeBuffer.rewind();
    }
}

