// Copyright 2014, Kevin Ko <kevin@faveset.com>

package com.faveset.khttp;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import java.nio.channels.Selector;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

import com.faveset.log.Log;
import com.faveset.log.NullLog;

public class HttpServer {
    private static final String sTag = HttpServer.class.toString();

    private Log mLog = new NullLog();

    private Map<String, HttpHandler> mHttpHandlerMap = new HashMap<String, HttpHandler>();

    private ServerSocketChannel mListenChan;
    // A SelectorHandler will be attached to each SelectionKey registered
    // with the Selector.
    private Selector mSelector;

    private SelectorHandler mListenSelectorHandler = new SelectorHandler() {
            public void onReady(SelectionKey key) {
                if (key.isValid() && key.isAcceptable()) {
                    handleAccept(key.channel());
                }
            }
    };

    private Set<HttpConnection> mConnectionSet = new HashSet<HttpConnection>();

    private HttpConnection.OnCloseCallback mCloseCallback = new HttpConnection.OnCloseCallback() {
        public void onClose(HttpConnection conn) {
            handleConnectionClose(conn);
        }
    };

    private volatile boolean mIsDone;

    public HttpServer() {}

    public void close() throws IOException {
        mListenChan.close();

        for (HttpConnection conn : mConnectionSet) {
            conn.close();
        }
        mConnectionSet.clear();

        mSelector.close();
    }

    private void handleAccept(SelectableChannel chanArg) {
        ServerSocketChannel chan = (ServerSocketChannel) chanArg;

        SocketChannel newChan;
        try {
            newChan = chan.accept();
        } catch (IOException e) {
            mLog.e(sTag, "accept failed; shutting down", e);

            // We have a serious problem, so just bring down the server.
            stop();

            return;
        }

        if (newChan == null) {
            // The selection key hint was incorrect, as the channel is not
            // ready to accept.
            return;
        }

        HttpConnection conn;
        try {
            conn = new HttpConnection(mSelector, newChan);
        } catch (IOException e) {
            mLog.e(sTag, "could not create HttpConnection, closing", e);

            try {
                newChan.close();
            } catch (IOException newChanE) {
                mLog.e(sTag, "could not close channel, ignoring", newChanE);
            }

            return;
        }

        conn.setOnCloseCallback(mCloseCallback);
        conn.setLog(mLog);

        conn.start(mHttpHandlerMap);

        mConnectionSet.add(conn);
    }

    private void handleConnectionClose(HttpConnection conn) {
        try {
            conn.close();
        } catch (IOException e) {
            mLog.e(sTag, "connection close failed, continuing", e);

            // We've tried our best to clean up.  It's safe to continue.
        }

        mConnectionSet.remove(conn);
    }

    /**
     * Starts the HTTP server and begins listening on the given address and
     * port.  This only returns if stop() is called by another thread.
     */
    public void listenAndServe(String listenAddr, int port) throws IllegalArgumentException, IOException {
        InetSocketAddress sa = new InetSocketAddress(listenAddr, port);

        mListenChan = ServerSocketChannel.open();
        mListenChan.configureBlocking(false);

        ServerSocket sock = mListenChan.socket();
        sock.bind(sa);

        mSelector = Selector.open();
        SelectionKey listenKey = mListenChan.register(mSelector, SelectionKey.OP_ACCEPT, mListenSelectorHandler);

        mIsDone = false;

        while (true) {
            // The return value of select() (number of keys whose
            // ready-operation sets were updated) is not the best indicator
            // to check for completeness, since a key will not be counted
            // if its ready set does not change.  (It might have been read
            // ready before the select() and read ready after, which would
            // not be counted.)
            mSelector.select();

            if (mIsDone) {
                break;
            }

            Set<SelectionKey> readyKeys = mSelector.selectedKeys();
            for (SelectionKey key : readyKeys) {
                if (!key.isValid()) {
                    continue;
                }

                SelectorHandler handler = (SelectorHandler) key.attachment();
                handler.onReady(key);
            }
        }

        close();
    }

    public void registerHandler(String url, HttpHandler handler) {
        mHttpHandlerMap.put(url, handler);
    }

    public void setLog(Log log) {
        mLog = log;
    }

    /**
     * Threadsafe method for stopping the HttpServer.
     */
    public synchronized void stop() {
        mIsDone = true;
        mSelector.wakeup();
    }

    public void unregisterHandler(String url) {
        mHttpHandlerMap.remove(url);
    }
};

@@ -233,24 +233,51 @@
 il.HashSet;%0A
+import java.util.Iterator;%0A
 import java.
@@ -4239,17 +4239,60 @@
 ness
-, since a
+ for two reasons:%0A            //%0A            // 1) A
  key
@@ -4311,31 +4311,16 @@
  counted
-%0A            //
  if its 
@@ -4345,17 +4345,31 @@
  change.
- 
+%0A            //
  (It mig
@@ -4385,31 +4385,16 @@
 een read
-%0A            //
  ready b
@@ -4416,24 +4416,39 @@
 t() and read
+%0A            //
  ready after
@@ -4460,31 +4460,16 @@
 ch would
-%0A            //
  not be 
@@ -4477,16 +4477,179 @@
 ounted.)
+%0A            //%0A            // 2) It might take an indefinite period of time to finish handling%0A            // connections before we get an empty selection result.
 %0A       
@@ -4751,19 +4751,24 @@
         
-Set
+Iterator
 %3CSelecti
@@ -4774,25 +4774,20 @@
 ionKey%3E 
-readyKeys
+iter
  = mSele
@@ -4805,16 +4805,27 @@
 edKeys()
+.iterator()
 ;%0A      
@@ -4830,21 +4830,57 @@
         
-for (
+while (iter.hasNext()) %7B%0A                
 Selectio
@@ -4892,22 +4892,22 @@
 key 
-: readyKeys) %7B
+= iter.next();
 %0A   
@@ -5105,16 +5105,103 @@
 dy(key);
+%0A%0A                // Remove for the next selection loop.%0A                iter.remove();
 %0A       

remove selected SelectionKey from the ready set after calling its handlers. This way, it will be ready for the next select operation.