package net.i2p.router.tunnel;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.i2p.router.RouterContext;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.TunnelId;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.data.i2np.TunnelDataMessage;
import net.i2p.data.i2np.TunnelGatewayMessage;
import net.i2p.router.peermanager.PeerProfile;
import net.i2p.router.JobImpl;
import net.i2p.router.Router;
import net.i2p.router.Service;
import net.i2p.util.Log;

/**
 * Handle the actual processing and forwarding of messages through the
 * various tunnels.
 *
 */
public class TunnelDispatcher implements Service {
    private RouterContext _context;
    private Log _log;
    private Map _outboundGateways;
    private Map _outboundEndpoints;
    private Map _participants;
    private Map _inboundGateways;
    /** id to HopConfig */
    private Map _participatingConfig;
    /** what is the date/time on which the last non-locally-created tunnel expires? */
    private long _lastParticipatingExpiration;
    private BloomFilterIVValidator _validator;
    private LeaveTunnel _leaveJob;
    
    /** Creates a new instance of TunnelDispatcher */
    public TunnelDispatcher(RouterContext ctx) {
        _context = ctx;
        _log = ctx.logManager().getLog(TunnelDispatcher.class);
        _outboundGateways = new HashMap();
        _outboundEndpoints = new HashMap();
        _participants = new HashMap();
        _inboundGateways = new HashMap();
        _participatingConfig = new HashMap();
        _lastParticipatingExpiration = 0;
        _validator = null;
        _leaveJob = new LeaveTunnel(ctx);
        ctx.statManager().createRateStat("tunnel.participatingTunnels", 
                                         "How many tunnels are we participating in?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.dispatchOutboundPeer", 
                                         "How many messages we send out a tunnel targetting a peer?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.dispatchOutboundTunnel", 
                                         "How many messages we send out a tunnel targetting a tunnel?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.dispatchInbound", 
                                         "How many messages we send through our tunnel gateway?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.dispatchParticipant", 
                                         "How many messages we send through a tunnel we are participating in?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.dispatchEndpoint", 
                                         "How many messages we receive as the outbound endpoint of a tunnel?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.joinOutboundGateway", 
                                         "How many tunnels we join as the outbound gateway?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.joinOutboundGatewayZeroHop", 
                                         "How many zero hop tunnels we join as the outbound gateway?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.joinInboundEndpoint", 
                                         "How many tunnels we join as the inbound endpoint?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.joinInboundEndpointZeroHop", 
                                         "How many zero hop tunnels we join as the inbound endpoint?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.joinParticipant", 
                                         "How many tunnels we join as a participant?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.joinOutboundEndpoint", 
                                         "How many tunnels we join as the outbound endpoint?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.joinInboundGateway", 
                                         "How many tunnels we join as the inbound gateway?", "Tunnels", 
                                         new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.dispatchGatewayTime", 
                                         "How long it takes to dispatch a TunnelGatewayMessage", "Tunnels", 
                                         new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.dispatchDataTime", 
                                         "How long it takes to dispatch a TunnelDataMessage", "Tunnels", 
                                         new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.dispatchOutboundTime", 
                                         "How long it takes to dispatch an outbound message", "Tunnels", 
                                         new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.dispatchOutboundZeroHopTime", 
                                         "How long it takes to dispatch an outbound message through a zero hop tunnel", "Tunnels", 
                                         new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.participatingMessageCount", 
                                         "How many messages are sent through a participating tunnel?", "Tunnels", 
                                         new long[] { 60*10*1000l, 60*60*1000l, 24*60*60*1000l });
        ctx.statManager().createRateStat("tunnel.ownedMessageCount", 
                                         "How many messages are sent through a tunnel we created (period == failures)?", "Tunnels", 
                                         new long[] { 60*1000l, 10*60*1000l, 60*60*1000l });
        ctx.statManager().createRateStat("tunnel.failedCompletelyMessages", 
                                         "How many messages are sent through a tunnel that failed prematurely (period == failures)?", "Tunnels", 
                                         new long[] { 60*1000l, 10*60*1000l, 60*60*1000l });
        ctx.statManager().createRateStat("tunnel.failedPartially", 
                                         "How many messages are sent through a tunnel that only failed partially (period == failures)?", "Tunnels", 
                                         new long[] { 60*1000l, 10*60*1000l, 60*60*1000l });
    }

