package fireTester.communicator.server;

import com.dragonsoft.tryapp.common.SubmissionObj;

import fireTester.interfaces.ResultsObserver;
import fireTester.interfaces.SubmissionTest;
import fireTester.messages.AcknowledgeMessage;
import fireTester.messages.KeepAliveMessage;
import fireTester.messages.TestUnitResults;
import fireTester.messages.TesterException;

/**
 * Runs a thread that monitors timeouts, resending tests when necessary.
 *
 */
public class ServerTimeoutMonitor implements ResultsObserver, Runnable {
	private Server _server;
	private static final long ALLOWABLE_LAG = 20000;
	
	/**
	 * Constructor.
	 *
	 * @param server the server
	 */
	public ServerTimeoutMonitor(Server server) {
		assert server != null;
		
		this._server = server;
		this._server.registerObserver(this);
		
		Thread t = new Thread(this);
		t.start();
	}
	
	/**
	 * Called by the client every 30 seconds to let the server know
	 * the submission is still in the queue.
	 * 
	 * @param keepAlive
	 */
	public void receive_keep_alive(KeepAliveMessage keepAlive) {
		assert keepAlive != null;
		
		System.out.println("keepAlive for " + keepAlive.get_submissions().length);
		
		for(int i = 0; i < keepAlive.get_submissions().length; i++) {
			SubmissionObj sub = keepAlive.get_submissions()[i];
			
			startingNewUnit(sub, -1);
		}
	}
	
	/**
	 * Called when a client begins execution for a submission test.
	 * 
	 * @param ack
	 */
	public void receive_acknowledge(AcknowledgeMessage ack) {
		assert ack != null;
		
		startingNewUnit(ack.get_submission(), 0);
	}
	
	/**
	 * @param r the results of a test unit (test may or may not be complete,
	 * check 'continue'.
	 */
	public void receive_unit_results(TestUnitResults r) {
		assert r != null;
		
		
		if(r.complete()) {
			testComplete(r.get_submission());
		} else {
			startingNewUnit(r.get_submission(), r.getNextUnitID());
		}
	}

	/**
	 * SubmissionTest failed and will not continue.
	 * 
	 * @param e the exception that was thrown 
	 */
	public void receive_test_failed(TesterException e) {
		assert e != null;
		
		SubmissionObj id = e.get_submission();
		testComplete(id);
	}

	private void testComplete(SubmissionObj sub) {
		assert sub != null;
		
		synchronized(this._server.pendingSubmissionCache) {
			// remove from cache
			for(int i = 0; i < this._server.pendingSubmissionCache.size(); i++) {
				CachedSubmission cache = (CachedSubmission)this._server.pendingSubmissionCache.get(i);
				if(cache.submission.get_submission().getSubmissionID().equals(sub.getSubmissionID())) {
					this._server.pendingSubmissionCache.remove(cache);
				}
			}
			
			// start next submissions that is waiting on this
			for(int i = 0; i < this._server.waitingSubmissions.size(); i++) {
				SubmissionTest test = (SubmissionTest)this._server.waitingSubmissions.get(i);
				if(test.get_submission().getActivityID().equals(sub.getActivityID())
						&& test.get_submission().getStudentUsername().equals(sub.getStudentUsername())) {
					this._server.waitingSubmissions.remove(i);
					this._server.queueTest(test);
					return;
				} 
			}
		}
	}
	
	private void startingNewUnit(SubmissionObj sub, int newUnit) {
		assert sub != null;
		
		synchronized(this._server.pendingSubmissionCache) {
			for(int i = 0; i < this._server.pendingSubmissionCache.size(); i++) {
				CachedSubmission cache = (CachedSubmission)this._server.pendingSubmissionCache.get(i);
				if(cache.submission.get_submission().getSubmissionID().equals(sub.getSubmissionID())) {
					cache.currentUnit = newUnit;
					cache.lastUpdateTime = System.currentTimeMillis();
				}
			}
		}
	}
	
	private void resend(CachedSubmission cache) {
		// start any submissions that were waiting on this
		for(int i = 0; i < this._server.pendingSubmissionCache.size(); i++) {
			SubmissionTest test = (SubmissionTest)this._server.waitingSubmissions.get(i);
			if(test.get_submission().getSubmissionID().equals(cache.submission.get_submission().getSubmissionID())) {
				this._server.queueTest(test);
				return;
			}
		}
		
		// or just queue it up
		this._server.pendingSubmissionCache.remove(cache);
		this._server.queueTest(cache.submission);
	}
	
	/**
	 * Thread runs to monitor timeouts and resend submissions when necessary.
	 */
	public void run() {
		Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
		while(true) {
			try {
				Thread.sleep(1000);
			} catch(InterruptedException e) {
				// ignore
			}
			
			synchronized(this._server.pendingSubmissionCache) {
				for(int i = 0; i < this._server.pendingSubmissionCache.size(); i++) {
					CachedSubmission cache = (CachedSubmission)this._server.pendingSubmissionCache.get(i);
					if(cache.currentUnit >= 0) { // if not in queue
						long elapsedTime = System.currentTimeMillis() - cache.lastUpdateTime;
						if(elapsedTime > cache.submission.getUnitTimeout(cache.currentUnit)
								+ ALLOWABLE_LAG) {
							System.err.println("Client dropped mid-test, resending " + cache.submission.get_submission().getSubmissionID());
							resend(cache);
						}
					} else {
						long elapsedTime = System.currentTimeMillis() - cache.lastUpdateTime;
						if(elapsedTime > 30000 + 10000 + ALLOWABLE_LAG) { // expecting keepAlive every 30s, requeue if timeout
							System.err.println("Client dropped, resending test " + cache.submission.get_submission().getSubmissionID());
							resend(cache);
						}
					}
				}
			}
			
		}
	}
}
