/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.commons.io;

import java.io.IOException;

/**
 * BufferedRandomOutputStream is a buffered output stream for {@link RandomAccessOutputStream} which, unlike a regular
 * <code>java.io.BufferedOutputStream</code>, makes it safe to seek in the underlying <code>RandomAccessOutputStream</code>.
 *
 * <p>This class uses {@link BufferPool} to create the internal buffer, to avoid excessive memory allocation and
 * garbage collection. The buffer is released when this stream is closed.</p>
 *
 * @author Maxence Bernard
 */
public class BufferedRandomOutputStream extends RandomAccessOutputStream {

    /** The underlying random access output stream */
    private RandomAccessOutputStream raos;

    /** The buffer where written bytes are accumulated before being sent to the underlying output stream */
    private byte buffer[];

    /** The current number of bytes waiting to be flushed to the underlying output stream */
    private int count;

    /** The default buffer size if none is specified */
    public final static int DEFAULT_BUFFER_SIZE = 65536;


    /**
     * Creates a new <code>BufferedRandomOutputStream</code> on top of the given {@link RandomAccessOutputStream}.
     * An internal buffer of {@link #DEFAULT_BUFFER_SIZE} bytes is created.
     *
     * @param raos the underlying RandomAccessOutputStream used by this buffered output stream
     */
    public BufferedRandomOutputStream(RandomAccessOutputStream raos) {
        this(raos, DEFAULT_BUFFER_SIZE);
    }

    /**
     * Creates a new <code>BufferedRandomOutputStream</code> on top of the given {@link RandomAccessOutputStream}.
     * An internal buffer of the specified size is created.
     *
     * @param raos the underlying RandomAccessOutputStream used by this buffered output stream
     * @param size size of the buffer in bytes
     */
    public BufferedRandomOutputStream(RandomAccessOutputStream raos, int size) {
        this.raos = raos;
        this.buffer = BufferPool.getByteArray(size);
    }

    /**
     * Flushes the internal buffer.
     *
     * @throws IOException if an error occurs
     */
    private void flushBuffer() throws IOException {
        if (count > 0) {
            raos.write(buffer, 0, count);
            count = 0;
        }
    }


    /////////////////////////////////////////////
    // RandomAccessOutputStream implementation //
    /////////////////////////////////////////////

    /**
     * Writes the specified byte to this buffered output stream.
     *
     * @param b the byte to be written
     * @throws IOException if an I/O error occurs
     */
    @Override
    public synchronized void write(int b) throws IOException {
        if (count >= buffer.length)
            flushBuffer();

        buffer[count++] = (byte)b;
    }

    /**
     * Writes the specified byte array to this buffered output stream.
     *
     * @param b the bytes to be written
     * @throws IOException if an I/O error occurs
     */
    @Override
    public synchronized void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }

    /**
     * Writes <code>len</code> bytes from the specified byte array starting at offset <code>off</code> to this
     * buffered output stream.
     *
     * <p>Usually this method stores bytes from the given array into this
     * stream's buffer, flushing the buffer to the underlying output stream as
     * needed. However, if the requested data length is equal or larger than this stream's
     * buffer, then this method will flush the buffer and write the
     * bytes directly to the underlying output stream. Thus redundant
     * <code>RandomBufferedOutputStream</code>s will not copy data unnecessarily.</p>
     *
     * @param b the data.
     * @param off the start offset in the data.
     * @param len the number of bytes to write.
     * @throws IOException if an I/O error occurs.
     */
    @Override
    public synchronized void write(byte b[], int off, int len) throws IOException {
        if (len >= buffer.length) {
            /* If the request length exceeds the size of the output buffer,
            flush the output buffer and then write the data directly.
            In this way buffered streams will cascade harmlessly. */
            flushBuffer();
            raos.write(b, off, len);
            return;
        }

        if (len > buffer.length - count)
            flushBuffer();

        System.arraycopy(b, off, buffer, count, len);
        count += len;
    }

    /**
     * Flushes this buffered output stream. This forces any buffered
     * output bytes to be written out to the underlying output stream.
     *
     * @throws IOException if an I/O error occurs.
     */
    @Override
    public synchronized void flush() throws IOException {
        flushBuffer();
        raos.flush();
    }

    public synchronized long getOffset() throws IOException {
        // Add the buffered byte count
        return raos.getOffset() + count;
    }

    public synchronized void seek(long offset) throws IOException {
        // Flush any buffered bytes before seeking, otherwise buffered bytes would be written at the wrong offset
        flush();

        raos.seek(offset);
    }

    public synchronized long getLength() throws IOException {
        // Anticipate if the file is to be expanded by the bytes awaiting in the buffer
        return Math.max(raos.getLength(), getOffset());
    }

    @Override
    public synchronized void setLength(long newLength) throws IOException {
        // Flush before changing the file's length, otherwise the behavior of setLength() would be modified, especially
        // when truncating the file
        flush();

        raos.setLength(newLength);
    }


    ////////////////////////
    // Overridden methods //
    ////////////////////////

    /**
     * This method is overridden to release the internal buffer when this stream is closed.
     */
    @Override
    public synchronized void close() throws IOException {
        if(buffer!=null) {      // buffer is null if close() was already called
            try {
                flush();
            }
            catch(IOException e) {
                // Continue anyway
            }

            // Release the buffer
            BufferPool.releaseByteArray(buffer);
            buffer = null;
        }

        raos.close();
    }

    /**
     * This method is overridden to release the internal buffer if {@link #close()} has not been called, to avoid any
     * memory leak.
     */
    @Override
    protected void finalize() throws Throwable {
        // If this stream hasn't been closed, release the buffer before finalizing the object
        if(buffer!=null)
            BufferPool.releaseByteArray(buffer);

        super.finalize();
    }
}
