/*******************************************************************************
 * Copyright (c) 2000, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Julien Ruaux: jruaux@octo.com 
 * 	   Vincent Massol: vmassol@octo.com 
 *******************************************************************************/
package org.eclipse.jdt.internal.junit.ui;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;

import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.Platform;

import org.eclipse.jdt.junit.ITestRunListener;

import org.eclipse.jdt.internal.junit.runner.MessageIds;

/**
 * The client side of the RemoteTestRunner. Handles the
 * marshaling of the different messages.
 */
public class RemoteTestRunnerClient {
	public abstract class ListenerSafeRunnable implements ISafeRunnable {
		public void handleException(Throwable exception) {
			JUnitPlugin.log(exception);
		}
	}
	/**
	 * A simple state machine to process requests from the RemoteTestRunner
	 */
	abstract class ProcessingState {
	    abstract ProcessingState readMessage(String message);
	}
	
	class DefaultProcessingState extends ProcessingState {
	    ProcessingState readMessage(String message) {
	        if (message.startsWith(MessageIds.TRACE_START)) {
	        	fFailedTrace.setLength(0);
	            return fTraceState;
	        }
	        if (message.startsWith(MessageIds.EXPECTED_START)) {
	            fExpectedResult.setLength(0);
	            return fExpectedState;
	        }
	        if (message.startsWith(MessageIds.ACTUAL_START)) {
	            fActualResult.setLength(0);
	            return fActualState;
	        }
	        if (message.startsWith(MessageIds.RTRACE_START)) {
	            fFailedRerunTrace.setLength(0); //$NON-NLS-1$
	            return fRerunState;
	        }
	        String arg= message.substring(MessageIds.MSG_HEADER_LENGTH);
	        if (message.startsWith(MessageIds.TEST_RUN_START)) {
	            // version < 2 format: count
	            // version >= 2 format: count+" "+version
	            int count= 0;
	            int v= arg.indexOf(' ');
	            if (v == -1) {
	                fVersion= "v1"; //$NON-NLS-1$
	                count= Integer.parseInt(arg);
	            } else {
	                fVersion= arg.substring(v+1);
	                String sc= arg.substring(0, v);
	                count= Integer.parseInt(sc);
	            }
	            notifyTestRunStarted(count);
	            return this;
	        }
	        if (message.startsWith(MessageIds.TEST_START)) {
	            notifyTestStarted(arg);
	            return this;
	        }
	        if (message.startsWith(MessageIds.TEST_END)) {
	            notifyTestEnded(arg);
	            return this;
	        }
	        if (message.startsWith(MessageIds.TEST_ERROR)) {
	            extractFailure(arg, ITestRunListener.STATUS_ERROR);
	            return this;
	        }
	        if (message.startsWith(MessageIds.TEST_FAILED)) {
	            extractFailure(arg, ITestRunListener.STATUS_FAILURE);
	            return this;
	        }
	        if (message.startsWith(MessageIds.TEST_RUN_END)) {
	            long elapsedTime = Long.parseLong(arg);
	            testRunEnded(elapsedTime);
	            return this;
	        }
	        if (message.startsWith(MessageIds.TEST_STOPPED)) {
	            long elapsedTime = Long.parseLong(arg);
	            notifyTestRunStopped(elapsedTime);
	            shutDown();
	            return this;
	        }
	        if (message.startsWith(MessageIds.TEST_TREE)) {
	            notifyTestTreeEntry(arg);
	            return this;
	        }
	        if (message.startsWith(MessageIds.TEST_RERAN)) {
	            if (hasTestId())
	                scanReranMessage(arg);
	            else 
	                scanOldReranMessage(arg);
	            return this;
	        }	
	        return this;
	    }
	}
	
	/**
	 * Base class for states in which messages are appended to an internal
	 * string buffer until an end message is read.
	 */
	class AppendingProcessingState extends ProcessingState {
		private final StringBuffer fBuffer;
		private String fEndString;

		AppendingProcessingState(StringBuffer buffer, String endString) {
			this.fBuffer= buffer;
			this.fEndString = endString;
		}
		
		ProcessingState readMessage(String message) {
			if (message.startsWith(fEndString)) {
				entireStringRead();
				return fDefaultState;
			}
			fBuffer.append(message);
			fBuffer.append('\n');
			return this;
		}

