package net.i2p.util;

/*
 * 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.util.Queue;

import net.i2p.app.ClientAppManager;
import net.i2p.app.NotificationService;

/**
 * Log writer thread that pulls log records from the LogManager and writes them to
 * the log.  This also periodically instructs the LogManager to reread its config
 * file.
 *
 * @since 0.9.19 pulled from FileLogWriter so Android may extend; renamed from LogWriterBase in 0.9.26
 */
abstract class LogWriter implements Runnable {
    /** every 10 seconds? why? Just have the gui force a reread after a change?? */
    private final static long CONFIG_READ_INTERVAL = 50 * 1000;
    final static long FLUSH_INTERVAL = 29 * 1000;
    private final static long MIN_FLUSH_INTERVAL = 2*1000;
    private final static long MAX_FLUSH_INTERVAL = 5*60*1000;
    // true for newest first on /logs page; false for oldest first
    private static final boolean BUFFER_DISPLAYED_REVERSE = false;
    private long _lastReadConfig;
    protected final LogManager _manager;

    protected volatile boolean _write;
    private LogRecord _last;
    private long _firstTimestamp;
    // ms
    private volatile long _flushInterval = FLUSH_INTERVAL;

    public LogWriter(LogManager manager) {
        _manager = manager;
        _lastReadConfig = Clock.getInstance().now();
    }

    /**
     *  File may not exist or have old logs in it if not opened yet
     *  @return non-null
     */
    public abstract String currentFile();

    /**
     * Write the provided LogRecord to the writer.
     * @param rec the LogRecord to write.
     * @param formatted a String pre-formatted from rec, may be ignored.
     */
    protected abstract void writeRecord(LogRecord rec, String formatted);
    /**
     * Write a single String verbatim to the writer.
     * @param priority the level to log the line at.
     * @param line the String to write.
     */
    protected abstract void writeRecord(int priority, String line);
    protected abstract void flushWriter();
    protected abstract void closeWriter();

    public void stopWriting() {
        _write = false;
    }

    /**
     *  @param interval ms
     *  @since 0.9.18
     */
    public void setFlushInterval(long interval) {
        _flushInterval = Math.min(MAX_FLUSH_INTERVAL, Math.max(MIN_FLUSH_INTERVAL, interval));
    }

    public void run() {
        _write = true;
        // don't bother on Android
        final boolean shouldReadConfig = !SystemVersion.isAndroid();
        try {
            while (_write) {
                flushRecords();
                if (_write && shouldReadConfig)
                    rereadConfig();
            }
        } catch (RuntimeException e) {
            System.err.println("Error writing the log: " + e);
            e.printStackTrace();
        }
        closeWriter();
    }

    public void flushRecords() { flushRecords(true); }

    public void flushRecords(boolean shouldWait) {
        try {
            // zero copy, drain the manager queue directly
            Queue<LogRecord> records = _manager.getQueue();
            if (records == null) return;
            if (!records.isEmpty()) {
                if (_last != null && _firstTimestamp < _manager.getContext().clock().now() - 30*60*1000)
                    _last = null;
                LogRecord rec;
                int dupCount = 0;
                while ((rec = records.poll()) != null) {
                    if (_manager.shouldDropDuplicates() && rec.equals(_last)) {
                        dupCount++;
                    } else {
                        if (dupCount > 0) {
                            writeDupMessage(dupCount, _last);
                            dupCount = 0;
                        }
                        writeRecord(rec);
                        _firstTimestamp = rec.getDate();
                    }
                    _last = rec;
                }
                if (dupCount > 0) {
                    writeDupMessage(dupCount, _last);
                }
                flushWriter();
            }
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            if (shouldWait) {
                try { 
                    synchronized (this) {
                        this.wait(_flushInterval); 
                    }
                } catch (InterruptedException ie) { // nop
                }
            }
        }
    }

