package net.i2p.router.message;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import net.i2p.data.Hash;
import net.i2p.data.Lease;
import net.i2p.data.LeaseSet;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.TunnelInfo;
import net.i2p.util.SimpleTimer;

/**
 *  Helper for OCMOSJ
 *
 * This is the place where we make I2P go fast.
 *
 * We have five static caches.
 * - The LeaseSet cache is used to decide whether to bundle our own leaseset,
 *   which minimizes overhead.
 * - The Lease cache is used to persistently send to the same lease for the destination,
 *   which keeps the streaming lib happy by minimizing out-of-order delivery.
 * - The Tunnel and BackloggedTunnel caches are used to persistently use the same outbound tunnel
 *   for the same destination,
 *   which keeps the streaming lib happy by minimizing out-of-order delivery.
 * - The last reply requested cache ensures that a reply is requested every so often,
 *   so that failed tunnels are recognized.
 *
 *  @since 0.9 moved out of OCMOSJ
 */
public class OutboundCache {

    /**
     * Use the same outbound tunnel as we did for the same destination previously,
     * if possible, to keep the streaming lib happy
     * Use two caches - although a cache of a list of tunnels per dest might be
     * more elegant.
     * Key the caches on the source+dest pair.
     *
     * NOT concurrent.
     */
    final Map<HashPair, TunnelInfo> tunnelCache = new HashMap<HashPair, TunnelInfo>(64);

    /*
     * NOT concurrent.
     */
    final Map<HashPair, TunnelInfo> backloggedTunnelCache = new HashMap<HashPair, TunnelInfo>(64);

    /**
      * Returns the reply lease set if forced to do so,
      * or if configured to do so,
      * or if a certain percentage of the time if configured to do so,
      * or if our lease set has changed since we last talked to them,
      * or 10% of the time anyway so they don't forget us (disabled for now),
      * or null otherwise.
      *
      * Note that wantACK randomly forces us another 5% of the time.
      *
      * We don't want to do this too often as a typical 2-lease leaseset
      * in a DatabaseStoreMessage is 861+37=898 bytes -
      * when added to garlic it's a 1056-byte addition total, which is huge.
      *
      * Key the cache on the source+dest pair.
      *
      * Concurrent.
      */
    final Map<HashPair, LeaseSet> leaseSetCache = new ConcurrentHashMap<HashPair, LeaseSet>(64);

    /**
     * Use the same inbound tunnel (i.e. lease) as we did for the same destination previously,
     * if possible, to keep the streaming lib happy
     * Key the caches on the source+dest pair.
     *
     * We're going to use the lease until it expires, as long as it remains in the current leaseSet.
     *
     * If not found,
     * fetch the next lease that we should try sending through, randomly chosen
     * from within the sorted leaseSet (NOT sorted by # of failures through each 
     * lease).
     *
     * Concurrent.
     */
    final ConcurrentHashMap<HashPair, Lease> leaseCache = new ConcurrentHashMap<HashPair, Lease>(64);

    /**
     * This cache is used to ensure that we request a reply every so often.
     * Hopefully this allows the router to recognize a failed tunnel and switch,
     * before upper layers like streaming lib fail, even for low-bandwidth
     * connections like IRC.
     *
     * Concurrent.
     */
    final Map<HashPair, Long> lastReplyRequestCache = new ConcurrentHashMap<HashPair, Long>(64);

    private final RouterContext _context;

    private static final int CLEAN_INTERVAL = 5*60*1000;
    
    public OutboundCache(RouterContext ctx) {
        _context = ctx;
        _context.simpleScheduler().addPeriodicEvent(new OCMOSJCacheCleaner(), CLEAN_INTERVAL, CLEAN_INTERVAL);
    }

    /**
     * Key used to cache things with based on source + dest
     * @since 0.8.3
     */
    static class HashPair {
        private final Hash sh, dh;

        public HashPair(Hash s, Hash d) {
            sh = s;
            dh = d;
        }

        public int hashCode() {
            return sh.hashCode() ^ dh.hashCode();
        }

