/*******************************************************************************
 * Copyright 2013-2015 alladin-IT 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.alladin.rmbt.client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.InputMismatchException;
import java.util.List;
import java.util.Locale;
import java.util.Scanner;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;

import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;

import at.alladin.rmbt.client.helper.TestStatus;

public class RMBTTest extends AbstractRMBTTest implements Callable<ThreadTestResult>
{
    private static final long nsecsL = 1000000000L;
//    private static final double nsecs = 1e9;
    
    private static final long UPLOAD_MAX_DISCARD_TIME = 1 * nsecsL;
    private static final long UPLOAD_MAX_WAIT_SECS = 3;
    
    private final CyclicBarrier barrier;
    private final AtomicBoolean fallbackToOneThread;
    
    private final boolean doDownload = true;
    private final boolean doUpload = true;
    
    private final AtomicLong curTransfer = new AtomicLong();
    private final AtomicLong curTime = new AtomicLong();
        
    private final long minDiffTime;
    private final int maxCoarseResults;
    private final int maxFineResults;
    
    private class SingleResult
    {
        private final Results fine;
        private final Results coarse;
        
        private int fineResults = 0;
        private int coarseResults = 0;
        
        SingleResult()
        {
            fine = new Results(maxFineResults);
            coarse = new Results(maxCoarseResults);
        }
        
        @Override
		public String toString() {
			return "SingleResult [fine=" + fine + ", coarse=" + coarse
					+ ", fineResults=" + fineResults + ", coarseResults="
					+ coarseResults + "]";
		}



		public void addResult(final long newBytes, final long newNsec)
        {
            
            boolean addToCoarse = coarseResults == 0;
            if (! addToCoarse)
            {
                final long diffTime = newNsec - coarse.nsec[(coarseResults - 1) % coarse.nsec.length];
                if (diffTime > minDiffTime)
                    addToCoarse = true;
            }
            
            if (coarse.bytes.length > 0) {
                if (addToCoarse)
                {
                    int coarsePos = coarseResults++ % coarse.bytes.length;
                    coarse.bytes[coarsePos] = newBytes;
                    coarse.nsec[coarsePos] = newNsec;
                }
                
                int finePos = fineResults++ % fine.bytes.length;
                fine.bytes[finePos] = newBytes;
                fine.nsec[finePos] = newNsec;
            }
        }
        
//        @SuppressWarnings("unused")
//        void logResult(final String type)
//        {
//            log(String.format(Locale.US, "thread %d - Time Diff %d", threadId, nsec));
//            log(String.format(Locale.US, "thread %d: %.0f kBit/s %s (%.2f kbytes / %.3f secs)", threadId, getSpeed() / 1e3, type,
//                    getBytes() / 1e3, getNsec() / nsecs));
//        }
        
//        // bit/s
//        double getSpeed()
//        {
//            return (double) getBytes() / (double) getNsec() * nsecs * 8.0;
//        }
        
        public long getBytes()
        {
            if (fineResults == 0)
                return 0;
            else
                return fine.bytes[(fineResults - 1) % fine.bytes.length];
        }
        
        public long getNsec()
        {
            if (fineResults == 0)
                return 0;
            else
                return fine.nsec[(fineResults - 1) % fine.nsec.length];
        }
        
        public Results getAllResults()
        {
            final int numResultsCoarse = Math.min(coarseResults, maxCoarseResults);
            final int numResultsFine = Math.min(fineResults, maxFineResults);
            final int numResults = numResultsCoarse + numResultsFine;
            
            long[] resultBytes = new long[numResults];
            long[] resultNsec = new long[numResults];
            
            int results = 0;
            int posCoarse = coarseResults - numResultsCoarse;
            int posFine = fineResults - numResultsFine;
            
            while (results < numResults && (posCoarse < coarseResults || posFine < fineResults))
            {
                final boolean coarseAvail = posCoarse < coarseResults;
                final boolean fineAvail = posFine < fineResults;
                final long thisCoarse = coarseAvail ? coarse.nsec[posCoarse % coarse.nsec.length] : -1;
                final long thisFine = fineAvail ? fine.nsec[posFine % fine.nsec.length] : -1;
                
                if ((thisFine <= thisCoarse || thisCoarse == -1) && fineAvail)
                {
                    resultNsec[results] = thisFine;
                    resultBytes[results++] = fine.bytes[posFine++ % fine.bytes.length];
                    
                    if (thisFine == thisCoarse && coarseAvail)
                        posCoarse++;
                }
                else if ((thisCoarse < thisFine || thisFine == -1) && coarseAvail)
                {
                    resultNsec[results] = thisCoarse;
                    resultBytes[results++] = coarse.bytes[posCoarse++ % coarse.bytes.length];
                }
                else // shoudn't happen; avoid endless loop
                    break;
            }
            
            if (results < numResults)
            {
//                resultBytes = Arrays.copyOf(resultBytes, results); // copyOf not avail in android sdk < 9
//                resultNsec = Arrays.copyOf(resultNsec, results);
                
                long[] newResultBytes = new long[results];
                long[] newResultNsec = new long[results];
                System.arraycopy(resultBytes, 0, newResultBytes, 0, results);
                System.arraycopy(resultNsec, 0, newResultNsec, 0, results);
                resultBytes = newResultBytes;
                resultNsec = newResultNsec;
            }
            final Results result = new Results(resultBytes, resultNsec);
            return result;
        }
        
        public void addCoarseSpeedItems(List<SpeedItem> list, boolean upload, int thread)
        {
            long lastNsec = 0;
            final int numResultsCoarse = Math.min(coarseResults, maxCoarseResults);
            for (int i = 0; i < numResultsCoarse; i++)
            {
                final long nsec = coarse.nsec[i % coarse.nsec.length];
                final long bytes = coarse.bytes[i % coarse.bytes.length];
                final SpeedItem item = new SpeedItem(upload, thread, nsec, bytes);
                list.add(item);
                lastNsec = nsec;
            }
            
            final long nsec = getNsec();
            if (nsec > lastNsec)
            {
                final long bytes = getBytes();
                final SpeedItem item = new SpeedItem(upload, thread, nsec, bytes);
                list.add(item);
            }
        }
    }
    
    public RMBTTest(final RMBTClient client, final RMBTTestParameter params, final int threadId,
            final CyclicBarrier barrier, final int storeResults, final long minDiffTime,
            final AtomicBoolean fallbackToOneThread)
    {
    	super (client, params, threadId);
        this.barrier = barrier;
        this.maxCoarseResults = storeResults;
        this.maxFineResults = storeResults;
        this.minDiffTime = minDiffTime;
        this.fallbackToOneThread = fallbackToOneThread;
    }
        
    static class CurrentSpeed
    {
        long trans;
        long time;

        @Override
		public String toString() {
			return "CurrentSpeed [trans=" + trans + ", time=" + time + "]";
		}
    }
    
    public CurrentSpeed getCurrentSpeed(CurrentSpeed result)
    {
        if (result == null)
            result = new CurrentSpeed();
        result.trans = curTransfer.get();
        result.time = curTime.get();
        return result;
    }
    
    protected Socket connect(final TestResult testResult) throws IOException
    {
        log(String.format(Locale.US, "thread %d: connecting...", threadId));
        
        final InetAddress inetAddress = InetAddress.getByName(params.getHost());
        
        System.out.println("connecting to: " + inetAddress.getHostName() + ":" + params.getPort());
        final Socket s = getSocket(inetAddress.getHostAddress(), params.getPort(), true, 20000);
        
        testResult.ip_local = s.getLocalAddress();
        testResult.ip_server = s.getInetAddress();
        
        testResult.port_remote = s.getPort();
        
        if (s instanceof SSLSocket)
        {
            final SSLSocket sslSocket = (SSLSocket) s;
            final SSLSession session = sslSocket.getSession();
            testResult.encryption = String.format(Locale.US, "%s (%s)", session.getProtocol(), session.getCipherSuite());
        }
        
        log(String.format(Locale.US, "thread %d: ReceiveBufferSize: '%s'.", threadId, s.getReceiveBufferSize()));
        log(String.format(Locale.US, "thread %d: SendBufferSize: '%s'.", threadId, s.getSendBufferSize()));
        
        if (in != null)
            totalDown += in.getCount();
        if (out != null)
            totalUp += out.getCount();
        
        in = new InputStreamCounter(s.getInputStream());
        reader = new BufferedReader(new InputStreamReader(in, "US-ASCII"), 4096);
        out = new OutputStreamCounter(s.getOutputStream());
        
        String line = reader.readLine();
        if (!line.equals(EXPECT_GREETING))
        {
            log(String.format(Locale.US, "thread %d: got '%s' expected '%s'", threadId, line, EXPECT_GREETING));
            return null;
        }
        
        line = reader.readLine();
        if (!line.startsWith("ACCEPT "))
        {
            log(String.format(Locale.US, "thread %d: got '%s' expected 'ACCEPT'", threadId, line));
            return null;
        }
        
        final String send = String.format(Locale.US, "TOKEN %s\n", params.getToken());
        
        out.write(send.getBytes("US-ASCII"));
        
        line = reader.readLine();
        
        if (line == null)
        {
            log(String.format(Locale.US, "thread %d: got no answer expected 'OK'", threadId, line));
            return null;
        }
        else if (!line.equals("OK"))
        {
            log(String.format(Locale.US, "thread %d: got '%s' expected 'OK'", threadId, line));
            return null;
        }
        
        line = reader.readLine();
        final Scanner scanner = new Scanner(line);
        try
        {
            if (!"CHUNKSIZE".equals(scanner.next()))
            {
                log(String.format(Locale.US, "thread %d: got '%s' expected 'CHUNKSIZE'", threadId, line));
                return null;
            }
            try
            {
                chunksize = scanner.nextInt();
                log(String.format(Locale.US, "thread %d: CHUNKSIZE is %d", threadId, chunksize));
            }
            catch (final Exception e)
            {
                log(String.format(Locale.US, "thread %d: invalid CHUNKSIZE: '%s'", threadId, line));
                return null;
            }
            if (buf == null || buf != null && buf.length != chunksize)
                buf = new byte[chunksize];
            return s;
        }
        finally
        {
            scanner.close();
        }
    }
    
    public ThreadTestResult call()
    {
        log(String.format(Locale.US, "thread %d: started.", threadId));
        final ThreadTestResult testResult = new ThreadTestResult();
        Socket s = null;
        try
        {
            
            s = connect(testResult);
            if (s == null)
                throw new Exception("error during connect to test server");
            
            log(String.format(Locale.US, "thread %d: connected, waiting for rest...", threadId));
            barrier.await();
            
            /***** short download *****/
            {
                final long targetTimeEnd = System.nanoTime() + params.getPretestDuration() * nsecsL;
                int chunks = 1;
                do
                {
                    downloadChunks(chunks);
                    chunks *= 2;
                }
                while (System.nanoTime() < targetTimeEnd);
                
                if (chunks <= 4)
                    // connection is quite slow, we'll only use 1 thread
                    fallbackToOneThread.set(true);
            }
            /*********************/
            
            boolean _fallbackToOneThread;
            setStatus(TestStatus.PING);
            /***** ping *****/
            {
                barrier.await();
                
                startTrafficService(TestStatus.PING);
                
                _fallbackToOneThread = fallbackToOneThread.get();
                
                if (_fallbackToOneThread && threadId != 0)
                    return null;
                
                final int NUMPINGS = params.getNumPings();
                long shortestPing = Long.MAX_VALUE;
                long medianPing = Long.MAX_VALUE;
                long[] pings = new long[NUMPINGS];
                final long timeStart = System.nanoTime();
                if (threadId == 0) // only one thread pings!
                {
                	for (int i = 0; i < NUMPINGS; i++)
                	{
                		final Ping ping = ping();
                		if (ping != null)
                		{
                		    client.updatePingStatus(timeStart, i+1, System.nanoTime());
                		    
                		    pings[i] = ping.server;
                			if (ping.client < shortestPing)
                				shortestPing = ping.client;

                			testResult.pings.add(ping);
                		}
                	}
                	
                	// median
                	Arrays.sort(pings);
                	int middle = ((pings.length) / 2);
                	if(pings.length % 2 == 0){
                		long medianA = pings[middle];
                		long medianB = pings[middle-1];
                		medianPing = (medianA + medianB) / 2;
                	} else{
                		medianPing = pings[middle + 1];
                	}
                	// display median ping
                	client.setPing(medianPing);
                }
                testResult.ping_shortest = shortestPing;
                testResult.ping_median = medianPing;

            }
            /*********************/
            
                        
            if (doDownload)
            {
                final int duration = params.getDuration();
            	//final int duration = 1;
                
                setStatus(TestStatus.DOWN);
                /***** download *****/
                
                if (!_fallbackToOneThread)
                    barrier.await();
                
                stopTrafficService(TestStatus.PING);
                startTrafficService(TestStatus.DOWN);
                
                curTransfer.set(0);
                curTime.set(0);
                
                final SingleResult result = new SingleResult();
                final boolean reinitSocket = download(duration, 0, result);
                if (reinitSocket)
                {
                    s.close();
                    s = connect(testResult);
                    log(String.format(Locale.US, "thread %d: reconnected", threadId));
                    if (s == null)
                        throw new Exception("error during connect to test server");
                }
                
                testResult.down = result.getAllResults();
                result.addCoarseSpeedItems(testResult.speedItems, false, threadId);
                
//                if (threadId == 0) {
//                	System.out.println("download speed items: " + testResult.speedItems);
//                	System.out.println("download raw results: " + result);
//                }
                
                curTransfer.set(result.getBytes());
                curTime.set(result.getNsec());
             
                
                /*********************/
                
            }
            
            if (doUpload)
            {
                final int duration = params.getDuration();
            	//final int duration = 1;
                
                setStatus(TestStatus.INIT_UP);
                /***** short upload *****/
                {
                    if (!_fallbackToOneThread)
                        barrier.await();
                    
                    stopTrafficService(TestStatus.DOWN);
                    
                    curTransfer.set(0);
                    curTime.set(0);
                    
                    final long targetTimeEnd = System.nanoTime() + params.getPretestDuration() * nsecsL;
                    int chunks = 1;
                    do
                    {
                        uploadChunks(chunks);
                        chunks *= 2;
                    }
                    while (System.nanoTime() < targetTimeEnd);
                }
                /*********************/
                
                /***** upload *****/
                
                setStatus(TestStatus.UP);
                
                startTrafficService(TestStatus.UP);
                
                curTransfer.set(0);
                curTime.set(0);
                
                if (!_fallbackToOneThread)
                    barrier.await();
                
                final SingleResult result = new SingleResult();
                
                upload(duration, result);
                
                testResult.up = result.getAllResults();
                result.addCoarseSpeedItems(testResult.speedItems, true, threadId);
                
                if (in != null)
                    totalDown += in.getCount();
                if (out != null)
                    totalUp += out.getCount();
                
                testResult.totalDownBytes = totalDown;
                testResult.totalUpBytes = totalUp;
                
                curTransfer.set(result.getBytes());
                curTime.set(result.getNsec());
                
                stopTrafficService(TestStatus.UP);
                
                /*********************/
            }
            
        }
        catch (final BrokenBarrierException e)
        {
            client.log("interrupted (BBE)");
            Thread.currentThread().interrupt();
        }
        catch (final InterruptedException e)
        {
            client.log("interrupted");
            Thread.currentThread().interrupt();
        }
        catch (final Exception e)
        {
            client.log(e);
            client.abortTest(true);
        }
        finally
        {
            if (s != null)
                try
                {
                    s.close();
                }
                catch (final IOException e)
                {
                    client.log(e);
                }
        }
        return testResult;
    }
    
    private void downloadChunks(final int chunks) throws InterruptedException, IOException
    {
        if (Thread.interrupted())
            throw new InterruptedException();
        
        if (chunks < 1)
            throw new IllegalArgumentException();
        
        log(String.format(Locale.US, "thread %d: getting %d chunk(s)", threadId, chunks));
        
        String line = reader.readLine();
        if (line == null)
            throw new IllegalStateException("connection lost");
        if (!line.startsWith("ACCEPT "))
        {
            log(String.format(Locale.US, "thread %d: got '%s' expected 'ACCEPT'", threadId, line));
            throw new IllegalStateException();
        }
        
        String send;
        send = String.format(Locale.US, "GETCHUNKS %d\n", chunks);
        out.write(send.getBytes("US-ASCII"));
        out.flush();
        
        // long expectBytes = chunksize * chunks;
        long totalRead = 0;
        long read;
        byte lastByte = (byte) 0;
        do
        {
            if (Thread.interrupted())
                throw new InterruptedException();
            read = in.read(buf);
            if (read > 0)
            {
                final int posLast = chunksize - 1 - (int) (totalRead % chunksize);
                if (read > posLast)
                    lastByte = buf[posLast];
                totalRead += read;
            }
        }
        while (read > 0 && lastByte != (byte) 0xff);
        
        send = "OK\n";
        out.write(send.getBytes("US-ASCII"));
        out.flush();
        
        line = reader.readLine(); // read TIME line
    }
    
    /**
     * perform single donwload test
     * 
     * @param seconds
     *            requested duration of the test
     * @param result
     *            SingleResult object to store the results in
     * @return true if the socket needs to be reinitialized, false if can be
     *         reused
     * @throws IOException
     * @throws UnsupportedEncodingException
     * @throws InterruptedException
     * @throws IllegalStateException
     */
    private boolean download(final int seconds, final int additionalWait, final SingleResult result)
            throws IOException, UnsupportedEncodingException, InterruptedException, IllegalStateException
    {
        if (Thread.interrupted())
            throw new InterruptedException();
        
        if (seconds < 1)
            throw new IllegalArgumentException();
        
        log(String.format(Locale.US, "thread %d: download test %d seconds", threadId, seconds));
        
        String line = reader.readLine();
        if (line == null)
            throw new IllegalStateException("connection lost");
        if (!line.startsWith("ACCEPT "))
        {
            log(String.format(Locale.US, "thread %d: got '%s' expected 'ACCEPT'", threadId, line));
            throw new IllegalStateException();
        }
        
        final long timeStart = System.nanoTime();
        final long timeLatestEnd = timeStart + (seconds + additionalWait) * nsecsL;
        
        String send;
        send = String.format(Locale.US, "GETTIME %d\n", seconds);
        out.write(send.getBytes("US-ASCII"));
        out.flush();
        
        long totalRead = 0;
        long read;
        byte lastByte = (byte) 0;
        
        do
        {
            if (Thread.interrupted())
                throw new InterruptedException();
            read = in.read(buf);
            if (read > 0)
            {
                final int posLast = chunksize - 1 - (int) (totalRead % chunksize);
                if (read > posLast)
                    lastByte = buf[posLast];
                totalRead += read;
                
                final long nsec = System.nanoTime() - timeStart;
                
                result.addResult(totalRead, nsec);
                curTransfer.set(totalRead);
                curTime.set(nsec);
            }
        }
        while (read > 0 && lastByte != (byte) 0xff && System.nanoTime() <= timeLatestEnd);
        
        final long timeEnd = System.nanoTime();
        
        if (read <= 0)
        {
            log(String.format(Locale.US, "thread %d: error while receiving data", threadId));
            throw new IllegalStateException();
        }
        
        final long nsec = timeEnd - timeStart;
        result.addResult(totalRead, nsec);
        curTransfer.set(totalRead);
        curTime.set(nsec);
        
        if (lastByte != (byte) 0xff)
            return true;
        
        send = "OK\n";
        out.write(send.getBytes("US-ASCII"));
        out.flush();
        
        line = reader.readLine();
        if (line == null)
            throw new IllegalStateException("connection lost");
        final Scanner s = new Scanner(line);
        s.findInLine("TIME (\\d+)");
        s.close();
        // result.nsecServer = Long.parseLong(s.match().group(1));
        return false;
        
    }
    
    private void uploadChunks(final int chunks) throws InterruptedException, IOException
    {
        if (Thread.interrupted())
            throw new InterruptedException();
        
        if (chunks < 1)
            throw new IllegalArgumentException();
        
        log(String.format(Locale.US, "thread %d: putting %d chunk(s)", threadId, chunks));
        
        String line = reader.readLine();
        if (line == null)
            throw new IllegalStateException("connection lost");
        if (!line.startsWith("ACCEPT "))
        {
            log(String.format(Locale.US, "thread %d: got '%s' expected 'ACCEPT'", threadId, line));
            throw new IllegalStateException();
        }
        
        out.write("PUTNORESULT\n".getBytes("US-ASCII"));
        out.flush();
        
        line = reader.readLine();
        if (line == null)
            throw new IllegalStateException("connection lost");
        if (!line.equals("OK"))
            throw new IllegalStateException();
        
        buf[chunksize - 1] = (byte) 0; // set last byte to continue value
        
        for (int i = 0; i < chunks; i++)
        {
            if (i == chunks - 1)
                buf[chunksize - 1] = (byte) 0xff; // set last byte to
                                                  // termination value
            out.write(buf, 0, chunksize);
        }
        
        line = reader.readLine(); // TIME line
    }
    
    /**
     * @param seconds
     *            requested duration of the test
     * @param result
     *            SingleResult object to store the results in
     * @return true if the socket needs to be reinitialized, false if can be
     *         reused
     * @throws IOException
     * @throws UnsupportedEncodingException
     * @throws InterruptedException
     * @throws IllegalStateException
     */
    private boolean upload(final int seconds, final SingleResult result) throws IOException,
            UnsupportedEncodingException, InterruptedException, IllegalStateException
    {
        if (Thread.interrupted())
            throw new InterruptedException();
        
        if (seconds < 1 && !params.isEncryption())
            throw new IllegalArgumentException();
        
        log(String.format(Locale.US, "thread %d: upload test %d seconds", threadId, seconds));
        
        long _enoughTime = (seconds - UPLOAD_MAX_DISCARD_TIME) * nsecsL;
        if (_enoughTime < 0)
            _enoughTime = 0;
        final long enoughTime = _enoughTime;
        
        String line = reader.readLine();
        if (line == null)
            throw new IllegalStateException("connection lost");
        if (!line.startsWith("ACCEPT "))
        {
            log(String.format(Locale.US, "thread %d: got '%s' expected 'ACCEPT'", threadId, line));
            throw new IllegalStateException();
        }
        
        out.write("PUT\n".getBytes("US-ASCII"));
        out.flush();
        
        line = reader.readLine();
        if (line == null)
            throw new IllegalStateException("connection lost");
        if (!line.equals("OK"))
            throw new IllegalStateException();
        
        final AtomicBoolean terminateRxIfEnough = new AtomicBoolean(false);
        final AtomicBoolean terminateRxAtAllEvents = new AtomicBoolean(false);
        
        final Future<Boolean> futureRx = RMBTClient.getCommonThreadPool().submit(new Callable<Boolean>()
        {
            public Boolean call() throws Exception
            {
                
                final Pattern patternFull = Pattern.compile("TIME (\\d+) BYTES (\\d+)");
                final Pattern patternTime = Pattern.compile("TIME (\\d+)");
                
                final Scanner s = new Scanner(reader);
                try
                {
                    s.useDelimiter("\n");
                    boolean terminate = false;
                    do
                    {
                        String next = null;
                        try
                        {
                            next = s.next(patternFull);
                        }
                        catch (final InputMismatchException e)
                        {
                        }
                        
                        if (next == null)
                        {
                            next = s.next(patternTime);
                            if (next == null)
                            {
                                System.out.println(s.nextLine());
                                throw new IllegalStateException();
                            }
                            return false;
                        }
                        
                        final MatchResult match = s.match();
                        if (match.groupCount() == 2)
                        {
                            final long nsec = Long.parseLong(match.group(1));
                            final long bytes = Long.parseLong(match.group(2));
                            result.addResult(bytes, nsec);
                            curTransfer.set(bytes);
                            curTime.set(nsec);
                        }
                        
                        if (terminateRxAtAllEvents.get())
                            terminate = true;
                        if (terminateRxIfEnough.get() && curTime.get() > enoughTime)
                            terminate = true;
                    }
                    while (! terminate);
                    return true;
                }
                finally
                {
                    s.close();
                }
            }
        });
        
        final long maxnsecs = seconds * 1000000000L;
        buf[chunksize - 1] = (byte) 0x00; // set last byte to continue value
        
        final byte[] bufTx = buf.clone();
        final AtomicBoolean terminateTx = new AtomicBoolean(false);
        final Future<Void> futureTx = RMBTClient.getCommonThreadPool().submit(new Callable<Void>()
        {
            public Void call() throws Exception
            {
                for (;;)
                {
                    if (Thread.interrupted())
                        throw new InterruptedException();
                    if (terminateTx.get())
                    {
                        // last package
                        bufTx[chunksize - 1] = (byte) 0xff; // set last byte to termination value
                        out.write(bufTx, 0, chunksize);
                        // forces buffered bytes to be written out.
                        out.flush();
                        return null;
                    }
                    else
                        out.write(bufTx, 0, chunksize);
                }
            }
        });

        Boolean returnValue = null;
        try
        {
            try
            {
                futureTx.get(maxnsecs, TimeUnit.NANOSECONDS);
//                System.out.println("futureTx regular");
            }
            catch (final TimeoutException e)
            {
                try
                {
                    terminateTx.set(true);
                    futureTx.get(250, TimeUnit.MILLISECONDS);
//                    System.out.println("futureTx after 250");
                }
                catch (final TimeoutException e2)
                {
                    futureTx.cancel(true);
//                    System.out.println("futureTx cancel");
                }
            }
        
            Thread.sleep(100);
            
            terminateRxIfEnough.set(true);
            
            try
            {
                returnValue = futureRx.get(UPLOAD_MAX_WAIT_SECS, TimeUnit.SECONDS);
//                System.out.println("futureRx regular");
            }
            catch (final TimeoutException e)
            {
                try
                {
                    terminateRxAtAllEvents.set(true);
                    returnValue = futureRx.get(250, TimeUnit.MILLISECONDS);
//                    System.out.println("futureRx after 250");
                }
                catch (final TimeoutException e2)
                {
                    futureRx.cancel(true);
//                    System.out.println("futureRx cancel");
                }
            }
        }
        catch (final ExecutionException e)
        {
            if (e.getCause() instanceof IOException)
                throw (IOException) e.getCause();
            else
                e.printStackTrace();
        }
        
        if (returnValue == null)
            returnValue = true;
        return returnValue;
    }
    
    private Ping ping() throws IOException
    {
        log(String.format(Locale.US, "thread %d: ping test", threadId));
        
        final long pingTimeNs = System.nanoTime();
        
        String line = reader.readLine();
        if (!line.startsWith("ACCEPT "))
        {
            log(String.format(Locale.US, "thread %d: got '%s' expected 'ACCEPT'", threadId, line));
            return null;
        }
        
        final byte[] data = "PING\n".getBytes("US-ASCII");
        final long timeStart = System.nanoTime();
        out.write(data);
        out.flush();
        line = reader.readLine();
        final long timeEnd = System.nanoTime();
        out.write("OK\n".getBytes("US-ASCII"));
        out.flush();
        if (!line.equals("PONG"))
            return null;
        
        line = reader.readLine();
        final Scanner s = new Scanner(line);
        s.findInLine("TIME (\\d+)");
        s.close();
        
        final long diffClient = timeEnd - timeStart;
        final long diffServer = Long.parseLong(s.match().group(1));
        
        final double pingClient = diffClient / 1e6;
        final double pingServer = diffServer / 1e6;
        
        log(String.format(Locale.US, "thread %d - client: %.3f ms ping", threadId, pingClient));
        log(String.format(Locale.US, "thread %d - server: %.3f ms ping", threadId, pingServer));
        return new Ping(diffClient, diffServer, pingTimeNs);
    }
        
    private void setStatus(final TestStatus status)
    {
        if (threadId == 0)
            client.setStatus(status);
    }
    
    private void startTrafficService(final TestStatus status) {
    	client.startTrafficService(threadId, status);
    }
    
    private void stopTrafficService(final TestStatus status) {
    	client.stopTrafficMeasurement(threadId, status);
    }
    
}