package net.i2p.sam;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.sam.client.SAMEventHandler;
import net.i2p.sam.client.SAMReader;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;

/**
 * Sit around on a SAM destination, receiving lots of data and sending lots of
 * data to whomever talks to us.
 *
 * Usage: TestSwarm samHost samPort myKeyFile [peerDestFile ]*
 *
 */
public class TestSwarm {
    private I2PAppContext _context;
    private Log _log;
    private String _samHost;
    private String _samPort;
    private String _destFile;
    private String _peerDestFiles[];
    private String _conOptions;
    private Socket _samSocket;
    private OutputStream _samOut;
    private InputStream _samIn;
    private SAMReader _reader;
    private boolean _dead;
    private SAMEventHandler _eventHandler;
    /** Connection id (Integer) to peer (Flooder) */
    private Map _remotePeers;
    
    public static void main(String args[]) {
        if (args.length < 3) {
            System.err.println("Usage: TestSwarm samHost samPort myDestFile [peerDestFile ]*");
            return;
        }
        I2PAppContext ctx = new I2PAppContext();
        String files[] = new String[args.length - 3];
        System.arraycopy(args, 3, files, 0, files.length);
        TestSwarm swarm = new TestSwarm(ctx, args[0], args[1], args[2], files);
        swarm.startup();
    }
    
    public TestSwarm(I2PAppContext ctx, String samHost, String samPort, String destFile, String peerDestFiles[]) {
        _context = ctx;
        _log = ctx.logManager().getLog(TestSwarm.class);
        _dead = false;
        _samHost = samHost;
        _samPort = samPort;
        _destFile = destFile;
        _peerDestFiles = peerDestFiles;
        _conOptions = "";
        _eventHandler = new SwarmEventHandler(_context);
        _remotePeers = new HashMap();
    }
    
    public void startup() {
        _log.debug("Starting up");
        boolean ok = connect();
        _log.debug("Connected: " + ok);
        if (ok) {
            _reader = new SAMReader(_context, _samIn, _eventHandler);
            _reader.startReading();
            _log.debug("Reader created");
            String ourDest = handshake();
            _log.debug("Handshake complete.  we are " + ourDest);
            if (ourDest != null) {
                boolean written = writeDest(ourDest);
                _log.debug("Dest written");
                if (written) {
                    connectWithPeers();
                    _log.debug("connected with peers");
                }
            }
        }
    }
    
    private class SwarmEventHandler extends SAMEventHandler {
        public SwarmEventHandler(I2PAppContext ctx) { super(ctx); }
        public void streamClosedReceived(String result, int id, String message) {
            Flooder flooder = null;
            synchronized (_remotePeers) {
                flooder = (Flooder)_remotePeers.remove(new Integer(id));
            }
            if (flooder != null) {
                flooder.closed();
                _log.debug("Connection " + flooder.getConnectionId() + " closed to " + flooder.getDestination());
            } else {
                _log.error("not connected to " + id + " but we were just closed?");
            }
        }
        public void streamDataReceived(int id, byte data[], int offset, int length) {
            Flooder flooder = null;
            synchronized (_remotePeers) {
                flooder = (Flooder)_remotePeers.get(new Integer(id));
            }
            long value = DataHelper.fromLong(data, 0, 4);
            if (flooder != null) {
                flooder.received(length, value);
            } else {
                _log.error("not connected to " + id + " but we received " + value + "?");
            }
        }
        public void streamConnectedReceived(String dest, int id) {  
            _log.debug("Connection " + id + " received from " + dest);

            Flooder flooder = new Flooder(id, dest);
            synchronized (_remotePeers) {
                _remotePeers.put(new Integer(id), flooder);
            }
            I2PThread t = new I2PThread(flooder, "Flood " + id);
            t.start();
        }

    }
    
    private boolean connect() {
        try {
            _samSocket = new Socket(_samHost, Integer.parseInt(_samPort));
            _samOut = _samSocket.getOutputStream();
            _samIn = _samSocket.getInputStream();
            return true;
        } catch (Exception e) {
            _log.error("Unable to connect to SAM at " + _samHost + ":" + _samPort, e);
            return false;
        }
    }
    
    private String handshake() {
        synchronized (_samOut) {
            try {
                _samOut.write(DataHelper.getASCII("HELLO VERSION MIN=1.0 MAX=1.0\n"));
                _samOut.flush();
                _log.debug("Hello sent");
                String serverVersion = _eventHandler.waitForHelloReply();
                _log.debug("Hello reply found: " + serverVersion);
                if (serverVersion == null)
                    throw new IOException("hello failed?");
                String req = "SESSION CREATE STYLE=STREAM DESTINATION=" + _destFile + " " + _conOptions + "\n";
                _samOut.write(DataHelper.getUTF8(req));
                _samOut.flush();
                _log.debug("Session create sent");
                boolean ok = _eventHandler.waitForSessionCreateReply();
                _log.debug("Session create reply found: " + ok);

                req = "NAMING LOOKUP NAME=ME\n";
                _samOut.write(DataHelper.getASCII(req));
                _samOut.flush();
                _log.debug("Naming lookup sent");
                String destination = _eventHandler.waitForNamingReply("ME");
                _log.debug("Naming lookup reply found: " + destination);
                if (destination == null) {
                    _log.error("No naming lookup reply found!");
                    return null;
                } else {
                    _log.info(_destFile + " is located at " + destination);
                }
                return destination;
            } catch (Exception e) {
                _log.error("Error handshaking", e);
                return null;
            }
        }
    }
    
