package net.i2p.router.peermanager;

import net.i2p.data.router.RouterInfo;
import net.i2p.router.RouterContext;
import net.i2p.router.NetworkDatabaseFacade;
import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
import net.i2p.stat.Rate;
import net.i2p.stat.RateAverages;
import net.i2p.stat.RateStat;

/**
 * Estimate how many of our tunnels the peer can join per hour.
 */
class CapacityCalculator {
    
    public static final String PROP_COUNTRY_BONUS = "profileOrganizer.sameCountryBonus";

    /** used to adjust each period so that we keep trying to expand the peer's capacity */
    static final long GROWTH_FACTOR = 5;
    
    /** the calculator estimates over a 1 hour period */
    private static long ESTIMATE_PERIOD = 60*60*1000;

    // total of all possible bonuses should be less than 4, since
    // crappy peers start at 1 and the base is 5.
    private static final double BONUS_NEW = 0.85;
    private static final double BONUS_ESTABLISHED = 0.65;
    private static final double BONUS_SAME_COUNTRY = 0;
    private static final double BONUS_XOR = .25;
    private static final double PENALTY_UNREACHABLE = 2;
    // we make this a bonus for non-ff, not a penalty for ff, so we
    // don't drive the ffs below the default
    private static final double BONUS_NON_FLOODFILL = 1.0;
    
    public static double calc(PeerProfile profile) {
        double capacity;

        if (tooOld(profile)) { 
            capacity = 1;
        } else {
            RateStat acceptStat = profile.getTunnelCreateResponseTime();
            RateStat rejectStat = profile.getTunnelHistory().getRejectionRate();
            RateStat failedStat = profile.getTunnelHistory().getFailedRate();
        
            double capacity10m = estimateCapacity(acceptStat, rejectStat, failedStat, 10*60*1000);
            // if we actively know they're bad, who cares if they used to be good?
            if (capacity10m <= 0) {
                capacity = 0;
            } else {
                double capacity30m = estimateCapacity(acceptStat, rejectStat, failedStat, 30*60*1000);
                double capacity60m = estimateCapacity(acceptStat, rejectStat, failedStat, 60*60*1000);
                double capacity1d  = estimateCapacity(acceptStat, rejectStat, failedStat, 24*60*60*1000);
        
                capacity = capacity10m * periodWeight(10*60*1000) + 
                           capacity30m * periodWeight(30*60*1000) + 
                           capacity60m * periodWeight(60*60*1000) + 
                           capacity1d  * periodWeight(24*60*60*1000);
            }
        }        
        
        // now take into account non-rejection tunnel rejections (which haven't 
        // incremented the rejection counter, since they were only temporary)
        RouterContext context = profile.getContext();
        long now = context.clock().now();
        if (profile.getTunnelHistory().getLastRejectedTransient() > now - 5*60*1000)
            capacity = 1;
        else if (profile.getTunnelHistory().getLastRejectedProbabalistic() > now - 5*60*1000)
            capacity -= context.random().nextInt(5);

        // boost new profiles
        if (now - profile.getFirstHeardAbout() < 45*60*1000)
            capacity += BONUS_NEW;
        // boost connected peers
        if (profile.isEstablished())
            capacity += BONUS_ESTABLISHED;

        // boost same country
        if (profile.isSameCountry()) {
            double bonus = BONUS_SAME_COUNTRY;
            String b = context.getProperty(PROP_COUNTRY_BONUS);
            if (b != null) {
                try {
                    bonus = Double.parseDouble(b);
                } catch (NumberFormatException nfe) {}
            }
            capacity += bonus;
        }

        // penalize unreachable peers
        if (profile.wasUnreachable())
            capacity -= PENALTY_UNREACHABLE;

        // credit non-floodfill to reduce conn limit issues at floodfills
        // TODO only if we aren't floodfill ourselves?
        // null for tests
        NetworkDatabaseFacade ndb =  context.netDb();
        if (ndb != null) {
            RouterInfo ri = ndb.lookupRouterInfoLocally(profile.getPeer());
            if (!FloodfillNetworkDatabaseFacade.isFloodfill(ri))
                capacity += BONUS_NON_FLOODFILL;
        }

        // a tiny tweak to break ties and encourage closeness, -.25 to +.25
        capacity -= profile.getXORDistance() * (BONUS_XOR / 128);

        capacity += profile.getCapacityBonus();
        if (capacity < 0)
            capacity = 0;
        
        return capacity;
    }
    
    /**
     * If we haven't heard from them in an hour, they aren't too useful.
     *
     */
    private static boolean tooOld(PeerProfile profile) {
        if (profile.getIsActive(60*60*1000)) 
            return false;
        else 
            return true;
    }
    
    /**
     * Compute a tunnel accept capacity-per-hour for the given period
     * This is perhaps the most critical part of the peer ranking and selection
     * system, so adjust with great care and testing to ensure good network
     * performance and prevent congestion collapse.
     *
     * The baseline or "growth factor" is 5.
     * Rejects will not reduce the baseline. Failures will.
     *
     * @param acceptStat Accept counter (1 = 1 accept)
     * @param rejectStat Reject counter (1 = 1 reject)
     * @param failedStat Failed counter (100 = 1 fail)
     *
     * Let A = accects, R = rejects, F = fails
     * @return estimated and adjusted accepts per hour, for the given period
     *         which is, more or less, max(0, 5 + (A * (A / (A + 2R))) - (4 * F))
     */
    private static double estimateCapacity(RateStat acceptStat, RateStat rejectStat, RateStat failedStat, int period) {
        Rate curAccepted = acceptStat.getRate(period);
        Rate curRejected = rejectStat.getRate(period);
        Rate curFailed = failedStat.getRate(period);
        RateAverages ra = RateAverages.getTemp();

        double eventCount = 0;
        if (curAccepted != null) {
            eventCount = curAccepted.computeAverages(ra, false).getTotalEventCount();
            // Punish for rejections.
            // We don't want to simply do eventCount -= rejected or we get to zero with 50% rejection,
            // and we don't want everybody to be at zero during times of congestion.
            if (eventCount > 0 && curRejected != null) {
                long rejected = curRejected.computeAverages(ra,false).getTotalEventCount();
                if (rejected > 0)
                    eventCount *= eventCount / (eventCount + (2 * rejected));
            }
        }

        double stretch = ((double)ESTIMATE_PERIOD) / period;
        double val = eventCount * stretch;

        // Let's say a failure is 4 times worse than a rejection.
        // It's actually much worse than that, but with 2-hop tunnels and a 8-peer
        // fast pool, for example, you have a 1/7 chance of being falsely blamed.
        // We also don't want to drive everybody's capacity to zero, that isn't helpful.
        if (curFailed != null) {
            double failed = curFailed.computeAverages(ra, false).getTotalValues();
            if (failed > 0) {
                //if ( (period <= 10*60*1000) && (curFailed.getCurrentEventCount() > 0) )
                //    return 0.0d; // their tunnels have failed in the last 0-10 minutes
                //else
                // .04 = 4.0 / 100.0 adjustment to failed
                val -= 0.04 * failed * stretch;
            }
        }
        
        val += GROWTH_FACTOR;
        
        if (val >= 0) {
            return val;
        } else {
            return 0.0d;
        }
    }
    
    private static double periodWeight(int period) {
        switch (period) {
            case 10*60*1000: return .4;
            case 30*60*1000: return .3;
            case 60*60*1000: return .2;
            case 24*60*60*1000: return .1;
            default: throw new IllegalArgumentException("undefined period passed, period [" + period + "]???");
        }
    }
}
