/*******************************************************************************
 * Copyright 2013-2016 alladin-IT GmbH
 * Copyright 2013-2016 Rundfunk und Telekom Regulierungs-GmbH (RTR-GmbH)
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package at.rtr.rmbt.qos.testserver;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.postgresql.util.Base64;

import at.rtr.rmbt.qos.testserver.ServerPreferences.TestServerServiceEnum;
import at.rtr.rmbt.qos.testserver.entity.ClientToken;
import at.rtr.rmbt.qos.testserver.servers.AbstractUdpServer;
import at.rtr.rmbt.qos.testserver.udp.UdpPacketReceivedCallback;
import at.rtr.rmbt.qos.testserver.udp.UdpTestCandidate;
import at.rtr.rmbt.qos.testserver.udp.UdpTestCompleteCallback;
import at.rtr.rmbt.qos.testserver.udp.VoipTestCandidate;
import at.rtr.rmbt.qos.testserver.util.TestServerConsole;
import at.rtr.rmbt.util.net.rtp.RealtimeTransportProtocol.PayloadType;
import at.rtr.rmbt.util.net.rtp.RealtimeTransportProtocol.RtpException;
import at.rtr.rmbt.util.net.rtp.RtpPacket;
import at.rtr.rmbt.util.net.rtp.RtpUtil;
import at.rtr.rmbt.util.net.rtp.RtpUtil.RtpQoSResult;

/**
 * handles all client requests
 * @author lb
 *
 */
public class ClientHandler implements Runnable {
	
	public final static boolean ABORT_ON_DUPLICATE_UDP_PACKETS = false;
	
	public final static Pattern ID_REGEX_PATTERN = Pattern.compile("\\+ID([\\d]*)");
	
	public final static Pattern TOKEN_REGEX_PATTERN = Pattern.compile("TOKEN ([\\d\\w-]*)_([\\d]*)_(.*)");
	
	/**
	 * enable/disable token check
	 */
	public final static boolean CHECK_TOKEN = false;
	
	private final ServerSocket serverSocket;
	
	private final Socket socket;
	
	protected final FilterInputStream in;
	
	protected final FilterOutputStream out;
	
	protected final BufferedReader reader;
	
	protected final String name;
	
	protected ConcurrentHashMap<Integer, UdpTestCandidate> clientUdpOutDataMap = new ConcurrentHashMap<>();
	
	protected ConcurrentHashMap<Integer, UdpTestCandidate> clientUdpInDataMap = new ConcurrentHashMap<>();
	
	protected ConcurrentHashMap<Integer, VoipTestCandidate> clientVoipDataMap = new ConcurrentHashMap<>();

	/**
	 * protocol version used by the client 
	 */
	protected String clientProtocolVersion = QoSServiceProtocol.PROTOCOL_VERSION_1;
	
	/**
	 * 
	 * @param serverSocket
	 * @param socket
	 * @throws IOException
	 */
	public ClientHandler(ServerSocket serverSocket, Socket socket) throws IOException {
		this.serverSocket = serverSocket;
		this.socket = socket;
		this.in = new BufferedInputStream(socket.getInputStream());
		this.out = new FilterOutputStream(socket.getOutputStream());
		this.reader = new BufferedReader(new InputStreamReader(in));
		this.name = "[ClientHandler " + socket.getInetAddress().toString() + "]";
	}
	
