/* Copyright (c) 2001-2021, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb.lib;

import java.io.CharArrayReader;
import java.io.IOException;
import java.io.Writer;

/*
 * @todo - finer-grained synchronization to reduce average
 * potential monitor contention
 */

/**
 * Provides Closable semantics ordinarily missing in a
 * {@link java.io.CharArrayWriter}. <p>
 *
 * Accumulates output in a character array that automatically grows as needed.<p>
 *
 * Data is retrieved using {@code toCharArray()}, {@code toCharArrayReader()}
 * and {@code toString()}. <p>
 *
 * {@link #close() Closing} a {@code ClosableCharArrayWriter} prevents
 * further write operations, but all other operations will succeed until after
 * the first invocation of {@link #free() free()}.<p>
 *
 * Freeing a {@code ClosableCharArrayWriter} closes the writer and
 * releases its internal buffer, preventing successful invocation of all
 * operations, with the exception of {@code size()}, {@code close()},
 * {@code isClosed()}, {@code free()} and {@code isFreed()}. <p>
 *
 * This class is especially useful when an accumulating writer must be
 * handed off to an extenal client under contract that the writer should
 * exhibit true Closable behaviour, both in response to internally tracked
 * events and to client invocation of the {@code Writer.close()} method.
 *
 * @author Campbell Burnet (campbell-burnet@users dot sourceforge.net)
 * @version 1.8.x
 * @since 1.8.x
 */
public class ClosableCharArrayWriter extends Writer {

    /**
     * Data buffer.
     */
    protected char[] buf;

    /**
     * # of valid characters in buffer.
     */
    protected int count;

    /**
     * Whether this writer is closed.
     */
    protected boolean closed;

    /**
     * Whether this writer is freed.
     */
    protected boolean freed;

    /**
     * Creates a new writer. <p>
     *
     * The buffer capacity is initially 32 characters, although its size
     * automatically increases when necessary.
     */
    public ClosableCharArrayWriter() {
        this(32);
    }

    /**
     * Creates a new writer with a buffer capacity of the specified
     * {@code size}, in characters.
     *
     * @param size the initial size.
     * @exception IllegalArgumentException if {@code size} is negative.
     */
    public ClosableCharArrayWriter(int size) throws IllegalArgumentException {

        if (size < 0) {
            throw new IllegalArgumentException("Negative initial size: "
                                               + size);    // NOI18N
        }

        buf = new char[size];
    }

    /**
     * Writes the specified single character.
     *
     * @param c the single character to be written.
     * @throws java.io.IOException if an I/O error occurs.
     *      In particular, an {@code IOException} may be thrown
     *      if this writer has been {@link #close() closed}.
     */
    public synchronized void write(int c) throws IOException {

        checkClosed();

        int newcount = count + 1;

        if (newcount > buf.length) {
            buf = copyOf(buf, Math.max(buf.length << 1, newcount));
        }

        buf[count] = (char) c;
        count      = newcount;
    }

    /**
     * Writes the designated portion of the designated character array <p>.
     *
     * @param c the source character sequence.
     * @param off the start offset in the source character sequence.
     * @param len the number of characters to write.
     * @throws java.io.IOException if an I/O error occurs.
     *      In particular, an {@code IOException} may be thrown
     *      if this writer has been {@link #close() closed}.
     */
    public synchronized void write(char[] c, int off,
                                   int len) throws IOException {

        checkClosed();

        if ((off < 0) || (off > c.length) || (len < 0)
                || ((off + len) > c.length) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }

        int newcount = count + len;

        if (newcount > buf.length) {
            buf = copyOf(buf, Math.max(buf.length << 1, newcount));
        }

        System.arraycopy(c, off, buf, count, len);

        count = newcount;
    }

    /**
     * Efficiently writes the designated portion of the designated string. <p>
     *
     * The operation occurs as if by calling
     * {@code str.getChars(off, off + len, buf, count)}. <p>
     *
     * @param str the string from which to write
     * @param off the start offset in the string.
     * @param len the number of characters to write.
     * @throws java.io.IOException if an I/O error occurs.
     *      In particular, an {@code IOException} may be thrown
     *      if this writer has been {@link #close() closed}.
     */
    public synchronized void write(String str, int off,
                                   int len) throws IOException {

        checkClosed();

        int strlen = str.length();

        if ((off < 0) || (off > strlen) || (len < 0) || ((off + len) > strlen)
                || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }

        int newcount = count + len;

        if (newcount > buf.length) {
            buf = copyOf(buf, Math.max(buf.length << 1, newcount));
        }

        str.getChars(off, off + len, buf, count);

        count = newcount;
    }

    /**
     * By default, does nothing. <p>
     *
     * @throws java.io.IOException if an I/O error occurs.
     *      In particular, an {@code IOException} may be thrown
     *      if this writer has been {@link #close() closed}.
     */
    public void flush() throws IOException {
        checkClosed();
    }

    /**
     * Writes the complete contents of this writer's buffered data to the
     * specified writer. <p>
     *
     * The operation occurs as if by calling {@code out.write(buf, 0, count)}.
     *
     * @param out the writer to which to write the data.
     * @throws java.io.IOException if an I/O error occurs.
     *      In particular, an {@code IOException} may be thrown
     *      if this writer has been {@link #free() freed}.
     */
    public synchronized void writeTo(Writer out) throws IOException {

        checkFreed();

        if (count > 0) {
            out.write(buf, 0, count);
        }
    }

