package org.torproject.jtor.circuits.impl;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import org.torproject.jtor.circuits.Circuit;
import org.torproject.jtor.circuits.CircuitBuildHandler;
import org.torproject.jtor.circuits.CircuitNode;
import org.torproject.jtor.circuits.Connection;
import org.torproject.jtor.directory.Directory;
import org.torproject.jtor.directory.Router;
import org.torproject.jtor.logging.Logger;

public class CircuitCreationTask implements Runnable {
	private final static int MAX_PENDING_CIRCUITS = 2;
	private final static int DEFAULT_CLEAN_CIRCUITS = 3;
	private final Directory directory;
	private final CircuitManagerImpl circuitManager;
	private final Logger logger;
	private final NodeChooser nodeChooser;
	private final Executor executor;

	// To avoid obnoxiously printing a warning every second
	private int notEnoughDirectoryInformationWarningCounter = 0;

	CircuitCreationTask(Directory directory, CircuitManagerImpl circuitManager, Logger logger) {
		this.directory = directory;
		this.circuitManager = circuitManager;
		this.logger = logger;
		this.nodeChooser = new NodeChooser(circuitManager, directory);
		this.executor = Executors.newCachedThreadPool();
	}

	public void run() {
		checkUnassignedPendingStreams();
		checkExpiredPendingCircuits();
		checkCircuitsForCreation();		
	}

	private void checkUnassignedPendingStreams() {
		final List<StreamExitRequest> pendingExitStreams = circuitManager.getPendingExitStreams();

		assignPendingStreamsToActiveCircuits(pendingExitStreams);

		removePendingStreamsByPendingCircuits(pendingExitStreams);

		buildCircuitsToHandleExitStreams(pendingExitStreams);
	}

	private void assignPendingStreamsToActiveCircuits(List<StreamExitRequest> pendingExitStreams) {
		if(pendingExitStreams.isEmpty())
			return;

		for(Circuit c: circuitManager.getRandomlyOrderedListOfActiveCircuits()) {
			final Iterator<StreamExitRequest> it = pendingExitStreams.iterator();
			while(it.hasNext()) {
				if(attemptHandleStreamRequest(c, it.next()))
					it.remove();
			}
		}
	}
	
	private void removePendingStreamsByPendingCircuits(List<StreamExitRequest> pendingExitStreams) {
		if(pendingExitStreams.isEmpty())
			return;

		for(Circuit c: circuitManager.getPendingCircuits()) {
			final Iterator<StreamExitRequest> it = pendingExitStreams.iterator();
			while(it.hasNext()) {
				if(c.canHandleExitTo(it.next()))
					it.remove();
			}
		}
	}

	private void buildCircuitsToHandleExitStreams(List<StreamExitRequest> pendingExitStreams) {
		if(pendingExitStreams.isEmpty())
			return;

		System.out.println("Building new circuits to handle "+ pendingExitStreams.size() +" pending streams");
		for(StreamExitRequest r: pendingExitStreams) {
			System.out.println("Request: "+ r);

		}

		for(StreamExitRequest req: pendingExitStreams)
			createCircuitForExitRequest(req);
	}

	private boolean attemptHandleStreamRequest(Circuit c, StreamExitRequest request) {
		if(c.canHandleExitTo(request)) {
			if(request.reserveRequest())
				executor.execute(newExitStreamTask(c, request));
			// else request is reserved meaning another circuit is already trying to handle it
			return true;
		}
		return false;
	}

	private OpenExitStreamTask newExitStreamTask(Circuit circuit, StreamExitRequest exitRequest) {
		return new OpenExitStreamTask(circuit, exitRequest, logger);
	}

	private void checkExpiredPendingCircuits() {
		// TODO Auto-generated method stub
	}

	private void createCircuitForExitRequest(StreamExitRequest request) {
		if(!directory.haveMinimumRouterInfo())
			return;
		if(circuitManager.getPendingCircuitCount() >= MAX_PENDING_CIRCUITS)
			return;

		final Circuit circuit = circuitManager.createNewCircuit();
		final NodeChoiceConstraints ncc = new NodeChoiceConstraints();
		// XXX
		ncc.setNeedCapacity(true);
		ncc.setNeedUptime(true);
		final Router exitRouter = nodeChooser.chooseExitNodeForTarget(request, ncc);
		ncc.addExcludedRouter(exitRouter);
		final Router middleRouter = nodeChooser.chooseMiddleNode(ncc);
		ncc.addExcludedRouter(middleRouter);
		final Router entryRouter = nodeChooser.chooseEntryNode(ncc);
		List<Router> path =  Arrays.asList(entryRouter, middleRouter, exitRouter);
		executor.execute(new OpenCircuitTask(circuit, path, createCircuitBuildHandler(), logger));
	}

	private void checkCircuitsForCreation() {

		if(!directory.haveMinimumRouterInfo()) {
			if(notEnoughDirectoryInformationWarningCounter % 20 == 0)
				logger.warning("Cannot build circuits because we don't have enough directory information");
			notEnoughDirectoryInformationWarningCounter++;
			return;
		}

		if((circuitManager.getCleanCircuitCount() + circuitManager.getPendingCircuitCount()) < DEFAULT_CLEAN_CIRCUITS &&
				circuitManager.getPendingCircuitCount() < MAX_PENDING_CIRCUITS) {
			final List<Router> path = choosePreemptiveExitPath();
			final Circuit circuit = circuitManager.createNewCircuit();
			executor.execute(new OpenCircuitTask(circuit, path, createCircuitBuildHandler(), logger));
		}
	}

	private List<Router> choosePreemptiveExitPath() {
		final NodeChoiceConstraints ncc = new NodeChoiceConstraints();
		ncc.setNeedCapacity(true);
		ncc.setNeedUptime(true);
		final Router exitRouter = nodeChooser.chooseExitNodeForPort(80, ncc);
		ncc.addExcludedRouter(exitRouter);
		final Router middleRouter = nodeChooser.chooseMiddleNode(ncc);
		ncc.addExcludedRouter(middleRouter);
		final Router entryRouter = nodeChooser.chooseEntryNode(ncc);
		return Arrays.asList(entryRouter, middleRouter, exitRouter);
	}

	private CircuitBuildHandler createCircuitBuildHandler() {
		return new CircuitBuildHandler() {

			public void circuitBuildCompleted(Circuit circuit) {
				logger.debug("Circuit completed to: "+ circuit);
				circuitOpenedHandler(circuit);
			}

			public void circuitBuildFailed(String reason) {
				logger.debug("Circuit build failed: "+ reason);
			}

			public void connectionCompleted(Connection connection) {
				logger.debug("Circuit connection completed to "+ connection);
			}

			public void connectionFailed(String reason) {
				logger.debug("Circuit connection failed: "+ reason);
			}

			public void nodeAdded(CircuitNode node) {
				//logger.debug("Node added to circuit: "+ node);
			}	
		};
	}

	private void circuitOpenedHandler(Circuit circuit) {
		final List<StreamExitRequest> pendingExitStreams = circuitManager.getPendingExitStreams();
		for(StreamExitRequest req: pendingExitStreams) {
			if(circuit.canHandleExitTo(req) && req.reserveRequest())
				executor.execute(newExitStreamTask(circuit, req));
		}
	}

}

@@ -2664,34 +2664,28 @@
 urn;%0A%0A%09%09
-System.out.println
+logger.debug
 (%22Buildi
@@ -2817,26 +2817,20 @@
 %0A%09%09%09
-System.out.println
+logger.debug
 (%22Re

Replace println() with logger calls