	/*
	 * (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 */
	@Override
	public void run() {
		TestServerConsole.log("New connection from: " + socket.getInetAddress().toString(), 
				0, TestServerServiceEnum.TEST_SERVER);
		String message;
		String command = null;
		
		try {
			socket.setSoTimeout(QoSServiceProtocol.TIMEOUT_CLIENTHANDLER_CONNECTION_MIN_VALUE);
			
			out.write(getBytesWithNewline(QoSServiceProtocol.RESPONSE_GREETING));
			out.write(getBytesWithNewline(QoSServiceProtocol.RESPONSE_ACCEPT_TOKEN));
			message = reader.readLine();
			TestServerConsole.log("GOT: " + message, 1, TestServerServiceEnum.TEST_SERVER);
			
			ClientToken token = checkToken(message);
			
			TestServerConsole.log("TOKEN OK", 1, TestServerServiceEnum.TEST_SERVER);
			
			out.write(getBytesWithNewline(QoSServiceProtocol.RESPONSE_OK));
			out.write(getBytesWithNewline(QoSServiceProtocol.RESPONSE_ACCEPT_COMMANDS));
			
			boolean quit = false;
			
			TestServer.clientHandlerSet.add(this);
			
			while(!quit) {
				try {
					command = reader.readLine();
					TestServerConsole.log("COMMAND: " + command + " from: " + socket.getInetAddress().toString(), 0, TestServerServiceEnum.TEST_SERVER);
					if (command != null) {
						if (command.startsWith(QoSServiceProtocol.CMD_NON_TRANSPARENT_PROXY_TEXT)) {
							runNonTransparentProxyTest(command);
						}
						else if (command.startsWith(QoSServiceProtocol.CMD_TCP_TEST_IN)) {
							runIncomingTcpTest(command, token);
						}
						else if (command.startsWith(QoSServiceProtocol.CMD_TCP_TEST_OUT)) {
							runOutgoingTcpTest(command, token);
						}
						else if (command.startsWith(QoSServiceProtocol.CMD_UDP_TEST_OUT)) {
							runOutgoingUdpTest(command, token);
						}
						else if (command.startsWith(QoSServiceProtocol.CMD_UDP_TEST_IN)) {
							runIncomingUdpTest(command, token);
						}
						else if (command.startsWith(QoSServiceProtocol.CMD_VOIP_TEST)) {
							runVoipTest(command, token);
						}
						else if (command.startsWith(QoSServiceProtocol.REQUEST_UDP_PORT_RANGE)) {
							sendCommand(TestServer.serverPreferences.getUdpPortMin() +  " " + TestServer.serverPreferences.getUdpPortMax(), command);
						}
						else if (command.startsWith(QoSServiceProtocol.REQUEST_UDP_PORT)) {
							sendRandomUdpPort(command);
						}
						else if (command.startsWith(QoSServiceProtocol.REQUEST_UDP_RESULT_OUT)) {
							runRcvCommand(command, token, false);
						}
						else if (command.startsWith(QoSServiceProtocol.REQUEST_UDP_RESULT_IN)) {
							runRcvCommand(command, token, true);
						}
						else if (command.startsWith(QoSServiceProtocol.REQUEST_VOIP_RESULT)) {
							runVoipResultCommand(command, token);
						}
						else if (command.startsWith(QoSServiceProtocol.REQUEST_QUIT)) {
							quit = true;
						}
						else if (command.startsWith(QoSServiceProtocol.REQUEST_NEW_CONNECTION_TIMEOUT)) {
							requestNewConnectionTimeout(command);
						}
						else if (command.startsWith(QoSServiceProtocol.REQUEST_PROTOCOL_VERSION)) {
							
						}
						else if (command.startsWith(QoSServiceProtocol.REQUEST_PROTOCOL_KEEPALIVE)) {
							
						}
						else {
							sendCommand(QoSServiceProtocol.RESPONSE_ACCEPT_COMMANDS, command);
							quit = true;
						}
					}
					else {
						quit = true;
					}
				}
				catch (Exception e) {
					TestServerConsole.error("ClientHandler: " + socket.getInetAddress().toString() 
							+ (command == null ? " [No command submitted]" : " [Command: " + command + "] - Exception catched and consumed."), 
							e, 0, TestServerServiceEnum.TEST_SERVER);
					throw e;
				}
			}
		} 
		catch (Exception e) {
			TestServerConsole.error(name, e, 0, TestServerServiceEnum.TEST_SERVER);		
		}
		finally {
			TestServer.clientHandlerSet.remove(this);
			if (socket != null && !socket.isClosed()) {
				try {
					socket.close();
					TestServerConsole.log(name + " Connection closed!", 
							0, TestServerServiceEnum.TEST_SERVER);
				} catch (Exception e) {
					TestServerConsole.error(name + " Could not close socket!", e, 0, TestServerServiceEnum.TEST_SERVER);
				}
			}
		}
	}

	/**
	 * 
	 * @param string	 * @return
	 */
	public synchronized static byte[] getBytesWithNewline(String string) {
		if (string.endsWith("\n")) {
			return getBytesWithNewline(string, false);
		}
		else {
			return getBytesWithNewline(string, true);
		}
	}
	
	/**
	 * 
	 * @param string
	 * @param appendNewLine
	 * @return
	 */
	public synchronized static byte[] getBytesWithNewline(String string, boolean appendNewLine) {
		if (appendNewLine) {
			return (string + "\n").getBytes();
		}
		else {
			return string.getBytes();
		}
	}

	/**
	 * 
	 * @param token
	 * @return
	 * @throws IOException
	 */
	private ClientToken checkToken(String token) throws IOException {
		ClientToken clientToken;
		
		try {
			TestServerConsole.log("Got token: " + token, 0, TestServerServiceEnum.TEST_SERVER);
			Matcher m = TOKEN_REGEX_PATTERN.matcher(token);
			m.find();
			
			if (m.groupCount()!=3) {
				throw new IOException("BAD TOKEN: Bad Arguments!\n");
			}
			else {
				String uuid = m.group(1);
				long timeStamp = Long.parseLong(m.group(2));
				String hmac = m.group(3);

				if (CHECK_TOKEN) {
					String controlHmac = calculateHMAC(TestServer.serverPreferences.getSecretKey(), uuid + "_" + timeStamp);
					if (controlHmac.equals(hmac) && (timeStamp + QoSServiceProtocol.TOKEN_LEGAL_TIME >= System.currentTimeMillis())) {
						clientToken = new ClientToken(uuid, timeStamp, hmac);	
						return clientToken;
					}
					else {
						throw new IOException("BAD TOKEN. Bad Key!\n" + controlHmac + " <-> " + hmac + "\n");
					}
				}
				else {
					return new ClientToken(uuid, timeStamp, hmac);
				}
			}
		}
		catch (IOException e) {
			throw e;
			
		}
		catch (Exception e) {
			e.printStackTrace();
			throw new IOException("BAD TOKEN: " + token);
		}
	}
	
