package net.i2p.client.impl;

/*
 * 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.BufferedInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

import net.i2p.CoreVersion;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PClient;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.client.I2PSessionListener;
import net.i2p.data.Base32;
import net.i2p.data.DataFormatException;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.LeaseSet;
import net.i2p.data.PrivateKey;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.i2cp.DestLookupMessage;
import net.i2p.data.i2cp.DestReplyMessage;
import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
import net.i2p.data.i2cp.GetDateMessage;
import net.i2p.data.i2cp.HostLookupMessage;
import net.i2p.data.i2cp.HostReplyMessage;
import net.i2p.data.i2cp.I2CPMessage;
import net.i2p.data.i2cp.I2CPMessageReader;
import net.i2p.data.i2cp.MessagePayloadMessage;
import net.i2p.data.i2cp.SessionId;
import net.i2p.data.i2cp.SessionStatusMessage;
import net.i2p.internal.I2CPMessageQueue;
import net.i2p.internal.InternalClientManager;
import net.i2p.internal.QueuedI2CPMessageReader;
import net.i2p.util.I2PAppThread;
import net.i2p.util.I2PSSLSocketFactory;
import net.i2p.util.LHMCache;
import net.i2p.util.Log;
import net.i2p.util.OrderedProperties;
import net.i2p.util.SimpleTimer2;
import net.i2p.util.SystemVersion;
import net.i2p.util.VersionComparator;

/**
 * Implementation of an I2P session running over TCP.  This class is NOT thread safe -
 * only one thread should send messages at any given time
 *
 * Public only for clearCache().
 * Except for methods defined in I2PSession and I2CPMessageEventListener,
 * not maintained as a public API, not for external use.
 * Use I2PClientFactory to get an I2PClient and then createSession().
 *
 * @author jrandom
 */