    private TunnelGateway.QueuePreprocessor createPreprocessor(HopConfig cfg) {
        if (true)
            return new BatchedRouterPreprocessor(_context, cfg); 
        else
            return new TrivialRouterPreprocessor(_context); 
    }
    private TunnelGateway.QueuePreprocessor createPreprocessor(TunnelCreatorConfig cfg) {
        if (true)
            return new BatchedRouterPreprocessor(_context, cfg); 
        else
            return new TrivialRouterPreprocessor(_context); 
    }
    
    /**
     * We are the outbound gateway - we created this tunnel 
     */
    public void joinOutbound(TunnelCreatorConfig cfg) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Outbound built successfully: " + cfg);
        if (cfg.getLength() > 1) {
            TunnelGateway.QueuePreprocessor preproc = createPreprocessor(cfg);
            TunnelGateway.Sender sender = new OutboundSender(_context, cfg);
            TunnelGateway.Receiver receiver = new OutboundReceiver(_context, cfg);
            TunnelGateway gw = new TunnelGateway(_context, preproc, sender, receiver);
            TunnelId outId = cfg.getConfig(0).getSendTunnel();
            synchronized (_outboundGateways) {
                _outboundGateways.put(outId, gw);
            }
            _context.statManager().addRateData("tunnel.joinOutboundGateway", 1, 0);
            _context.messageHistory().tunnelJoined("outbound", cfg);
        } else {
            TunnelGatewayZeroHop gw = new TunnelGatewayZeroHop(_context, cfg);
            TunnelId outId = cfg.getConfig(0).getSendTunnel();
            synchronized (_outboundGateways) {
                _outboundGateways.put(outId, gw);
            }
            _context.statManager().addRateData("tunnel.joinOutboundGatewayZeroHop", 1, 0);
            _context.messageHistory().tunnelJoined("outboundZeroHop", cfg);
        }
    }
    /** 
     * We are the inbound endpoint - we created this tunnel
     */
    public void joinInbound(TunnelCreatorConfig cfg) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Inbound built successfully: " + cfg);
        
        if (cfg.getLength() > 1) {
            TunnelParticipant participant = new TunnelParticipant(_context, new InboundEndpointProcessor(_context, cfg, _validator));
            TunnelId recvId = cfg.getConfig(cfg.getLength()-1).getReceiveTunnel();
            synchronized (_participants) {
                _participants.put(recvId, participant);
            }
            _context.statManager().addRateData("tunnel.joinInboundEndpoint", 1, 0);
            _context.messageHistory().tunnelJoined("inboundEndpoint", cfg);
        } else {
            TunnelGatewayZeroHop gw = new TunnelGatewayZeroHop(_context, cfg);
            TunnelId recvId = cfg.getConfig(0).getReceiveTunnel();
            synchronized (_inboundGateways) {
                _inboundGateways.put(recvId, gw);
            }
            _context.statManager().addRateData("tunnel.joinInboundEndpointZeroHop", 1, 0);
            _context.messageHistory().tunnelJoined("inboundEndpointZeroHop", cfg);
        }
    }
    
    /** 
     * We are a participant in this tunnel, but not as the endpoint or gateway
     *
     */
    public void joinParticipant(HopConfig cfg) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Joining as participant: " + cfg);
        TunnelId recvId = cfg.getReceiveTunnel();
        TunnelParticipant participant = new TunnelParticipant(_context, cfg, new HopProcessor(_context, cfg, _validator));
        synchronized (_participants) {
            _participants.put(recvId, participant);
        }
        int numParticipants = 0;
        synchronized (_participatingConfig) {
            _participatingConfig.put(recvId, cfg);
            numParticipants = _participatingConfig.size();
        }
        _context.messageHistory().tunnelJoined("participant", cfg);
        _context.statManager().addRateData("tunnel.participatingTunnels", numParticipants, 0);
        _context.statManager().addRateData("tunnel.joinParticipant", 1, 0);
        if (cfg.getExpiration() > _lastParticipatingExpiration)
            _lastParticipatingExpiration = cfg.getExpiration();
        _leaveJob.add(cfg);
    }
    /**
     * We are the outbound endpoint in this tunnel, and did not create it
     *
     */
    public void joinOutboundEndpoint(HopConfig cfg) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Joining as outbound endpoint: " + cfg);
        TunnelId recvId = cfg.getReceiveTunnel();
        OutboundTunnelEndpoint endpoint = new OutboundTunnelEndpoint(_context, cfg, new HopProcessor(_context, cfg, _validator));
        synchronized (_outboundEndpoints) {
            _outboundEndpoints.put(recvId, endpoint);
        }
        int numParticipants = 0;
        synchronized (_participatingConfig) {
            _participatingConfig.put(recvId, cfg);
            numParticipants = _participatingConfig.size();
        }
        _context.messageHistory().tunnelJoined("outboundEndpoint", cfg);
        _context.statManager().addRateData("tunnel.participatingTunnels", numParticipants, 0);
        _context.statManager().addRateData("tunnel.joinOutboundEndpoint", 1, 0);

        if (cfg.getExpiration() > _lastParticipatingExpiration)
            _lastParticipatingExpiration = cfg.getExpiration();
        _leaveJob.add(cfg);
    }
    
    /**
     * We are the inbound gateway in this tunnel, and did not create it
     *
     */
    public void joinInboundGateway(HopConfig cfg) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Joining as inbound gateway: " + cfg);
        TunnelGateway.QueuePreprocessor preproc = createPreprocessor(cfg);
        TunnelGateway.Sender sender = new InboundSender(_context, cfg);
        TunnelGateway.Receiver receiver = new InboundGatewayReceiver(_context, cfg);
        TunnelGateway gw = new TunnelGateway(_context, preproc, sender, receiver);
        TunnelId recvId = cfg.getReceiveTunnel();
        synchronized (_inboundGateways) {
            _inboundGateways.put(recvId, gw);
        }
        int numParticipants = 0;
        synchronized (_participatingConfig) {
            _participatingConfig.put(recvId, cfg);
            numParticipants = _participatingConfig.size();
        }
        _context.messageHistory().tunnelJoined("inboundGateway", cfg);
        _context.statManager().addRateData("tunnel.participatingTunnels", numParticipants, 0);
        _context.statManager().addRateData("tunnel.joinInboundGateway", 1, 0);

        if (cfg.getExpiration() > _lastParticipatingExpiration)
            _lastParticipatingExpiration = cfg.getExpiration();
        _leaveJob.add(cfg);
    }

    public int getParticipatingCount() {
        synchronized (_participatingConfig) {
            return _participatingConfig.size();
        }
    }
    
    /** what is the date/time on which the last non-locally-created tunnel expires? */
    public long getLastParticipatingExpiration() { return _lastParticipatingExpiration; }
    
    /**
     * We no longer want to participate in this tunnel that we created
     */
    public void remove(TunnelCreatorConfig cfg) {
        if (cfg.isInbound()) {
            TunnelId recvId = cfg.getConfig(cfg.getLength()-1).getReceiveTunnel();
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("removing our own inbound " + cfg);
            TunnelParticipant participant = null;
            synchronized (_participants) {
                participant = (TunnelParticipant)_participants.remove(recvId);
            }
            if (participant == null) {
                synchronized (_inboundGateways) {
                    _inboundGateways.remove(recvId);
                }
            } else {
                // update stats based off getCompleteCount() + getFailedCount()
                for (int i = 0; i < cfg.getLength(); i++) {
                    Hash peer = cfg.getPeer(i);
                    PeerProfile profile = _context.profileOrganizer().getProfile(peer);
                    if (profile != null) {
                        int ok = participant.getCompleteCount();
                        int fail = participant.getFailedCount();
                        profile.getTunnelHistory().incrementProcessed(ok, fail);
                    }
                }
            }
        } else {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("removing our own outbound " + cfg);
            TunnelId outId = cfg.getConfig(0).getSendTunnel();
            TunnelGateway gw = null;
            synchronized (_outboundGateways) {
                gw = (TunnelGateway)_outboundGateways.remove(outId);
            }   
            if (gw != null) {
                // update stats based on gw.getMessagesSent()
            }
        }
        long msgs = cfg.getProcessedMessagesCount();
        int failures = cfg.getTunnelFailures();
        boolean failed = cfg.getTunnelFailed();
        _context.statManager().addRateData("tunnel.ownedMessageCount", msgs, failures);
        if (failed) {
            _context.statManager().addRateData("tunnel.failedCompletelyMessages", msgs, failures);
        } else if (failures > 0) {
            _context.statManager().addRateData("tunnel.failedPartiallyMessages", msgs, failures);
        }
    }
    
    /**
     * No longer participate in the tunnel that someone asked us to be a member of
     *
     */
    public void remove(HopConfig cfg) {
        TunnelId recvId = cfg.getReceiveTunnel();
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("removing " + cfg);
        
        boolean removed = false;
        synchronized (_participatingConfig) {
            removed = (null != _participatingConfig.remove(recvId));
        }
        if (!removed) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Participating tunnel, but no longer listed in participatingConfig? " + cfg);
        }
        
        synchronized (_participants) {
            removed = (null != _participants.remove(recvId));
        }
        if (removed) return;
        synchronized (_inboundGateways) {
            removed = (null != _inboundGateways.remove(recvId));
        }
        if (removed) return;
        synchronized (_outboundEndpoints) {
            removed = (null != _outboundEndpoints.remove(recvId));
        }
        
        _context.statManager().addRateData("tunnel.participatingMessageCount", cfg.getProcessedMessagesCount(), 10*60*1000);
    }
    
    /**
     * We are participating in a tunnel (perhaps we're even the endpoint), so 
     * take the message and do what it says.  If there are later hops, that 
     * means encrypt a layer and forward it on.  If there aren't later hops,
     * how we handle it depends upon whether we created it or not.  If we didn't,
     * simply honor the instructions.  If we did, unwrap all the layers of 
     * encryption and honor those instructions (within reason).
     *
     */
    public void dispatch(TunnelDataMessage msg, Hash recvFrom) {
        long before = _context.clock().now();
        TunnelParticipant participant = null;
        synchronized (_participants) {
            participant = (TunnelParticipant)_participants.get(msg.getTunnelIdObj());
        }
        if (participant != null) {
            // we are either just a random participant or the inbound endpoint 
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("dispatch to participant " + participant + ": " + msg.getUniqueId() + " from " 
                           + recvFrom.toBase64().substring(0,4));
            _context.messageHistory().tunnelDispatched(msg.getUniqueId(), msg.getTunnelId(), "participant");
            participant.dispatch(msg, recvFrom);
            _context.statManager().addRateData("tunnel.dispatchParticipant", 1, 0);
        } else {
            OutboundTunnelEndpoint endpoint = null;
            synchronized (_outboundEndpoints) {
                endpoint = (OutboundTunnelEndpoint)_outboundEndpoints.get(msg.getTunnelIdObj());
            }
            if (endpoint != null) {
                // we are the outobund endpoint
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug("dispatch where we are the outbound endpoint: " + endpoint + ": " 
                               + msg + " from " + recvFrom.toBase64().substring(0,4));
                _context.messageHistory().tunnelDispatched(msg.getUniqueId(), msg.getTunnelId(), "outbound endpoint");
                endpoint.dispatch(msg, recvFrom);
                
                _context.statManager().addRateData("tunnel.dispatchEndpoint", 1, 0);
            } else {
                _context.messageHistory().droppedTunnelDataMessageUnknown(msg.getUniqueId(), msg.getTunnelId());
                int level = (_context.router().getUptime() > 10*60*1000 ? Log.WARN : Log.DEBUG);
                if (_log.shouldLog(level))
                    _log.log(level, "no matching participant/endpoint for id=" + msg.getTunnelId() 
                             + " expiring in " + DataHelper.formatDuration(msg.getMessageExpiration()-_context.clock().now())
                             + ": existing = " + _participants.size() + " / " + _outboundEndpoints.size());
            }
        }
        
        long dispatchTime = _context.clock().now() - before;
        _context.statManager().addRateData("tunnel.dispatchDataTime", dispatchTime, dispatchTime);
    }

    /**
     * We are the inbound tunnel gateway, so encrypt it as necessary and forward
     * it on.
     *
     */
    public void dispatch(TunnelGatewayMessage msg) {
        long before = _context.clock().now();
        TunnelGateway gw = null;
        synchronized (_inboundGateways) {
            gw = (TunnelGateway)_inboundGateways.get(msg.getTunnelId());
        }
        if (gw != null) {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("dispatch where we are the inbound gateway: " + gw + ": " + msg);
            if ( (msg.getMessageExpiration() < before - Router.CLOCK_FUDGE_FACTOR) || (msg.getMessage().getMessageExpiration() < before - Router.CLOCK_FUDGE_FACTOR) ) {
                if (_log.shouldLog(Log.ERROR))
                    _log.error("Not dispatching a gateway message for tunnel " + msg.getTunnelId().getTunnelId()
                               + " as the wrapper's expiration is in " + DataHelper.formatDuration(msg.getMessageExpiration()-before)
                               + " and/or the content's expiration is in " + DataHelper.formatDuration(msg.getMessage().getMessageExpiration()-before)
                               + " with messageId " + msg.getUniqueId() + "/" + msg.getMessage().getUniqueId() + " and message type "
                               + msg.getMessage().getClass().getName());
                return;
            }
            //_context.messageHistory().tunnelDispatched("message " + msg.getUniqueId() + "/" + msg.getMessage().getUniqueId() + " on tunnel " 
            //                                               + msg.getTunnelId().getTunnelId() + " as inbound gateway");
            _context.messageHistory().tunnelDispatched(msg.getUniqueId(), msg.getMessage().getUniqueId(), msg.getTunnelId().getTunnelId(), "inbound gateway");
            gw.add(msg);
            _context.statManager().addRateData("tunnel.dispatchInbound", 1, 0);
        } else {
            _context.messageHistory().droppedTunnelGatewayMessageUnknown(msg.getUniqueId(), msg.getTunnelId().getTunnelId());
            int level = (_context.router().getUptime() > 10*60*1000 ? Log.ERROR : Log.WARN);
            if (_log.shouldLog(level))
                _log.log(level, "no matching tunnel for id=" + msg.getTunnelId().getTunnelId() 
                           + ": gateway message expiring in " 
                           + DataHelper.formatDuration(msg.getMessageExpiration()-_context.clock().now())
                           + "/" 
                           + DataHelper.formatDuration(msg.getMessage().getMessageExpiration()-_context.clock().now())
                           + " messageId " + msg.getUniqueId()
                           + "/" + msg.getMessage().getUniqueId()
                           + " messageType: " + msg.getMessage().getClass().getName()
                           + " existing = " + _inboundGateways.size(), new Exception("source"));
        }
        
        long dispatchTime = _context.clock().now() - before;
        _context.statManager().addRateData("tunnel.dispatchGatewayTime", dispatchTime, dispatchTime);
    }
    
    /**
     * We are the outbound tunnel gateway (we created it), so wrap up this message
     * with instructions to be forwarded to the targetPeer when it reaches the 
     * endpoint.
     *
     * @param msg raw message to deliver to the target peer
     * @param outboundTunnel tunnel to send the message out
     * @param targetPeer peer to receive the message
     */
    public void dispatchOutbound(I2NPMessage msg, TunnelId outboundTunnel, Hash targetPeer) {
        dispatchOutbound(msg, outboundTunnel, null, targetPeer);
    }
    /**
     * We are the outbound tunnel gateway (we created it), so wrap up this message
     * with instructions to be forwarded to the targetTunnel on the targetPeer when
     * it reaches the endpoint.
     *
     * @param msg raw message to deliver to the targetTunnel on the targetPeer
     * @param outboundTunnel tunnel to send the message out
     * @param targetTunnel tunnel on the targetPeer to deliver the message to
     * @param targetPeer gateway to the tunnel to receive the message
     */
    public void dispatchOutbound(I2NPMessage msg, TunnelId outboundTunnel, TunnelId targetTunnel, Hash targetPeer) {
        if (outboundTunnel == null) throw new IllegalArgumentException("wtf, null outbound tunnel?");
        long before = _context.clock().now();
        TunnelGateway gw = null;
        synchronized (_outboundGateways) {
            gw = (TunnelGateway)_outboundGateways.get(outboundTunnel);
        }
        if (gw != null) {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("dispatch outbound through " + outboundTunnel.getTunnelId()
                           + ": " + msg);
            if (msg.getMessageExpiration() < before - Router.CLOCK_FUDGE_FACTOR) {
                if (_log.shouldLog(Log.ERROR))
                    _log.error("why are you sending a tunnel message that expired " 
                               + (before-msg.getMessageExpiration()) + "ms ago? " 
                               + msg, new Exception("cause"));
                return;
            } else if (msg.getMessageExpiration() < before) {
                // nonfatal, as long as it was remotely created
                if (_log.shouldLog(Log.WARN))
                    _log.warn("why are you sending a tunnel message that expired " 
                               + (before-msg.getMessageExpiration()) + "ms ago? " 
                               + msg, new Exception("cause"));
            }
            long tid1 = (outboundTunnel != null ? outboundTunnel.getTunnelId() : -1);
            long tid2 = (targetTunnel != null ? targetTunnel.getTunnelId() : -1);
            _context.messageHistory().tunnelDispatched(msg.getUniqueId(), tid1, tid2, targetPeer, "outbound gateway");
            gw.add(msg, targetPeer, targetTunnel);
            if (targetTunnel == null)
                _context.statManager().addRateData("tunnel.dispatchOutboundPeer", 1, 0);
            else
                _context.statManager().addRateData("tunnel.dispatchOutboundTunnel", 1, 0);
        } else {
            _context.messageHistory().droppedTunnelGatewayMessageUnknown(msg.getUniqueId(), outboundTunnel.getTunnelId());

            int level = (_context.router().getUptime() > 10*60*1000 ? Log.ERROR : Log.WARN);
            if (_log.shouldLog(level))
                _log.log(level, "no matching outbound tunnel for id=" + outboundTunnel
                           + ": existing = " + _outboundGateways.size(), new Exception("src"));
        }
        
        long dispatchTime = _context.clock().now() - before;
        if (gw instanceof TunnelGatewayZeroHop)
            _context.statManager().addRateData("tunnel.dispatchOutboundZeroHopTime", dispatchTime, dispatchTime);
        else
            _context.statManager().addRateData("tunnel.dispatchOutboundTime", dispatchTime, dispatchTime);
    }
    
    public List listParticipatingTunnels() {
        synchronized (_participatingConfig) {
            return new ArrayList(_participatingConfig.values());
        }
    }
    
    public void startup() {
        // NB: 256 == assume max rate (size adjusted to handle 256 messages per second)
        _validator = new BloomFilterIVValidator(_context, 256);
    }
    public void shutdown() {
        if (_validator != null)
            _validator.destroy();
        _validator = null;
    }
    public void restart() { 
        shutdown(); 
        startup(); 
    }
    
    public void renderStatusHTML(Writer out) throws IOException {}    
    
    private class LeaveTunnel extends JobImpl {
        private List _configs;
        private List _times;
        
        public LeaveTunnel(RouterContext ctx) {
            super(ctx);
            getTiming().setStartAfter(ctx.clock().now());
            _configs = new ArrayList(128);
            _times = new ArrayList(128);
        }
        
        public void add(HopConfig cfg) {
            Long dropTime = new Long(cfg.getExpiration() + 2*Router.CLOCK_FUDGE_FACTOR);
            synchronized (LeaveTunnel.this) {
                _configs.add(cfg);
                _times.add(dropTime);
            }
            
            if (_log.shouldLog(Log.INFO)) {
                long now = getContext().clock().now();
                _log.info("Scheduling leave in " + DataHelper.formatDuration(dropTime.longValue()-now) +": " + cfg);
            }
            
            long oldAfter = getTiming().getStartAfter();
            if ( (oldAfter <= 0) || (oldAfter < getContext().clock().now()) || (oldAfter >= dropTime.longValue()) ) {
                getTiming().setStartAfter(dropTime.longValue());
                getContext().jobQueue().addJob(LeaveTunnel.this);
            } else {
                // already scheduled for the future, and before this expiration
            }
        }
        
        public String getName() { return "Leave participant"; }
        public void runJob() {
            HopConfig cur = null;
            Long nextTime = null;
            long now = getContext().clock().now();
            while (true) {
                synchronized (LeaveTunnel.this) {
                    if (_configs.size() <= 0)
                        return;
                    nextTime = (Long)_times.get(0);
                    if (nextTime.longValue() <= now) {
                        cur = (HopConfig)_configs.remove(0);
                        _times.remove(0);
                        if (_times.size() > 0)
                            nextTime = (Long)_times.get(0);
                        else
                            nextTime = null;
                    } else {
                        cur = null;
                    }
                }

                if (cur != null) 
                    remove(cur);
                else
                    break;
            }
            
            if (nextTime != null) {
                getTiming().setStartAfter(nextTime.longValue());
                getContext().jobQueue().addJob(LeaveTunnel.this);
            }
        }
    }
}
