package anastore.util;

import java.io.*;

/**
 * Simple logging class.  Each logged line is automatically prefixed
 * with the class for which the Log object was constructed.  All logs
 * are synchronized so log lines are written atomically to the
 * screen.  All log output goes to stderr.
 */
public class Log extends PrintStream
{
    private final static Object _outLock = new Object();

    private final static ThreadLocal<String> _tLabels =
        new InheritableThreadLocal<String>();

    /**
     * Construct an enabled log that uses the class name of the caller
     * of this constructor.  This works well, for example, for
     * statically constructed fields.
     */
    public Log()
    {
        this(true, getCaller().getClassName());
    }

    /**
     * Construct an enabled log that ascribes its output to the
     * specified class.
     */
    public Log(Class cls)
    {
        this(true, cls.getName());
    }

    /**
     * Construct a log that may or may not be enabled that ascribes
     * its output to the caller of the constructor.
     */
    public Log(boolean enabled)
    {
        this(enabled, getCaller().getClassName());
    }

    /**
     * Construct a log that may or may not be enabled that ascribes
     * its output to the specified class.
     */
    public Log(boolean enabled, Class cls)
    {
        this(enabled, cls.getName());
    }

    /**
     * Construct a log that may or may not be enabled that ascribes
     * its output to the specified class name.  clsName may be null.
     */
    private Log(boolean enabled, String clsName)
    {
        super(enabled
              ? new AugmentedOS(System.err, getPrefix(clsName))
              : new DisabledOS(), true);
    }

    /**
     * Construct the string prefix that should be used to ascribe
     * messages to the given class name.  clsName may be null.
     */
    private static String getPrefix(String clsName)
    {
        if (clsName == null) {
            clsName = "(Unknown)";
        } else {
            clsName = clsName.replace("$",".");
            clsName = clsName.substring(clsName.lastIndexOf(".")+1);
        }
        return clsName;
    }

    /**
     * Set a label to use for log output from this thread and all
     * child threads created from this thread after this point.
     */
    public static void setThreadLabel(String label)
    {
        _tLabels.set(label);
    }

    /**
     * A filter output stream that buffers each line and prefixes each
     * line with a string before flushing it.
     */
    private static class AugmentedOS extends OutputStream
    {
        private byte[] _buf = new byte[16];
        private int _capacity = 16;
        private int _limit = 0;

        private final PrintStream _out;
        private final String _prefix;

        /** Cache of the formatted prefix */
        private String _formattedPrefix;
        private String _tLabel;

        public AugmentedOS(PrintStream out, String prefix)
        {
            _out = out;
            _prefix = prefix;
        }

        public synchronized void write(int b)
        {
            ensure(1);
            _buf[_limit++] = (byte)b;
            if (b == '\n') {
                String tLabel = _tLabels.get();
                if (_formattedPrefix == null || tLabel != _tLabel) {
                    StringBuilder sb = new StringBuilder("[");
                    sb.append(_prefix);
                    if (tLabel != null) {
                        sb.append('@');
                        sb.append(tLabel);
                    }
                    sb.append(']');
                    _formattedPrefix = String.format("%-20s ", sb);
                    _tLabel = tLabel;
                }
                synchronized (_outLock) {
                    _out.print(_formattedPrefix);
                    _out.write(_buf, 0, _limit);
                    _out.flush();
                }
                reset();
            }
        }

        private void ensure(int count)
        {
            if (_capacity - _limit < count) {
                int origCap = _capacity;
                byte[] origBuf = _buf;
                while (_capacity - _limit < count) {
                    _capacity *= 2;
                }
                _buf = new byte[_capacity];
                System.arraycopy(origBuf, 0, _buf, 0, origCap);
            }
        }

        private void reset()
        {
            _limit = 0;
        }
    }

    /**
     * An output stream that eats everything written to it.
     */
    private static class DisabledOS extends OutputStream
    {
        public void write(int b)
        {
        }

        public void write(byte[] b)
        {
        }

        public void write(byte[] b, int off, int len)
        {
        }
    }

    /**
     * Print an empty trace message.
     */
    public void trace()
    {
        trace(null);
    }

    /**
     * Print a trace message with the specified string.  The trace
     * message will automatically include the file name and line
     * number of the call, if that information is available.
     */
    public void trace(String s)
    {
        StackTraceElement ste = getCaller();
        int line = ste.getLineNumber();
        String fname = ste.getFileName();

        StringBuilder out = new StringBuilder("Trace ");
        if (fname != null) {
            out.append(fname);
            if (line >= 0) {
                out.append(':');
                out.append(line);
            }
        } else {
            out.append("(Unknown source)");
        }
        if (s != null) {
            out.append(": ");
            out.append(s);
        }
        println(out);
    }

    /**
     * Get the highest entry in the current stack trace that isn't
     * inside the Log class itself.
     */
    private static StackTraceElement getCaller()
    {
        StackTraceElement st[] = new Throwable().getStackTrace();
        String myCn = Log.class.getName();
        for (StackTraceElement ste : st) {
            String cn = ste.getClassName();
            if (cn != null && !cn.startsWith(myCn))
                return ste;
        }
        throw new IllegalStateException("Couldn't find caller");
    }
}
