/*

   Derby - Class org.apache.derby.impl.io.vfmem.BlockedByteArray

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You 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 org.apache.derby.impl.io.vfmem;

import org.apache.derby.shared.common.sanity.SanityManager;

/**
 * Stores data in blocks, and supports reading/writing data from/into these
 * blocks.
 * <p>
 * The blocked array is expanded and shrunk as required.
 */
public class BlockedByteArray {

    /** Constant for 4 KB. */
    private static final int _4K = 4*1024;
    /** Constant for 8 KB. */
    private static final int _8K = 8*1024;
    /** Constant for 16 KB. */
    private static final int _16K = 16*1024;
    /** Constant for 32 KB. */
    private static final int _32K = 32*1024;
    /** The default block size. */
    private static final int DEFAULT_BLOCKSIZE = _4K;

    /** The default number of slots for holding a block of data. */
    private static final int INITIAL_BLOCK_HOLDER_SIZE = 1024;
    /**
     * Minimum number of holder slots to grow with when the block holder array
     * has to grow to be able to reference all the data arrays.
     */
    private static final int MIN_HOLDER_GROWTH = 1024;

    /** References to blocks of data. */
    private byte[][] blocks;
    /** The size of a block of data (the allocation unit). */
    private int blockSize;
    /** The number of allocated blocks. */
    private int allocatedBlocks;
    /** The number of bytes stored in the blocked byte array. */
    private long length;

    /**
     * Creates a new blocked byte array with the default number of slots to
     * hold byte arrays (blocks).
     * <p>
     * No blocks are pre-allocated.
     *
     * @see #INITIAL_BLOCK_HOLDER_SIZE
     */
    public BlockedByteArray() {
        blocks = new byte[INITIAL_BLOCK_HOLDER_SIZE][];
    }

    /**
     * Returns the byte at the given position.
     *
     * @param pos position to read from
     * @return A single byte.
     */
    public synchronized int read(long pos) {
        if (pos < length) {
            int block = (int)(pos / blockSize);
            int index = (int)(pos % blockSize);
            return (blocks[block][index] & 0xFF);
        }
        return -1;
    }

    /**
     * Reads up to {@code len} bytes.
     *
     * @param pos the position to start reading at
     * @param buf the destination buffer
     * @param offset offset into the destination buffer
     * @param len the number of bytes to read
     * @return The number of bytes read.
     */
     public synchronized int read(long pos, byte[] buf, int offset, int len) {
        // Due to the loop condition below, we have to check the length here.
        // The check is only required because calling code expects an exception.
        if (len < 0) {
            throw new ArrayIndexOutOfBoundsException(len);
        }
        // Check for EOF.
        if (pos >= length) {
            return -1;
        }
        // Adjust the length if required.
        len = (int)Math.min(len, length - pos);
        int block = (int)(pos / blockSize);
        int index = (int)(pos % blockSize);
        int read = 0;
        while (read < len) {
            int toRead = Math.min(len - read, blockSize - index);
            System.arraycopy(blocks[block], index, buf, offset + read, toRead);
            read += toRead;
            block++;
            index = 0;
        }
        return read;
     }

    /**
     * Returns the number of bytes allocated.
     *
     * @return Bytes allocated.
     */
    public synchronized long length() {
        return length;
    }

    /**
     * Changes the allocated length of the data.
     * <p>
     * If the new length is larger than the current length, the blocked byte
     * array will be extended with new blocks. If the new length is smaller,
     * existing (allocated) blocks will be removed if possible.
     *
     * @param newLength the new length of the allocated data in bytes
     */
    public synchronized void setLength(final long newLength) {
        // If capacity is requested before any writes has taken place.
        if (blockSize == 0) {
            checkBlockSize((int)Math.min(Integer.MAX_VALUE, newLength));
        }
        final long currentCapacity = (long)allocatedBlocks * blockSize;
        if (newLength > currentCapacity) {
            // Allocate more blocks.
            increaseCapacity(newLength);
        } else if (newLength < currentCapacity) {
            if (newLength <= 0L) {
                // Just clear everything.
                allocatedBlocks = 0;
                blocks = new byte[INITIAL_BLOCK_HOLDER_SIZE][];
            } else {
                // Nullify the surplus data.
                int blocksToKeep = (int)(newLength / blockSize) +1;
                for (int i=blocksToKeep; i <= allocatedBlocks; i++) {
                    blocks[i] = null;
                }
                allocatedBlocks = Math.min(allocatedBlocks, blocksToKeep);
                // We keep the holder slots around, since the overhead for
                // doing so is pretty small.
            }
        }
        length = Math.max(0L, newLength);
    }

