package net.i2p.client.streaming.impl;

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

import net.i2p.I2PAppContext;
import static net.i2p.client.streaming.impl.I2PSocketOptionsImpl.getDouble;
import net.i2p.data.Destination;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2;

/**
 *  Share important TCP Control Block parameters across Connections
 *  to the same remote peer.
 *  This is intended for "temporal" sharing at connection open/close time,
 *  not "ensemble" sharing during a connection. Ref. RFC 2140.
 *  
 *  There is a TCB share per ConnectionManager (i.e. per local Destination)
 *  so that there is no information leakage to other Destinations on the
 *  same router.
 *
 */
class TCBShare {
    private final I2PAppContext _context;
    private final Log _log;
    private final Map<Destination, Entry> _cache;
    private final CleanEvent _cleaner;
    private final double _rttDampening, _wdwDampening, _rttDevDampening;

    // Must be less than SessionKeyManager outbound timeout (12 minutes)
    // to avoid large number of DH operations
    private static final long EXPIRE_TIME = 10*60*1000;
    private static final long CLEAN_TIME = 5*60*1000;
    ///// constants defined in rfc 2140
    ///// do not change unless you know what you're doing
    private static final double RTT_DAMPENING = 0.75;
    private static final double RTTDEV_DAMPENING = 0.75;
    private static final double WDW_DAMPENING = 0.75;
    private static final String RTT_DAMP_PROP="i2p.streaming.tcbcache.rttDampening";
    private static final String WDW_DAMP_PROP="i2p.streaming.tcbcache.wdwDampening";
    private static final String RTTDEV_DAMP_PROP="i2p.streaming.tcbcache.rttdevDampening";
    /////
    private static final int MAX_RTT = Connection.MAX_RESEND_DELAY / 2;
    private static final int MAX_RTT_DEV = (int) (MAX_RTT * 1.5);
    private static final int MAX_WINDOW_SIZE = ConnectionPacketHandler.MAX_SLOW_START_WINDOW;
    
    public TCBShare(I2PAppContext ctx, SimpleTimer2 timer) {
        _context = ctx;
        _log = ctx.logManager().getLog(TCBShare.class);
        
        final Properties props = ctx.getProperties();
        _rttDampening = getDouble(props, RTT_DAMP_PROP, RTT_DAMPENING);
        _wdwDampening = getDouble(props, WDW_DAMP_PROP, WDW_DAMPENING);
        _rttDevDampening = getDouble(props, RTTDEV_DAMP_PROP, RTTDEV_DAMPENING);
        
        _cache = new ConcurrentHashMap<Destination,Entry>(4);
        _cleaner = new CleanEvent(timer);
        _cleaner.schedule(CLEAN_TIME);
        
        if (_log.shouldLog(Log.DEBUG)) {
            String log = "Creating TCBCache with rttDamp=%s, rttDevDamp=%s, wdwDamp=%s, "+
                    "expire=%d, clean=%d";
            log = String.format(log,_rttDampening,_rttDevDampening,_wdwDampening,
                    EXPIRE_TIME,CLEAN_TIME);
             _log.debug(log);
        }
    }

    /**
     *  Cannot be restarted.
     */
    public void stop() {
        _cleaner.cancel();
        _cache.clear();
    }

    /** retrieve from cache */
    public void updateOptsFromShare(Connection con) {
        Destination dest = con.getRemotePeer();
        if (dest == null)
            return;
        ConnectionOptions opts = con.getOptions();
        if (opts == null)
            return;
        Entry e = _cache.get(dest);
        if (e == null || e.isExpired())
            return;
        final int rtt, rttDev, wdw;
        synchronized(e) {
            rtt = e.getRTT();
            rttDev = e.getRTTDev();
            wdw = e.getWindowSize();
        }
        if (_log.shouldLog(Log.DEBUG)) {
            _log.debug("From cache: " +
                       con.getSession().getMyDestination().calculateHash().toBase64().substring(0, 4) +
                       '-' +
                       dest.calculateHash().toBase64().substring(0, 4) +
                       " RTT: " + rtt + 
                       " RTTDev: "+ rttDev +
                       " wdw: " + wdw );
        }
        opts.loadFromCache(rtt,rttDev,wdw);
    }

    /** store to cache */
    public void updateShareOpts(Connection con) {
        Destination dest = con.getRemotePeer();
        if (dest == null)
            return;
        if (con.getAckedPackets() <= 0)
            return;
        ConnectionOptions opts = con.getOptions();
        if (opts == null)
            return;
        int old = -1;
        int oldw = -1;
        int oldDev = -1;
        Entry e = _cache.get(dest);
        if (e == null || e.isExpired()) {
            e = new Entry(opts.getRTT(), opts.getWindowSize(), opts.getRTTDev());
            _cache.put(dest, e);
        } else {
            synchronized(e) {
                old = e.getRTT();
                oldw = e.getWindowSize();
                oldDev = e.getRTTDev();
                e.setRTT(opts.getRTT());
                e.setWindowSize(opts.getWindowSize());
                e.setRTTDev(opts.getRTTDev());
            }
        }
        if (_log.shouldLog(Log.DEBUG)) {
            _log.debug("To cache: " +
                       con.getSession().getMyDestination().calculateHash().toBase64().substring(0, 4) +
                       '-' +
                       dest.calculateHash().toBase64().substring(0, 4) +
                       " old: " + old + " con: " + opts.getRTT() + " new: " + e.getRTT() +
                       " oldDev: " + oldDev + " conDev: " + opts.getRTTDev() + " newDev: " + e.getRTTDev() +
                       " oldw: " + oldw + " conw: " + opts.getWindowSize() + " neww: " + e.getWindowSize());
        }
    }

    private class Entry {
        int _rtt;
        int _wdw;
        int _rttDev;
        long _updated;

        public Entry(int ms, int wdw, int rttDev) {
            _rtt = ms;
            _wdw = wdw;
            _rttDev = rttDev;
            _updated = _context.clock().now();
        }
        public synchronized int getRTT() { return _rtt; }
        public synchronized void setRTT(int ms) {
            _rtt = (int)(_rttDampening*_rtt + (1-_rttDampening)*ms);        
            if (_rtt > MAX_RTT)
                _rtt = MAX_RTT;
            _updated = _context.clock().now();
        }
        public synchronized int getRTTDev() { return _rttDev; }
        public synchronized void setRTTDev(int count) {
            _rttDev = (int)(_rttDevDampening*_rttDev + (1-_rttDevDampening)*count);        
            if (_rttDev > MAX_RTT_DEV)
                _rttDev = MAX_RTT_DEV;
            _updated = _context.clock().now();
        }
        public synchronized int getWindowSize() { return _wdw; }
        public synchronized void setWindowSize(int wdw) {
            _wdw = (int)(0.5 + _wdwDampening*_wdw + (1-_wdwDampening)*wdw);       
            if (_wdw > MAX_WINDOW_SIZE)
                _wdw = MAX_WINDOW_SIZE;
            _updated = _context.clock().now();
        }
        public synchronized boolean isExpired() {
            return _updated < _context.clock().now() - EXPIRE_TIME;
        }
    }

    private class CleanEvent extends SimpleTimer2.TimedEvent {
        public CleanEvent(SimpleTimer2 timer) {
            // Use router's SimpleTimer2
            super(timer);
        }
        public void timeReached() {
            for (Iterator<Entry> iter = _cache.values().iterator(); iter.hasNext(); ) {
                if (iter.next().isExpired())
                    iter.remove();
            }
            schedule(CLEAN_TIME);
        }
    }
}