        public boolean equals(Object o) {
            if (o == null || !(o instanceof HashPair))
                return false;
            HashPair hp = (HashPair) o;
            return sh.equals(hp.sh) && dh.equals(hp.dh);
        }
    }

    /**
     * Called on failure to give us a better chance of success next time.
     * Of course this is probably 60s too late.
     * And we could pick the bad ones at random again.
     * Or remove entries that were sent and succeeded after this was sent but before this failed.
     * But it's a start.
     *
     * @param lease may be null
     * @param inTunnel may be null
     * @param outTunnel may be null
     */
    void clearCaches(HashPair hashPair, Lease lease, TunnelInfo inTunnel, TunnelInfo outTunnel) {
        if (inTunnel != null) {   // if we wanted an ack, we sent our lease too
                leaseSetCache.remove(hashPair);
        }
        if (lease != null) {
            // remove only if still equal to lease (concurrent)
            leaseCache.remove(hashPair, lease);
        }
        if (outTunnel != null) {
            synchronized(tunnelCache) {
                TunnelInfo t = backloggedTunnelCache.get(hashPair);
                if (t != null && t.equals(outTunnel))
                    backloggedTunnelCache.remove(hashPair);
                t = tunnelCache.get(hashPair);
                if (t != null && t.equals(outTunnel))
                    tunnelCache.remove(hashPair);
            }
        }
    }

    /**
     *  @since 0.8.8
     */
    public void clearAllCaches() {
        leaseSetCache.clear();
        leaseCache.clear();
        synchronized(tunnelCache) {
            backloggedTunnelCache.clear();
            tunnelCache.clear();
        }
        lastReplyRequestCache.clear();
    }

    /**
     * Clean out old leaseSets
     */
    private static void cleanLeaseSetCache(RouterContext ctx, Map<HashPair, LeaseSet> tc) {
        long now = ctx.clock().now();
        for (Iterator<LeaseSet> iter = tc.values().iterator(); iter.hasNext(); ) {
            LeaseSet l = iter.next();
            if (l.getEarliestLeaseDate() < now)
                iter.remove();
        }
    }

    /**
     * Clean out old leases
     */
    private static void cleanLeaseCache(Map<HashPair, Lease> tc) {
        for (Iterator<Lease> iter = tc.values().iterator(); iter.hasNext(); ) {
            Lease l = iter.next();
            if (l.isExpired(Router.CLOCK_FUDGE_FACTOR))
                iter.remove();
        }
    }

    /**
     * Clean out old tunnels
     * Caller must synchronize on tc.
     */
    private static void cleanTunnelCache(RouterContext ctx, Map<HashPair, TunnelInfo> tc) {
        for (Iterator<Map.Entry<HashPair, TunnelInfo>> iter = tc.entrySet().iterator(); iter.hasNext(); ) {
            Map.Entry<HashPair, TunnelInfo> entry = iter.next();
            HashPair k = entry.getKey();
            TunnelInfo tunnel = entry.getValue();
            // This is a little sneaky, but get the _from back out of the "opaque" hash key
            if (!ctx.tunnelManager().isValidTunnel(k.sh, tunnel))
                iter.remove();
        }
    }

    /**
     * Clean out old reply times
     */
    private static void cleanReplyCache(RouterContext ctx, Map<HashPair, Long> tc) {
        long now = ctx.clock().now();
        for (Iterator<Long> iter = tc.values().iterator(); iter.hasNext(); ) {
            Long l = iter.next();
            if (l.longValue() < now - CLEAN_INTERVAL)
                iter.remove();
        }
    }

    private class OCMOSJCacheCleaner implements SimpleTimer.TimedEvent {
        public void timeReached() {
            cleanLeaseSetCache(_context, leaseSetCache);
            cleanLeaseCache(leaseCache);
            synchronized(tunnelCache) {
                cleanTunnelCache(_context, tunnelCache);
                cleanTunnelCache(_context, backloggedTunnelCache);
            }
            cleanReplyCache(_context, lastReplyRequestCache);
        }
    }
}
