/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
 * (c) 2003 - 2004 mihi
 */
package net.i2p.i2ptunnel.udpTunnel;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import net.i2p.I2PException;
import net.i2p.client.I2PClient;
import net.i2p.client.I2PClientFactory;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.data.Destination;
import net.i2p.i2ptunnel.I2PTunnel;
import net.i2p.i2ptunnel.I2PTunnelTask;
import net.i2p.i2ptunnel.Logging;
import net.i2p.i2ptunnel.udp.*;
import net.i2p.util.EventDispatcher;
import net.i2p.util.Log;

    /**
     * Base client class that sets up an I2P Datagram server destination.
     * The UDP side is not implemented here, as there are at least
     * two possibilities:
     *
     * 1) UDP side is a "client"
     *    Example: Streamr Producer
     *    - configure an inbound port
     *    - External application receives no data
     *    - Extending class must have a constructor with a port argument
     *
     * 2) UDP side is a client/server
     *    Example: DNS
     *    - configure an inbound port and a destination host and port
     *    - External application sends and receives data
     *    - Extending class must have a constructor with host and 2 port arguments
     *
     * So the implementing class must create a UDPSource and/or UDPSink,
     * and must call setSink().
     *
     * @author zzz with portions from welterde's streamr
     */

public class I2PTunnelUDPServerBase extends I2PTunnelTask implements Source, Sink {

    private final Log _log;

    private final Object lock = new Object();
    protected Object slock = new Object();

    protected Logging l;

    private static final long DEFAULT_READ_TIMEOUT = -1; // 3*60*1000;
    /** default timeout to 3 minutes - override if desired */
    protected long readTimeout = DEFAULT_READ_TIMEOUT;

    private I2PSession _session;
    private Source _i2pSource;
    private Sink _i2pSink;

    /**
     *
     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
     *                                  badly that we cant create a socketManager
     *
     */
    public I2PTunnelUDPServerBase(File privkey, String privkeyname, Logging l,
                           EventDispatcher notifyThis, I2PTunnel tunnel) {
        super("UDPServer <- " + privkeyname, notifyThis, tunnel);
        _log = tunnel.getContext().logManager().getLog(I2PTunnelUDPServerBase.class);
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(privkey);
            init(fis, privkeyname, l);
        } catch (IOException ioe) {
            _log.error("Error starting server", ioe);
            notifyEvent("openServerResult", "error");
        } finally {
            if (fis != null)
                try { fis.close(); } catch (IOException ioe) {}
        }
    }

    private void init(InputStream privData, String privkeyname, Logging l) {
        this.l = l;

        // create i2pclient
        I2PClient client = I2PClientFactory.createClient();

        try {
            // FIXME this may not pick up non-default I2CP host/port settings from tunnel
            _session = client.createSession(privData, getTunnel().getClientOptions());
            connected(_session);
        } catch(I2PSessionException exc) {
            throw new RuntimeException("failed to create session", exc);
        }

        // Setup the source. Always expect repliable datagrams, listen on all ports.
        _i2pSource = new I2PSource(_session, I2PSource.Protocol.REPLIABLE);

        // Setup the sink. Always send raw datagrams.
        _i2pSink = new I2PSinkAnywhere(_session, true);
    }
    
    /**
     * Classes should override to start UDP side as well.
     *
     * Not specified in I2PTunnelTask but used in both
     * I2PTunnelClientBase and I2PTunnelServer so let's
     * implement it here too.
     */
    public void startRunning() {
        //synchronized (startLock) {
            try {
                _session.connect();
            } catch(I2PSessionException exc) {
                throw new RuntimeException("failed to connect session", exc);
            }
            start();
        //}

        notifyEvent("openServerResult", "ok");
        open = true;
    }

    /**
     * Set the read idle timeout for newly-created connections (in
     * milliseconds).  After this time expires without data being reached from
     * the I2P network, the connection itself will be closed.
     */
    public void setReadTimeout(long ms) {
        readTimeout = ms;
    }
    
    /**
     * Get the read idle timeout for newly-created connections (in
     * milliseconds).
     *
     * @return The read timeout used for connections
     */
    public long getReadTimeout() {
        return readTimeout;
    }

    /**
     * I2PTunnelTask Methods
     *
     * Classes should override to close UDP side as well
     */
    public boolean close(boolean forced) {
        if (!open) return true;
        synchronized (lock) {
            l.log("Shutting down server " + toString());
            try {
                if (_session != null) {
                    _session.destroySession();
                }
            } catch (I2PException ex) {
                _log.error("Error destroying the session", ex);
            }
            l.log("Server shut down.");
            open = false;
            return true;
        }
    }

    /**
     *  Source Methods
     *
     *  Sets the receiver of the UDP datagrams from I2P
     *  Subclass must call this after constructor
     *  and before start()
     */
    public void setSink(Sink s) {
        _i2pSource.setSink(s);
    }

    /** start the source */
    public void start() {
        _i2pSource.start();
    }

    /**
     *  Sink Methods
     *
     * @param to
     * @since 0.9.53 added fromPort and toPort parameters
     * @throws RuntimeException if session is closed
     *
     */
    public void send(Destination to, int fromPort, int toPort, byte[] data) {
        _i2pSink.send(to, fromPort, toPort, data);
    }
}

