package com.subgraph.orchid.dashboard;

import java.io.Closeable;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.logging.Logger;

import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.misc.GuardedBy;

/**
 * A debugging utility which displays continuously updated information about the internal state
 * of various components to clients which connect to a network port listening on localhost.
 */
public class Dashboard implements DashboardRenderable, DashboardRenderer {
	private final static Logger logger = Logger.getLogger(Dashboard.class.getName());
	
	private final static String DASHBOARD_PORT_PROPERTY = "com.subgraph.orchid.dashboard.port";
	
	private final static int DEFAULT_LISTENING_PORT = 12345;
	private final static int DEFAULT_FLAGS = DASHBOARD_CIRCUITS | DASHBOARD_STREAMS;
	private final static IPv4Address LOCALHOST = IPv4Address.createFromString("127.0.0.1");
			
	@GuardedBy("this") private int listeningPort;
	@GuardedBy("this") private int flags = DEFAULT_FLAGS;
	@GuardedBy("this") private ServerSocket listeningSocket;
	@GuardedBy("this") private boolean isListening;
	
	private final List<DashboardRenderable> renderables;
	private final Executor executor;
	
	public Dashboard() {
		renderables = new CopyOnWriteArrayList<DashboardRenderable>();
		renderables.add(this);
		executor = Executors.newCachedThreadPool();
		listeningPort = chooseListeningPort();
	}
	
	private static int chooseListeningPort() {
		final String dbPort = System.getProperty(DASHBOARD_PORT_PROPERTY);
		final int port = parsePortProperty(dbPort);
		if(port > 0 && port <= 0xFFFF) {
			return port;
		} else if(dbPort != null) {
			logger.warning(DASHBOARD_PORT_PROPERTY + " was not a valid port value: "+ dbPort);
		}
		return DEFAULT_LISTENING_PORT;
	}
	
	private static int parsePortProperty(String dbPort) {
		if(dbPort == null) {
			return -1;
		}
		try {
			return Integer.parseInt(dbPort);
		} catch (NumberFormatException e) {
			return -1;
		}
	}
	
	public void addRenderables(Object...objects) {
		for(Object ob: objects) {
			if(ob instanceof DashboardRenderable) {
				renderables.add((DashboardRenderable) ob);
			}
		}
	}

	public void addRenderable(DashboardRenderable renderable) {
		renderables.add(renderable);
	}

	public synchronized void enableFlag(int flag) {
		flags |= flag;
	}
	
	public synchronized void disableFlag(int flag) {
		flags &= ~flag;
	}
	
	
	public synchronized boolean isEnabled(int f) {
		return (flags & f) != 0;
	}
	
	public synchronized void setListeningPort(int port) {
		if(port != listeningPort) {
			listeningPort = port;
			if(isListening) {
				stopListening();
				startListening();
			}
		}
	}
	
	public boolean isEnabledByProperty() {
		return System.getProperty(DASHBOARD_PORT_PROPERTY) != null;
	}

	public synchronized void startListening() {
		if(isListening) {
			return;
		}
		try {
			listeningSocket = new ServerSocket(listeningPort, 50, LOCALHOST.toInetAddress());
			isListening = true;
			logger.info("Dashboard listening on "+ LOCALHOST + ":"+ listeningPort);
			executor.execute(createAcceptLoopRunnable(listeningSocket));
		} catch (IOException e) {
			logger.warning("Failed to create listening Dashboard socket on port "+ listeningPort +": "+ e);
		}
	}
	
	public synchronized void stopListening() {
		if(!isListening) {
			return;
		}
		if(listeningSocket != null) {
			closeQuietly(listeningSocket);
			listeningSocket = null;
		}
		isListening = false;
	}
	
	public synchronized boolean isListening() {
		return isListening;
	}

	private Runnable createAcceptLoopRunnable(final ServerSocket ss) {
		return new Runnable() {
			public void run() {
				acceptConnections(ss);
			}
		};
	}

	private void acceptConnections(ServerSocket ss) {
		while(true) {
			try {
				Socket s = ss.accept();
				executor.execute(new DashboardConnection(this, s));
			} catch (IOException e) {
				if(!ss.isClosed()) {
					logger.warning("IOException on dashboard server socket: "+ e);
				}
				stopListening();
				return;
			}
		}
	}
	
	void renderAll(PrintWriter writer) throws IOException {
		for(DashboardRenderable dr: renderables) {
			dr.dashboardRender(this, writer, flags);
		}
	}

	private void closeQuietly(Closeable closeable) {
		try {
			closeable.close();
		} catch (IOException e) { }
	}

	public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) {
		writer.println("[Dashboard]");
		writer.println();
	}

	public void renderComponent(PrintWriter writer, int flags, Object component) throws IOException {
		if(!(component instanceof DashboardRenderable)) {
			return;
		}
		final DashboardRenderable renderable = (DashboardRenderable) component;
		renderable.dashboardRender(this, writer, flags);
	}
}

@@ -4292,16 +4292,78 @@
 ption %7B%0A
+%09%09final int fs;%0A%09%09synchronized (this) %7B%0A%09%09%09fs = flags;%0A%09%09%7D%0A%09%09%0A
 %09%09for(Da
@@ -4432,27 +4432,24 @@
 s, writer, f
-lag
 s);%0A%09%09%7D%0A%09%7D%0A%0A

Access to flags should be synchronized but don't want to hold lock during callbacks