public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessageEventListener {
    protected final Log _log;
    /** who we are */
    private final Destination _myDestination;
    /** private key for decryption */
    private final PrivateKey _privateKey;
    /** private key for signing */
    private   /* final */   SigningPrivateKey _signingPrivateKey;
    /** configuration options */
    private final Properties _options;
    /** this session's Id */
    private SessionId _sessionId;
    /** currently granted lease set, or null */
    protected volatile LeaseSet _leaseSet;

    // subsession stuff
    // registered subsessions
    private final List<SubSession> _subsessions;
    // established subsessions
    private final ConcurrentHashMap<SessionId, SubSession> _subsessionMap;
    private final Object _subsessionLock = new Object();
    private static final String MIN_SUBSESSION_VERSION = "0.9.21";
    private volatile boolean _routerSupportsSubsessions;

    /** hostname of router - will be null if in RouterContext */
    protected final String _hostname;
    /** port num to router - will be 0 if in RouterContext */
    protected final int _portNum;
    /** socket for comm */
    protected Socket _socket;
    /** reader that always searches for messages */
    protected I2CPMessageReader _reader;
    /** writer message queue */
    protected ClientWriterRunner _writer;

    /**
     *  Used for internal connections to the router.
     *  If this is set, _socket and _writer will be null.
     *  @since 0.8.3
     */
    protected I2CPMessageQueue _queue;

    /** who we send events to */
    protected I2PSessionListener _sessionListener;

    /** class that generates new messages */
    protected final I2CPMessageProducer _producer;
    /** map of Long --&gt; MessagePayloadMessage */
    protected Map<Long, MessagePayloadMessage> _availableMessages;

    /** hashes of lookups we are waiting for */
    protected final LinkedBlockingQueue<LookupWaiter> _pendingLookups = new LinkedBlockingQueue<LookupWaiter>();
    private final AtomicInteger _lookupID = new AtomicInteger();
    protected final Object _bwReceivedLock = new Object();
    protected volatile int[] _bwLimits;
    
    protected final I2PClientMessageHandlerMap _handlerMap;
    
    /** used to separate things out so we can get rid of singletons */
    protected final I2PAppContext _context;

    /** monitor for waiting until a lease set has been granted */
    protected final Object _leaseSetWait = new Object();

    /**
     *  @since 0.9.8
     */
    protected enum State {
        /** @since 0.9.20 */
        INIT,
        OPENING,
        /** @since 0.9.11 */
        GOTDATE,
        OPEN,
        CLOSING,
        CLOSED
    }

    protected State _state = State.INIT;
    protected final Object _stateLock = new Object();

    /** 
     * thread that we tell when new messages are available who then tells us 
     * to fetch them.  The point of this is so that the fetch doesn't block the
     * reading of other messages (in turn, potentially leading to deadlock)
     *
     */
    protected AvailabilityNotifier _availabilityNotifier;

    private long _lastActivity;
    private boolean _isReduced;
    private final boolean _fastReceive;
    private volatile boolean _routerSupportsFastReceive;
    private volatile boolean _routerSupportsHostLookup;

    protected static final int CACHE_MAX_SIZE = SystemVersion.isAndroid() ? 32 : 128;
    /**
     *  Since 0.9.11, key is either a Hash or a String
     *  @since 0.8.9
     */
    private static final Map<Object, Destination> _lookupCache = new LHMCache<Object, Destination>(CACHE_MAX_SIZE);
    private static final String MIN_HOST_LOOKUP_VERSION = "0.9.11";
    private static final boolean TEST_LOOKUP = false;

    /** SSL interface (only) @since 0.8.3 */
    protected static final String PROP_ENABLE_SSL = "i2cp.SSL";
    protected static final String PROP_USER = "i2cp.username";
    protected static final String PROP_PW = "i2cp.password";

    /**
     * Use Unix domain socket (or similar) to connect to a router
     * @since 0.9.14
     */
    protected static final String PROP_DOMAIN_SOCKET = "i2cp.domainSocket";

    private static final long VERIFY_USAGE_TIME = 60*1000;

    private static final long MAX_SEND_WAIT = 10*1000;

    private static final String MIN_FAST_VERSION = "0.9.4";

    /** @param routerVersion as rcvd in the SetDateMessage, may be null for very old routers */
    void dateUpdated(String routerVersion) {
        _routerSupportsFastReceive = _context.isRouterContext() ||
                                     (routerVersion != null && routerVersion.length() > 0 &&
                                      VersionComparator.comp(routerVersion, MIN_FAST_VERSION) >= 0);
        _routerSupportsHostLookup = _context.isRouterContext() ||
                                    TEST_LOOKUP ||
                                     (routerVersion != null && routerVersion.length() > 0 &&
                                      VersionComparator.comp(routerVersion, MIN_HOST_LOOKUP_VERSION) >= 0);
        _routerSupportsSubsessions = _context.isRouterContext() ||
                                     (routerVersion != null && routerVersion.length() > 0 &&
                                      VersionComparator.comp(routerVersion, MIN_SUBSESSION_VERSION) >= 0);
        synchronized (_stateLock) {
            if (_state == State.OPENING) {
                changeState(State.GOTDATE);
            }
        }
    }

    public static final int LISTEN_PORT = 7654;

    private static final int BUF_SIZE = 32*1024;

    /**
     * for extension by SimpleSession (no dest)
     */
    protected I2PSessionImpl(I2PAppContext context, Properties options,
                             I2PClientMessageHandlerMap handlerMap) {
        this(context, options, handlerMap, null, false);
    }

    /*
     * For extension by SubSession via I2PSessionMuxedImpl and I2PSessionImpl2
     *
     * @param destKeyStream stream containing the private key data,
     *                             format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     * @param options set of options to configure the router with, if null will use System properties
     * @since 0.9.21
     */
    protected I2PSessionImpl(I2PSessionImpl primary, InputStream destKeyStream, Properties options) throws I2PSessionException {
        this(primary.getContext(), options, primary.getHandlerMap(), primary.getProducer(), true);
        _availabilityNotifier = new AvailabilityNotifier();
        try {
            readDestination(destKeyStream);
        } catch (DataFormatException dfe) {
            throw new I2PSessionException("Error reading the destination key stream", dfe);
        } catch (IOException ioe) {
            throw new I2PSessionException("Error reading the destination key stream", ioe);
        }
    }

    /**
     * Basic setup of finals
     * @since 0.9.7
     */
    private I2PSessionImpl(I2PAppContext context, Properties options,
                           I2PClientMessageHandlerMap handlerMap,
                           I2CPMessageProducer producer,
                           boolean hasDest) {
        _context = context;
        _handlerMap = handlerMap;
        _log = context.logManager().getLog(getClass());
        _subsessions = new CopyOnWriteArrayList<SubSession>();
        _subsessionMap = new ConcurrentHashMap<SessionId, SubSession>(4);
        if (options == null)
            options = (Properties) System.getProperties().clone();
        _options = loadConfig(options);
        _hostname = getHost();
        _portNum = getPort();
        _fastReceive = Boolean.parseBoolean(_options.getProperty(I2PClient.PROP_FAST_RECEIVE));
        if (hasDest) {
            _producer = producer;
            _availableMessages = new ConcurrentHashMap<Long, MessagePayloadMessage>();
            _myDestination = new Destination();
            _privateKey = new PrivateKey();
            _signingPrivateKey = new SigningPrivateKey();
        } else {
            _producer = null;
            _availableMessages = null;
            _myDestination = null;
            _privateKey = null;
            _signingPrivateKey = null;
        }
        _routerSupportsFastReceive = _context.isRouterContext();
        _routerSupportsHostLookup = _context.isRouterContext();
        _routerSupportsSubsessions = _context.isRouterContext();
    }

    /**
     * Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey
     * from the destKeyStream, and using the specified options to connect to the router
     *
     * As of 0.9.19, defaults in options are honored.
     *
     * @param destKeyStream stream containing the private key data,
     *                             format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     * @param options set of options to configure the router with, if null will use System properties
     * @throws I2PSessionException if there is a problem loading the private keys
     */
    public I2PSessionImpl(I2PAppContext context, InputStream destKeyStream, Properties options) throws I2PSessionException {
        this(context, options, new I2PClientMessageHandlerMap(context), new I2CPMessageProducer(context), true);
        _availabilityNotifier = new AvailabilityNotifier();
        try {
            readDestination(destKeyStream);
        } catch (DataFormatException dfe) {
            throw new I2PSessionException("Error reading the destination key stream", dfe);
        } catch (IOException ioe) {
            throw new I2PSessionException("Error reading the destination key stream", ioe);
        }
    }
    
    /**
     *  Router must be connected or was connected... for now.
     *
     *  @return a new subsession, non-null
     *  @param privateKeyStream null for transient, if non-null must have same encryption keys as primary session
     *                          and different signing keys
     *  @param opts subsession options if any, may be null
     *  @since 0.9.21
     */
    public I2PSession addSubsession(InputStream privateKeyStream, Properties opts) throws I2PSessionException {
        if (!_routerSupportsSubsessions)
            throw new I2PSessionException("Router does not support subsessions");
        SubSession sub;
        synchronized(_subsessionLock) {
            if (_subsessions.size() > _subsessionMap.size())
                throw new I2PSessionException("Subsession request already pending");
            sub = new SubSession(this, privateKeyStream, opts);
            for (SubSession ss : _subsessions) {
                 if (ss.getDecryptionKey().equals(sub.getDecryptionKey()) &&
                     ss.getPrivateKey().equals(sub.getPrivateKey())) {
                    throw new I2PSessionException("Dup subsession");
                }
            }
            _subsessions.add(sub);
        }

        synchronized (_stateLock) {
            if (_state == State.OPEN) {
                _producer.connect(sub);
            } // else will be called in connect()
        }
        return sub;
    }
    
    /**
     *  @since 0.9.21
     */
    public void removeSubsession(I2PSession session) {
        if (!(session instanceof SubSession))
            return;
        synchronized(_subsessionLock) {
            _subsessions.remove(session);
            SessionId id = ((SubSession) session).getSessionId();
            if (id != null)
                _subsessionMap.remove(id);
            /// tell the subsession
            try {
                // doesn't really throw
                session.destroySession();
            } catch (I2PSessionException ise) {}
        }
    }
    
    /**
     *  @return a list of subsessions, non-null, does not include the primary session
     *  @since 0.9.21
     */
    public List<I2PSession> getSubsessions() {
        synchronized(_subsessionLock) {
            return new ArrayList<I2PSession>(_subsessions);
        }
    }

    /**
     * Parse the config for anything we know about.
     * Also fill in the authorization properties if missing.
     */
    private final Properties loadConfig(Properties opts) {
        Properties options = new Properties();
        options.putAll(filter(opts));

        // auto-add auth if required, not set in the options, and we are not in the same JVM
        if ((!_context.isRouterContext()) &&
            _context.getBooleanProperty("i2cp.auth") &&
            ((!opts.containsKey(PROP_USER)) || (!opts.containsKey(PROP_PW)))) {
            String configUser = _context.getProperty(PROP_USER);
            String configPW = _context.getProperty(PROP_PW);
            if (configUser != null && configPW != null) {
                options.setProperty(PROP_USER, configUser);
                options.setProperty(PROP_PW, configPW);
            }
        }
        if (options.getProperty(I2PClient.PROP_FAST_RECEIVE) == null)
            options.setProperty(I2PClient.PROP_FAST_RECEIVE, "true");
        if (options.getProperty(I2PClient.PROP_RELIABILITY) == null)
            options.setProperty(I2PClient.PROP_RELIABILITY, "none");
        return options;
    }

    /**
     * Get I2CP host from the config
     * @since 0.9.7 was in loadConfig()
     */
    private String getHost() {
        if (_context.isRouterContext())
            // just for logging
            return "[internal connection]";
        else if (SystemVersion.isAndroid() &&
                Boolean.parseBoolean(_options.getProperty(PROP_DOMAIN_SOCKET)))
            // just for logging
            return "[Domain socket connection]";
        return _options.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1");
    }

    /**
     * Get I2CP port from the config
     * @since 0.9.7 was in loadConfig()
     */
    private int getPort() {
        if (_context.isRouterContext() ||
                (SystemVersion.isAndroid() &&
                        Boolean.parseBoolean(_options.getProperty(PROP_DOMAIN_SOCKET))))
            // just for logging
            return 0;
        String portNum = _options.getProperty(I2PClient.PROP_TCP_PORT, LISTEN_PORT + "");
        try {
            return Integer.parseInt(portNum);
        } catch (NumberFormatException nfe) {
            if (_log.shouldLog(Log.WARN))
                _log.warn(getPrefix() + "Invalid port number specified, defaulting to "
                          + LISTEN_PORT, nfe);
            return LISTEN_PORT;
        }
    }

    /**
     *  Save some memory, don't pass along the pointless properties.
     *  As of 0.9.19, defaults from options will be promoted to real values in rv.
     *  @return a new Properties without defaults
     */
    private Properties filter(Properties options) {
        Properties rv = new Properties();
        for (String key : options.stringPropertyNames()) {
            if (key.startsWith("java.") ||
                key.startsWith("user.") ||
                key.startsWith("os.") ||
                key.startsWith("sun.") ||
                key.startsWith("awt.") ||
                key.startsWith("file.") ||
                key.equals("line.separator") ||
                key.equals("path.separator") ||
                key.equals("prng.buffers") ||
                key.equals("router.trustedUpdateKeys") ||
                key.startsWith("router.update") ||
                key.startsWith("routerconsole.") ||
                key.startsWith("time.") ||
                key.startsWith("stat.") ||
                key.startsWith("gnu.") ||  // gnu JVM
                key.startsWith("net.i2p.router.web.") ||  // console nonces
                key.equals("loggerFilenameOverride") ||
                key.equals("router.version") ||
                key.equals("i2p.dir.base") ||
                key.startsWith("networkaddress.cache.") ||
                key.startsWith("http.") ||
                key.startsWith("jetty.") ||
                key.startsWith("org.mortbay.") ||
                key.startsWith("wrapper.")) {
                if (_log.shouldLog(Log.DEBUG)) _log.debug("Skipping property: " + key);
                continue;
            }
            String val = options.getProperty(key);
            // Long strings MUST be removed, even in router context,
            // as the session config properties must be serialized to be signed.
            // fixme, bytes could still be over 255 (unlikely)
            if (key.length() > 255 || val.length() > 255) {
                if (_log.shouldLog(Log.WARN))
                    _log.warn("Not passing on property ["
                              + key
                              + "] in the session config, key or value is too long (max = 255): "
                              + val);
            } else {
                rv.setProperty(key, val);
            }
        }
        return rv;
    }

    /**
     * Update the tunnel and bandwidth settings
     * @since 0.8.4
     */
    public void updateOptions(Properties options) {
        _options.putAll(filter(options));
        _producer.updateBandwidth(this);
        try {
            _producer.updateTunnels(this, 0);
        } catch (I2PSessionException ise) {}
    }

    /**
     *  @since 0.9.4
     */
    public boolean getFastReceive() {
        return _fastReceive && _routerSupportsFastReceive;
    }

    void setLeaseSet(LeaseSet ls) {
        _leaseSet = ls;
        if (ls != null) {
            synchronized (_leaseSetWait) {
                _leaseSetWait.notifyAll();
            }
        }
    }

    LeaseSet getLeaseSet() {
        return _leaseSet;
    }

    protected void changeState(State state) {
        if (_log.shouldInfo())
            _log.info(getPrefix() + "Change state to " + state);
        synchronized (_stateLock) {
            _state = state;
            _stateLock.notifyAll();
        }
    }

    /**
     * Load up the destKeyFile for our Destination, PrivateKey, and SigningPrivateKey
     *
     * @throws DataFormatException if the file is in the wrong format or keys are invalid
     * @throws IOException if there is a problem reading the file
     */
    private void readDestination(InputStream destKeyStream) throws DataFormatException, IOException {
        _myDestination.readBytes(destKeyStream);
        _privateKey.readBytes(destKeyStream);
        _signingPrivateKey = new SigningPrivateKey(_myDestination.getSigningPublicKey().getType());
        _signingPrivateKey.readBytes(destKeyStream);
    }

    /**
     * Connect to the router and establish a session.  This call blocks until 
     * a session is granted.
     *
     * Should be threadsafe, other threads will block until complete.
     * Disconnect / destroy from another thread may be called simultaneously and
     * will (should?) interrupt the connect.
     *
     * Connecting a primary session will not automatically connect subsessions.
     * Connecting a subsession will automatically connect the primary session
     * if not previously connected.
     *
     * @throws I2PSessionException if there is a configuration error or the router is
     *                             not reachable
     */
    public void connect() throws I2PSessionException {
        synchronized(_stateLock) {
            boolean wasOpening = false;
            boolean loop = true;
            while (loop) {
                switch (_state) {
                    case INIT:
                        loop = false;
                        break;
                    case CLOSED:
                        if (wasOpening)
                            throw new I2PSessionException("connect by other thread failed");
                        loop = false;
                        break;
                    case OPENING:
                    case GOTDATE:
                        wasOpening = true;
                        try {
                            _stateLock.wait(10*1000);
                        } catch (InterruptedException ie) {
                            throw new I2PSessionException("Interrupted", ie);
                        }
                        break;
                    case CLOSING:
                        throw new I2PSessionException("close in progress");
                    case OPEN:
                        return;
                }
            }
            changeState(State.OPENING);
        }

        _availabilityNotifier.stopNotifying();
        
        if ( (_options != null) && 
             (I2PClient.PROP_RELIABILITY_GUARANTEED.equals(_options.getProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_BEST_EFFORT))) ) {
            if (_log.shouldLog(Log.ERROR))
                _log.error("I2CP guaranteed delivery mode has been removed, using best effort.");
        }
            
        boolean success = false;
        long startConnect = _context.clock().now();
        try {
            // protect w/ closeSocket()
            synchronized(_stateLock) {
                // If we are in the router JVM, connect using the internal queue
                if (_context.isRouterContext()) {
                    // _socket and _writer remain null
                    InternalClientManager mgr = _context.internalClientManager();
                    if (mgr == null)
                        throw new I2PSessionException("Router is not ready for connections");
                    // the following may throw an I2PSessionException
                    _queue = mgr.connect();
                    _reader = new QueuedI2CPMessageReader(_queue, this);
                } else {
                    if (SystemVersion.isAndroid() &&
                            _options.getProperty(PROP_DOMAIN_SOCKET) != null) {
                        try {
                            Class<?> clazz = Class.forName("net.i2p.client.DomainSocketFactory");
                            Constructor<?> ctor = clazz.getDeclaredConstructor(I2PAppContext.class);
                            Object fact = ctor.newInstance(_context);
                            Method createSocket = clazz.getDeclaredMethod("createSocket", String.class);
                            try {
                                _socket = (Socket) createSocket.invoke(fact, _options.getProperty(PROP_DOMAIN_SOCKET));
                            } catch (InvocationTargetException e) {
                                throw new I2PSessionException("Cannot create domain socket", e);
                            }
                        } catch (ClassNotFoundException e) {
                            throw new I2PSessionException("Cannot load DomainSocketFactory", e);
                        } catch (NoSuchMethodException e) {
                            throw new I2PSessionException("Cannot load DomainSocketFactory", e);
                        } catch (InstantiationException e) {
                            throw new I2PSessionException("Cannot load DomainSocketFactory", e);
                        } catch (IllegalAccessException e) {
                            throw new I2PSessionException("Cannot load DomainSocketFactory", e);
                        } catch (InvocationTargetException e) {
                            throw new I2PSessionException("Cannot load DomainSocketFactory", e);
                        }
                    } else if (Boolean.parseBoolean(_options.getProperty(PROP_ENABLE_SSL))) {
                        try {
                            I2PSSLSocketFactory fact = new I2PSSLSocketFactory(_context, false, "certificates/i2cp");
                            _socket = fact.createSocket(_hostname, _portNum);
                            _socket.setKeepAlive(true);
                        } catch (GeneralSecurityException gse) {
                            IOException ioe = new IOException("SSL Fail");
                            ioe.initCause(gse);
                            throw ioe;
                        }
                    } else {
                        _socket = new Socket(_hostname, _portNum);
                        _socket.setKeepAlive(true);
                    }
                    // _socket.setSoTimeout(1000000); // Uhmmm we could really-really use a real timeout, and handle it.
                    OutputStream out = _socket.getOutputStream();
                    out.write(I2PClient.PROTOCOL_BYTE);
                    out.flush();
                    _writer = new ClientWriterRunner(out, this);
                    _writer.startWriting();
                    InputStream in = new BufferedInputStream(_socket.getInputStream(), BUF_SIZE);
                    _reader = new I2CPMessageReader(in, this);
                }
            }
            if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "before startReading");
            _reader.startReading();
            if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Before getDate");
            Properties auth = null;
            if ((!_context.isRouterContext()) && _options.containsKey(PROP_USER) && _options.containsKey(PROP_PW)) {
                // Only supported by routers 0.9.11 or higher, but we don't know the version yet.	
                // Auth will also be sent in the SessionConfig.
                auth = new OrderedProperties();
                auth.setProperty(PROP_USER, _options.getProperty(PROP_USER));
                auth.setProperty(PROP_PW, _options.getProperty(PROP_PW));
            }
            sendMessage_unchecked(new GetDateMessage(CoreVersion.VERSION, auth));
            waitForDate();

            if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Before producer.connect()");
            _producer.connect(this);
            if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "After producer.connect()");

            // wait until we have created a lease set
            int waitcount = 0;
            while (_leaseSet == null) {
                if (waitcount++ > 5*60) {
                    throw new IOException("No tunnels built after waiting 5 minutes. Your network connection may be down, or there is severe network congestion.");
                }
                synchronized (_leaseSetWait) {
                    // InterruptedException caught below
                    _leaseSetWait.wait(1000);
                }
                // if we got a disconnect message while waiting
                if (isClosed())
                    throw new IOException("Disconnected from router while waiting for tunnels");
            }
            if (_log.shouldLog(Log.INFO)) {
                long connected = _context.clock().now();
                 _log.info(getPrefix() + "Lease set created with inbound tunnels after "
                           + (connected - startConnect)
                           + "ms - ready to participate in the network!");
            }
            Thread notifier = new I2PAppThread(_availabilityNotifier, "ClientNotifier " + getPrefix(), true);
            notifier.start();
            startIdleMonitor();
            startVerifyUsage();
            success = true;

            // now send CreateSessionMessages for all subsessions, one at a time, must wait for each response
            synchronized(_subsessionLock) {
                for (SubSession ss : _subsessions) {
                   if (_log.shouldLog(Log.INFO))
                       _log.info(getPrefix() + "Connecting subsession " + ss);
                    _producer.connect(ss);
                }
            }

        } catch (InterruptedException ie) {
            throw new I2PSessionException("Interrupted", ie);
        } catch (UnknownHostException uhe) {
            throw new I2PSessionException(getPrefix() + "Cannot connect to the router on " + _hostname + ':' + _portNum, uhe);
        } catch (IOException ioe) {
            // Generate the best error message as this will be logged
            String msg;
            if (_context.isRouterContext())
                msg = "Failed to build tunnels";
            else if (SystemVersion.isAndroid() &&
                    _options.getProperty(PROP_DOMAIN_SOCKET) != null)
                msg = "Failed to bind to the router on " + _options.getProperty(PROP_DOMAIN_SOCKET) + " and build tunnels";
            else
                msg = "Cannot connect to the router on " + _hostname + ':' + _portNum + " and build tunnels";
            throw new I2PSessionException(getPrefix() + msg, ioe);
        } finally {
            if (success) {
                changeState(State.OPEN);
            } else {
                _availabilityNotifier.stopNotifying();
                synchronized(_stateLock) {
                    changeState(State.CLOSING);
                    try {
                        _producer.disconnect(this);
                    } catch (I2PSessionException ipe) {}
                    closeSocket();
                }
            }
        }
    }

    /**
     *  @since 0.9.11 moved from connect()
     */
    protected void waitForDate() throws InterruptedException, IOException {
            if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "After getDate / begin waiting for a response");
            int waitcount = 0;
            while (true) {
                if (waitcount++ > 30) {
                    throw new IOException("No handshake received from the router");
                }
                synchronized(_stateLock) {
                    if (_state == State.GOTDATE)
                        break;
                    if (_state != State.OPENING && _state != State.INIT)
                        throw new IOException("Socket closed, state=" + _state);
                    // InterruptedException caught by caller
                    _stateLock.wait(1000);
                }
            }
            if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "After received a SetDate response");
    }

    /**
     * Pull the unencrypted data from the message that we've already prefetched and
     * notified the user that its available.
     *
     */
    public byte[] receiveMessage(int msgId) throws I2PSessionException {
        MessagePayloadMessage msg = _availableMessages.remove(Long.valueOf(msgId));
        if (msg == null) {
            _log.error("Receive message " + msgId + " had no matches");
            return null;
        }
        updateActivity();
        return msg.getPayload().getUnencryptedData();
    }

    /**
     * Report abuse with regards to the given messageId
     */
    public void reportAbuse(int msgId, int severity) throws I2PSessionException {
        verifyOpen();
        _producer.reportAbuse(this, msgId, severity);
    }

    public abstract void receiveStatus(int msgId, long nonce, int status);

