/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.util;

import java.io.PrintWriter;
import java.io.Writer;
import java.util.Arrays;

/**
 * A writer that breaks up its output into chunks before writing to its out writer,
 * and which is linebreak aware, i.e., chunks will created along line breaks, if
 * possible.
 *
 * Note: this class is not thread-safe.
 */
public class LineBreakBufferedWriter extends PrintWriter {

    /**
     * A buffer to collect data until the buffer size is reached.
     *
     * Note: we manage a char[] ourselves to avoid an allocation when printing to the
     *       out writer. Otherwise a StringBuilder would have been simpler to use.
     */
    private char[] buffer;

    /**
     * The index of the first free element in the buffer.
     */
    private int bufferIndex;

    /**
     * The chunk size (=maximum buffer size) to use for this writer.
     */
    private final int bufferSize;


    /**
     * Index of the last newline character discovered in the buffer. The writer will try
     * to split there.
     */
    private int lastNewline = -1;

    /**
     * The line separator for println().
     */
    private final String lineSeparator;

    /**
     * Create a new linebreak-aware buffered writer with the given output and buffer
     * size. The initial capacity will be a default value.
     * @param out The writer to write to.
     * @param bufferSize The maximum buffer size.
     */
    public LineBreakBufferedWriter(Writer out, int bufferSize) {
        this(out, bufferSize, 16);  // 16 is the default size of a StringBuilder buffer.
    }

    /**
     * Create a new linebreak-aware buffered writer with the given output, buffer
     * size and initial capacity.
     * @param out The writer to write to.
     * @param bufferSize The maximum buffer size.
     * @param initialCapacity The initial capacity of the internal buffer.
     */
    public LineBreakBufferedWriter(Writer out, int bufferSize, int initialCapacity) {
        super(out);
        this.buffer = new char[Math.min(initialCapacity, bufferSize)];
        this.bufferIndex = 0;
        this.bufferSize = bufferSize;
        this.lineSeparator = System.getProperty("line.separator");
    }

    /**
     * Flush the current buffer. This will ignore line breaks.
     */
    @Override
    public void flush() {
        writeBuffer(bufferIndex);
        bufferIndex = 0;
        super.flush();
    }

    @Override
    public void write(int c) {
        if (bufferIndex < buffer.length) {
            buffer[bufferIndex] = (char)c;
            bufferIndex++;
            if ((char)c == '\n') {
                lastNewline = bufferIndex;
            }
        } else {
            // This should be an uncommon case, we mostly expect char[] and String. So
            // let the chunking be handled by the char[] case.
            write(new char[] { (char)c }, 0 ,1);
        }
    }

    @Override
    public void println() {
        write(lineSeparator);
    }

    @Override
    public void write(char[] buf, int off, int len) {
        while (bufferIndex + len > bufferSize) {
            // Find the next newline in the buffer, see if that's below the limit.
            // Repeat.
            int nextNewLine = -1;
            int maxLength = bufferSize - bufferIndex;
            for (int i = 0; i < maxLength; i++) {
                if (buf[off + i] == '\n') {
                    if (bufferIndex + i < bufferSize) {
                        nextNewLine = i;
                    } else {
                        break;
                    }
                }
            }

            if (nextNewLine != -1) {
                // We can add some more data.
                appendToBuffer(buf, off, nextNewLine);
                writeBuffer(bufferIndex);
                bufferIndex = 0;
                lastNewline = -1;
                off += nextNewLine + 1;
                len -= nextNewLine + 1;
            } else if (lastNewline != -1) {
                // Use the last newline.
                writeBuffer(lastNewline);
                removeFromBuffer(lastNewline + 1);
                lastNewline = -1;
            } else {
                // OK, there was no newline, break at a full buffer.
                int rest = bufferSize - bufferIndex;
                appendToBuffer(buf, off, rest);
                writeBuffer(bufferIndex);
                bufferIndex = 0;
                off += rest;
                len -= rest;
            }
        }

        // Add to the buffer, this will fit.
        if (len > 0) {
            // Add the chars, find the last newline.
            appendToBuffer(buf, off, len);
            for (int i = len - 1; i >= 0; i--) {
                if (buf[off + i] == '\n') {
                    lastNewline = bufferIndex - len + i;
                    break;
                }
            }
        }
    }

