package net.i2p.router;
/*
 * 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.Writer;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import java.util.Properties;

import net.i2p.CoreVersion;
import net.i2p.data.DataHelper;
import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
import net.i2p.stat.Rate;
import net.i2p.stat.RateStat;
import net.i2p.util.Log;

/**
 * Publishes some statistics about the router in the netDB.
 *
 */
public class StatisticsManager implements Service {
    private final Log _log;
    private final RouterContext _context;
    
    public final static String PROP_PUBLISH_RANKINGS = "router.publishPeerRankings";
    /** enhance anonymity by only including build stats one out of this many times */
    private static final int RANDOM_INCLUDE_STATS = 16;

    private final DecimalFormat _fmt;
    private final DecimalFormat _pct;

    public StatisticsManager(RouterContext context) {
        _context = context;
        _fmt = new DecimalFormat("###,##0.00", new DecimalFormatSymbols(Locale.UK));
        _pct = new DecimalFormat("#0.00%", new DecimalFormatSymbols(Locale.UK));
        _log = context.logManager().getLog(StatisticsManager.class);
    }
        
    /** noop */
    public void shutdown() {}

    /** noop */
    public void restart() {}

    /** noop */
    public void startup() {}
    
    /** Retrieve a snapshot of the statistics that should be published */
    public Properties publishStatistics() { 
        Properties stats = new Properties();
        stats.setProperty("router.version", RouterVersion.VERSION);
        stats.setProperty("coreVersion", CoreVersion.VERSION);

        // No longer expose, to make build tracking more expensive
        // stats.setProperty("router.id", RouterVersion.ID);
        // stats.setProperty("core.id", CoreVersion.ID);

/***
        int newlines = 0;
        FileInputStream in = null;
        try {
            in = new FileInputStream(Router.IDENTLOG);
            int c = -1;
            // perhaps later filter this to only include ident changes this
            // day/week/month
            while ( (c = in.read()) != -1) {
                if (c == '\n')
                    newlines++;
            }
        } catch (IOException ioe) {
            // ignore
        } finally {
            if (in != null)
                try { in.close(); } catch (IOException ioe) {}
        }
        if (newlines > 0)
            stats.setProperty("stat_identities", newlines+"");
***/
        
        if (_context.getBooleanPropertyDefaultTrue(PROP_PUBLISH_RANKINGS) &&
            _context.random().nextInt(RANDOM_INCLUDE_STATS) == 0) {
            //long publishedUptime = _context.router().getUptime();
            // Don't publish these for first hour
            // Disabled in 0.9
            //if (publishedUptime > 62*60*1000)
            //    includeAverageThroughput(stats);
            //includeRate("router.invalidMessageTime", stats, new long[] { 10*60*1000 });
            //includeRate("router.duplicateMessageId", stats, new long[] { 24*60*60*1000 });
            //includeRate("tunnel.duplicateIV", stats, new long[] { 24*60*60*1000 });
            //includeRate("tunnel.fragmentedDropped", stats, new long[] { 10*60*1000, 3*60*60*1000 });
            //includeRate("tunnel.fullFragments", stats, new long[] { 10*60*1000, 3*60*60*1000 });
            //includeRate("tunnel.smallFragments", stats, new long[] { 10*60*1000, 3*60*60*1000 });
            //includeRate("tunnel.testFailedTime", stats, new long[] { 10*60*1000 });
            
            //includeRate("tunnel.batchDelaySent", stats, new long[] { 10*60*1000, 60*60*1000 });
            //includeRate("tunnel.batchMultipleCount", stats, new long[] { 10*60*1000, 60*60*1000 });
            //includeRate("tunnel.corruptMessage", stats, new long[] { 60*60*1000l, 3*60*60*1000l });
            
            //includeRate("router.throttleTunnelProbTestSlow", stats, new long[] { 60*60*1000 });
            //includeRate("router.throttleTunnelProbTooFast", stats, new long[] { 60*60*1000 });
            //includeRate("router.throttleTunnelProcessingTime1m", stats, new long[] { 60*60*1000 });

            //includeRate("router.fastPeers", stats, new long[] { 60*60*1000 });
            
            //includeRate("udp.statusOK", stats, new long[] { 20*60*1000 });
            //includeRate("udp.statusDifferent", stats, new long[] { 20*60*1000 });
            //includeRate("udp.statusReject", stats, new long[] { 20*60*1000 });
            //includeRate("udp.statusUnknown", stats, new long[] { 20*60*1000 });
            //includeRate("udp.statusKnownCharlie", stats, new long[] { 1*60*1000, 10*60*1000 });
            //includeRate("udp.addressUpdated", stats, new long[] { 1*60*1000 });
            //includeRate("udp.addressTestInsteadOfUpdate", stats, new long[] { 1*60*1000 });

            //includeRate("clock.skew", stats, new long[] { 10*60*1000, 3*60*60*1000, 24*60*60*1000 });
            
            //includeRate("transport.sendProcessingTime", stats, new long[] { 60*60*1000 });
            //includeRate("jobQueue.jobRunSlow", stats, new long[] { 10*60*1000l, 60*60*1000l });
            //includeRate("crypto.elGamal.encrypt", stats, new long[] { 60*60*1000 });
            // total event count can be used to track uptime
            includeRate("tunnel.participatingTunnels", stats, new long[] { 60*60*1000 }, true);
            //includeRate("tunnel.testSuccessTime", stats, new long[] { 10*60*1000l });
            //includeRate("client.sendAckTime", stats, new long[] { 60*60*1000 }, true);
            //includeRate("udp.sendConfirmTime", stats, new long[] { 10*60*1000 });
            //includeRate("udp.sendVolleyTime", stats, new long[] { 10*60*1000 });
            //includeRate("udp.ignoreRecentDuplicate", stats, new long[] { 60*1000 });
            //includeRate("udp.congestionOccurred", stats, new long[] { 10*60*1000 });
            //includeRate("stream.con.sendDuplicateSize", stats, new long[] { 60*60*1000 });
            //includeRate("stream.con.receiveDuplicateSize", stats, new long[] { 60*60*1000 });

            //stats.setProperty("stat__rateKey", "avg;maxAvg;pctLifetime;[sat;satLim;maxSat;maxSatLim;][num;lifetimeFreq;maxFreq]");
            
            //includeRate("tunnel.decryptRequestTime", stats, new long[] { 60*1000, 10*60*1000 });
            //includeRate("udp.packetDequeueTime", stats, new long[] { 60*1000 });
            //includeRate("udp.packetVerifyTime", stats, new long[] { 60*1000 });
            
            //includeRate("tunnel.buildRequestTime", stats, new long[] { 10*60*1000 });
            long rate = 60*60*1000;
            //includeTunnelRates("Client", stats, rate);
            includeTunnelRates("Exploratory", stats, rate);
            //includeRate("tunnel.rejectTimeout", stats, new long[] { 10*60*1000 });
            //includeRate("tunnel.rejectOverloaded", stats, new long[] { 10*60*1000 });
            //includeRate("tunnel.acceptLoad", stats, new long[] { 10*60*1000 });
        }

        // So that we will still get build requests
        stats.setProperty("stat_uptime", "90m");
        if (FloodfillNetworkDatabaseFacade.isFloodfill(_context.router().getRouterInfo())) {
            stats.setProperty("netdb.knownRouters", ""+_context.netDb().getKnownRouters());
            stats.setProperty("netdb.knownLeaseSets", ""+_context.netDb().getKnownLeaseSets());
        }

        return stats;
    }
    
/*****
    private void includeRate(String rateName, Properties stats, long selectedPeriods[]) {
        includeRate(rateName, stats, selectedPeriods, false);
    }
*****/