    /**
     * Returns the current capacity of this writer's data buffer.
     *
     * @return  the current capacity (the length of the internal
     *          data array)
     * @throws java.io.IOException if an I/O error occurs.
     *      In particular, an {@code IOException} may be thrown
     *      if this writer has been {@link #free() freed}.
     */
    public synchronized int capacity() throws IOException {

        checkFreed();

        return buf.length;
    }

    /**
     * Resets the {@code count} field of this writer to zero, so that all
     * currently accumulated output is effectively discarded. Further write
     * operations will reuse the allocated buffer space.
     *
     * @see #count
     * @throws java.io.IOException if an I/O error occurs.
     *      In particular, an {@code IOException} may be thrown
     *      if this output stream has been {@link #close() closed}.
     */
    public synchronized void reset() throws IOException {

        checkClosed();

        count = 0;
    }

    /**
     * Attempts to reduce this writer's buffer capacity to its current size. <p>
     *
     * If the buffer is larger than necessary to hold its current sequence of
     * characters, then it may be resized to become more space efficient.
     * Calling this method may, but is not required to, affect the value
     * returned by a subsequent call to the {@link #capacity()} method.
     */
    public synchronized void trimToSize() throws IOException {

        checkFreed();

        if (buf.length > count) {
            buf = copyOf(buf, count);
        }
    }

    /**
     * Creates a newly allocated character array. Its size is the current
     * size of this writer and the valid contents of the buffer
     * have been copied into it.
     *
     * @return the current contents of this writer, as a character array.
     * @see #size()
     * @throws java.io.IOException if an I/O error occurs.
     *      In particular, an {@code IOException} may be thrown
     *      if this writer has been {@link #free() freed}.
     */
    public synchronized char[] toCharArray() throws IOException {

        checkFreed();

        return copyOf(buf, count);
    }

    /**
     * Returns the current size of this writer's accumulated character data.
     *
     * @return the value of the {@code count} field, which is the number
     *      of valid characters accumulated in this writer.
     * @see #count
     */
    public synchronized int size() {
        return count;
    }

    /**
     * Sets the size of this writer's accumulated character data. <p>
     *
     * @param   newSize the new size of this writer's accumulated data
     * @throws  ArrayIndexOutOfBoundsException if new size is negative
     */
    public synchronized void setSize(int newSize) {

        if (newSize < 0) {
            throw new ArrayIndexOutOfBoundsException(newSize);
        } else if (newSize > buf.length) {
            buf = copyOf(buf, Math.max(buf.length << 1, newSize));
        }

        count = newSize;
    }

    /**
     * Performs an efficient (zero-copy) conversion of the character data
     * accumulated in this writer to a reader. <p>
     *
     * To ensure the integrity of the resulting reader, {@link #free()
     * free} is invoked upon this writer as a side-effect.
     *
     * @return a reader representing this writer's accumulated
     *      character data
     * @throws java.io.IOException if an I/O error occurs.
     *      In particular, an {@code IOException} may be thrown
     *      if this writer has been {@link #free() freed}.
     */
    public synchronized CharArrayReader toCharArrayReader()
    throws IOException {

        checkFreed();

        CharArrayReader reader = new CharArrayReader(buf, 0, count);

        //System.out.println("toCharArrayReader::buf.length: " + buf.length);
        free();

        return reader;
    }

    /**
     * Converts this writer's accumulated data into a string.
     *
     * @return String constructed from this writer's accumulated data
     * @throws RuntimeException may be thrown if this writer has been
     *      {@link #free() freed}.
     */
    public synchronized String toString() {

        try {
            checkFreed();
        } catch (IOException ex) {
            throw new RuntimeException(ex.toString());
        }

        return new String(buf, 0, count);
    }

    /**
     * Closes this object for further writing. <p>
     *
     * Other operations may continue to succeed until after the first invocation
     * of {@link #free() free()}.
     */
    public synchronized void close() {
        closed = true;
    }

    /**
     * @return {@code true} if this writer is closed, else {@code false}
     */
    public synchronized boolean isClosed() {
        return closed;
    }

    /**
     * Closes this object and releases the underlying buffer for
     * garbage collection.
     */
    public synchronized void free() {

        closed = true;
        freed  = true;
        buf    = null;
        count  = 0;
    }

    /**
     * @return {@code true} if this writer is freed; else {@code false}.
     */
    public synchronized boolean isFreed() {
        return freed;
    }

    /**
     * @throws java.io.IOException if this writer is closed.
     */
    protected synchronized void checkClosed() throws IOException {

        if (closed) {
            throw new IOException("writer is closed.");    // NOI18N
        }
    }

    /**
     * @throws java.io.IOException if this writer is freed.
     */
    protected synchronized void checkFreed() throws IOException {

        if (freed) {
            throw new IOException("write buffer is freed.");    // NOI18N
        }
    }

    /**
     * Retrieves a copy of {@code original} with the given
     * {@code newLength}. <p>
     *
     * @param original the object to copy
     * @param newLength the length of the copy
     * @return copy of {@code original} with the given {@code newLength}
     */
    protected char[] copyOf(char[] original, int newLength) {

        char[] copy = new char[newLength];

        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));

        return copy;
    }
}