/****** no end-to-end crypto
    protected static final Set createNewTags(int num) {
        Set tags = new HashSet();
        for (int i = 0; i < num; i++)
            tags.add(new SessionTag(true));
        return tags;
    }
*******/

    /**
     * Recieve a payload message and let the app know its available
     */
    public void addNewMessage(MessagePayloadMessage msg) {
        Long mid = Long.valueOf(msg.getMessageId());
        _availableMessages.put(mid, msg);
        long id = msg.getMessageId();
        byte data[] = msg.getPayload().getUnencryptedData();
        if ((data == null) || (data.length <= 0)) {
            if (_log.shouldLog(Log.CRIT))
                _log.log(Log.CRIT, getPrefix() + "addNewMessage of a message with no unencrypted data",
                           new Exception("Empty message"));
        } else {
            int size = data.length;
            _availabilityNotifier.available(id, size);
            if (_log.shouldLog(Log.INFO))
                _log.info(getPrefix() + "Notified availability for session " + _sessionId + ", message " + id);
        }
    }

    /**
     *  Fire up a periodic task to check for unclaimed messages
     *  @since 0.9.1
     */
    protected void startVerifyUsage() {
        new VerifyUsage();
    }

    /**
     *  Check for unclaimed messages, without wastefully setting a timer for each
     *  message. Just copy all unclaimed ones and check some time later.
     */
    private class VerifyUsage extends SimpleTimer2.TimedEvent {
        private final List<Long> toCheck = new ArrayList<Long>();
        
        public VerifyUsage() {
             super(_context.simpleTimer2(), VERIFY_USAGE_TIME);
        }

        public void timeReached() {
            if (isClosed())
                return;
            //if (_log.shouldLog(Log.DEBUG))
            //    _log.debug(getPrefix() + " VerifyUsage of " + toCheck.size());
            if (!toCheck.isEmpty()) {
                for (Long msgId : toCheck) {
                    MessagePayloadMessage removed = _availableMessages.remove(msgId);
                    if (removed != null)
                        _log.error(getPrefix() + " Client not responding? Message not processed! id=" + msgId + ": " + removed);
                }
                toCheck.clear();
            }
            toCheck.addAll(_availableMessages.keySet());
            schedule(VERIFY_USAGE_TIME);
        }
    }

    /**
     *  This notifies the client of payload messages.
     *  Needs work.
     */
    protected class AvailabilityNotifier implements Runnable {
        private final List<Long> _pendingIds;
        private final List<Integer> _pendingSizes;
        private volatile boolean _alive;
 
        public AvailabilityNotifier() {
            _pendingIds = new ArrayList<Long>(2);
            _pendingSizes = new ArrayList<Integer>(2);
        }
        
        public void stopNotifying() { 
            _alive = false; 
            synchronized (AvailabilityNotifier.this) {
                AvailabilityNotifier.this.notifyAll();
            }
        }
        
        public void available(long msgId, int size) {
            synchronized (AvailabilityNotifier.this) {
                _pendingIds.add(Long.valueOf(msgId));
                _pendingSizes.add(Integer.valueOf(size));
                AvailabilityNotifier.this.notifyAll();
            }
        }
        public void run() {
            _alive = true;
            while (_alive) {
                Long msgId = null;
                Integer size = null;
                synchronized (AvailabilityNotifier.this) {
                    if (_pendingIds.isEmpty()) {
                        try {
                            AvailabilityNotifier.this.wait();
                        } catch (InterruptedException ie) { // nop
                        }
                    }
                    if (!_pendingIds.isEmpty()) {
                        msgId = _pendingIds.remove(0);
                        size = _pendingSizes.remove(0);
                    }
                }
                if ( (msgId != null) && (size != null) ) {
                    if (_sessionListener != null) {
                        try {
                            long before = System.currentTimeMillis();
                            _sessionListener.messageAvailable(I2PSessionImpl.this, msgId.intValue(), size.intValue());
                            long duration = System.currentTimeMillis() - before;
                            if ((duration > 100) && _log.shouldLog(Log.INFO)) 
                                _log.info("Message availability notification for " + msgId.intValue() + " took " 
                                           + duration + " to " + _sessionListener);
                        } catch (RuntimeException e) {
                            _log.log(Log.CRIT, "Error notifying app of message availability", e);
                        }
                    } else {
                        _log.log(Log.CRIT, "Unable to notify an app that " + msgId + " of size " + size + " is available!");
                    }
                }
            }
        }
    }
    
    /**
     * The I2CPMessageEventListener callback.
     * Recieve notification of some I2CP message and handle it if possible.
     *
     * We route the message based on message type AND session ID.
     *
     * The following types never contain a session ID and are not routable to
     * a subsession:
     *     BandwidthLimitsMessage, DestReplyMessage
     *
     * The following types may not contain a valid session ID
     * even when intended for a subsession, so we must take special care:
     *     SessionStatusMessage
     *
     * @param reader unused
     */
    public void messageReceived(I2CPMessageReader reader, I2CPMessage message) {
        int type = message.getType();
        SessionId id = message.sessionId();
        SessionId currId = _sessionId;
        if (id == null || id.equals(currId) ||
            (currId == null && id != null && type == SessionStatusMessage.MESSAGE_TYPE) ||
            ((id == null || id.getSessionId() == 65535) &&
             (type == HostReplyMessage.MESSAGE_TYPE || type == DestReplyMessage.MESSAGE_TYPE))) {
            // it's for us
            I2CPMessageHandler handler = _handlerMap.getHandler(type);
            if (handler != null) {
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug(getPrefix() + "Message received of type " + type
                               + " to be handled by " + handler.getClass().getSimpleName());
                handler.handleMessage(message, this);
            } else {
                if (_log.shouldLog(Log.WARN))
                    _log.warn(getPrefix() + "Unknown message or unhandleable message received: type = "
                              + type);
            }
        } else {
            SubSession sub = _subsessionMap.get(id);
            if (sub != null) {
                // it's for a subsession
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug(getPrefix() + "Message received of type " + type
                               + " to be handled by " + sub);
                sub.messageReceived(reader, message);
            } else if (id != null && type == SessionStatusMessage.MESSAGE_TYPE) {
                // look for a subsession without a session
                synchronized (_subsessionLock) {
                    for (SubSession sess : _subsessions) {
                        if (sess.getSessionId() == null) {
                            sess.messageReceived(reader, message);
                            id = sess.getSessionId();
                            if (id != null) {
                                if (id.equals(_sessionId)) {
                                    // shouldnt happen
                                    sess.setSessionId(null);
                                    if (_log.shouldLog(Log.WARN))
                                        _log.warn("Dup or our session id " + id);
                                } else {
                                    SubSession old = _subsessionMap.putIfAbsent(id, sess);
                                    if (old != null) {
                                        // shouldnt happen
                                        sess.setSessionId(null);
                                        if (_log.shouldLog(Log.WARN))
                                            _log.warn("Dup session id " + id);
                                    }
                                }
                            }
                            return;
                        }
                        if (_log.shouldLog(Log.WARN))
                            _log.warn(getPrefix() + "No session " + id + " to handle message: type = "
                                      + type);
                    }
                }
            } else {
                // it's for nobody
                if (_log.shouldLog(Log.WARN))
                    _log.warn(getPrefix() + "No session " + id + " to handle message: type = "
                              + type);
            }
        }
    }

    /** 
     * The I2CPMessageEventListener callback.
     * Recieve notifiation of an error reading the I2CP stream.
     * @param reader unused
     * @param error non-null
     */
    public void readError(I2CPMessageReader reader, Exception error) {
        propogateError("There was an error reading data", error);
        disconnect();
    }

    /**
     * Retrieve the destination of the session
     */
    public Destination getMyDestination() { return _myDestination; }

    /**
     * Retrieve the decryption PrivateKey 
     */
    public PrivateKey getDecryptionKey() { return _privateKey; }

    /**
     * Retrieve the signing SigningPrivateKey
     */
    public SigningPrivateKey getPrivateKey() { return _signingPrivateKey; }

    /**
     * Retrieve the helper that generates I2CP messages
     */
    I2CPMessageProducer getProducer() { return _producer; }

    /**
     *  For Subsessions
     *  @since 0.9.21
     */
    I2PClientMessageHandlerMap getHandlerMap() { return _handlerMap; }

    /**
     *  For Subsessions
     *  @since 0.9.21
     */
    I2PAppContext getContext() { return _context; }

    /**
     * Retrieve the configuration options, filtered.
     * All defaults passed in via constructor have been promoted to the primary map.
     *
     * @return non-null, if insantiated with null options, this will be the System properties.
     */
    Properties getOptions() { return _options; }

    /** 
     * Retrieve the session's ID
     */
    SessionId getSessionId() { return _sessionId; }
    void setSessionId(SessionId id) { _sessionId = id; }

    /** configure the listener */
    public void setSessionListener(I2PSessionListener lsnr) { _sessionListener = lsnr; }

    /**
     *  Has the session been closed (or not yet connected)?
     *  False when open and during transitions. Synchronized.
     */
    public boolean isClosed() {
        synchronized (_stateLock) {
            return _state == State.CLOSED || _state == State.INIT;
        }
    }

    /**
     *  Throws I2PSessionException if uninitialized, closed or closing.
     *  Blocks if opening.
     *
     *  @since 0.9.23
     */
    protected void verifyOpen() throws I2PSessionException {
        synchronized (_stateLock) {
            while (true) {
                switch (_state) {
                    case INIT:
                        throw new I2PSessionException("Not open, must call connect() first");

                    case OPENING:  // fall thru
                    case GOTDATE:
                        try {
                            _stateLock.wait(5*1000);
                            continue;
                        } catch (InterruptedException ie) {
                            throw new I2PSessionException("Interrupted", ie);
                        }

                    case OPEN:
                        return;

                    case CLOSING:  // fall thru
                    case CLOSED:
                        throw new I2PSessionException("Already closed");
                }
            }
        }
    }

    /**
     * Deliver an I2CP message to the router
     * As of 0.9.3, may block for several seconds if the write queue to the router is full
     *
     * @throws I2PSessionException if the message is malformed or there is an error writing it out
     */
    void sendMessage(I2CPMessage message) throws I2PSessionException {
        verifyOpen();
        sendMessage_unchecked(message);
    }

    /**
     * Deliver an I2CP message to the router.
     * Does NOT check state. Call only from connect() or other methods that need to
     * send messages when not in OPEN state.
     *
     * @throws I2PSessionException if the message is malformed or there is an error writing it out
     * @since 0.9.23
     */
    void sendMessage_unchecked(I2CPMessage message) throws I2PSessionException {
        if (_queue != null) {
            // internal
            try {
                if (!_queue.offer(message, MAX_SEND_WAIT))
                    throw new I2PSessionException("Timed out waiting while write queue was full");
            } catch (InterruptedException ie) {
                throw new I2PSessionException("Interrupted", ie);
            }
        } else {
            ClientWriterRunner writer = _writer;
            if (writer == null) {
                throw new I2PSessionException("Already closed or not open");
            } else {
                writer.addMessage(message);
            }
        }
    }

    /**
     * Pass off the error to the listener
     * Misspelled, oh well.
     * @param error non-null
     */
    void propogateError(String msg, Throwable error) {
        // Only log as WARN if the router went away
        int level;
        String msgpfx;
        if (error instanceof EOFException) {
            level = Log.WARN;
            msgpfx = "Router closed connection: ";
        } else {
            level = Log.ERROR;
            msgpfx = "Error occurred communicating with router: ";
        }

        if (_log.shouldLog(level)) 
            _log.log(level, getPrefix() + msgpfx + msg, error);
        if (_sessionListener != null) _sessionListener.errorOccurred(this, msg, error);
    }

    /**
     * Tear down the session, and do NOT reconnect.
     *
     * Blocks if session has not been fully started.
     */
    public void destroySession() {
        destroySession(true);
    }

    /**
     * Tear down the session, and do NOT reconnect.
     * 
     * Will interrupt an open in progress.
     */
    public void destroySession(boolean sendDisconnect) {
        synchronized(_stateLock) {
            if (_state == State.CLOSING || _state == State.CLOSED || _state == State.INIT)
                return;
            changeState(State.CLOSING);
        }
        
        if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "Destroy the session", new Exception("DestroySession()"));
        if (sendDisconnect && _producer != null) {    // only null if overridden by I2PSimpleSession
            try {
                _producer.disconnect(this);
            } catch (I2PSessionException ipe) {
                //propogateError("Error destroying the session", ipe);
                if (_log.shouldLog(Log.WARN))
                    _log.warn("Error destroying the session", ipe);
            }
        }
        // SimpleSession does not initialize
        if (_availabilityNotifier != null)
            _availabilityNotifier.stopNotifying();
        closeSocket();
        _subsessionMap.clear();
        if (_sessionListener != null) _sessionListener.disconnected(this);
    }

    /**
     * Close the socket carefully.
     */
    private void closeSocket() {
        if (_log.shouldLog(Log.INFO))
            _log.info(getPrefix() + "Closing the socket", new Exception("closeSocket"));
        synchronized(_stateLock) {
            changeState(State.CLOSING);
            locked_closeSocket();
            changeState(State.CLOSED);
        }
        synchronized (_subsessionLock) {
            for (SubSession sess : _subsessions) {
                sess.changeState(State.CLOSED);
                sess.setSessionId(null);
                sess.setLeaseSet(null);
            }
        }
    }

    /**
     * Close the socket carefully.
     * Caller must change state.
     */
    private void locked_closeSocket() {
        if (_reader != null) {
            _reader.stopReading();
            _reader = null;
        }
        if (_queue != null) {
            // internal
            _queue.close();
        }
        if (_writer != null) {
            _writer.stopWriting();
            _writer = null;
        }

        if (_socket != null) {
            try {
                _socket.close();
            } catch (IOException ioe) {
                propogateError("Caught an IO error closing the socket.  ignored", ioe);
            } finally {
                _socket = null; // so when propogateError calls closeSocket, it doesnt loop
            }
        }
        setSessionId(null);
        setLeaseSet(null);
    }

    /**
     * The I2CPMessageEventListener callback.
     * Recieve notification that the I2CP connection was disconnected.
     * @param reader unused
     */
    public void disconnected(I2CPMessageReader reader) {
        if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Disconnected", new Exception("Disconnected"));
        disconnect();
    }

    /**
     * Will interrupt a connect in progress.
     */
    protected void disconnect() {
        State oldState;
        synchronized(_stateLock) {
            if (_state == State.CLOSING || _state == State.CLOSED || _state == State.INIT)
                return;
            oldState = _state;
            changeState(State.CLOSING);
        }
        if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Disconnect() called", new Exception("Disconnect"));
        // don't try to reconnect if it failed before GETTDATE
        if (oldState != State.OPENING && shouldReconnect()) {
            if (reconnect()) {
                if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "I2CP reconnection successful");
                return;
            }
            if (_log.shouldLog(Log.ERROR)) _log.error(getPrefix() + "I2CP reconnection failed");
        }

        if (_log.shouldLog(Log.ERROR))
            _log.error(getPrefix() + "Disconned from the router, and not trying to reconnect");
        if (_sessionListener != null) _sessionListener.disconnected(this);

        closeSocket();
        changeState(State.CLOSED);
        // break out of wait for initial LS in connect()
        synchronized (_leaseSetWait) {
            _leaseSetWait.notifyAll();
        }
    }

    private final static int MAX_RECONNECT_DELAY = 320*1000;
    private final static int BASE_RECONNECT_DELAY = 10*1000;

    protected boolean shouldReconnect() {
        return true;
    }

    protected boolean reconnect() {
        closeSocket();
        if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "Reconnecting...");
        int i = 0;
        while (true) {
            long delay = BASE_RECONNECT_DELAY << i;
            i++;
            if ( (delay > MAX_RECONNECT_DELAY) || (delay <= 0) )
                delay = MAX_RECONNECT_DELAY;
            try {
                Thread.sleep(delay);
            } catch (InterruptedException ie) {
                return false;
            }
            
            try {
                connect();
                if (_log.shouldLog(Log.INFO)) 
                    _log.info(getPrefix() + "Reconnected on attempt " + i);
                return true;
            } catch (I2PSessionException ise) {
                if (_log.shouldLog(Log.ERROR)) 
                    _log.error(getPrefix() + "Error reconnecting on attempt " + i, ise);
            }
        }
    }
    
    /**
     * try hard to make a decent identifier as this will appear in error logs
     */
    protected String getPrefix() {
        StringBuilder buf = new StringBuilder();
        buf.append('[');
        buf.append(_state.toString()).append(' ');
        String s = _options.getProperty("inbound.nickname");
        if (s != null)
            buf.append(s);
        else
            buf.append(getClass().getSimpleName());
        SessionId id = _sessionId;
        if (id != null)
            buf.append(" #").append(id.getSessionId());
        buf.append("]: ");
        return buf.toString();
    }

    /**
     *  Called by the message handler
     *  on reception of DestReplyMessage
     *  @param d non-null
     */
    void destReceived(Destination d) {
        Hash h = d.calculateHash();
        synchronized (_lookupCache) {
            _lookupCache.put(h, d);
        }
        for (LookupWaiter w : _pendingLookups) {
            if (h.equals(w.hash)) {
                synchronized (w) {
                    w.destination = d;
                    w.notifyAll();
                }
            }
        }
    }

    /**
     *  Called by the message handler
     *  on reception of DestReplyMessage
     *  @param h non-null
     */
    void destLookupFailed(Hash h) {
        for (LookupWaiter w : _pendingLookups) {
            if (h.equals(w.hash)) {
                synchronized (w) {
                    w.notifyAll();
                }
            }
        }
    }

    /**
     *  Called by the message handler
     *  on reception of HostReplyMessage
     *  @param d non-null
     *  @since 0.9.11
     */
    void destReceived(long nonce, Destination d) {
        // notify by nonce and hash
        Hash h = d.calculateHash();
        for (LookupWaiter w : _pendingLookups) {
            if (nonce == w.nonce || h.equals(w.hash)) {
                synchronized (_lookupCache) {
                    if (w.name != null)
                        _lookupCache.put(w.name, d);
                    _lookupCache.put(h, d);
                }
                synchronized (w) {
                    w.destination = d;
                    w.notifyAll();
                }
            }
        }
    }

    /**
     *  Called by the message handler
     *  on reception of HostReplyMessage
     *  @since 0.9.11
     */
    void destLookupFailed(long nonce) {
        for (LookupWaiter w : _pendingLookups) {
            if (nonce == w.nonce) {
                synchronized (w) {
                    w.notifyAll();
                }
            }
        }
    }

    /** called by the message handler */
    void bwReceived(int[] i) {
        _bwLimits = i;
        synchronized (_bwReceivedLock) {
            _bwReceivedLock.notifyAll();
        }
    }

    /**
     *  Simple object to wait for lookup replies
     *  @since 0.8.3
     */
    private static class LookupWaiter {
        /** the request (Hash mode) */
        public final Hash hash;
        /** the request (String mode) */
        public final String name;
        /** the request (nonce mode) */
        public final long nonce;
        /** the reply; synch on this */
        public Destination destination;

        public LookupWaiter(Hash h) {
            this(h, -1);
        }

        /** @since 0.9.11 */
        public LookupWaiter(Hash h, long nonce) {
            this.hash = h;
            this.name = null;
            this.nonce = nonce;
        }

        /** @since 0.9.11 */
        public LookupWaiter(String name, long nonce) {
            this.hash = null;
            this.name = name;
            this.nonce = nonce;
        }
    }

    /** @since 0.9.20 */
    public static void clearCache() {
        synchronized (_lookupCache) {
            _lookupCache.clear();
        }
    }

    /**
     *  Blocking. Waits a max of 10 seconds by default.
     *  See lookupDest with maxWait parameter to change.
     *  Implemented in 0.8.3 in I2PSessionImpl;
     *  previously was available only in I2PSimpleSession.
     *  Multiple outstanding lookups are now allowed.
     *  @return null on failure
     */
    public Destination lookupDest(Hash h) throws I2PSessionException {
        return lookupDest(h, 10*1000);
    }

    /**
     *  Blocking.
     *  @param maxWait ms
     *  @since 0.8.3
     *  @return null on failure
     */
    public Destination lookupDest(Hash h, long maxWait) throws I2PSessionException {
        synchronized (_lookupCache) {
            Destination rv = _lookupCache.get(h);
            if (rv != null)
                return rv;
        }
        synchronized (_stateLock) {
            // not before GOTDATE
            if (_state == State.CLOSED ||
                _state == State.INIT ||
                _state == State.OPENING) {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Session closed, cannot lookup " + h);
                return null;
            }
        }
        LookupWaiter waiter;
        long nonce;
        if (_routerSupportsHostLookup) {
            nonce = _lookupID.incrementAndGet() & 0x7fffffff;
            waiter = new LookupWaiter(h, nonce);
        } else {
            nonce = 0; // won't be used
            waiter = new LookupWaiter(h);
        }
        _pendingLookups.offer(waiter);
        Destination rv = null;
        try {
            if (_routerSupportsHostLookup) {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Sending HostLookup for " + h);
                SessionId id = _sessionId;
                if (id == null)
                    id = new SessionId(65535);
                sendMessage_unchecked(new HostLookupMessage(id, h, nonce, maxWait));
            } else {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Sending DestLookup for " + h);
                sendMessage_unchecked(new DestLookupMessage(h));
            }
            try {
                synchronized (waiter) {
                    waiter.wait(maxWait);
                    rv = waiter.destination;
                }
            } catch (InterruptedException ie) {
                throw new I2PSessionException("Interrupted", ie);
            }
        } finally {
            _pendingLookups.remove(waiter);
        }
        return rv;
    }

    /**
     *  Ask the router to lookup a Destination by host name.
     *  Blocking. Waits a max of 10 seconds by default.
     *
     *  This only makes sense for a b32 hostname, OR outside router context.
     *  Inside router context, just query the naming service.
     *  Outside router context, this does NOT query the context naming service.
     *  Do that first if you expect a local addressbook.
     *
     *  This will log a warning for non-b32 in router context.
     *
     *  See interface for suggested implementation.
     *
     *  Requires router side to be 0.9.11 or higher. If the router is older,
     *  this will return null immediately.
     *
     *  @since 0.9.11
     */
    public Destination lookupDest(String name) throws I2PSessionException {
        return lookupDest(name, 10*1000);
    }

    /**
     *  Ask the router to lookup a Destination by host name.
     *  Blocking. See above for details.
     *  @param maxWait ms
     *  @since 0.9.11
     *  @return null on failure
     */
    public Destination lookupDest(String name, long maxWait) throws I2PSessionException {
        if (name.length() == 0)
            return null;
        // Shortcut for b64
        if (name.length() >= 516) {
            try {
                return new Destination(name);
            } catch (DataFormatException dfe) {
                return null;
            }
        }
        // won't fit in Mapping
        if (name.length() >= 256 && !_context.isRouterContext())
            return null;
        synchronized (_lookupCache) {
            Destination rv = _lookupCache.get(name);
            if (rv != null)
                return rv;
        }
        if (isClosed()) {
            if (_log.shouldLog(Log.INFO))
                _log.info("Session closed, cannot lookup " + name);
            return null;
        }
        if (!_routerSupportsHostLookup) {
            // do them a favor and convert to Hash lookup
            if (name.length() == 60 && name.toLowerCase(Locale.US).endsWith(".b32.i2p"))
                return lookupDest(Hash.create(Base32.decode(name.toLowerCase(Locale.US).substring(0, 52))), maxWait);
            // else unsupported
            if (_log.shouldLog(Log.WARN))
                _log.warn("Router does not support HostLookup for " + name);
            return null;
        }
        int nonce = _lookupID.incrementAndGet() & 0x7fffffff;
        LookupWaiter waiter = new LookupWaiter(name, nonce);
        _pendingLookups.offer(waiter);
        Destination rv = null;
        try {
            if (_log.shouldLog(Log.INFO))
                _log.info("Sending HostLookup for " + name);
            SessionId id = _sessionId;
            if (id == null)
                id = new SessionId(65535);
            sendMessage_unchecked(new HostLookupMessage(id, name, nonce, maxWait));
            try {
                synchronized (waiter) {
                    waiter.wait(maxWait);
                    rv = waiter.destination;
                }
            } catch (InterruptedException ie) {
                throw new I2PSessionException("Interrupted", ie);
            }
        } finally {
            _pendingLookups.remove(waiter);
        }
        return rv;
    }

    /**
     *  Blocking. Waits a max of 5 seconds.
     *  But shouldn't take long.
     *  Implemented in 0.8.3 in I2PSessionImpl;
     *  previously was available only in I2PSimpleSession.
     *  Multiple outstanding lookups are now allowed.
     *  @return null on failure
     */
    public int[] bandwidthLimits() throws I2PSessionException {
        synchronized (_stateLock) {
            // not before GOTDATE
            if (_state == State.CLOSED ||
                _state == State.INIT ||
                _state == State.OPENING) {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Session closed, cannot get bw limits");
                return null;
            }
        }
        sendMessage_unchecked(new GetBandwidthLimitsMessage());
        try {
            synchronized (_bwReceivedLock) {
                _bwReceivedLock.wait(5*1000);
            }
        } catch (InterruptedException ie) {
            throw new I2PSessionException("Interrupted", ie);
        }
        return _bwLimits;
    }

    protected void updateActivity() {
        _lastActivity = _context.clock().now();
        if (_isReduced) {
            _isReduced = false;
            if (_log.shouldLog(Log.WARN)) 
                _log.warn(getPrefix() + "Restoring original tunnel quantity");
            try {
                _producer.updateTunnels(this, 0);
            } catch (I2PSessionException ise) {
                _log.error(getPrefix() + "bork restore from reduced");
            }
        }
    }

    public long lastActivity() {
        return _lastActivity;
    }

    public void setReduced() {
        _isReduced = true;
    }

    private void startIdleMonitor() {
        _isReduced = false;
        boolean reduce = Boolean.parseBoolean(_options.getProperty("i2cp.reduceOnIdle"));
        boolean close = Boolean.parseBoolean(_options.getProperty("i2cp.closeOnIdle"));
        if (reduce || close) {
            updateActivity();
            _context.simpleTimer2().addEvent(new SessionIdleTimer(_context, this, reduce, close), SessionIdleTimer.MINIMUM_TIME);
        }
    }

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder(32);
        buf.append("Session: ");
        if (_myDestination != null)
            buf.append(_myDestination.calculateHash().toBase64().substring(0, 4));
        else
            buf.append("[null dest]");
        buf.append(getPrefix());
        return buf.toString();
    }
}