    /**
     * Writes the given bytes into the blocked byte array.
     *
     * @param pos the position to start writing at
     * @param buf the source buffer
     * @param offset the offset into the source buffer
     * @param len the number of bytes to write
     * @return The number of bytes written.
     */
    public synchronized int writeBytes(final long pos, final byte[] buf,
                                       int offset, final int len) {
        // Optimize block size if possible on first write.
        if (blockSize == 0) {
            checkBlockSize(len);
        }
        // Due to the loop condition below, we have to check the length here.
        // The check is only required because calling code expects an exception.
        if (len < 0) {
            throw new ArrayIndexOutOfBoundsException(len);
        }
        // Increase the capacity if required.
        increaseCapacity(pos + len);
        // Calculate the block number and the index within this block.
        int block = (int)(pos / blockSize);
        int index = (int)(pos % blockSize);

        int written = 0;
        while (written < len) {
            int toWrite = Math.min(len - written, blockSize - index);
            System.arraycopy(buf, offset, blocks[block], index, toWrite);
            written += toWrite;
            offset += toWrite;
            if (written < len) {
                block++;
                index = 0;
            } else {
                index += toWrite;
            }
        }

        // Update the length if we wrote past the previous length.
        length = Math.max(length, pos + len);
        return written;
    }

    /**
     * Writes the given byte into the blocked byte array.
     *
     * @param pos the position to write the byte at
     * @param b the byte to write
     * @return {@code 1}, which is the number of bytes written.
     */
    public synchronized int writeByte(long pos, byte b) {
        // Optimize block size if possible on first write.
        if (blockSize == 0) {
            checkBlockSize(0);
        }
        // Increase the capacity if required.
        increaseCapacity(pos);
        // Calculate the block number and the index within this block.
        int block = (int)(pos / blockSize);
        int index = (int)(pos % blockSize);
        blocks[block][index] = b;
        // Update the length if we wrote past the previous length.
        length = Math.max(length, pos +1);
        return 1; // The number of bytes written, always one.
    }

    /**
     * Returns an input stream serving the data in the blocked byte array.
     *
     * @return An {@code InputStream}-object.
     */
    synchronized BlockedByteArrayInputStream getInputStream() {
        return new BlockedByteArrayInputStream(this, 0L);
    }

    /**
     * Returns an output stream writing data into the blocked byte array.
     *
     * @param pos initial position of the output stream
     * @return An {@code OutputStream}-object.
     */
    synchronized BlockedByteArrayOutputStream getOutputStream(long pos) {
        if (pos < 0) {
            throw new IllegalArgumentException(
                                        "Position cannot be negative: " + pos);
        }
        return new BlockedByteArrayOutputStream(this, pos);
    }

    /**
     * Releases this array.
     */
    synchronized void release() {
        blocks = null;
        length = allocatedBlocks = -1;
    }

    /**
     * Tries to optimize the block size by setting it equal to the the page
     * size used by the database.
     * <p>
     * Since we don't have a way of knowing which page size will be used, wait
     * to set the block size until the first write request and see how many
     * bytes are written then.
     *
     * @param len the requested number of bytes to be written
     */
    private void checkBlockSize(int len) {
        // Optimize on the block size (if possible).
        if (len == _4K || len == _8K || len == _16K || len == _32K) {
            blockSize = len;
        } else {
            blockSize = DEFAULT_BLOCKSIZE;
        }
    }

    /**
     * Increases the capacity of this blocked byte array by allocating more
     * blocks.
     *
     * @param lastIndex the index that must fit into the array
     */
    //@GuardedBy("this")
    private void increaseCapacity(long lastIndex) {
        if (SanityManager.DEBUG) {
            SanityManager.ASSERT(blockSize > 0, "Invalid/unset block size");
        }
        // Safe-guard to avoid overwriting existing data.
        if (lastIndex < (long)allocatedBlocks * blockSize) {
            return;
        }
        // Calculate required number of blocks, and create those lacking.
        // We may allocate one more array than required.
        final int blocksRequired = (int)((lastIndex) / blockSize) +1;
        if (blocksRequired > blocks.length) {
            // Grow the block holder array.
            // Make sure we have enough slots. Note that we only grow the block
            // holder array, we don't fill it with data blocks before needed.
            int growTo = Math.max(
                    // Grow at least ~33%.
                    blocks.length + (blocks.length / 3),
                    // For cases where we need to grow more than 33%.
                    blocksRequired + MIN_HOLDER_GROWTH);
            byte[][] tmpBlocks = blocks;
            blocks = new byte[growTo][];
            // Copy the data array references.
            System.arraycopy(tmpBlocks, 0, blocks, 0, allocatedBlocks);
        }
        // Allocate new data arrays to accomodate lastIndex bytes.
        for (int i=allocatedBlocks; i < blocksRequired; i++) {
            blocks[i] = new byte[blockSize];
        }
        allocatedBlocks = blocksRequired;
    }
}
