package net.i2p.sam;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;

import net.i2p.data.DataHelper;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;

/**
 * <ol>
 *  <li>start up SAM</li>
 *  <li>Alice connects as 'Alice', gets her destination, stashes it away, and 
 *      listens for any streams, echoing back whatever she receives.</li>
 *  <li>Bob connects as 'Bob', establishes a stream to the destination Alice
 *      stashed away, sends a few bundles of data, and closes the stream.</li>
 *  <li>Alice and Bob disconnect from SAM</li>
 *  <li>SAM bridge taken down</li>
 * </ol>
 */
public class TestStreamTransfer {
    private static Log _log = new Log(TestStreamTransfer.class);
    private static String _alice = null;
    private static boolean _dead = false;
    private static Object _counterLock = new Object();
    private static int _recvCounter = 0, _closeCounter = 0;
    
    private static void runTest(String samHost, int samPort, String conOptions) {
        int nTests = 20;
        startAlice(samHost, samPort, conOptions);
        /* Start up nTests different test threads. */
        for (int i = 0; i < nTests; i++) {
            testBob("bob" + i, samHost, samPort, conOptions);
            if (i % 2 == 1)
                try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
        }
        /* Wait until the correct number of messages have been received
           by Alices and the correct number of streams have been closed
           by Bobs. */
        while (true) {
            synchronized (_counterLock) {
                if (_recvCounter == nTests * 2 && _closeCounter == nTests) {
                    break;
                }
            }
            try { Thread.sleep(1000); } catch (InterruptedException ie) {}
            _log.info("Receive counter is: " + _recvCounter + " Close counter is: " + _closeCounter);
        }
        /* Return, assuming the test has passed. */
        _log.info("Unit test passed.");
    }
    
    private static void startAlice(String host, int port, String conOptions) {
        _log.info("\n\nStarting up Alice");
        try {
            Socket s = new Socket(host, port);
            OutputStream out = s.getOutputStream();
            out.write(DataHelper.getASCII("HELLO VERSION MIN=1.0 MAX=1.0\n"));
            BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
            String line = reader.readLine();
            _log.debug("line read for valid version: " + line);
            String req = "SESSION CREATE STYLE=STREAM DESTINATION=Alice " + conOptions + "\n";
            out.write(DataHelper.getASCII(req));
            line = reader.readLine();
            _log.info("Response to creating the session with destination Alice: " + line);
            
            req = "NAMING LOOKUP NAME=ME\n";
            out.write(DataHelper.getASCII(req));
            line = reader.readLine();
            Properties props = SAMUtils.parseParams(line);
            String value = props.getProperty("VALUE");
            if (value == null) {
                _log.error("No value for ME found!  [" + line + "]");
                return;
            } else {
                _log.info("Alice is located at " + value);
            }
            _alice = value;
            I2PThread aliceThread = new I2PThread(new AliceRunner(reader, out, s));
            aliceThread.setName("Alice");
            aliceThread.start();
        } catch (Exception e) {
            _log.error("Error testing for valid version", e);
        }
    }
    
    private static class AliceRunner implements Runnable {
        private BufferedReader _reader;
        private OutputStream _out;
        private Socket _s;
        /** ID (string) to base64 destination */
        private Map _streams;
        public AliceRunner(BufferedReader reader, OutputStream out, Socket s) {
            _reader = reader;
            _out = out;
            _s = s;
            _streams = Collections.synchronizedMap(new HashMap(4));
        }
        public void run() {
            while (!_dead) {
                try {
                    doRun();
                } catch (Exception e) {
                    _log.error("Error running alice", e);
                    try { _reader.close(); } catch (IOException ioe) {}
                    try { _out.close(); } catch (IOException ioe) {}
                    try { _s.close(); } catch (IOException ioe) {}
                    _streams.clear();
                    _dead = true;
                }
            }
        }
        private void doRun() throws IOException, SAMException {
            String line = _reader.readLine();
            _log.debug("Read: " + line);
            Properties props = SAMUtils.parseParams(line);
            String maj = props.getProperty(SAMUtils.COMMAND);
            String min = props.getProperty(SAMUtils.OPCODE);
            if ( ("STREAM".equals(maj)) && ("CONNECTED".equals(min)) ) {
                String dest = props.getProperty("DESTINATION");
                String id = props.getProperty("ID");
                if ( (dest == null) || (id == null) ) {
                    _log.error("Invalid STREAM CONNECTED line: [" + line + "]");
                    return;
                }
                dest = dest.trim();
                id = id.trim();
                _streams.put(id, dest);
            } else if ( ("STREAM".equals(maj)) && ("CLOSED".equals(min)) ) {
                String id = props.getProperty("ID");
                if (id == null) {
                    _log.error("Invalid STREAM CLOSED line: [" + line + "]");
                    return;
                }
                _streams.remove(id);
            } else if ( ("STREAM".equals(maj)) && ("RECEIVED".equals(min)) ) {
                String id = props.getProperty("ID");
                String size = props.getProperty("SIZE");
                if ( (id == null) || (size == null) ) {
                    _log.error("Invalid STREAM RECEIVED line: [" + line + "]");
                    return;
                }
                id = id.trim();
                size = size.trim();
                int payloadSize = -1;
                try {
                    payloadSize = Integer.parseInt(size);
                } catch (NumberFormatException nfe) {
                    _log.error("Invalid SIZE in message [" + size + "]");
                    return;
                }
                // i know, its bytes, but this test uses chars
                char payload[] = new char[payloadSize];
                int read = _reader.read(payload);
                if (read != payloadSize) {
                    _log.error("Incorrect size read - expected " + payloadSize + " got " + read);
                    return;
                }
                _log.info("\n== Received from the stream " + id + ": [" + new String(payload) + "]");
                synchronized (_counterLock) {
                    _recvCounter++;
                }
                try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
                /*
                // now echo it back
                String reply = "STREAM SEND ID=" + id + 
                               " SIZE=" + payloadSize + 
                               "\n" + new String(payload);
                _out.write(reply.getBytes());
                _out.flush();
                _log.info("Reply sent back [" + new String(reply.getBytes()) + "]");
                 */
            } else {
                _log.error("Received unsupported type [" + maj + "/"+ min + "]");
                return;
            }
        }
    }
    
