package net.i2p.i2ptunnel.irc;

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.i2ptunnel.I2PTunnel;
import net.i2p.i2ptunnel.I2PTunnelRunner;
import net.i2p.i2ptunnel.I2PTunnelServer;
import net.i2p.i2ptunnel.Logging;
import net.i2p.util.EventDispatcher;
import net.i2p.util.Log;

/**
 * A standard server that only answers for registered ports,
 * and each port can only be used once.
 *
 * <pre>
 *
 *                                            direct conn
 *                &lt;---&gt; I2PTunnelDCCServer &lt;---------------&gt;I2PTunnelDCCClient &lt;----&gt;
 *   originating                                                                     responding
 *   chat client                                                                     chat client
 *        CHAT    ---&gt; I2PTunnelIRCClient --&gt; IRC server --&gt; I2TunnelIRCClient -----&gt;
 *        SEND    ---&gt; I2PTunnelIRCClient --&gt; IRC server --&gt; I2TunnelIRCClient -----&gt;
 *        RESUME  &lt;--- I2PTunnelIRCClient &lt;-- IRC server &lt;-- I2TunnelIRCClient &lt;-----
 *        ACCEPT  ---&gt; I2PTunnelIRCClient --&gt; IRC server --&gt; I2TunnelIRCClient -----&gt;
 *
 * </pre>
 *
 * @since 0.8.9
 */
public class I2PTunnelDCCServer extends I2PTunnelServer {

    /** key is the server's local I2P port */
    private final ConcurrentHashMap<Integer, LocalAddress> _outgoing;
    /** key is the server's local I2P port */
    private final ConcurrentHashMap<Integer, LocalAddress> _active;
    /** key is the server's local I2P port */
    private final ConcurrentHashMap<Integer, LocalAddress> _resume;
    private final List<I2PSocket> _sockList;

    /** just to keep super() happy */
    private static final InetAddress DUMMY;
    static {
        InetAddress dummy = null;
        try {
            dummy = InetAddress.getByAddress(new byte[4]);
        } catch (UnknownHostException uhe) {}
        DUMMY = dummy;
    }

    private static final int MIN_I2P_PORT = 1;
    private static final int MAX_I2P_PORT = 65535;
    private static final int MAX_OUTGOING_PENDING = 20;
    private static final int MAX_OUTGOING_ACTIVE = 20;
    private static final long OUTBOUND_EXPIRE = 30*60*1000;
    private static final long DEFAULT_DCC_READ_TIMEOUT = -1;

    /**
     * There's no support for unsolicited incoming I2P connections,
     * so there's no server host or port parameters.
     *
     * @param sktMgr an existing socket manager
     * @throws IllegalArgumentException if the I2PTunnel does not contain
     *                                  valid config to contact the router
     */
    public I2PTunnelDCCServer(I2PSocketManager sktMgr, Logging l,
                              EventDispatcher notifyThis, I2PTunnel tunnel) {
        super(DUMMY, 0, sktMgr, l, notifyThis, tunnel);
        _outgoing = new ConcurrentHashMap<Integer, LocalAddress>(8);
        _active = new ConcurrentHashMap<Integer, LocalAddress>(8);
        _resume = new ConcurrentHashMap<Integer, LocalAddress>(8);
        _sockList = new CopyOnWriteArrayList<I2PSocket>();
        readTimeout = DEFAULT_DCC_READ_TIMEOUT;
    }

    /**
     *  An incoming DCC connection, only accept for a known port.
     *  Passed through without filtering.
     */
    @Override
    protected void blockingHandle(I2PSocket socket) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Incoming connection to '" + toString() + "' from: " + socket.getPeerDestination().calculateHash().toBase64());