		/**
		 * subclasses can override to do special things when end message is read
		 */
		void entireStringRead() {
		}
	}
	
	class TraceProcessingState extends AppendingProcessingState {
		TraceProcessingState() {
			super(fFailedTrace, MessageIds.TRACE_END);
		}
		
		void entireStringRead() {
            notifyTestFailed();
            fExpectedResult.setLength(0);
            fActualResult.setLength(0);
		}
		
	    ProcessingState readMessage(String message) {
	        if (message.startsWith(MessageIds.TRACE_END)) {
	            notifyTestFailed();
	            fFailedTrace.setLength(0);
	            fActualResult.setLength(0);
	            fExpectedResult.setLength(0);
	            return fDefaultState;
	        }
	        fFailedTrace.append(message + '\n');
	        return this;
	    }
	}
	
	/**
	 * The failed trace that is currently reported from the RemoteTestRunner
	 */
	private final StringBuffer fFailedTrace = new StringBuffer();
	/**
	 * The expected test result
	 */
	private final StringBuffer fExpectedResult = new StringBuffer();
	/**
	 * The actual test result
	 */
	private final StringBuffer fActualResult = new StringBuffer();
	/**
	 * The failed trace of a reran test
	 */
	private final StringBuffer fFailedRerunTrace = new StringBuffer();

	
	ProcessingState fDefaultState= new DefaultProcessingState();
	ProcessingState fTraceState= new TraceProcessingState();
	ProcessingState fExpectedState= new AppendingProcessingState(fExpectedResult, MessageIds.EXPECTED_END);
	ProcessingState fActualState= new AppendingProcessingState(fActualResult, MessageIds.ACTUAL_END);
	ProcessingState fRerunState= new AppendingProcessingState(fFailedRerunTrace, MessageIds.RTRACE_END);
	ProcessingState fCurrentState= fDefaultState;
	
	/**
	 * An array of listeners that are informed about test events.
	 */
	private ITestRunListener[] fListeners;

	/**
	 * The server socket
	 */
	private ServerSocket fServerSocket;
	private Socket fSocket;
	private int fPort= -1;
	private PrintWriter fWriter;
	private BufferedReader fBufferedReader;
	/**
	 * The protocol version
	 */ 
	private String fVersion;
	/**
	 * The failed test that is currently reported from the RemoteTestRunner
	 */
	private String fFailedTest;
	/**
	 * The Id of the failed test
	 */
	private String fFailedTestId;
	/**
	 * The kind of failure of the test that is currently reported as failed
	 */
	private int fFailureKind;
	
	private boolean fDebug= false;
	
	/**
	 * Reads the message stream from the RemoteTestRunner
	 */
	private class ServerConnection extends Thread {
		int fServerPort;
		
		public ServerConnection(int port) {
			super("ServerConnection"); //$NON-NLS-1$
			fServerPort= port;
		}
		
		public void run() {
			try {
				if (fDebug)
					System.out.println("Creating server socket "+fServerPort); //$NON-NLS-1$
				fServerSocket= new ServerSocket(fServerPort);
				fSocket= fServerSocket.accept();	
				try {
				    fBufferedReader= new BufferedReader(new InputStreamReader(fSocket.getInputStream(), "UTF-8")); //$NON-NLS-1$
				} catch (UnsupportedEncodingException e) {
				    fBufferedReader= new BufferedReader(new InputStreamReader(fSocket.getInputStream()));				    
				}
				try {
				    fWriter= new PrintWriter(new OutputStreamWriter(fSocket.getOutputStream(), "UTF-8"), true); //$NON-NLS-1$
	            } catch (UnsupportedEncodingException e1) {
	                fWriter= new PrintWriter(new OutputStreamWriter(fSocket.getOutputStream()), true);
	            }
				String message;
				while(fBufferedReader != null && (message= readMessage(fBufferedReader)) != null)
					receiveMessage(message);
			} catch (SocketException e) {
				notifyTestRunTerminated();
			} catch (IOException e) {
				System.out.println(e);
				// fall through
			}
			shutDown();
		}
	}

