package fr.umlv.ir2.tcp;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * A Server that replies, for each line of text it receives from a client, 
 * with the uppercase line of the same text. This server requires two threads:
 * one is for accepting new client connections (blocking) and the other is 
 * for serving all other accepted clients (not blocking).
 * 
 * @author duris
 *
 */
public class UpperCaseAcceptBlockingServer implements Runnable {
  final static int DEFAULT_PORT = 1234; 
  final static int SIZE = 1024;
  private final ServerSocketChannel serverChannel;
  
  private final Selector selector = Selector.open();
  private final Object monitor = new Object();
  
  /**
   * Constructs a server bound to the given port. 
   * @param port the port to bind the server socket.
   * @throws IOException If an I/O error occurs,
   * if the bind operation fails, or if the socket is already bound.
   */
  public UpperCaseAcceptBlockingServer(int port) throws IOException {
    serverChannel = ServerSocketChannel.open();
    serverChannel.socket().bind(new InetSocketAddress(port)); 
  }
  
  /**
   * Constructs a server bound to the default port. 
   * @throws IOException If an I/O error occurs,
   * if the bind operation fails, or if the socket is already bound.
   */
  public UpperCaseAcceptBlockingServer() throws IOException {
    this(DEFAULT_PORT);
  }
  
  /**
   * Writes in buffer <code>out</code> the uppercase
   * version of the string found in buffer <code>in</code>.
   */
  void serve(ByteBuffer in, ByteBuffer out) throws IOException {
    // version "brutale" sans tenir compte du codage!!!
    byte[] buf = new byte[in.remaining()];
    in.get(buf);
    out.put(new String(buf).toUpperCase().getBytes());
  }
  
  /**
   * This version implements blocking server-socket
   * accept but non-blocking socket read from connections.
   */
  public void launchAcceptBlocking() throws IOException {
    // starts another thread (this.run()) to handle 
    // the channel selector that deals with accepted clients
    Thread thread = new Thread(this,"selectorThread");
    thread.start();
    
    SocketChannel clientChannel = null;
    while (true) {
      // accepts a new client connection 
      clientChannel = serverChannel.accept();
      System.err.println("New connection accepted from " + clientChannel.socket().getRemoteSocketAddress());
      
      // One must ensures that selector awakes and waits
      // the end of registration before enterring again in selection operation
      clientChannel.configureBlocking(false);
      /* a pretty good solution */
      synchronized (monitor) {
        selector.wakeup();
        clientChannel.register(selector,SelectionKey.OP_READ);
      }
      /* To illustrate deadlock 
      System.out.println("Main avant wakeup()");
      selector.wakeup();
      System.out.println("Main après wakeup()");
      clientChannel.register(selector,SelectionKey.OP_READ);
      System.out.println("Main après enregistrement");
      */
    }
  }
  
  /**
   * Handles the selection of ready channels and serves them.
   */
  public void run() {
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    
    // listen the selector forever 
    while (true) {
      try {
      
        // One must ensures that the selector, awaked by the main thread
        // for the registration process, waits this latter's end  
        synchronized (monitor) { /* nop */ } // synchronization barrier (pretty good solution)
        
        
        System.out.println("selectorThread avant select()");
        // blocks until something is readable on a channel
        selector.select();
        System.out.println("selectorThread après select()");
        
        // deals whith each ready channels
        for(Iterator<SelectionKey> iKeys = selectedKeys.iterator(); iKeys.hasNext(); ) {
          SelectionKey iKey = iKeys.next();
          SocketChannel clientChannel = (SocketChannel) iKey.channel();
          System.out.println("channel for " 
              + clientChannel.socket().getRemoteSocketAddress() 
              + " is ready to read");
          if (!useConnection(clientChannel)) {
            // end of stream
            iKey.cancel();
            clientChannel.close();
          }
          iKeys.remove();
          
        }
      } catch(IOException ioe) {
        ioe.printStackTrace();
      }
    }
  }
  
  /**
   * Reads and writes bytes on clientChannel.
   * Returns false if connection is terminated.
   */
  boolean useConnection(SocketChannel clientChannel) throws IOException {
    ByteBuffer bbIn = ByteBuffer.allocate(SIZE);
    ByteBuffer bbOut = ByteBuffer.allocate(SIZE);    
    int bytes = 0;
    while ((bytes = clientChannel.read(bbIn)) > 0) {
      bbIn.flip();
      serve(bbIn,bbOut);
      bbOut.flip();
      clientChannel.write(bbOut);
      bbIn.clear();
      bbOut.clear();
    }
    if (bytes==-1) {
      System.out.println("End of connection...");
      return false; // end of stream
    }
    return true; // nothing more to read by now
  }
  
  /**
   * Main for test.
   */
  public static void main(String[] args) throws IOException {
    UpperCaseAcceptBlockingServer server;
    if(args.length == 0) {
      server = new UpperCaseAcceptBlockingServer();
    } else {
      server = new UpperCaseAcceptBlockingServer(Integer.parseInt(args[0]));
    }
    server.launchAcceptBlocking();
  }
}
