/*
 * Copyright (c) 2009, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */


package org.graalvm.compiler.debug;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;

/**
 * A utility for printing compiler debug and informational output to an output stream.
 *
 * A {@link LogStream} instance maintains an internal buffer that is flushed to the underlying
 * output stream every time one of the {@code println} methods is invoked, or a newline character (
 * {@code '\n'}) is written.
 *
 * All of the {@code print} and {@code println} methods return the {code LogStream} instance on
 * which they were invoked. This allows chaining of these calls to mitigate use of String
 * concatenation by the caller.
 *
 * A {@code LogStream} maintains a current {@linkplain #indentationLevel() indentation} level. Each
 * line of output written to this stream has {@code n} spaces prefixed to it where {@code n} is the
 * value that would be returned by {@link #indentationLevel()} when the first character of a new
 * line is written.
 *
 * A {@code LogStream} maintains a current {@linkplain #position() position} for the current line
 * being written. This position can be advanced to a specified position by
 * {@linkplain #fillTo(int, char) filling} this stream with a given character.
 */
public class LogStream {

    /**
     * Null output stream that simply swallows any output sent to it.
     */
    public static final LogStream SINK = new LogStream();

    private static final PrintStream SINK_PS = new PrintStream(new OutputStream() {

        @Override
        public void write(int b) throws IOException {
        }
    });

    private LogStream() {
        this.ps = null;
        this.lineBuffer = null;
    }

    /**
     * The output stream to which this log stream writes.
     */
    private final PrintStream ps;

    private final StringBuilder lineBuffer;
    private int indentationLevel;
    private char indentation = ' ';
    private boolean indentationDisabled;

    public final PrintStream out() {
        if (ps == null) {
            return SINK_PS;
        }
        return ps;
    }

    /**
     * The system dependent line separator.
     */
    public static final String LINE_SEPARATOR = System.getProperty("line.separator");

    /**
     * Creates a new log stream.
     *
     * @param os the underlying output stream to which prints are sent
     */
    public LogStream(OutputStream os) {
        ps = os instanceof PrintStream ? (PrintStream) os : new PrintStream(os);
        lineBuffer = new StringBuilder(100);
    }

    /**
     * Creates a new log stream that shares the same {@linkplain #ps output stream} as a given
     * {@link LogStream}.
     *
     * @param log a LogStream whose output stream is shared with this one
     */
    public LogStream(LogStream log) {
        ps = log.ps;
        lineBuffer = new StringBuilder(100);
    }

    /**
     * Prepends {@link #indentation} to the current output line until its write position is equal to
     * the current {@linkplain #indentationLevel()} level.
     */
    private void indent() {
        if (ps != null) {
            if (!indentationDisabled && indentationLevel != 0) {
                while (lineBuffer.length() < indentationLevel) {
                    lineBuffer.append(indentation);
                }
            }
        }
    }

    private LogStream flushLine(boolean withNewline) {
        if (ps != null) {
            if (withNewline) {
                lineBuffer.append(LINE_SEPARATOR);
            }
            ps.print(lineBuffer.toString());
            ps.flush();
            lineBuffer.setLength(0);
        }
        return this;
    }

    /**
     * Flushes the stream. This is done by terminating the current line if it is not at position 0
     * and then flushing the underlying output stream.
     */
    public void flush() {
        if (ps != null) {
            if (lineBuffer.length() != 0) {
                flushLine(false);
            }
            ps.flush();
        }
    }

    /**
     * Gets the current column position of this log stream.
     *
     * @return the current column position of this log stream
     */
    public int position() {
        return lineBuffer == null ? 0 : lineBuffer.length();

    }

    /**
     * Gets the current indentation level for this log stream.
     *
     * @return the current indentation level for this log stream.
     */
    public int indentationLevel() {
        return indentationLevel;
    }

    /**
     * Adjusts the current indentation level of this log stream.
     *
     * @param delta
     */
    public void adjustIndentation(int delta) {
        if (delta < 0) {
            indentationLevel = Math.max(0, indentationLevel + delta);
        } else {
            indentationLevel += delta;
        }
    }

    /**
     * Gets the current indentation character of this log stream.
     */
    public char indentation() {
        return indentation;
    }

    public void disableIndentation() {
        indentationDisabled = true;
    }

    public void enableIndentation() {
        indentationDisabled = false;
    }

    /**
     * Sets the character used for indentation.
     */
    public void setIndentation(char c) {
        indentation = c;
    }

    /**
     * Advances this stream's {@linkplain #position() position} to a given position by repeatedly
     * appending a given character as necessary.
     *
     * @param position the position to which this stream's position will be advanced
     * @param filler the character used to pad the stream
     */
    public LogStream fillTo(int position, char filler) {
        if (ps != null) {
            indent();
            while (lineBuffer.length() < position) {
                lineBuffer.append(filler);
            }
        }
        return this;
    }