	/**
	 * Start listening to a test run. Start a server connection that
	 * the RemoteTestRunner can connect to.
	 */
	public synchronized void startListening(
		ITestRunListener[] listeners,
		int port) {
		fListeners = listeners;
		fPort = port;
		ServerConnection connection = new ServerConnection(port);
		connection.start();
	}
	
	/**
	 * Requests to stop the remote test run.
	 */
	public synchronized void stopTest() {
		if (isRunning()) {
			fWriter.println(MessageIds.TEST_STOP);
			fWriter.flush();
		}
	}

	private synchronized void shutDown() {
		if (fDebug) 
			System.out.println("shutdown "+fPort); //$NON-NLS-1$
		
		if (fWriter != null) {
			fWriter.close();
			fWriter= null;
		}
		try {
			if (fBufferedReader != null) {
				fBufferedReader.close();
				fBufferedReader= null;
			}
		} catch(IOException e) {
		}	
		try {
			if (fSocket != null) {
				fSocket.close();
				fSocket= null;
			}
		} catch(IOException e) {
		}
		try{
			if (fServerSocket != null) {
				fServerSocket.close();
				fServerSocket= null;
			}
		} catch(IOException e) {
		}
	}
	
	public boolean isRunning() {
		return fSocket != null;
	}
	
	private String readMessage(BufferedReader in) throws IOException {
		return in.readLine();
	}
		
	private void receiveMessage(String message) {
	    fCurrentState= fCurrentState.readMessage(message);
	}

	private void scanOldReranMessage(String arg) {
		// OLD V1 format
		// format: className" "testName" "status
		// status: FAILURE, ERROR, OK
		int c= arg.indexOf(" "); //$NON-NLS-1$
		int t= arg.indexOf(" ", c+1); //$NON-NLS-1$
		String className= arg.substring(0, c);
		String testName= arg.substring(c+1, t);
		String status= arg.substring(t+1);
		String testId = className+testName;
		notifyTestReran(testId, className, testName, status);
	}

	private void scanReranMessage(String arg) {
		// format: testId" "className" "testName" "status
		// status: FAILURE, ERROR, OK
		int i= arg.indexOf(' ');
		int c= arg.indexOf(' ', i+1); //$NON-NLS-1$
		int t= arg.indexOf(' ', c+1); //$NON-NLS-1$
		String testId= arg.substring(0, i);
		String className= arg.substring(i+1, c);
		String testName= arg.substring(c+1, t);
		String status= arg.substring(t+1);
		notifyTestReran(testId, className, testName, status);
	}
	
	private void notifyTestReran(String testId, String className, String testName, String status) {
		int statusCode= ITestRunListener.STATUS_OK;
		if (status.equals("FAILURE")) //$NON-NLS-1$
			statusCode= ITestRunListener.STATUS_FAILURE;
		else if (status.equals("ERROR")) //$NON-NLS-1$
			statusCode= ITestRunListener.STATUS_ERROR;
				
		String trace= ""; //$NON-NLS-1$
		if (statusCode != ITestRunListener.STATUS_OK)
			trace = fFailedRerunTrace.toString();
		// assumption a rerun trace was sent before
		notifyTestReran(testId, className, testName, statusCode, trace);
	}

	private void extractFailure(String arg, int status) {
		String s[]= extractTestId(arg);
		fFailedTestId= s[0];
		fFailedTest= s[1];
		fFailureKind= status;
	}

	/**
	 * Returns an array with two elements. The first one is the testId, the second one the testName.
	 */
	String[] extractTestId(String arg) {
		String[] result= new String[2];
		if (!hasTestId()) {
			result[0]= arg; // use the test name as the test Id
			result[1]= arg;
			return result;
		}
		int i= arg.indexOf(',');
		result[0]= arg.substring(0, i);
		result[1]= arg.substring(i+1, arg.length());
		return result;
	}
	
	private boolean hasTestId() {
		if (fVersion == null) // TODO fix me
			return true;
		return fVersion.equals("v2"); //$NON-NLS-1$
	}

	private void notifyTestReran(final String testId, final String className, final String testName, final int statusCode, final String trace) {
		for (int i= 0; i < fListeners.length; i++) {
			final ITestRunListener listener= fListeners[i];
			Platform.run(new ListenerSafeRunnable() { 
				public void run() {
				    if (listener instanceof ITestRunListener3 ) 
				        ((ITestRunListener3) listener).testReran(testId,
								className, testName, statusCode, trace,
								fExpectedResult.toString(), fActualResult.toString());
				    else
						listener.testReran(testId, className, testName, statusCode, trace);
				}
			});
		}
	}