    /**
     *  Write a msg with the date stamp of the last duplicate
     *  @since 0.9.21
     */
    private void writeDupMessage(int dupCount, LogRecord lastRecord) {
        String dmsg = dupMessage(dupCount, lastRecord, false, false);
        writeRecord(lastRecord.getPriority(), dmsg);
        if (_manager.getDisplayOnScreenLevel() <= lastRecord.getPriority() && _manager.displayOnScreen())
            System.out.print(dmsg);
        dmsg = dupMessage(dupCount, lastRecord, BUFFER_DISPLAYED_REVERSE, true);
        _manager.getBuffer().add(dmsg);
        if (lastRecord.getPriority() >= Log.CRIT)
            _manager.getBuffer().addCritical(dmsg);
    }

    /**
     *  Return a msg with the date stamp of the last duplicate
     *  @since 0.9.3
     */
    private String dupMessage(int dupCount, LogRecord lastRecord, boolean reverse, boolean html) {
        boolean nohtml = !html || SystemVersion.isAndroid();
        String arrows = reverse ? (nohtml ? "vvv" : "&darr;&darr;&darr;")
                                : (nohtml ? "^^^" : "&uarr;&uarr;&uarr;");
        return LogRecordFormatter.getWhen(_manager, lastRecord) + ' ' + arrows + ' ' +
               ngettext("{0} similar message omitted", "{0} similar messages omitted", dupCount) + ' ' + arrows +
               LogRecordFormatter.NL;
    }
    
    private static final String BUNDLE_NAME = "net.i2p.util.messages";
    private static final String ROUTER_BUNDLE_NAME = "net.i2p.router.web.messages";

    /**
     *  gettext
     *  @since 0.9.3
     */
    private String ngettext(String b, String c, int a) {
        return Translate.getString(a, b, c, _manager.getContext(), BUNDLE_NAME);
    }

    private void rereadConfig() {
        long now = Clock.getInstance().now();
        if (now - _lastReadConfig > CONFIG_READ_INTERVAL) {
            _manager.rereadConfig();
            _lastReadConfig = now;
        }
    }

    private void writeRecord(LogRecord rec) {
        String val = LogRecordFormatter.formatRecord(_manager, rec, true);
        writeRecord(rec, val);

        // we always add to the console buffer, but only sometimes write to stdout
        _manager.getBuffer().add(val);
        int priority = rec.getPriority();
        if (priority >= Log.CRIT)
            _manager.getBuffer().addCritical(val);
        // Default is CRIT
        if (_manager.getDisplayOnScreenLevel() <= priority) {
            if (_manager.displayOnScreen()) {
                // wrapper and android logs already do time stamps, so reformat without the date
                if (_manager.getContext().hasWrapper() || SystemVersion.isAndroid())
                    System.out.print(LogRecordFormatter.formatRecord(_manager, rec, false));
                else
                    System.out.print(val);

                //
                //  Display a popup on DTG
                //  Experimental, may disable or add a separate config after testing
                //  These also aren't translated.
                //  May be appropriate for advanced users only.
                //
                String msg = rec.getMessage();
                if (priority >= Log.ERROR && msg != null && !SystemVersion.isAndroid()) {
                    ClientAppManager cmgr = _manager.getContext().clientAppManager();
                    if (cmgr != null) {
                        NotificationService ns = (NotificationService) cmgr.getRegisteredApp("desktopgui");
                        if (ns != null) {
                            String name;
                            Class cls = rec.getSource();
                            if (cls != null) {
                                name = cls.getSimpleName();
                            } else {
                                name = rec.getSourceName();
                                if (name == null)
                                    name = "I2P";
                            }
                            // the class name usually won't be translated,
                            // but it will be for "Router"
                            String tname = Translate.getString(name, _manager.getContext(), ROUTER_BUNDLE_NAME);
                            ns.notify(name, null, priority, tname, msg, null);
                        }
                        if (priority >= Log.CRIT) {
                            // Console sidebar
                            cmgr.addBubble(PortMapper.SVC_LOGS, msg);
                        }
                    }
                }
            }
        }
    }
}