        try {
            expireOutbound();
            int myPort = socket.getLocalPort();
            // Port is a one-time-use only
            LocalAddress local = _outgoing.remove(Integer.valueOf(myPort));
            if (local == null) {
                if (_log.shouldLog(Log.WARN))
                    _log.warn("Rejecting incoming DCC connection for unknown port " + myPort);
                try {
                    socket.close();
                } catch (IOException ioe) {}
                return;
            }
            if (_log.shouldLog(Log.WARN))
                _log.warn("Incoming DCC connection for I2P port " + myPort +
                          " sending to " + local.ia + ':' + local.port);
            try {
                Socket s = new Socket(local.ia, local.port);
                _sockList.add(socket);
                Thread t = new I2PTunnelRunner(s, socket, slock, null, null, _sockList,
                                               (I2PTunnelRunner.FailCallback) null);
                // run in the unlimited client pool
                //t.start();
                _clientExecutor.execute(t);
                local.socket = socket;
                local.expire = getTunnel().getContext().clock().now() + OUTBOUND_EXPIRE;
                _active.put(Integer.valueOf(myPort), local);
            } catch (SocketException ex) {
                try {
                    socket.reset();
                } catch (IOException ioe) {}
                _log.error("Error relaying incoming DCC connection to IRC client at " + local.ia + ':' + local.port, ex);
            }
        } catch (IOException ex) {
            _log.error("Error while waiting for I2PConnections", ex);
        }
    }

    @Override
    public boolean close(boolean forced) {
        _outgoing.clear();
        _active.clear();
        for (I2PSocket s : _sockList) {
            try {
                s.close();
            } catch (IOException ioe) {}
        }
        _sockList.clear();
        return super.close(forced);
    }

    /**
     *  An outgoing DCC request
     *
     *  @param ip local irc client IP
     *  @param port local irc client port
     *  @param type ignored
     *  @return i2p port or -1 on error
     */
    public int newOutgoing(byte[] ip, int port, String type) {
        return newOutgoing(ip, port, type, 0);
    }

    /**
     *  @param port local dcc server I2P port or 0 to pick one at random
     */
    private int newOutgoing(byte[] ip, int port, String type, int i2pPort) {
        expireOutbound();
        if (_outgoing.size() >= MAX_OUTGOING_PENDING ||
            _active.size() >= MAX_OUTGOING_ACTIVE) {
            _log.error("Too many outgoing DCC, max is " + MAX_OUTGOING_PENDING +
                       '/' + MAX_OUTGOING_ACTIVE + " pending/active");
            return -1;
        }
        InetAddress ia;
        try {
            ia = InetAddress.getByAddress(ip);
        } catch (UnknownHostException uhe) {
            return -1;
        }
        int limit = i2pPort > 0 ? 10 : 1;
        LocalAddress client = new LocalAddress(ia, port, getTunnel().getContext().clock().now() + OUTBOUND_EXPIRE);
        for (int i = 0; i < limit; i++) {
            int iport;
            if (i2pPort > 0)
                iport = i2pPort;
            else
                iport = MIN_I2P_PORT + getTunnel().getContext().random().nextInt(1 + MAX_I2P_PORT - MIN_I2P_PORT);
            if (_active.containsKey(Integer.valueOf(iport)))
                continue;
            LocalAddress old = _outgoing.putIfAbsent(Integer.valueOf(iport), client);
            if (old != null)
                continue;
            // TODO expire in a few minutes
            return iport;
        }
        // couldn't find an unused i2p port
        return -1;
    }

    /**
     *  An incoming RESUME request
     *
     *  @param port local dcc server I2P port
     *  @return local IRC client DCC port or -1 on error
     */
    public int resumeIncoming(int port) {
        Integer iport = Integer.valueOf(port);
        LocalAddress local = _active.remove(iport);
        if (local != null) {
            local.expire = getTunnel().getContext().clock().now() + OUTBOUND_EXPIRE;
            _resume.put(Integer.valueOf(local.port), local);
            return local.port;
        }
        local = _outgoing.get(iport);
        if (local != null) {
            // shouldn't happen
            local.expire = getTunnel().getContext().clock().now() + OUTBOUND_EXPIRE;
            return local.port;
        }
        return -1;
    }

    /**
     *  An outgoing ACCEPT response
     *
     *  @param port local irc client DCC port
     *  @return local DCC server i2p port or -1 on error
     */
    public int acceptOutgoing(int port) {
        // do a reverse lookup
        for (Iterator<Map.Entry<Integer, LocalAddress>> iter = _resume.entrySet().iterator(); iter.hasNext(); ) {
            Map.Entry<Integer, LocalAddress> e = iter.next();
            LocalAddress local = e.getValue();
            if (local.port == port) {
                iter.remove();
                return newOutgoing(local.ia.getAddress(), port, "ACCEPT", e.getKey().intValue());
            }
        }
        return -1;
    }

    private void expireOutbound() {
        for (Iterator<LocalAddress> iter = _outgoing.values().iterator(); iter.hasNext(); ) {
            LocalAddress a = iter.next();
            if (a.expire < getTunnel().getContext().clock().now())
                iter.remove();
        }
        for (Iterator<LocalAddress> iter = _active.values().iterator(); iter.hasNext(); ) {
            LocalAddress a = iter.next();
            I2PSocket s = a.socket;
            if (s != null && s.isClosed())
                iter.remove();
        }
    }

    private static class LocalAddress {
        public final InetAddress ia;
        public final int port;
        public long expire;
        public I2PSocket socket;

        public LocalAddress(InetAddress a, int p, long exp) {
            ia = a;
            port = p;
            expire = exp;
        }
    }
}