	private void notifyTestTreeEntry(final String treeEntry) {
		for (int i= 0; i < fListeners.length; i++) {
			if (fListeners[i] instanceof ITestRunListener2) {
				ITestRunListener2 listener= (ITestRunListener2)fListeners[i];
				if (!hasTestId()) 
					listener.testTreeEntry(fakeTestId(treeEntry));
				else
					listener.testTreeEntry(treeEntry);
			}
		}
	}

	private String fakeTestId(String treeEntry) {
		// extract the test name and add it as the testId
		int index0= treeEntry.indexOf(',');
		String testName= treeEntry.substring(0, index0).trim();
		return testName+","+treeEntry; //$NON-NLS-1$
	}

	private void notifyTestRunStopped(final long elapsedTime) {
		if (JUnitPlugin.isStopped())
			return;
		for (int i= 0; i < fListeners.length; i++) {
			final ITestRunListener listener= fListeners[i];
			Platform.run(new ListenerSafeRunnable() { 
				public void run() {
					listener.testRunStopped(elapsedTime);
				}
			});
		}
	}

	private void testRunEnded(final long elapsedTime) {
		if (JUnitPlugin.isStopped())
			return;
		for (int i= 0; i < fListeners.length; i++) {
			final ITestRunListener listener= fListeners[i];
			Platform.run(new ListenerSafeRunnable() { 
				public void run() {
					listener.testRunEnded(elapsedTime);
				}
			});
		}
	}

	private void notifyTestEnded(final String test) {
		if (JUnitPlugin.isStopped())
			return;
		for (int i= 0; i < fListeners.length; i++) {
			final ITestRunListener listener= fListeners[i];
			Platform.run(new ListenerSafeRunnable() { 
				public void run() {
					String s[]= extractTestId(test);
					listener.testEnded(s[0], s[1]);
				}
			});
		}
	}

	private void notifyTestStarted(final String test) {
		if (JUnitPlugin.isStopped())
			return;
		for (int i= 0; i < fListeners.length; i++) {
			final ITestRunListener listener= fListeners[i];
			Platform.run(new ListenerSafeRunnable() { 
				public void run() {
					String s[]= extractTestId(test);
					listener.testStarted(s[0], s[1]);
				}
			});
		}
	}

	private void notifyTestRunStarted(final int count) {
		if (JUnitPlugin.isStopped())
			return;
		for (int i= 0; i < fListeners.length; i++) {
			final ITestRunListener listener= fListeners[i];
			Platform.run(new ListenerSafeRunnable() { 
				public void run() {
					listener.testRunStarted(count);
				}
			});
		}
	}

	private void notifyTestFailed() {
		if (JUnitPlugin.isStopped())
			return;
		for (int i= 0; i < fListeners.length; i++) {
			final ITestRunListener listener= fListeners[i];
			Platform.run(new ListenerSafeRunnable() { 
				public void run() {
				    if (listener instanceof ITestRunListener3 )
				        ((ITestRunListener3)listener).testFailed(fFailureKind, fFailedTestId, 
				        		fFailedTest, fFailedTrace.toString(), fExpectedResult.toString(), fActualResult.toString());
				    else
				        listener.testFailed(fFailureKind, fFailedTestId, fFailedTest, fFailedTrace.toString());
				}
			});
		}
	}
	
	private void notifyTestRunTerminated() {
		// fix for 77771 RemoteTestRunnerClient doing work after junit shutdown [JUnit]
		if (JUnitPlugin.isStopped())
			return;
		for (int i= 0; i < fListeners.length; i++) {
			final ITestRunListener listener= fListeners[i];
			Platform.run(new ListenerSafeRunnable() { 
				public void run() {
					listener.testRunTerminated();
				}
			});
		}
	}

	public void rerunTest(String testId, String className, String testName) {
		if (isRunning()) {
			fWriter.println(MessageIds.TEST_RERUN+testId+" "+className+" "+testName); //$NON-NLS-1$ //$NON-NLS-2$
			fWriter.flush();
		}
	}
}
