package net.i2p.client.streaming;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.Properties;

import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;

/**
 * Listen to a destination, receiving any sockets and writing anything they 
 * send to a new file. See the {@link #main}
 *
 */
public class StreamSinkServer {
    private Log _log;
    private String _sinkDir;
    private String _destFile;
    private String _i2cpHost;
    private int _i2cpPort;
    private int _handlers;
    
    /**
     * Create but do not start the streaming server.  
     *
     * @param sinkDir Directory to store received files in
     * @param ourDestFile filename to write our binary destination to
     */
    public StreamSinkServer(String sinkDir, String ourDestFile) {
        this(sinkDir, ourDestFile, null, -1, 3);
    }
    public StreamSinkServer(String sinkDir, String ourDestFile, String i2cpHost, int i2cpPort, int handlers) {
        _sinkDir = sinkDir;
        _destFile = ourDestFile;
        _i2cpHost = i2cpHost;
        _i2cpPort = i2cpPort;
        _handlers = handlers;
        _log = I2PAppContext.getGlobalContext().logManager().getLog(StreamSinkServer.class);
    }
    
    /**
     * Actually fire up the server - this call blocks forever (or until the server 
     * socket closes)
     * 
     */
    public void runServer() {
        I2PSocketManager mgr = null;
        if (_i2cpHost != null)
            mgr = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, new Properties());
        else 
            mgr = I2PSocketManagerFactory.createManager();
        Destination dest = mgr.getSession().getMyDestination();
        if (_log.shouldLog(Log.INFO))
            _log.info("Listening for connections on: " + dest.calculateHash().toBase64());
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(_destFile);
            dest.writeBytes(fos);
        } catch (IOException ioe) {
            _log.error("Error writing out our destination to " + _destFile, ioe);
            return;
        } catch (DataFormatException dfe) {
            _log.error("Error formatting the destination", dfe);
            return;
        } finally {
            if (fos != null) try { fos.close(); } catch (IOException ioe) {}
        }
        
        I2PServerSocket sock = mgr.getServerSocket();
        startup(sock);
    }
    
    public void startup(I2PServerSocket sock) {
        for (int i = 0; i < _handlers; i++) {
            I2PThread t = new I2PThread(new ClientRunner(sock));
            t.setName("Handler " + i);
            t.setDaemon(false);
            t.start();
        }
    }
    
    /**
     * Actually deal with a client - pull anything they send us and write it to a file.
     *
     */
    private class ClientRunner implements Runnable {
        private I2PServerSocket _socket;
        public ClientRunner(I2PServerSocket socket) {
            _socket = socket;
        }
        public void run() {
            while (true) {
                try {
                    I2PSocket socket = _socket.accept();
                    if (socket != null)
                        handle(socket);
                } catch (I2PException ie) {
                    _log.error("Error accepting connection", ie);
                    return;
                } catch (ConnectException ce) {
                    _log.error("Connection already dropped", ce);
                    return;
                } catch(SocketTimeoutException ste) {
                    // ignored
                }       
            }
        }
        
        private void handle(I2PSocket sock) {
            FileOutputStream fos = null;
            try {
                File sink = new File(_sinkDir);
                if (!sink.exists())
                    sink.mkdirs();
                File cur = File.createTempFile("clientSink", ".dat", sink);
                fos = new FileOutputStream(cur);
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug("Writing to " + cur.getAbsolutePath());
            } catch (IOException ioe) {
                _log.error("Error creating sink", ioe);
                return;
            }
            
            long start = System.currentTimeMillis();
            try {
                InputStream in = sock.getInputStream();
                byte buf[] = new byte[4096];
                long written = 0;
                int read = 0;
                while ( (read = in.read(buf)) != -1) {
                    //_fos.write(buf, 0, read);
                    written += read;
                    if (_log.shouldLog(Log.DEBUG))
                        _log.debug("read and wrote " + read + " (" + written + ")");
                }
                fos.write((DataHelper.getUTF8("written: [" + written + "]\n")));
                long lifetime = System.currentTimeMillis() - start;
                _log.info("Got EOF from client socket [written=" + written + " lifetime=" + lifetime + "]");
            } catch (IOException ioe) {
                _log.error("Error writing the sink", ioe);
            } finally {
                if (fos != null) try { fos.close(); } catch (IOException ioe) {}
                if (sock != null) try { sock.close(); } catch (IOException ioe) {}
                _log.debug("Client socket closed");
            }
        }
    }
    
    /**
     * Fire up the streaming server.  <code>Usage: StreamSinkServer [i2cpHost i2cpPort] sinkDir ourDestFile [numHandlers]</code><br>
     * <ul>
     *  <li><b>sinkDir</b>: Directory to store received files in</li>
     *  <li><b>ourDestFile</b>: filename to write our binary destination to</li>
     *  <li><b>numHandlers</b>: how many concurrent connections to handle</li>
     * </ul>
     * @param args [i2cpHost i2cpPort] sinkDir ourDestFile [numHandlers]
     */
    public static void main(String args[]) {
        StreamSinkServer server = null;
        switch (args.length) {
            case 0:
                server = new StreamSinkServer("dataDir", "server.key", "localhost", 7654, 3);
                break;
            case 2:
                server = new StreamSinkServer(args[0], args[1]);
                break;
            case 4:
            case 5:
                int handlers = 3;
                if (args.length == 5) {
                    try {
                        handlers = Integer.parseInt(args[4]);
                    } catch (NumberFormatException nfe) {}
                }
                try { 
                    int port = Integer.parseInt(args[1]);
                    server = new StreamSinkServer(args[2], args[3], args[0], port, handlers);
                } catch (NumberFormatException nfe) {
                    System.out.println("Usage: StreamSinkServer [i2cpHost i2cpPort] sinkDir ourDestFile [handlers]");
                }
                break;
            default:
                System.out.println("Usage: StreamSinkServer [i2cpHost i2cpPort] sinkDir ourDestFile [handlers]");
        }
        if (server != null)
            server.runServer();
    }
}
