package net.i2p.router.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.SystemVersion;

/**
 *  Simple event logger for occasional events,
 *  with caching for reads.
 *  Does not keep the file open.
 *  @since 0.9.3
 */
public class EventLog {

    private final I2PAppContext _context;
    private final File _file;
    /** event to cached map */
    private final Map<String, SortedMap<Long, String>> _cache;
    /** event to starting time of cached map */
    private final Map<String, Long> _cacheTime;

    /** for convenience, not required */
    public static final String ABORTED = "aborted";
    public static final String BECAME_FLOODFILL = "becameFloodfill";
    public static final String CHANGE_IP = "changeIP";
    public static final String CHANGE_PORT = "changePort";
    public static final String CLOCK_SHIFT = "clockShift";
    public static final String CRASHED = "crashed";
    public static final String CRITICAL = "critical";
    public static final String DEADLOCK = "deadlock";
    public static final String INSTALLED = "installed";
    public static final String INSTALL_FAILED = "installFailed";
    public static final String NETWORK = "network";
    public static final String NEW_IDENT = "newIdent";
    public static final String NOT_FLOODFILL = "disabledFloodfill";
    public static final String OOM = "oom";
    public static final String REACHABILITY = "reachability";
    public static final String REKEYED = "rekeyed";
    public static final String RESEED = "reseed";
    public static final String SOFT_RESTART = "softRestart";
    public static final String STARTED = "started";
    public static final String STOPPED = "stopped";
    public static final String UPDATED = "updated";
    public static final String WATCHDOG = "watchdog";

    /**
     *  @param file should be absolute
     */
    public EventLog(I2PAppContext ctx, File file) {
        //if (!file.isAbsolute())
        //    throw new IllegalArgumentException();
        _context = ctx;
        _file = file;
        _cache = new HashMap<String, SortedMap<Long, String>>(4);
        _cacheTime = new HashMap<String, Long>(4);
    }

    /**
     *  Append an event. Fails silently.
     *  @param event no spaces, e.g. "started"
     *  @throws IllegalArgumentException if event contains a space or newline
     */
    public void addEvent(String event) {
        addEvent(event, null);
    }

    /**
     *  Append an event. Fails silently.
     *  @param event no spaces or newlines, e.g. "started"
     *  @param info no newlines, may be blank or null
     *  @throws IllegalArgumentException if event contains a space or either contains a newline
     */
    public synchronized void addEvent(String event, String info) {
        if (event.contains(" ") || event.contains("\n") ||
            (info != null && info.contains("\n")))
            throw new IllegalArgumentException();
        _cache.remove(event);
        _cacheTime.remove(event);
        OutputStream out = null;
        try {
            out = new SecureFileOutputStream(_file, true);
            StringBuilder buf = new StringBuilder(128);
            buf.append(_context.clock().now()).append(' ').append(event);
            if (info != null && info.length() > 0)
                buf.append(' ').append(info);
            if (SystemVersion.isWindows())
                buf.append('\r');
            buf.append('\n');
            out.write(buf.toString().getBytes("UTF-8"));
        } catch (IOException ioe) {
        } finally {
            if (out != null) try { out.close(); } catch (IOException ioe) {}
        }
    }

    /**
     *  Caches.
     *  Fails silently.
     *  @param event matching this event only, case sensitive
     *  @param since since this time, 0 for all
     *  @return non-null, Map of times to (possibly empty) info strings, sorted, earliest first, unmodifiable
     */
    public synchronized SortedMap<Long, String> getEvents(String event, long since) {
        SortedMap<Long, String> rv = _cache.get(event);
        if (rv != null) {
            Long cacheTime = _cacheTime.get(event);
            if (cacheTime != null) {
                if (since >= cacheTime.longValue())
                    return rv.tailMap(Long.valueOf(since));
            }
        }
        rv = new TreeMap<Long, String>();
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(
                    new FileInputStream(_file), "UTF-8"));
            String line = null;
            while ( (line = br.readLine()) != null) {
                try {
                    String[] s = DataHelper.split(line.trim(), " ", 3);
                    if (!s[1].equals(event))
                        continue;
                    long time = Long.parseLong(s[0]);
                    if (time <= since)
                        continue;
                    Long ltime = Long.valueOf(time);
                    String info = s.length > 2 ? s[2] : "";
                    rv.put(ltime, info);
                } catch (IndexOutOfBoundsException ioobe) {
                } catch (NumberFormatException nfe) {
                }
            }
            rv = Collections.unmodifiableSortedMap(rv);
            _cache.put(event, rv);
            _cacheTime.put(event, Long.valueOf(since));
        } catch (IOException ioe) {
        } finally {
            if (br != null) try { br.close(); } catch (IOException ioe) {}
        }
        return rv;
    }

    /**
     *  All events since a given time.
     *  Does not cache. Fails silently.
     *  Values in the returned map have the format "event[ info]".
     *  Events do not contain spaces.
     *
     *  @param since since this time, 0 for all
     *  @return non-null, Map of times to info strings, sorted, earliest first, unmodifiable
     *  @since 0.9.14
     */
    public synchronized SortedMap<Long, String> getEvents(long since) {
        SortedMap<Long, String> rv = new TreeMap<Long, String>();
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(
                    new FileInputStream(_file), "UTF-8"));
            String line = null;
            while ( (line = br.readLine()) != null) {
                try {
                    String[] s = DataHelper.split(line.trim(), " ", 2);
                    if (s.length < 2)
                        continue;
                    long time = Long.parseLong(s[0]);
                    if (time <= since)
                        continue;
                    Long ltime = Long.valueOf(time);
                    rv.put(ltime, s[1]);
                } catch (IndexOutOfBoundsException ioobe) {
                } catch (NumberFormatException nfe) {
                }
            }
            rv = Collections.unmodifiableSortedMap(rv);
        } catch (IOException ioe) {
        } finally {
            if (br != null) try { br.close(); } catch (IOException ioe) {}
        }
        return rv;
    }

    /**
     *  Timestamp of last event.
     *
     *  @param event matching this event, case sensitive
     *  @param since since this time, 0 for all
     *  @return last event time, or 0 for none
     *  @since 0.9.47
     */
    public synchronized long getLastEvent(String event, long since) {
        long rv = 0;
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(
                    new FileInputStream(_file), "UTF-8"));
            String line = null;
            while ( (line = br.readLine()) != null) {
                try {
                    String[] s = DataHelper.split(line.trim(), " ", 3);
                    if (s.length < 2)
                        continue;
                    if (!s[1].equals(event))
                        continue;
                    long time = Long.parseLong(s[0]);
                    if (time <= since)
                        continue;
                    rv = time;
                } catch (NumberFormatException nfe) {
                }
            }
        } catch (IOException ioe) {
        } finally {
            if (br != null) try { br.close(); } catch (IOException ioe) {}
        }
        return rv;
    }
}