    private boolean writeDest(String dest) {
        try {
            FileOutputStream fos = new FileOutputStream(_destFile);
            fos.write(DataHelper.getASCII(dest));
            fos.close();
            return true;
        } catch (Exception e) {
            _log.error("Error writing to " + _destFile, e);
            return false;
        }
    }
    
    private void connectWithPeers() {
        if (_peerDestFiles != null) {
            for (int i = 0; i < _peerDestFiles.length; i++) {
                try {
                    FileInputStream fin = new FileInputStream(_peerDestFiles[i]);
                    byte dest[] = new byte[1024];
                    int read = DataHelper.read(fin, dest);
                    
                    String remDest = new String(dest, 0, read);
                    int con = 0;
                    Flooder flooder = null;
                    synchronized (_remotePeers) {
                        con = _remotePeers.size() + 1;
                        flooder = new Flooder(con, remDest);
                        _remotePeers.put(new Integer(con), flooder);
                    }
                    
                    byte msg[] = (DataHelper.getUTF8("STREAM CONNECT ID=" + con + " DESTINATION=" + remDest + "\n"));
                    synchronized (_samOut) {
                        _samOut.write(msg);
                        _samOut.flush();
                    }
                    I2PThread flood = new I2PThread(flooder, "Flood " + con);
                    flood.start();
                    _log.debug("Starting flooder with peer from " + _peerDestFiles[i] + ": " + con);
                } catch (IOException ioe) {
                    _log.error("Unable to read the peer from " + _peerDestFiles[i]);
                }
            }
        }
    }
    
    private class Flooder implements Runnable {
        private int _connectionId; 
        private String _remoteDestination;
        private boolean _closed;
        private long _started;
        private long _totalSent;
        private long _totalReceived;
        private long _lastReceived;
        private long _lastReceivedOn;
        private boolean _outOfSync;
        
        public Flooder(int conId, String remDest) {
            _connectionId = conId;
            _remoteDestination = remDest;
            _closed = false;
            _outOfSync = false;
            _lastReceived = -1;
            _lastReceivedOn = _context.clock().now();
            _context.statManager().createRateStat("swarm." + conId + ".totalReceived", "Data size received", "swarm", new long[] { 30*1000, 60*1000, 5*60*1000 });
            _context.statManager().createRateStat("swarm." + conId + ".totalSent", "Data size sent", "swarm", new long[] { 30*1000, 60*1000, 5*60*1000 });
            _context.statManager().createRateStat("swarm." + conId + ".started", "When we start", "swarm", new long[] { 5*60*1000 });
            _context.statManager().createRateStat("swarm." + conId + ".lifetime", "How long we talk to a peer", "swarm", new long[] { 5*60*1000 });
        }
        
        public int getConnectionId() { return _connectionId; }
        public String getDestination() { return _remoteDestination; }
        
        public void closed() {
            _closed = true;
            long lifetime = _context.clock().now() - _started;
            _context.statManager().addRateData("swarm." + _connectionId + ".lifetime", lifetime, lifetime);
        }
        public void run() {
            _started = _context.clock().now();
            _context.statManager().addRateData("swarm." + _connectionId + ".started", 1, 0);
            byte data[] = new byte[32*1024];
            long value = 0;
            long lastSend = _context.clock().now();
            while (!_closed) {
                byte msg[] = (DataHelper.getASCII("STREAM SEND ID=" + _connectionId + " SIZE=" + data.length + "\n"));
                DataHelper.toLong(data, 0, 4, value);
                try {
                    synchronized (_samOut) {
                        _samOut.write(msg);
                        _samOut.write(data);
                        _samOut.flush();
                    }
                } catch (IOException ioe) {
                    _log.error("Error talking to SAM", ioe);
                    return;
                }
                _totalSent += data.length;
                _context.statManager().addRateData("swarm." + _connectionId + ".totalSent", _totalSent, 0);
                value++;
                try { Thread.sleep(20); } catch (InterruptedException ie) {}
                long now = _context.clock().now();
                _log.debug("Sending " + value + " on " + _connectionId + " after " + (now-lastSend));
                lastSend = now;
            }
        }
        public void received(int len, long value) {
            _totalReceived += len;
            if ( (!_outOfSync) && (len % 32*1024 != 0) ) {
                _outOfSync = true;
                if (_log.shouldLog(Log.ERROR))
                    _log.error("Out of sync (len=" + len + " after " + (_totalReceived-len) + ")");
            }
            _context.statManager().addRateData("swarm." + getConnectionId() + ".totalReceived", _totalReceived, 0);
            if (value != _lastReceived + 1) {
                if (!_outOfSync)
                    _log.error("Received " + value + " when expecting " + (_lastReceived+1) + " on " 
                               + _connectionId + " with " + _remoteDestination.substring(0,6));
                else
                    _log.debug("(out of sync) Received " + value + " when expecting " + (_lastReceived+1) + " on " 
                               + _connectionId + " with " + _remoteDestination.substring(0,6));
            } else {
                _log.debug("Received " + value + " on " + _connectionId + " after " + (_context.clock().now()-_lastReceivedOn)
                           + "ms with " + _remoteDestination.substring(0,6));
            }
            _lastReceived = value;
            _lastReceivedOn = _context.clock().now();
        }
    }
}
