package net.i2p.stat;

import java.io.IOException;
import java.io.OutputStream;
import java.text.Collator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

import net.i2p.I2PAppContext;
import net.i2p.util.Log;

/** 
 * Coordinate the management of various frequencies and rates within I2P components,
 * both allowing central update and retrieval, as well as distributed creation and 
 * use.  This does not provide any persistence, but the data structures exposed can be
 * read and updated to manage the complete state.
 * 
 */
public class StatManager {
    private final I2PAppContext _context;
    private final Log _log;

    /** stat name to FrequencyStat */
    private final ConcurrentHashMap<String, FrequencyStat> _frequencyStats;
    /** stat name to RateStat */
    private final ConcurrentHashMap<String, RateStat> _rateStats;
    /** may be null */
    private StatLog _statLog;

    private int coalesceCounter;
    /** every this many minutes for frequencies */
    private static final int FREQ_COALESCE_RATE = 9;


    /**
     *  Comma-separated stats or * for all.
     *  This property must be set at startup, or
     *  logging is disabled.
     */
    public static final String PROP_STAT_FILTER = "stat.logFilters";
    public static final String PROP_STAT_FILE = "stat.logFile";
    public static final String DEFAULT_STAT_FILE = "stats.log";
    /** default false */
    public static final String PROP_STAT_FULL = "stat.full";
    
    /**
     * The stat manager should only be constructed and accessed through the 
     * application context.  This constructor should only be used by the 
     * appropriate application context itself.
     *
     */
    public StatManager(I2PAppContext context) {
        _context = context;
        _log = context.logManager().getLog(getClass());
        _frequencyStats = new ConcurrentHashMap<String,FrequencyStat>(8);
        _rateStats = new ConcurrentHashMap<String,RateStat>(128);
        String filter = getStatFilter();
        if (filter != null && filter.length() > 0)
            _statLog = new BufferedStatLog(context);
    }
    
    /** @since 0.8.8 */
    public synchronized void shutdown() {
        _frequencyStats.clear();
        _rateStats.clear();
    }

    /**
     *  Gets the default stat log for RateStats
     *  Deprecated, unused
     *  @return null always
     */
    public synchronized StatLog getStatLog() { return _statLog; }

    /**
     *  Sets the default stat log for ALL known RateStats.
     *  Deprecated, unused
     *  @deprecated unused
     */
    @Deprecated
    public synchronized void setStatLog(StatLog log) { 
        _statLog = log; 
        for (RateStat rs : _rateStats.values()) {
            rs.setStatLog(log);
        }
    }

    /**
     * Create a new statistic to monitor the frequency of some event.
     * The stat is ONLY created if the stat.full property is true or we are not in the router context.
     *
     * @param name unique name of the statistic
     * @param description simple description of the statistic
     * @param group used to group statistics together
     * @param periods array of period lengths (in milliseconds)
     */
    public void createFrequencyStat(String name, String description, String group, long periods[]) {
        if (ignoreStat(name)) return;
        createRequiredFrequencyStat(name, description, group, periods);
    }

    /**
     * Create a new statistic to monitor the frequency of some event.
     * The stat is always created, independent of the stat.full setting or context.
     *
     * @param name unique name of the statistic
     * @param description simple description of the statistic
     * @param group used to group statistics together
     * @param periods array of period lengths (in milliseconds)
     * @since 0.8.7
     */
    public void createRequiredFrequencyStat(String name, String description, String group, long periods[]) {
        if (_frequencyStats.containsKey(name)) return;
        _frequencyStats.putIfAbsent(name, new FrequencyStat(name, description, group, periods));
    }

    /**
     * Create a new statistic to monitor the average value and confidence of some action.
     * The stat is ONLY created if the stat.full property is true or we are not in the router context.
     *
     * @param name unique name of the statistic
     * @param description simple description of the statistic
     * @param group used to group statistics together
     * @param periods array of period lengths (in milliseconds)
     */
    public void createRateStat(String name, String description, String group, long periods[]) {
        if (ignoreStat(name)) return;
        createRequiredRateStat(name, description, group, periods);
    }

    /**
     * Create a new statistic to monitor the average value and confidence of some action.
     * The stat is always created, independent of the stat.full setting or context.
     *
     * @param name unique name of the statistic
     * @param description simple description of the statistic
     * @param group used to group statistics together
     * @param periods array of period lengths (in milliseconds)
     * @since 0.8.7
     */
    public void createRequiredRateStat(String name, String description, String group, long periods[]) {
            if (_rateStats.containsKey(name)) return;
            RateStat rs = new RateStat(name, description, group, periods);
            if (_statLog != null) rs.setStatLog(_statLog);
            _rateStats.putIfAbsent(name, rs);
    }