    /**
     * @param fudgeQuantity the data being published in this stat is too sensitive to, uh
     *                      publish, so we're kludge the quantity (allowing the fairly safe
     *                      publication of the average values
     */
    private void includeRate(String rateName, Properties stats, long selectedPeriods[], 
                             boolean fudgeQuantity) {
        RateStat rate = _context.statManager().getRate(rateName);
        if (rate == null) return;
        long periods[] = rate.getPeriods();
        for (int i = 0; i < periods.length; i++) {
            if (periods[i] > _context.router().getUptime()) continue;
            if (selectedPeriods != null) {
                boolean found = false;
                for (int j = 0; j < selectedPeriods.length; j++) {
                    if (selectedPeriods[j] == periods[i]) {
                        found = true;
                        break;
                    }
                }
                if (!found) continue;
            }

            Rate curRate = rate.getRate(periods[i]);
            if (curRate == null) continue;
            if (curRate.getLifetimeEventCount() <= 0) continue;
            stats.setProperty("stat_" + rateName + '.' + getPeriod(curRate), renderRate(curRate, fudgeQuantity));
        }
    }
    
    private String renderRate(Rate rate, boolean fudgeQuantity) {
        StringBuilder buf = new StringBuilder(128);
        buf.append(num(rate.getAverageValue())).append(';');
        buf.append(num(rate.getExtremeAverageValue())).append(';');
        buf.append(pct(rate.getPercentageOfLifetimeValue())).append(';');
        if (rate.getLifetimeTotalEventTime() > 0) {
            buf.append(pct(rate.getLastEventSaturation())).append(';');
            buf.append(num(rate.getLastSaturationLimit())).append(';');
            buf.append(pct(rate.getExtremeEventSaturation())).append(';');
            buf.append(num(rate.getExtremeSaturationLimit())).append(';');
        }
        long numPeriods = rate.getLifetimePeriods();
        if (fudgeQuantity) {
            buf.append("555;");
            if (numPeriods > 0) {
                buf.append("555;555;");
            }
        } else {
            buf.append(num(rate.getLastEventCount())).append(';');
            if (numPeriods > 0) {
                double avgFrequency = rate.getLifetimeEventCount() / (double)numPeriods;
                buf.append(num(avgFrequency)).append(';');
                buf.append(num(rate.getExtremeEventCount())).append(';');
                buf.append(num(rate.getLifetimeEventCount())).append(';');
            }
        }
        return buf.toString();
    }

