package net.i2p.i2ptunnel.irc;

import java.util.Iterator;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;

import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.data.Base32;
import net.i2p.i2ptunnel.I2PTunnel;
import net.i2p.i2ptunnel.Logging;
import net.i2p.util.EventDispatcher;
import net.i2p.util.Log;

/**
 *  Start, track, and expire the I2PTunnelDCCClients.
 *
 * <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 DCCClientManager extends EventReceiver {
    private final I2PSocketManager sockMgr;
    private final EventDispatcher _dispatch;
    private final Logging l;
    private final I2PTunnel _tunnel;
    private final Log _log;

    /** key is the DCC client's local port */
    private final ConcurrentHashMap<Integer, I2PTunnelDCCClient> _incoming;
    /** key is the DCC client's local port */
    private final ConcurrentHashMap<Integer, I2PTunnelDCCClient> _active;
    /** key is the DCC client's local port */
    private final ConcurrentHashMap<Integer, I2PTunnelDCCClient> _complete;

    // list of client tunnels?
    private static long _id;

    private static final int MAX_INCOMING_PENDING = 10;
    private static final int MAX_INCOMING_ACTIVE = 10;
    public DCCClientManager(I2PSocketManager sktMgr, Logging logging,
                            EventDispatcher dispatch, I2PTunnel tunnel) {
        sockMgr = sktMgr;
        l = logging;
        _dispatch = dispatch;
        _tunnel = tunnel;
        _log = tunnel.getContext().logManager().getLog(DCCClientManager.class);
        _incoming = new ConcurrentHashMap<Integer, I2PTunnelDCCClient>(8);
        _active = new ConcurrentHashMap<Integer, I2PTunnelDCCClient>(8);
        _complete = new ConcurrentHashMap<Integer, I2PTunnelDCCClient>(8);
    }

    public boolean close(boolean forced) {
        for (I2PTunnelDCCClient c : _incoming.values()) {
            c.stop();
        }
        _incoming.clear();
        for (I2PTunnelDCCClient c : _active.values()) {
            c.stop();
        }
        _active.clear();
        _complete.clear();
        return true;
    }

    /**
     *  An incoming DCC request
     *
     *  @param b32 remote dcc server b32 address
     *  @param port remote dcc server I2P port
     *  @param type ignored
     *  @return local DCC client tunnel port or -1 on error
     */
    public int newIncoming(String b32, int port, String type) {
        return newIncoming(b32, port, type, 0);
    }

    /**
     *  @param localPort bind to port or 0; if nonzero it will be the rv
     */
    private int newIncoming(String b32, int port, String type, int localPort) {
        b32 = b32.toLowerCase(Locale.US);
        // do some basic verification before starting the client
        if (b32.length() != 60 || !b32.endsWith(".b32.i2p"))
            return -1;
        byte[] dec = Base32.decode(b32.substring(0, 52));
        if (dec == null || dec.length != 32)
            return -1;
        expireInbound();
        if (_incoming.size() >= MAX_INCOMING_PENDING ||
            _active.size() >= MAX_INCOMING_PENDING) {
            _log.error("Too many incoming DCC, max is " + MAX_INCOMING_PENDING +
                       '/' + MAX_INCOMING_ACTIVE + " pending/active");
            return -1;
        }
        try {
            // Transparent tunnel used for all types...
            // Do we need to do any filtering for chat?
            I2PTunnelDCCClient cTunnel = new I2PTunnelDCCClient(b32, localPort, port, l, sockMgr,
                                                                _dispatch, _tunnel, ++_id);
            cTunnel.attachEventDispatcher(this);
            cTunnel.startRunning();
            int lport = cTunnel.getLocalPort();
            if (_log.shouldLog(Log.WARN))
                _log.warn("Opened client tunnel at port " + lport +
                          " pointing to " + b32 + ':' + port);
            _incoming.put(Integer.valueOf(lport), cTunnel);
            return lport;
        } catch (IllegalArgumentException uhe) {
            l.log("Could not find listen host to bind to [" + _tunnel.host + "]");
            _log.error("Error finding host to bind", uhe);
            return -1;
        }
    }

    /**
     *  An outgoing RESUME request
     *
     *  @param port local DCC client tunnel port
     *  @return remote DCC server i2p port or -1 on error
     */
    public int resumeOutgoing(int port) {
        Integer lport = Integer.valueOf(port);
        I2PTunnelDCCClient tun = _complete.get(lport);
        if (tun == null) {
            tun = _active.get(lport);
            if (tun == null)
                // shouldn't happen
                tun = _incoming.get(lport);
        }
        if (tun != null) {
            tun.stop();
            return tun.getLocalPort();
        }
        return -1;
    }

    /**
     *  An incoming ACCEPT response
     *
     *  @param port remote dcc server I2P port
     *  @return local DCC client tunnel port or -1 on error
     */
    public int acceptIncoming(int port) {
        // do a reverse lookup
        for (I2PTunnelDCCClient tun : _complete.values()) {
            if (tun.getRemotePort() == port)
                return newIncoming(tun.getDest(), port, "ACCEPT", tun.getLocalPort());
        }
        for (I2PTunnelDCCClient tun : _active.values()) {
            if (tun.getRemotePort() == port)
                return newIncoming(tun.getDest(), port, "ACCEPT", tun.getLocalPort());
        }
        for (I2PTunnelDCCClient tun : _incoming.values()) {
            if (tun.getRemotePort() == port) {
                // shouldn't happen
                tun.stop();
                return newIncoming(tun.getDest(), port, "ACCEPT", tun.getLocalPort());
            }
        }
        return -1;
    }

    /**
     *  The EventReceiver callback
     */
    public void notifyEvent(String eventName, Object args) {
        if (eventName.equals(I2PTunnelDCCClient.CONNECT_START_EVENT)) {
            try {
                I2PTunnelDCCClient client = (I2PTunnelDCCClient) args;
                connStarted(client);
            } catch (ClassCastException cce) {}
        } else if (eventName.equals(I2PTunnelDCCClient.CONNECT_STOP_EVENT)) {
            try {
                Integer port = (Integer) args;
                connStopped(port);
            } catch (ClassCastException cce) {}
        }
    }

    private void connStarted(I2PTunnelDCCClient client) {
        Integer lport = Integer.valueOf(client.getLocalPort());
        I2PTunnelDCCClient c = _incoming.remove(lport);
        if (c != null) {
            _active.put(lport, client);
            if (_log.shouldLog(Log.WARN))
                _log.warn("Added client tunnel for port " + lport +
                          " pending count now: " + _incoming.size() +
                          " active count now: " + _active.size() +
                          " complete count now: " + _complete.size());
        }
    }

    private void connStopped(Integer lport) {
        I2PTunnelDCCClient tun = _incoming.remove(lport);
        if (tun != null)
            _complete.put(lport, tun);
        tun = _active.remove(lport);
        if (tun != null)
            _complete.put(lport, tun);
        if (_log.shouldLog(Log.WARN))
            _log.warn("Removed client tunnel for port " + lport +
                      " pending count now: " + _incoming.size() +
                      " active count now: " + _active.size() +
                      " complete count now: " + _complete.size());
    }

    private void expireInbound() {
        for (Iterator<I2PTunnelDCCClient> iter = _incoming.values().iterator(); iter.hasNext(); ) {
            I2PTunnelDCCClient c = iter.next();
            if (c.getExpires() < _tunnel.getContext().clock().now()) {
                iter.remove();
                c.stop();
            }
        }
        // shouldn't need to expire active
        for (Iterator<I2PTunnelDCCClient> iter = _complete.values().iterator(); iter.hasNext(); ) {
            I2PTunnelDCCClient c = iter.next();
            if (c.getExpires() < _tunnel.getContext().clock().now()) {
                iter.remove();
                c.stop();
            }
        }
    }
}