	/**
	 * 
	 * @param secret
	 * @param data
	 * @return
	 */
    private static String calculateHMAC(final String secret, final String data)
    {
        try
        {
            final SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), "HmacSHA1");
            final Mac mac = Mac.getInstance("HmacSHA1");
            mac.init(signingKey);
            final byte[] rawHmac = mac.doFinal(data.getBytes());
            final String result = new String(Base64.encodeBytes(rawHmac));
            return result;
        }
        catch (final GeneralSecurityException e)
        {
            
            TestServerConsole.log("Unexpected error while creating hash: " + e.getMessage(), 2, TestServerServiceEnum.TEST_SERVER);
            return "";
        }
    }
    
    /**
     * 
     * @param token
     * @throws IOException 
     */
    private void sendRandomUdpPort(final String command) throws IOException {
    	int randomPort = 0;
		Random rand = new Random();
		if ((TestServer.serverPreferences.getUdpPortMax() > 0) && (TestServer.serverPreferences.getUdpPortMin() <= TestServer.serverPreferences.getUdpPortMax())) {
			randomPort = rand.nextInt(TestServer.serverPreferences.getUdpPortMax() - TestServer.serverPreferences.getUdpPortMin()) + 
					TestServer.serverPreferences.getUdpPortMin();			
			
		}
		TestServerConsole.log("Requested UDP Port. Picked random port number: " + randomPort, 0, TestServerServiceEnum.TEST_SERVER);
		sendCommand(String.valueOf(randomPort), command);
    }

    /**
     * 
     * @param command
     * @param token
     * @throws IOException
     */
    private void runIncomingTcpTest(String command, ClientToken token) throws IOException {
    	final int port;
    	
		Pattern p = Pattern.compile(QoSServiceProtocol.CMD_TCP_TEST_IN + " ([\\d]*)");
		Matcher m = p.matcher(command);
		m.find();
		if (m.groupCount()!=1) {
			throw new IOException("tcp incoming test command syntax error: " + command);
		}
		else {
			port = Integer.parseInt(m.group(1));
		}
		
		Runnable tcpInRunnable = new Runnable() {
			
			@Override
			public void run() {
				Socket testSocket = null;
				try {
					testSocket = new Socket(socket.getInetAddress(), port);
					BufferedOutputStream out = new BufferedOutputStream(testSocket.getOutputStream());
					out.write(getBytesWithNewline("HELLO TO " + port));
					out.flush();
					testSocket.close();
				}
				catch (Exception e) {
					TestServerConsole.error(name, e, 2, TestServerServiceEnum.TCP_SERVICE);
				}
				finally {
					if (testSocket != null && !testSocket.isClosed()) {
						try {
							testSocket.close();
						} catch (IOException e) {
							e.printStackTrace();
						}
					}			
				}				
			}
		};
		
		TestServer.getCommonThreadPool().execute(tcpInRunnable);
    }
    
    /**
     * 
     * @param command
     * @param token
     * @throws IOException 
     * @throws InterruptedException 
     */
    private void runOutgoingTcpTest(String command, ClientToken token) throws Exception {
    	int port;
    	
		Pattern p = Pattern.compile(QoSServiceProtocol.CMD_TCP_TEST_OUT + " ([\\d]*)");
		Matcher m = p.matcher(command);
		m.find();
		if (m.groupCount()!=1) {
			throw new IOException("tcp outgoing test command syntax error: " + command);
		}
		else {
			port = Integer.parseInt(m.group(1));
		}
		
		try {
			TestServer.registerTcpCandidate(port, socket);
						
			sendCommand(QoSServiceProtocol.RESPONSE_OK, command);
		}
		catch (Exception e) {
			TestServerConsole.error(name + (command == null ? 
					" [No command submitted]" : " [Command: " + command + "]"), e, 1, TestServerServiceEnum.TCP_SERVICE);
		}
		finally {
			//is beeing done inside TcpServer now:
			//tcpServer.removeCandidate(socket.getInetAddress());
		}
    }
    
    /**
     * 
     * @param command
     * @param token
     * @throws IOException
     * @throws InterruptedException 
     */
    private void runIncomingUdpTest(final String command, final ClientToken token) throws IOException, InterruptedException {
    	final int port;
    	final int timeout = 5000;
		final int numPackets;
    	
		Pattern p = Pattern.compile(QoSServiceProtocol.CMD_UDP_TEST_IN + " ([\\d]*) ([\\d]*)");
		Matcher m = p.matcher(command);
		m.find();
		if (m.groupCount()!=2) {
			throw new IOException("udp incoming test command syntax error: " + command);
		}
		else {
			port = Integer.parseInt(m.group(1));
			numPackets = Integer.parseInt(m.group(2));
		}	
		
		//DatagramSocket sock = new DatagramSocket(port);
		final UdpTestCandidate clientData = new UdpTestCandidate();
		clientData.setNumPackets(numPackets);
		final DatagramSocket sock = new DatagramSocket();
		
		final CountDownLatch latch = new CountDownLatch(1);
		final Runnable sendUdpPacketsRunnable = new Runnable() {
			
			@Override
			public void run() {
				sendUdpPackets(socket.getInetAddress(), sock, port, 3000, numPackets, true, 100, token, clientData);
				latch.countDown();
			}
		};
		
		TestServer.getCommonThreadPool().execute(sendUdpPacketsRunnable);
		
		final Matcher idMatcher = ID_REGEX_PATTERN.matcher(command);
		if (!idMatcher.find()) {
			latch.await(timeout, TimeUnit.MILLISECONDS);
			sendRcvResult(clientData, port, command);
		}
    }

    /**
     * 
     * @param targetHost
     * @param sock
     * @param port
     * @param timeOut
     * @param numPackets
     * @param awaitResponse
     * @param delay
     * @param token
     * @return
     */
    private UdpTestCandidate sendUdpPackets(InetAddress targetHost, DatagramSocket sock, int port, int timeOut, 
    		int numPackets, boolean awaitResponse, int delay, ClientToken token, final UdpTestCandidate clientData) {
    	clientUdpInDataMap.put(port, clientData);
    	
	    ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
	    DataOutputStream dataOut = new DataOutputStream(byteOut);
	 
	    TestServerConsole.log("INIT sending UDP packets (amount = " + numPackets + ") to " + targetHost + " on port " + port
	    		+ " - using DatagramSocket: " + sock.getLocalAddress() + ":" + sock.getLocalPort(), 
	    		2, TestServerServiceEnum.UDP_SERVICE);

	    
	    try {
			sock.setSoTimeout(timeOut);
	    } catch (SocketException e) {
	    	e.printStackTrace();
	    	return clientData;
		}
	 
	    byte[] data;

	    for (int i = 0; i < numPackets; i++) {
            
	    	byteOut.reset();
	    	try {
	    		Thread.sleep(delay);
	    		dataOut.writeByte(QoSServiceProtocol.UDP_TEST_AWAIT_RESPONSE_IDENTIFIER);
	    		dataOut.writeByte(i);
    			dataOut.write(token.getUuid().getBytes());
	    	} catch (IOException | InterruptedException e) {
	    		e.printStackTrace();
	    		sock.close();
	    		return clientData;
			}
	      	 
	    	try {
		    	byteOut.flush();
		    	data = byteOut.toByteArray();
		    	
			    DatagramPacket packet = new DatagramPacket(data, data.length, targetHost, port);
	    		sock.send(packet);
	    		
	    		if (awaitResponse) {
	    			try {
	    			    byte buffer[] = new byte[1024];

	    			    DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
    			    	sock.receive(dp);
    			    	
	    				int packetNumber = buffer[1];
	    				
	    			    TestServerConsole.log(dp.getAddress() + ": UDP Test received packet: #" + packetNumber + " -> " + buffer, 
	    			    		2, TestServerServiceEnum.UDP_SERVICE);
	    			    
	    				//check udp packet:
	    				if (buffer[0] != QoSServiceProtocol.UDP_TEST_RESPONSE) {
	    					TestServerConsole.log(dp.getAddress() + ": bad UDP IN TEST packet identifier", 0, TestServerServiceEnum.UDP_SERVICE);
	    					throw new IOException("bad UDP IN TEST packet identifier");
	    				}
	    				
						//check for duplicate packets:
	    				if (clientData.getPacketsReceived().contains(packetNumber)) {
	    					TestServerConsole.log(dp.getAddress() + ": duplicate UDP IN TEST packet id", 0, TestServerServiceEnum.UDP_SERVICE);
	    					clientData.getPacketDuplicates().add(packetNumber);
	    					if (ABORT_ON_DUPLICATE_UDP_PACKETS) {
	    						throw new IOException("duplicate UDP IN TEST packet id");
	    					}
	    				}
	    				else {
	    					clientData.getPacketsReceived().add(new Integer(packetNumber));
	    				}	    				
	    			}
	    			catch (SocketTimeoutException e) {
	    				//packet not received
						Logger.getLogger(ClientHandler.class.getName()).log(Level.INFO, "udp socket timeout (port " + port + ")");
						//e.printStackTrace();
	    			}
	    		}
	    	} catch (IOException e) {
	    		e.printStackTrace();
	    		sock.close();
	    		return clientData;
	    	}
	    	
    		TestServerConsole.log("Sent packet pnum:" + i + " to " + targetHost + ":" + port +", sent message:" + data, 
    				2, TestServerServiceEnum.TEST_SERVER);
	    }

	    try {
	    	byteOut.close();
	    } catch (Exception e) {
	    	e.printStackTrace();
	    	return clientData;
	    }
	    finally {
	    	sock.close();
	    }
	    
		TestServerConsole.log(socket.getInetAddress() + ": Udp Incoming finished! RCV PACKETS: " + clientData.getPacketsReceived().size() 
				+ ", DUP: " + clientData.getPacketDuplicates().size(), 2, TestServerServiceEnum.UDP_SERVICE);
	    
	    return clientData;
    }
    
    /**
     * 
     * @param command
     * @throws IOException 
     * @throws InterruptedException 
     */
    private void runOutgoingUdpTest(final String command, final ClientToken token) throws IOException, InterruptedException {
    	final long timeout = 5000;
		final Pattern p = Pattern.compile(QoSServiceProtocol.CMD_UDP_TEST_OUT + " ([\\d]*) ([\\d]*)");
		final Matcher m = p.matcher(command);
		m.find();

		if (m.groupCount()!=2) {
			throw new IOException("udp outgoing test command syntax error: " + command);
		}

    	
    	final int port = Integer.parseInt(m.group(1));
		final int numPackets = Integer.parseInt(m.group(2));

		TestServerConsole.log("Starting UDP OUT TEST (requested packets: " + numPackets + ") on port :" + port + " for " + socket.getInetAddress().toString(), 
				1, TestServerServiceEnum.UDP_SERVICE);

		final UdpTestCandidate udpData = new UdpTestCandidate();
		udpData.setNumPackets(numPackets);
		
		clientUdpOutDataMap.put(port, udpData);
		
		final UdpPacketReceivedCallback receiveCallback = new UdpPacketReceivedCallback() {
			
			@Override
			public boolean onReceive(final DatagramPacket dp, final String uuid, final AbstractUdpServer<?> udpServer) {				
				final byte[] data = dp.getData();
				final int packetNumber = data[1];
				final UdpTestCandidate clientUdpData = udpServer.getClientData(uuid);

				//check udp packet:
				if (data[0] != QoSServiceProtocol.UDP_TEST_ONE_DIRECTION_IDENTIFIER && data[0] != QoSServiceProtocol.UDP_TEST_AWAIT_RESPONSE_IDENTIFIER) {
					TestServerConsole.log(dp.getAddress() +  ": bad UDP IN TEST packet identifier", 0, TestServerServiceEnum.UDP_SERVICE);
					clientUdpData.setError(true);
					clientUdpData.setErrorMsg("bad UDP IN TEST packet identifier");
				}
								
				//check for duplicate packets:
				if (clientUdpData.getPacketsReceived().contains(packetNumber)) {
					//DUP
					TestServerConsole.log(dp.getAddress() + ": duplicate UDP IN TEST packet id", 0, TestServerServiceEnum.UDP_SERVICE);
					clientUdpData.getPacketDuplicates().add(packetNumber);
					if (ABORT_ON_DUPLICATE_UDP_PACKETS) {
						clientUdpData.setError(true);
						clientUdpData.setErrorMsg("duplicate UDP IN TEST packet id");
					}
				}
				else {
					//regular packet received:
					clientUdpData.getPacketsReceived().add(new Integer(packetNumber));

					if (data[0] == QoSServiceProtocol.UDP_TEST_AWAIT_RESPONSE_IDENTIFIER) {
						data[0] = QoSServiceProtocol.UDP_TEST_RESPONSE;
						DatagramPacket response = new DatagramPacket(data, dp.getLength(), dp.getAddress(), dp.getPort());
						try {
							udpServer.send(response);
						}
						catch (Exception e) {
							//ignore exception (can be a blocked outgoing port; in this case the test should continue normally)
						}
					}
					
					TestServerConsole.log(name + " received regular Packet #"+ packetNumber, 2, TestServerServiceEnum.UDP_SERVICE);
					
					//if all packets have been received and an onComplete callback exists run it and remove the client data
					//from the udp server
					if (clientUdpData.getPacketsReceived().size() >= clientUdpData.getNumPackets() 
							&& clientUdpData.getOnUdpTestCompleteCallback() != null) {
						if (clientUdpData.getOnUdpTestCompleteCallback().onComplete(udpServer)) {
							udpServer.pollClientData(token.getUuid());
						}
					}
					
					return true;
				}
				
				return false;
			}
		};
		
		//packet receive callback
		udpData.setOnUdpPacketReceivedCallback(receiveCallback);
		
		final CountDownLatch latch = new CountDownLatch(1);
		
		final UdpTestCompleteCallback finishCallback = new UdpTestCompleteCallback() {
			
			@Override
			public boolean onComplete(final AbstractUdpServer<?> udpServer) {
				try {
					TestServerConsole.log("UDP OUT TEST on port :" + port + " for " + socket.getInetAddress().toString() + ":" + socket.getPort() 
							+ " finished successfully...", 1, TestServerServiceEnum.UDP_SERVICE);
					latch.countDown();
					return true;
				}
				catch (Exception e) {
					e.printStackTrace();
				}
				
				return false;
			}
		};
		
		//add callback to udp client data in case all udp packets will arrive. in this case we can send back "RCV" before the
		//final timeout is reached
		udpData.setOnUdpTestCompleteCallback(finishCallback);
		
		//register udp client data
		TestServer.registerUdpCandidate(socket.getLocalAddress(), port, token.getUuid(), udpData);

		//tell the client that we are ready
		try {
			sendCommand(QoSServiceProtocol.RESPONSE_OK, command);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		final Matcher idMatcher = ID_REGEX_PATTERN.matcher(command);
		if (!idMatcher.find()) {
			latch.await(timeout, TimeUnit.MILLISECONDS);
			sendRcvResult(clientUdpOutDataMap.get(port), port, command);
		}
    }
    
    /**
     * 
     * @param command
     * @param token
     * @throws IOException
     * @throws InterruptedException
     */
    private void runVoipTest(final String command, final ClientToken token) throws IOException, InterruptedException { 
    	/*
    	 * syntax: VOIPTEST 0 1 2 3 4 5 6 7 
    	 * 	0 = outgoing port (server port)
    	 * 	1 = incoming port (client port) 
    	 *  2 = sample rate (in Hz)
    	 * 	3 = bits per sample
    	 * 	4 = packet delay in ms 
    	 * 	5 = call duration (test duration) in ms 
    	 * 	6 = starting sequence number (see rfc3550, rtp header: sequence number)
    	 *  7 = payload type
    	 */
		final Pattern p = Pattern.compile(QoSServiceProtocol.CMD_VOIP_TEST + " ([\\d]*) ([\\w]*) ([\\d]*) ([\\d]*) ([\\d]*) ([\\d]*) ([\\d]*) ([\\d]*)");
		final Matcher m = p.matcher(command);
		m.find();

		if (m.groupCount()!=8) {
			throw new IOException("voip test command syntax error: " + command);
		}

    	final int portOut = Integer.parseInt(m.group(1));
    	int portIn;
    	try {
    		portIn = Integer.parseInt(m.group(2));
    	}
    	catch (final Exception e) {
    		portIn = 0;
    	}
    	final int sampleRate = Integer.parseInt(m.group(3));
    	final int bps = Integer.parseInt(m.group(4));
    	final int delay = Integer.parseInt(m.group(5));
    	final int callDuration = Integer.parseInt(m.group(6));
    	final long sequenceNumber = Integer.parseInt(m.group(7));
    	final int payloadTypeValue = Integer.parseInt(m.group(8));
    	final int ssrc = TestServer.randomizer.next();

		TestServerConsole.log("Starting VOIP TEST (sample rate: " + sampleRate + ", bps: " + bps + ", delay: " + delay 
				+ ", call duration: " + callDuration + ", ssrc: " + ssrc + ", seq number: " + sequenceNumber 
				+ ") on outgoing port :" + portOut + "/incoming port: " + portIn + " for " + socket.getInetAddress().toString(), 1, TestServerServiceEnum.UDP_SERVICE);
		
		final VoipTestCandidate voipData = new VoipTestCandidate(sequenceNumber, sampleRate);
		
		clientVoipDataMap.put(ssrc, voipData);
				
		final UdpPacketReceivedCallback receiveCallback = new UdpPacketReceivedCallback() {
			
			@Override
			public boolean onReceive(final DatagramPacket dp, final String uuid, final AbstractUdpServer<?> udpServer) {
				final long timestampNs = System.nanoTime();
				final byte[] data = dp.getData();
				final VoipTestCandidate clientVoipData = (VoipTestCandidate) udpServer.getClientData(uuid);

				try {
					if (clientVoipData.getRtpControlDataList().size() == 0) {
						final InetAddress targetAddr = dp.getAddress();
						final int targetPort = dp.getPort();
						
						TestServerConsole.log(getName() + " Voip: Received first packet! Starting response stream for: " + targetAddr.toString() + ":" + targetPort, 1, TestServerServiceEnum.UDP_SERVICE);
						final Runnable rtpStreamSendRunnable = new Runnable() {
							
							@Override
							public void run() {
								PayloadType payloadType = PayloadType.getByCodecValue(payloadTypeValue);
								payloadType = PayloadType.UNKNOWN.equals(payloadType) ? PayloadType.PCMA : payloadType;
								try {
									RtpUtil.runVoipStream(udpServer.getSocket(), false, targetAddr, targetPort, sampleRate, bps, 
												payloadType, sequenceNumber, ssrc, callDuration, delay, 10000, true, null);
								} catch (Exception e) {
									TestServerConsole.error(getName(), e, 0, TestServerServiceEnum.UDP_SERVICE);
								}
							}
						};

						TestServer.getCommonThreadPool().execute(rtpStreamSendRunnable);
					}
					
					RtpPacket rtpPacket = new RtpPacket(data);
					TestServerConsole.log(getName() + " RTP Packet received. Sequence Number: " 
							+ rtpPacket.getSequnceNumber() + ", TS: " + timestampNs + ", SSRC: " + rtpPacket.getSsrc(), 1, TestServerServiceEnum.UDP_SERVICE);
					clientVoipData.resetTtl(3000);
					clientVoipData.addRtpControlData(rtpPacket, timestampNs);
				} catch (RtpException e) {
					TestServerConsole.error(getName(), e, 1, TestServerServiceEnum.UDP_SERVICE);
					return true;
				}
				//check udp packet:
				return true;
			}
		};
		
		//packet receive callback
		voipData.setOnUdpPacketReceivedCallback(receiveCallback);
		
		//register udp client data
		TestServer.registerUdpCandidate(socket.getLocalAddress(), portOut, "VOIP_" + String.valueOf(ssrc), voipData);

		//tell the client that we are ready
		try {
			sendCommand(QoSServiceProtocol.RESPONSE_OK + " " + String.valueOf(ssrc), command);
		} catch (IOException e) {
			TestServerConsole.error(getName(), e, 0, TestServerServiceEnum.UDP_SERVICE);
		}	
    }
    
    /**
     * 
     * @param command
     * @param token
     * @throws IOException
     */
    private void runRcvCommand(String command, ClientToken token, boolean isIncoming) throws IOException {
		Pattern p = Pattern.compile((isIncoming ? QoSServiceProtocol.REQUEST_UDP_RESULT_IN : QoSServiceProtocol.REQUEST_UDP_RESULT_OUT) + " ([\\d]*)");
		Matcher m = p.matcher(command);
		m.find();

		if (m.groupCount()!=1) {
			throw new IOException("RCV command syntax error: " + command);
		}
		else {
			final int port = Integer.parseInt(m.group(1));
			sendRcvResult(isIncoming ? clientUdpInDataMap.get(port) : clientUdpOutDataMap.get(port), port, command);
		}
    }

    
    private void sendRcvResult(final UdpTestCandidate result, final int port, final String command) throws IOException {
		if (result != null && result.getPacketsReceived() != null && !result.isError()) {
			TestServerConsole.log("RESULT OK, RCV PACKETS: " + result.getPacketsReceived().size() + ", DUP: " + result.getPacketDuplicates().size(), 1, TestServerServiceEnum.UDP_SERVICE);
			sendCommand(QoSServiceProtocol.RESPONSE_UDP_NUM_PACKETS_RECEIVED + " " + result.getPacketsReceived().size() + " " + result.getPacketDuplicates().size(), command);
		}
		else {
			TestServerConsole.log("RESULT ERROR, error: " + (result != null ? result.getErrorMsg() : "sorry, no error message available!"), 
					1, TestServerServiceEnum.UDP_SERVICE);
			sendCommand(QoSServiceProtocol.RESPONSE_UDP_NUM_PACKETS_RECEIVED + " 0 0", command);
		}
    }
    
    /**
     * 
     * @param command
     * @param token
     * @throws IOException
     */
    private void runVoipResultCommand(String command, ClientToken token) throws IOException {
		Pattern p = Pattern.compile(QoSServiceProtocol.REQUEST_VOIP_RESULT + " ([\\d]*)");
		Matcher m = p.matcher(command);
		m.find();

		if (m.groupCount()!=1) {
			throw new IOException("GET VOIPRESULT command syntax error: " + command);
		}
		else {
			final int ssrc = Integer.parseInt(m.group(1));
			sendVoipResult(command, token, ssrc);
		}
    }
    
    private void sendVoipResult(String command, ClientToken token, int ssrc) throws IOException {
		final VoipTestCandidate voipTc = clientVoipDataMap.get(ssrc);
		if (voipTc != null) {
			try {
				RtpQoSResult result = RtpUtil.calculateQoS(voipTc.getRtpControlDataList(), 
						voipTc.getInitialSequenceNumber(), voipTc.getSampleRate());

				final String voipResult = QoSServiceProtocol.RESPONSE_VOIP_RESULT + " " + result.getMaxJitter() + " " 
						+ result.getMeanJitter() + " " + result.getMaxDelta() + " " + result.getSkew() + " "
						+ result.getReceivedPackets() + " " + result.getOutOfOrder() + " " 
						+ result.getMinSequential() + " " + result.getMaxSequencial();

				TestServerConsole.log("Sending VOIP results for SSRC " + ssrc + ": " + voipResult, 2, TestServerServiceEnum.UDP_SERVICE);
				sendCommand(voipResult, command);				
			}
			catch (Exception e) {
				TestServerConsole.error(getName(), e, 1, TestServerServiceEnum.UDP_SERVICE);
			}
		}
		else {
			sendCommand(QoSServiceProtocol.RESPONSE_ERROR_ILLEGAL_ARGUMENT + " " + ssrc, command);
		}
    }

    
    /**
	 * runs the non transparent proxy test:
	 * 
	 * 1. open socket on requested port and send "OK" to let client continue the test 
	 * 2. wait for incoming HTTP protocol request
	 * 3. send request back to client (echo)
	 *  
     * @param command
     * @throws IOException 
     * @throws InterruptedException 
     */
    private void runNonTransparentProxyTest(String command) throws Exception {
		int echoPort;
		
		Pattern p = Pattern.compile(QoSServiceProtocol.CMD_NON_TRANSPARENT_PROXY_TEXT + " ([\\d]*)");
		Matcher m = p.matcher(command);
		m.find();
		if (m.groupCount()!=1) {
			throw new IOException("non transparent proxy test command syntax error: " + command);
		}
		else {
			echoPort = Integer.parseInt(m.group(1));
		}
		
		try {
			TestServer.registerTcpCandidate(echoPort, socket);
			
			sendCommand(QoSServiceProtocol.RESPONSE_OK, command);
			TestServerConsole.log("NTP: sendind OK. waiting for request...", 1, TestServerServiceEnum.TCP_SERVICE);
		}
		catch (Exception e) {
			TestServerConsole.error(name, e, 1, TestServerServiceEnum.TCP_SERVICE);
		}
		finally {
			//is beeing done inside TcpServer now:
			//tcpServer.removeCandidate(socket.getInetAddress());
		}
    }
    
    /**
     * 
     * @param command
     * @throws IOException
     */
    public void requestNewConnectionTimeout(String command) throws IOException {
		final Pattern p = Pattern.compile(QoSServiceProtocol.REQUEST_NEW_CONNECTION_TIMEOUT + " ([\\d]*)");
		final Matcher m = p.matcher(command);
		m.find();

		if (m.groupCount()!=1) {
			throw new IOException("request new connection timeout command syntax error: " + command);
		}
		
		Integer requestedConnTimeout = Integer.parseInt(m.group(1));
		if (requestedConnTimeout < QoSServiceProtocol.TIMEOUT_CLIENTHANDLER_CONNECTION_MIN_VALUE) {
			sendErrorCommand(QoSServiceProtocol.RESPONSE_ERROR_ILLEGAL_ARGUMENT + " " + requestedConnTimeout, command);
		}
		else {
			socket.setSoTimeout(requestedConnTimeout);
			sendCommand(QoSServiceProtocol.RESPONSE_OK, command);
		}
    }

    /**
     * 
     * @param command
     * @throws IOException
     */
    public void requestProtocolVersion(String command) throws IOException {
		final Pattern p = Pattern.compile(QoSServiceProtocol.REQUEST_PROTOCOL_VERSION + " ([a-zA-Z0-9]*)");
		final Matcher m = p.matcher(command);
		m.find();

		if (m.groupCount()!=1) {
			throw new IOException("request protocol version command syntax error: " + command);
		}
		
		String requestedProtocolVersion = m.group(1);
		if (!QoSServiceProtocol.SUPPORTED_PROTOCOL_VERSION_SET.contains(requestedProtocolVersion)) {
			sendErrorCommand(QoSServiceProtocol.RESPONSE_ERROR_UNSUPPORTED + " " + requestedProtocolVersion, command);
		}
		else {
			clientProtocolVersion = requestedProtocolVersion;
			sendCommand(QoSServiceProtocol.RESPONSE_OK, command);
		}
    }

    /**
     * 
     * @return
     */
    public ServerSocket getServerSocket() {
    	return this.serverSocket;
    }
    
    /**
     * 
     * @param outCommand
     * @throws IOException
     */
    public synchronized void sendCommand(String outCommand) throws IOException {
    	sendCommand(outCommand, null);
    }

    /**
     * 
     * @param outCommand
     * @param inCommand
     * @throws IOException 
     */
    public synchronized void sendCommand(String outCommand, String inCommand) throws IOException {
    	String id = null;
    	if (inCommand != null) {
			Matcher m = ID_REGEX_PATTERN.matcher(inCommand);
			if (m.find()) {
				id = m.group(1);
				outCommand = outCommand + (id != null ? " +ID" + id : "");
			}
    	}

    	TestServerConsole.log(name + " sending answer: [" + outCommand + "] to: [" + inCommand +"] ", 2, TestServerServiceEnum.TEST_SERVER);
		out.write(getBytesWithNewline(outCommand));
    }

    /**
     * 
     * @param error
     * @throws IOException
     */
    public synchronized void sendErrorCommand(String error) throws IOException {
    	sendCommand(QoSServiceProtocol.RESPONSE_ERROR_RESPONSE + error);
    }
    
    /**
     * 
     * @param error
     * @param inCommand
     * @throws IOException
     */
    public synchronized void sendErrorCommand(String error, String inCommand) throws IOException {
    	sendCommand(QoSServiceProtocol.RESPONSE_ERROR_RESPONSE + error, inCommand);
    }

	@Override
	public String toString() {
		return "ClientHandler [socket=" + socket + ", name=" + name
				+ ", clientUdpOutDataMap=" + clientUdpOutDataMap
				+ ", clientUdpInDataMap=" + clientUdpInDataMap + "]";
	}
	
	public String getName() {
		return name;
	}

	public ConcurrentHashMap<Integer, UdpTestCandidate> getClientUdpOutDataMap() {
		return clientUdpOutDataMap;
	}

	public ConcurrentHashMap<Integer, UdpTestCandidate> getClientUdpInDataMap() {
		return clientUdpInDataMap;
	}
}