package net.azurewebsites.thehen101.rainserver.thread;

import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

import net.azurewebsites.thehen101.rainserver.RainServer;
import net.azurewebsites.thehen101.rainserver.util.RequestFilter;
import net.azurewebsites.thehen101.rainserver.util.RequestWithHeader;

/**
 * This thread is used to listen on a specified port for incoming connections.
 * Each connection is dealt with in a new thread.
 * 
 * @author thehen101
 *
 */
public class ThreadConnectionListener extends Thread {
	private final ArrayList<ThreadSocketHandler> connectedClients;
	private final RainServer server;
	private final RequestFilter filter;
	private final InetAddress bindAddress;
	private final int backlog, port;
	private boolean shouldListen;
	private final int maxIdleTimeMS, maxMessageTimeMS, maxMessageSizeBytes;

	public ThreadConnectionListener(InetAddress bindAddress, int backlog, int port, RainServer server,
			RequestFilter filter, int maxIdleTimeMS, int maxMessageTimeMS, int maxMessageSizeBytes) {
		this.connectedClients = new ArrayList<ThreadSocketHandler>();
		this.bindAddress = bindAddress;
		this.backlog = backlog;
		this.port = port;
		this.server = server;
		this.filter = filter;
		this.shouldListen = true;
		this.maxIdleTimeMS = maxIdleTimeMS;
		this.maxMessageTimeMS = maxMessageTimeMS;
		this.maxMessageSizeBytes = maxMessageSizeBytes;
	}

	@Override
	public void run() {
		try {
			ServerSocket ss = new ServerSocket(this.port, this.backlog, this.bindAddress);
			System.out.println("ServerSocket bound to " + ss.getLocalSocketAddress().toString());
			while (this.shouldListen) {
				System.out.println("Listening for client connection on " + ss.getLocalSocketAddress() + " "
						+ this.connectedClients.size());
				Socket client = ss.accept();
				// if we have a filter, check this IP doesn't have too many connections open
				if (this.filter != null) {
					this.verifyThreads(this.connectedClients);
					String IP = client.getInetAddress().getHostAddress();
					String connect = this.filter.shouldAcceptConnection(IP, connectedClients);
					if (connect != null) {
						client.getOutputStream().write(connect.getBytes());
						client.getOutputStream().flush();
						client.close();
						System.out.println("Dropped " + IP + " as: " + connect);
						continue;
					}
				}
				System.out.println("Client connected");
				client.setSoTimeout(this.maxIdleTimeMS);
				ThreadSocketHandler handler = new ThreadSocketHandler(
						client, this.server, this.maxMessageSizeBytes);
				this.connectedClients.add(handler);
				handler.start();
				this.verifyThreads(this.connectedClients);
			}
			ss.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void broadcastNewBlock(String blockJson) {
		System.out.println("Received new block, broadcasting to clients...");
		this.verifyThreads(this.connectedClients);
		ArrayList<Thread> threads = new ArrayList<Thread>();
		for (int i = 0; i < this.connectedClients.size(); i++) {
			final int count = i;
			Thread t = new Thread() {
				@Override
				public void run() {
					ThreadSocketHandler t = connectedClients.get(count);
					try {
						OutputStream client = t.getClient().getOutputStream();
						client.write(new RequestWithHeader(true, blockJson.getBytes()).get());
						client.flush();
						System.out.println("Broadcasted to " + t.getClient().getInetAddress().getHostAddress());
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			};
			threads.add(t);
			t.start();
			try {
				Thread.sleep(5);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		Thread idleKiller = new Thread() {
			@Override
			public void run() {
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				for (int i = 0; i < threads.size(); i++) {
					Thread t = threads.get(i);
					if (!t.getState().equals(State.TERMINATED)) {
						t.stop();
						System.out.println("### Forcefully stopped a hanging thread!!! ###");
					}
				}
			}
		};
		idleKiller.start();
	}
	
	/**
	 * This method verifies an array of ThreadSocketHandler to make sure that they are valid. 
	 * It does this by removing any terminated threads from the passed array, and it will
	 * close the socket of a thread if it is taking too long to receive a message. This is
	 * to stop slowloris style attacks.
	 * 
	 * @param array An array of threads to be verified
	 */
	private void verifyThreads(ArrayList<ThreadSocketHandler> array) {
		for (int i = 0; i < array.size(); i++) {
			ThreadSocketHandler t = array.get(i);
			if (t.getState().equals(Thread.State.TERMINATED)) {
				t = null;
				array.remove(i);
			}
			if (t != null && t.isReadingRequest()) {
				if (System.currentTimeMillis() > (t.getMessageStartTime() + this.maxMessageTimeMS)) {
					t.setListen(false);
					try {
						t.getClient().getOutputStream().write(new RequestWithHeader(false, 
										RainServer.ErrorCode.INVALIDCOMMAND.getErrorString()).get());
					} catch (Exception e) {
						e.printStackTrace();
					}
					try {
						t.getClient().close();
					} catch (Exception e) {
						e.printStackTrace();
					}
					t = null;
					array.remove(i);
				}
			}
		}
	}

	public InetAddress getBindAddress() {
		return this.bindAddress;
	}

	public int getPort() {
		return this.port;
	}

	public RainServer getServer() {
		return this.server;
	}

	public ArrayList<ThreadSocketHandler> getConnectedClients() {
		return this.connectedClients;
	}
}