package org.apache.lucene.util;

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

/**
 * 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.
 */

/**
 * This class is used as a wrapper to a byte array, extending
 * {@link OutputStream}. Data is written in the given byte[] buffer, until its
 * length is insufficient. Than the buffer size is doubled and the data is
 * written.
 * 
 * This class is Unsafe as it is using a buffer which potentially can be changed
 * from the outside. Moreover, when {@link #toByteArray()} is called, the buffer
 * itself is returned, and not a copy.
 * 
 * @lucene.experimental
 */
public class UnsafeByteArrayOutputStream extends OutputStream {

  private byte[] buffer;
  private int index;
  private int startIndex;

  /**
   * Constructs a new output stream, with a default allocated buffer which can
   * later be obtained via {@link #toByteArray()}.
   */
  public UnsafeByteArrayOutputStream() {
    reInit(new byte[32], 0);
  }

  /**
   * Constructs a new output stream, with a given buffer. Writing will start
   * at index 0 as a default.
   * 
   * @param buffer
   *            some space to which writing will be made
   */
  public UnsafeByteArrayOutputStream(byte[] buffer) {
    reInit(buffer, 0);
  }

  /**
   * Constructs a new output stream, with a given buffer. Writing will start
   * at a given index.
   * 
   * @param buffer
   *            some space to which writing will be made.
   * @param startPos
   *            an index (inclusive) from white data will be written.
   */
  public UnsafeByteArrayOutputStream(byte[] buffer, int startPos) {
    reInit(buffer, startPos);
  }

  private void grow(int newLength) {
    // It actually should be: (Java 1.6)
    // buffer = Arrays.copyOf(buffer, newLength);
    byte[] newBuffer = new byte[newLength];
    System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
    buffer = newBuffer;
  }

  /**
   * For reuse-ability, this stream object can be re-initialized with another
   * given buffer and starting position.
   * 
   * @param buffer some space to which writing will be made.
   * @param startPos an index (inclusive) from white data will be written.
   */
  public void reInit(byte[] buffer, int startPos) {
    if (buffer.length == 0) {
      throw new IllegalArgumentException("initial buffer length must be greater than 0.");
    }
    this.buffer = buffer;
    startIndex = startPos;
    index = startIndex;
  }

  /**
   * For reuse-ability, this stream object can be re-initialized with another
   * given buffer, using 0 as default starting position.
   * 
   * @param buffer some space to which writing will be made.
   */
  public void reInit(byte[] buffer) {
    reInit(buffer, 0);
  }

  /**
   * writes a given byte(at the form of an int) to the buffer. If the buffer's
   * empty space is insufficient, the buffer is doubled.
   * 
   * @param value byte value to be written
   */
  @Override
  public void write(int value) throws IOException {
    if (index >= buffer.length) {
      grow(buffer.length << 1);
    }
    buffer[index++] = (byte) value;
  }

  /**
   * writes a given byte[], with offset and length to the buffer. If the
   * buffer's empty space is insufficient, the buffer is doubled until it
   * could contain all the data.
   * 
   * @param b
   *            byte buffer, containing the source data to be written
   * @param off
   *            index from which data from the buffer b should be written
   * @param len
   *            number of bytes that should be written
   */
  @Override
  public void write(byte[] b, int off, int len) throws IOException {
    // If there's not enough space for the data
    int targetLength = index + len;
    if (targetLength >= buffer.length) {
      // Calculating the new required length of the array, keeping the array
      // size a power of 2 if it was initialized like that.
      int newlen = buffer.length;
      while ((newlen <<= 1) < targetLength) {}
      grow(newlen);
    }

    // Now that we have enough spare space, we could copy the rest of the
    // data
    System.arraycopy(b, off, buffer, index, len);

    // Updating the index to next available index.
    index += len;
  }

  /**
   * Returns the byte array saved within the buffer AS IS.
   * 
   * @return the actual inner buffer - not a copy of it.
   */
  public byte[] toByteArray() {
    return buffer;
  }

  /**
   * Returns the number of relevant bytes. This objects makes sure the buffer
   * is at least the size of it's data. But it can also be twice as big. The
   * user would want to process the relevant bytes only. For that he would
   * need the count.
   * 
   * @return number of relevant bytes
   */
  public int length() {
    return index;
  }

  /**
   * Returns the start position data was written to. This is useful in case you
   * used {@link #reInit(byte[], int)} or
   * {@link #UnsafeByteArrayOutputStream(byte[], int)} and passed a start
   * position which is not 0.
   */
  public int getStartPos() {
    return startIndex;
  }

}