    /**
     * Writes a boolean value to this stream as {@code "true"} or {@code "false"}.
     *
     * @param b the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream print(boolean b) {
        if (ps != null) {
            indent();
            lineBuffer.append(b);
        }
        return this;
    }

    /**
     * Writes a boolean value to this stream followed by a {@linkplain #LINE_SEPARATOR line
     * separator}.
     *
     * @param b the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream println(boolean b) {
        if (ps != null) {
            indent();
            lineBuffer.append(b);
            return flushLine(true);
        }
        return this;
    }

    /**
     * Writes a character value to this stream.
     *
     * @param c the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream print(char c) {
        if (ps != null) {
            indent();
            lineBuffer.append(c);
            if (c == '\n') {
                if (lineBuffer.indexOf(LINE_SEPARATOR, lineBuffer.length() - LINE_SEPARATOR.length()) != -1) {
                    flushLine(false);
                }
            }
        }
        return this;
    }

    /**
     * Writes a character value to this stream followed by a {@linkplain #LINE_SEPARATOR line
     * separator}.
     *
     * @param c the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream println(char c) {
        if (ps != null) {
            indent();
            lineBuffer.append(c);
            flushLine(true);
        }
        return this;
    }

    /**
     * Prints an int value.
     *
     * @param i the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream print(int i) {
        if (ps != null) {
            indent();
            lineBuffer.append(i);
        }
        return this;
    }

    /**
     * Writes an int value to this stream followed by a {@linkplain #LINE_SEPARATOR line separator}.
     *
     * @param i the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream println(int i) {
        if (ps != null) {
            indent();
            lineBuffer.append(i);
            return flushLine(true);
        }
        return this;
    }

    /**
     * Writes a float value to this stream.
     *
     * @param f the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream print(float f) {
        if (ps != null) {
            indent();
            lineBuffer.append(f);
        }
        return this;
    }

    /**
     * Writes a float value to this stream followed by a {@linkplain #LINE_SEPARATOR line separator}
     * .
     *
     * @param f the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream println(float f) {
        if (ps != null) {
            indent();
            lineBuffer.append(f);
            return flushLine(true);
        }
        return this;
    }

    /**
     * Writes a long value to this stream.
     *
     * @param l the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream print(long l) {
        if (ps != null) {
            indent();
            lineBuffer.append(l);
        }
        return this;
    }

    /**
     * Writes a long value to this stream followed by a {@linkplain #LINE_SEPARATOR line separator}.
     *
     * @param l the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream println(long l) {
        if (ps != null) {
            indent();
            lineBuffer.append(l);
            return flushLine(true);
        }
        return this;
    }

    /**
     * Writes a double value to this stream.
     *
     * @param d the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream print(double d) {
        if (ps != null) {
            indent();
            lineBuffer.append(d);
        }
        return this;
    }

    /**
     * Writes a double value to this stream followed by a {@linkplain #LINE_SEPARATOR line
     * separator}.
     *
     * @param d the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream println(double d) {
        if (ps != null) {
            indent();
            lineBuffer.append(d);
            return flushLine(true);
        }
        return this;
    }

    /**
     * Writes a {@code String} value to this stream. This method ensures that the
     * {@linkplain #position() position} of this stream is updated correctly with respect to any
     * {@linkplain #LINE_SEPARATOR line separators} present in {@code s}.
     *
     * @param s the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream print(String s) {
        if (ps != null) {
            if (s == null) {
                indent();
                lineBuffer.append(s);
                return this;
            }

            int index = 0;
            int next = s.indexOf(LINE_SEPARATOR, index);
            while (index < s.length()) {
                indent();
                if (next > index || next == 0) {
                    lineBuffer.append(s.substring(index, next));
                    flushLine(true);
                    index = next + LINE_SEPARATOR.length();
                    next = s.indexOf(LINE_SEPARATOR, index);
                } else {
                    lineBuffer.append(s.substring(index));
                    break;
                }
            }
        }
        return this;
    }

    /**
     * Writes a {@code String} value to this stream followed by a {@linkplain #LINE_SEPARATOR line
     * separator}.
     *
     * @param s the value to be printed
     * @return this {@link LogStream} instance
     */
    public LogStream println(String s) {
        if (ps != null) {
            print(s);
            flushLine(true);
        }
        return this;
    }

    /**
     * Writes a formatted string to this stream.
     *
     * @param format a format string as described in {@link String#format(String, Object...)}
     * @param args the arguments to be formatted
     * @return this {@link LogStream} instance
     */
    public LogStream printf(String format, Object... args) {
        if (ps != null) {
            print(String.format(format, args));
        }
        return this;
    }

    /**
     * Writes a {@linkplain #LINE_SEPARATOR line separator} to this stream.
     *
     * @return this {@code LogStream} instance
     */
    public LogStream println() {
        if (ps != null) {
            indent();
            flushLine(true);
        }
        return this;
    }
}