    // Hope this doesn't cause any problems with unsynchronized accesses like addRateData() ...
    public void removeRateStat(String name) {
            _rateStats.remove(name);
    }

    /** update the given frequency statistic, taking note that an event occurred (and recalculating all frequencies) */
    public void updateFrequency(String name) {
        FrequencyStat freq = _frequencyStats.get(name);
        if (freq != null)
            freq.eventOccurred();
        else
            if (_log.shouldLog(Log.WARN))
                _log.warn("Invalid frequency stat : " + name);
    }

    /** update the given rate statistic, taking note that the given data point was received (and recalculating all rates) */
    public void addRateData(String name, long data, long eventDuration) {
        RateStat stat = _rateStats.get(name); // unsynchronized
        if (stat != null)
            stat.addData(data, eventDuration);
        else
            if (_log.shouldLog(Log.WARN))
                _log.warn("Invalid rate stat : " + name);
    }

    /**
     * Update the given rate statistic, taking note that the given data point was received (and recalculating all rates).
     * Zero duration.
     * @since 0.8.10
     */
    public void addRateData(String name, long data) {
        RateStat stat = _rateStats.get(name); // unsynchronized
        if (stat != null)
            stat.addData(data);
        else
            if (_log.shouldLog(Log.WARN))
                _log.warn("Invalid rate stat : " + name);
    }

    public synchronized void coalesceStats() {
        if (++coalesceCounter % FREQ_COALESCE_RATE == 0) {
                for (FrequencyStat stat : _frequencyStats.values()) {
                    if (stat != null) {
                        stat.coalesceStats();
                    }
                }
        }
        for (RateStat stat : _rateStats.values()) {
            stat.coalesceStats();
        }
    }

    /**
     *  Misnamed, as it returns a FrequencyStat, not a Frequency.
     */
    public FrequencyStat getFrequency(String name) {
        return _frequencyStats.get(name);
    }

    /**
     *  Misnamed, as it returns a RateStat, not a Rate.
     */
    public RateStat getRate(String name) {
        return _rateStats.get(name);
    }

    /** @return a copy */
    public Set<String> getFrequencyNames() {
        return new HashSet<String>(_frequencyStats.keySet());
    }

    /** @return a copy */
    public Set<String> getRateNames() {
        return new HashSet<String>(_rateStats.keySet());
    }

    /** is the given stat a monitored rate? */
    public boolean isRate(String statName) {
        return _rateStats.containsKey(statName);
    }

    /** is the given stat a monitored frequency? */
    public boolean isFrequency(String statName) {
        return _frequencyStats.containsKey(statName);
    }

    /**
     * Group name (untranslated String) to a SortedSet of untranslated stat names.
     * Map is unsorted.
     */
    public Map<String, SortedSet<String>> getStatsByGroup() {
        Map<String, SortedSet<String>> groups = new HashMap<String, SortedSet<String>>(32);
        for (FrequencyStat stat : _frequencyStats.values()) {
            String gname = stat.getGroupName();
            SortedSet<String> names = groups.get(gname);
            if (names == null) {
                names = new TreeSet<String>(Collator.getInstance());
                groups.put(gname, names);
            }
            names.add(stat.getName());
        }
        for (RateStat stat : _rateStats.values()) {
            String gname = stat.getGroupName();
            SortedSet<String> names = groups.get(gname);
            if (names == null) {
                names = new TreeSet<String>(Collator.getInstance());
                groups.put(gname, names);
            }
            names.add(stat.getName());
        }
        return groups;
    }

    public String getStatFilter() { return _context.getProperty(PROP_STAT_FILTER); }
    public String getStatFile() { return _context.getProperty(PROP_STAT_FILE, DEFAULT_STAT_FILE); }

    /**
     * Save memory by not creating stats unless they are required for router operation.
     * For backward compatibility of any external clients, always returns false if not in router context.
     *
     * @param statName ignored
     * @return true if the stat should be ignored.
     */
    public boolean ignoreStat(String statName) {
        return _context.isRouterContext() && !_context.getBooleanProperty(PROP_STAT_FULL);
    }
    
    /**
     * Serializes all Frequencies and Rates to the provided OutputStream
     * @param out to write to
     * @param prefix to use when serializing
     * @throws IOException if something goes wrong
     * @since 0.9.23
     */
    public void store(OutputStream out, String prefix) throws IOException {
        for (FrequencyStat fs : _frequencyStats.values()) {
            fs.store(out, prefix);
        }
        for (RateStat rs : _rateStats.values()) {
            rs.store(out,prefix);
        }
    }
}