    private static final String[] tunnelStats = { "Expire", "Reject", "Success" };

    /**
     *  Add tunnel build rates with some mods to hide absolute quantities
     *  In particular, report counts normalized to 100 (i.e. a percentage)
     */
    private void includeTunnelRates(String tunnelType, Properties stats, long selectedPeriod) {
        long totalEvents = 0;
        for (String tunnelStat : tunnelStats) {
            String rateName = "tunnel.build" + tunnelType + tunnelStat;
            RateStat stat = _context.statManager().getRate(rateName);
            if (stat == null) continue;
            Rate curRate = stat.getRate(selectedPeriod);
            if (curRate == null) continue;
            totalEvents += curRate.getLastEventCount();
        }
        if (totalEvents <= 0)
            return;
        for (String tunnelStat : tunnelStats) {
            String rateName = "tunnel.build" + tunnelType + tunnelStat;
            RateStat stat = _context.statManager().getRate(rateName);
            if (stat == null) continue;
            Rate curRate = stat.getRate(selectedPeriod);
            if (curRate == null) continue;
            double fudgeQuantity = 100.0d * curRate.getLastEventCount() / totalEvents;
            stats.setProperty("stat_" + rateName + '.' + getPeriod(curRate), renderRate(curRate, fudgeQuantity));
        }
    }
    
    private String renderRate(Rate rate, double fudgeQuantity) {
        StringBuilder buf = new StringBuilder(128);
        buf.append(num(rate.getAverageValue())).append(';');
        buf.append(num(rate.getExtremeAverageValue())).append(';');
        buf.append(pct(rate.getPercentageOfLifetimeValue())).append(';');
        if (rate.getLifetimeTotalEventTime() > 0) {
            // bah saturation
            buf.append("0;0;0;0;");
        }
        buf.append(num(fudgeQuantity)).append(';');
        return buf.toString();
    }

    /* report the same data for tx and rx, for enhanced anonymity */
    private void includeAverageThroughput(Properties stats) {
        RateStat sendRate = _context.statManager().getRate("bw.sendRate");
        RateStat recvRate = _context.statManager().getRate("bw.recvRate");
        if (sendRate == null || recvRate == null)
            return;
        Rate s = sendRate.getRate(60*60*1000);
        Rate r = recvRate.getRate(60*60*1000);
        if (s == null || r == null)
            return;
        double speed = (s.getAverageValue() + r.getAverageValue()) / 2;
        double max = Math.max(s.getExtremeAverageValue(), r.getExtremeAverageValue());
        String str = num(speed) + ';' + num(max) + ";0;0;";
        stats.setProperty("stat_bandwidthSendBps.60m", str);
        stats.setProperty("stat_bandwidthReceiveBps.60m", str);
    }

    private static String getPeriod(Rate rate) { return DataHelper.formatDuration(rate.getPeriod()); }

    private final String num(double num) { 
        if (num < 0) num = 0;
        synchronized (_fmt) { return _fmt.format(num); } 
    }

    private final String pct(double num) { 
        if (num < 0) num = 0;
        synchronized (_pct) { return _pct.format(num); } 
    }
   
    public void renderStatusHTML(Writer out) { }
}