    private static void testBob(String sessionName, String host, int port, String conOptions) {
        I2PThread t = new I2PThread(new TestBob(sessionName, host, port, conOptions), sessionName);
        t.start();
    }
    private static class TestBob implements Runnable {
        private String _sessionName;
        private String _host;
        private int _port;
        private String _opts;
        public TestBob(String name, String host, int port, String opts) {
            _sessionName = name;
            _host = host;
            _port = port;
            _opts = opts;
        }
        public void run() {
            doTestBob(_sessionName, _host, _port, _opts);
        }
    }
    private static void doTestBob(String sessionName, String host, int port, String conOptions) {
        _log.info("\n\nTesting " + sessionName + "\n\n\n");
        try {
            Socket s = new Socket(host, port);
            OutputStream out = s.getOutputStream();
            out.write(DataHelper.getASCII("HELLO VERSION MIN=1.0 MAX=1.0\n"));
            BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
            String line = reader.readLine();
            _log.debug("line read for valid version: " + line);
            String req = "SESSION CREATE STYLE=STREAM DESTINATION=" + sessionName + " " + conOptions + "\n";
            out.write(DataHelper.getASCII(req));
            line = reader.readLine();
            _log.info("Response to creating the session with destination "+ sessionName+": " + line);
            req = "STREAM CONNECT ID=42 DESTINATION=" + _alice + "\n";
            out.write(DataHelper.getASCII(req));
            line = reader.readLine();
            _log.info("Response to the stream connect from "+sessionName+" to Alice: " + line);
            Properties props = SAMUtils.parseParams(line);
            _log.info("props = " + props);
            String result = props.getProperty("RESULT");
            if (!("OK".equals(result))) {
                _log.error("Unable to connect!");
                //_dead = true;
                return;
            }
            try { Thread.sleep(5*1000) ; } catch (InterruptedException ie) {}
            req = "STREAM SEND ID=42 SIZE=10\nBlahBlah!!";
            _log.info("\n** Sending BlahBlah!!");
            out.write(DataHelper.getASCII(req));
            out.flush();
            try { Thread.sleep(5*1000) ; } catch (InterruptedException ie) {}
            req = "STREAM SEND ID=42 SIZE=10\nFooBarBaz!";
            _log.info("\n** Sending FooBarBaz!");
            out.write(DataHelper.getASCII(req));
            out.flush();
            /* Don't delay here, so we can test whether all data is
               sent even if we do a STREAM CLOSE immediately. */
            _log.info("Sending close");
            req = "STREAM CLOSE ID=42\n";
            out.write(DataHelper.getASCII(req));
            out.flush();
            synchronized (_counterLock) {
                _closeCounter++;
            }
            try { Thread.sleep(30*1000); } catch (InterruptedException ie) {}
            //_dead = true;
            s.close();
        } catch (Exception e) {
            _log.error("Error testing for valid version", e);
        }
    }
    
    public static void main(String args[]) {
        // "i2cp.tcp.host=www.i2p.net i2cp.tcp.port=7765 tunnels.inboundDepth=0";
        // "i2cp.tcp.host=localhost i2cp.tcp.port=7654 tunnels.inboundDepth=0"; 
        String conOptions = "i2cp.tcp.host=localhost i2cp.tcp.port=10001 tunnels.inboundDepth=0";
        if (args.length > 0) {
            conOptions = "";
            for (int i = 0; i < args.length; i++)
                conOptions = conOptions + " " + args[i];
        }
        try {
            TestUtil.startupBridge(6000);
            runTest("localhost", 6000, conOptions);
        } catch (Throwable t) {
            _log.error("Error running test", t);
        }
        //try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
        //System.exit(0);
    }
}


