package net.i2p.stat;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;

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

/**
 * Note - if no filter is defined in stat.logFilters at startup, this class will not
 * be instantiated - see StatManager.
 */
public class BufferedStatLog implements StatLog {
    private final I2PAppContext _context;
    private final Log _log;
    private final StatEvent _events[];
    private int _eventNext;
    private int _lastWrite;
    /** flush stat events to disk after this many events (or 30s)*/
    private int _flushFrequency;
    private final List<String> _statFilters;
    private String _lastFilters;
    private BufferedWriter _out;
    private String _outFile;
    /** short circuit for adding data, set to true if some filters are set, false if its empty (so we can skip the sync) */
    private volatile boolean _filtersSpecified;
    
    private static final int BUFFER_SIZE = 1024;
    private static final boolean DISABLE_LOGGING = false;
    
    public BufferedStatLog(I2PAppContext ctx) {
        _context = ctx;
        _log = ctx.logManager().getLog(BufferedStatLog.class);
        _events = new StatEvent[BUFFER_SIZE];
        if (DISABLE_LOGGING) return;
        for (int i = 0; i < BUFFER_SIZE; i++)
            _events[i] = new StatEvent();
        _eventNext = 0;
        _lastWrite = _events.length-1;
        _statFilters = new ArrayList<String>(10);
        _flushFrequency = 500;
        updateFilters();
        I2PThread writer = new I2PThread(new StatLogWriter(), "StatLogWriter");
        writer.setDaemon(true);
        writer.start();
    }
    
    public void addData(String scope, String stat, long value, long duration) {
        if (DISABLE_LOGGING) return;
        if (!shouldLog(stat)) return;
        synchronized (_events) {
            _events[_eventNext].init(scope, stat, value, duration);
            _eventNext = (_eventNext + 1) % _events.length;
            
            if (_eventNext == _lastWrite)
                _lastWrite = (_lastWrite + 1) % _events.length; // drop an event
            
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("AddData next=" + _eventNext + " lastWrite=" + _lastWrite);
            
            if (_eventNext > _lastWrite) {
                if (_eventNext - _lastWrite >= _flushFrequency)
                    _events.notifyAll();
            } else {
                if (_events.length - 1 - _lastWrite + _eventNext >= _flushFrequency)
                    _events.notifyAll();
            }
        }
    }    

    private boolean shouldLog(String stat) {
        if (!_filtersSpecified) return false;
        synchronized (_statFilters) {
            return _statFilters.contains(stat) || _statFilters.contains("*");
        }
    }
    
    private void updateFilters() {
        String val = _context.getProperty(StatManager.PROP_STAT_FILTER);
        if (val != null) {
            if ( (_lastFilters != null) && (_lastFilters.equals(val)) ) {
                // noop
            } else {
                StringTokenizer tok = new StringTokenizer(val, ",");
                synchronized (_statFilters) {
                    _statFilters.clear();
                    while (tok.hasMoreTokens())
                        _statFilters.add(tok.nextToken().trim());
                    _filtersSpecified = !_statFilters.isEmpty();
                }
            }
            _lastFilters = val;
        } else {
            synchronized (_statFilters) {
                _statFilters.clear(); 
                _filtersSpecified = false;
            }
        }
        
        String filename = _context.getProperty(StatManager.PROP_STAT_FILE, StatManager.DEFAULT_STAT_FILE);
        File foo = new File(filename);
        if (!foo.isAbsolute())
            filename = (new File(_context.getRouterDir(), filename)).getAbsolutePath();
        if ( (_outFile != null) && (_outFile.equals(filename)) ) {
            // noop
        } else {
            if (_out != null) try { _out.close(); } catch (IOException ioe) {}
            _outFile = filename;
            try {
                _out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(_outFile, true), "UTF-8"), 32*1024);
            } catch (IOException ioe) { ioe.printStackTrace(); }
        }
    }
    
    private class StatLogWriter implements Runnable {
        private final SimpleDateFormat _fmt = new SimpleDateFormat("yyyyMMdd HH:mm:ss.SSS");
        public void run() {
            int writeStart = -1;
            int writeEnd = -1;
            while (true) {
                try {
                    synchronized (_events) {
                        if (_eventNext > _lastWrite) {
                            if (_eventNext - _lastWrite < _flushFrequency)
                                _events.wait(30*1000);
                        } else {
                            if (_events.length - 1 - _lastWrite + _eventNext < _flushFrequency)
                                _events.wait(30*1000);
                        }
                        writeStart = (_lastWrite + 1) % _events.length;
                        writeEnd = _eventNext;
                        _lastWrite = (writeEnd == 0 ? _events.length-1 : writeEnd - 1);
                    }
                    if (writeStart != writeEnd) {
                        try {
                            if (_log.shouldLog(Log.DEBUG))
                                _log.debug("writing " + writeStart +"->"+ writeEnd);
                            writeEvents(writeStart, writeEnd);
                        } catch (RuntimeException e) {
                            _log.error("error writing " + writeStart +"->"+ writeEnd, e);
                        }
                    }
                } catch (InterruptedException ie) {}
            }
        }
        
        private void writeEvents(int start, int end) {
            try {    
                updateFilters();
                int cur = start;
                while (cur != end) {
                    //if (shouldLog(_events[cur].getStat())) {
                        String when = null;
                        synchronized (_fmt) {
                            when = _fmt.format(new Date(_events[cur].getTime()));
                        }
                        _out.write(when);
                        _out.write(" ");
                        if (_events[cur].getScope() == null)
                            _out.write("noScope");
                        else
                            _out.write(_events[cur].getScope());
                        _out.write(" ");
                        _out.write(_events[cur].getStat());
                        _out.write(" ");
                        _out.write(Long.toString(_events[cur].getValue()));
                        _out.write(" ");
                        _out.write(Long.toString(_events[cur].getDuration()));
                        _out.write("\n");
                    //}
                    cur = (cur + 1) % _events.length;
                }
                _out.flush();
            } catch (IOException ioe) {
                _log.error("Error writing out", ioe);
            }
        }
    }
    
    private class StatEvent {
        private long _time;
        private String _scope;
        private String _stat;
        private long _value;
        private long _duration;
        
        public long getTime() { return _time; } 
        public String getScope() { return _scope; }
        public String getStat() { return _stat; }
        public long getValue() { return _value; }
        public long getDuration() { return _duration; }
        
        public void init(String scope, String stat, long value, long duration) {
            _scope = scope;
            _stat = stat;
            _value = value;
            _duration = duration;
            _time = _context.clock().now();
        }
    }    
}