    @Override
    public void write(String s, int off, int len) {
        while (bufferIndex + len > bufferSize) {
            // Find the next newline in the buffer, see if that's below the limit.
            // Repeat.
            int nextNewLine = -1;
            int maxLength = bufferSize - bufferIndex;
            for (int i = 0; i < maxLength; i++) {
                if (s.charAt(off + i) == '\n') {
                    if (bufferIndex + i < bufferSize) {
                        nextNewLine = i;
                    } else {
                        break;
                    }
                }
            }

            if (nextNewLine != -1) {
                // We can add some more data.
                appendToBuffer(s, off, nextNewLine);
                writeBuffer(bufferIndex);
                bufferIndex = 0;
                lastNewline = -1;
                off += nextNewLine + 1;
                len -= nextNewLine + 1;
            } else if (lastNewline != -1) {
                // Use the last newline.
                writeBuffer(lastNewline);
                removeFromBuffer(lastNewline + 1);
                lastNewline = -1;
            } else {
                // OK, there was no newline, break at a full buffer.
                int rest = bufferSize - bufferIndex;
                appendToBuffer(s, off, rest);
                writeBuffer(bufferIndex);
                bufferIndex = 0;
                off += rest;
                len -= rest;
            }
        }

        // Add to the buffer, this will fit.
        if (len > 0) {
            // Add the chars, find the last newline.
            appendToBuffer(s, off, len);
            for (int i = len - 1; i >= 0; i--) {
                if (s.charAt(off + i) == '\n') {
                    lastNewline = bufferIndex - len + i;
                    break;
                }
            }
        }
    }

    /**
     * Append the characters to the buffer. This will potentially resize the buffer,
     * and move the index along.
     * @param buf The char[] containing the data.
     * @param off The start index to copy from.
     * @param len The number of characters to copy.
     */
    private void appendToBuffer(char[] buf, int off, int len) {
        if (bufferIndex + len > buffer.length) {
            ensureCapacity(bufferIndex + len);
        }
        System.arraycopy(buf, off, buffer, bufferIndex, len);
        bufferIndex += len;
    }

    /**
     * Append the characters from the given string to the buffer. This will potentially
     * resize the buffer, and move the index along.
     * @param s The string supplying the characters.
     * @param off The start index to copy from.
     * @param len The number of characters to copy.
     */
    private void appendToBuffer(String s, int off, int len) {
        if (bufferIndex + len > buffer.length) {
            ensureCapacity(bufferIndex + len);
        }
        s.getChars(off, off + len, buffer, bufferIndex);
        bufferIndex += len;
    }

    /**
     * Resize the buffer. We use the usual double-the-size plus constant scheme for
     * amortized O(1) insert. Note: we expect small buffers, so this won't check for
     * overflow.
     * @param capacity The size to be ensured.
     */
    private void ensureCapacity(int capacity) {
        int newSize = buffer.length * 2 + 2;
        if (newSize < capacity) {
            newSize = capacity;
        }
        buffer = Arrays.copyOf(buffer, newSize);
    }

    /**
     * Remove the characters up to (and excluding) index i from the buffer. This will
     * not resize the buffer, but will update bufferIndex.
     * @param i The number of characters to remove from the front.
     */
    private void removeFromBuffer(int i) {
        int rest = bufferIndex - i;
        if (rest > 0) {
            System.arraycopy(buffer, bufferIndex - rest, buffer, 0, rest);
            bufferIndex = rest;
        } else {
            bufferIndex = 0;
        }
    }

    /**
     * Helper method, write the given part of the buffer, [start,length), to the output.
     * @param length The number of characters to flush.
     */
    private void writeBuffer(int length) {
        if (length > 0) {
            super.write(buffer, 0, length);
        }
    }
}
