package net.i2p.router.client;
/*
 * free (adj.): unencumbered; not under the control of others
 * Written by jrandom in 2003 and released into the public domain 
 * with no warranty of any kind, either expressed or implied.  
 * It probably won't make your computer catch on fire, or eat 
 * your children, but it might.  Use at your own risk.
 *
 */

import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

import net.i2p.client.I2PClient;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;
import net.i2p.util.PortMapper;

/**
 * Listen for connections on the specified port, and toss them onto the client manager's
 * set of connections once they are established.
 *
 * @author jrandom
 */
class ClientListenerRunner implements Runnable {
    protected final Log _log;
    protected final RouterContext _context;
    protected final ClientManager _manager;
    protected ServerSocket _socket;
    protected final int _port;
    protected final boolean _bindAllInterfaces;
    protected volatile boolean _running;
    protected volatile boolean _listening;
    
    public static final String BIND_ALL_INTERFACES = "i2cp.tcp.bindAllInterfaces";

    public ClientListenerRunner(RouterContext context, ClientManager manager, int port) {
        _context = context;
        _log = _context.logManager().getLog(getClass());
        _manager = manager;
        _port = port;
        _bindAllInterfaces = context.getBooleanProperty(BIND_ALL_INTERFACES);
    }
    
    public boolean isListening() { return _running && _listening; }
    
    /** 
     * Get a ServerSocket.
     * Split out so it can be overridden for SSL.
     * @since 0.8.3
     */
    protected ServerSocket getServerSocket() throws IOException {
        if (_bindAllInterfaces) {
            if (_log.shouldLog(Log.INFO))
                _log.info("Listening on port " + _port + " on all interfaces");
            return new ServerSocket(_port);
        } else {
            String listenInterface = _context.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_HOST, 
                                                          ClientManagerFacadeImpl.DEFAULT_HOST);
            if (_log.shouldLog(Log.INFO))
                _log.info("Listening on port " + _port + " of the specific interface: " + listenInterface);
            return new ServerSocket(_port, 0, InetAddress.getByName(listenInterface));
        }
    }

    public void run() { runServer(); }

    /** 
     * Start up the socket listener, listens for connections, and
     * fires those connections off via {@link #runConnection runConnection}.  
     * This only returns if the socket cannot be opened or there is a catastrophic
     * failure.
     *
     */
    protected void runServer() {
        _running = true;
        int curDelay = 1000;
        final String portMapperService = (this instanceof SSLClientListenerRunner) ? PortMapper.SVC_I2CP_SSL
                                                                                   : PortMapper.SVC_I2CP;
        while (_running) {
            try {
                _socket = getServerSocket();
                
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug("ServerSocket created, before accept: " + _socket);
                if (_port > 0) {
                    // not for DomainClientListenerRunner
                    _context.portMapper().register(portMapperService, _socket.getInetAddress().getHostAddress(), _port);
                }                
                curDelay = 1000;
                _listening = true;
                while (_running) {
                    try {
                        Socket socket = _socket.accept();
                        if (validate(socket)) {
                            if (_log.shouldLog(Log.DEBUG))
                                _log.debug("Connection received");
                            socket.setKeepAlive(true);
                            runConnection(socket);
                        } else {
                            if (_log.shouldLog(Log.WARN))
                                _log.warn("Refused connection from " + socket.getInetAddress());
                            try {
                                socket.close();
                            } catch (IOException ioe) {}
                        }
                    } catch (IOException ioe) {
                        if (isAlive()) 
                            _log.error("Server error accepting", ioe);
                    } catch (Throwable t) {
                        if (isAlive()) 
                            _log.error("Fatal error running client listener - killing the thread!", t);
                        _listening = false;
                        return;
                    }
                }
            } catch (IOException ioe) {
                if (isAlive()) 
                    _log.error("Error listening on port " + _port, ioe);
            } finally {
                if (_port > 0) {
                    // not for DomainClientListenerRunner
                    _context.portMapper().unregister(portMapperService);
                }                
            }
            
            _listening = false;
            if (_socket != null) {
                try { _socket.close(); } catch (IOException ioe) {}
                _socket = null; 
            }
            
            if (!isAlive()) break;
            
            if (curDelay < 60*1000)
                _log.error("Error listening, waiting " + (curDelay/1000) + "s before we try again");
            else
                _log.log(Log.CRIT, "I2CP error listening to port " + _port + " - is another I2P instance running? Resolve conflicts and restart");
            try { Thread.sleep(curDelay); } catch (InterruptedException ie) {}
            curDelay = Math.min(curDelay*3, 60*1000);
        }

        if (isAlive())
            _log.error("CANCELING I2CP LISTEN", new Exception("I2CP Listen cancelled!!!"));
        _running = false;
    }
    
    /** 
     *  Just so unit tests don't NPE, where router could be null.
     *  @since 0.9.20
     */
    private boolean isAlive() {
        Router r = _context.router();
        return r == null || r.isAlive();
    }	

    /** give the i2cp client 5 seconds to show that they're really i2cp clients */
    protected final static int CONNECT_TIMEOUT = 5*1000;
    private final static int LOOP_DELAY = 250;

    /**
     *  Verify the first byte.
     *  The InternalSocket doesn't support SoTimeout, so use available()
     *  instead to prevent hanging.
     */
    protected boolean validate(Socket socket) {
        try {
            InputStream is = socket.getInputStream();
            for (int i = 0; i < CONNECT_TIMEOUT / LOOP_DELAY; i++) {
                if (is.available() > 0)
                    return is.read() == I2PClient.PROTOCOL_BYTE;
                try { Thread.sleep(LOOP_DELAY); } catch (InterruptedException ie) {}
            }
        } catch (IOException ioe) {}
        if (_log.shouldLog(Log.WARN))
             _log.warn("Peer did not authenticate themselves as I2CP quickly enough, dropping");
        return false;
    }

    /**
     * Handle the connection by passing it off to a {@link ClientConnectionRunner ClientConnectionRunner}
     *
     */
    protected void runConnection(Socket socket) {
        ClientConnectionRunner runner = new ClientConnectionRunner(_context, _manager, socket);
        _manager.registerConnection(runner);
    }
    
    public void stopListening() { 
        _running = false; 
        if (_socket != null) try { 
            _socket.close(); 
            _socket = null;
        } catch (IOException ioe) {}
    }
}
