/*
 * Copyright (C) 2016 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.google.android.exoplayer2.util;

import static java.lang.Math.min;

import com.google.common.base.Charsets;
import java.nio.charset.Charset;

/** Wraps a byte array, providing methods that allow it to be read as a bitstream. */
public final class ParsableBitArray {

  public byte[] data;

  // The offset within the data, stored as the current byte offset, and the bit offset within that
  // byte (from 0 to 7).
  private int byteOffset;
  private int bitOffset;
  private int byteLimit;

  /** Creates a new instance that initially has no backing data. */
  public ParsableBitArray() {
    data = Util.EMPTY_BYTE_ARRAY;
  }

  /**
   * Creates a new instance that wraps an existing array.
   *
   * @param data The data to wrap.
   */
  public ParsableBitArray(byte[] data) {
    this(data, data.length);
  }

  /**
   * Creates a new instance that wraps an existing array.
   *
   * @param data The data to wrap.
   * @param limit The limit in bytes.
   */
  public ParsableBitArray(byte[] data, int limit) {
    this.data = data;
    byteLimit = limit;
  }

  /**
   * Updates the instance to wrap {@code data}, and resets the position to zero.
   *
   * @param data The array to wrap.
   */
  public void reset(byte[] data) {
    reset(data, data.length);
  }

  /**
   * Sets this instance's data, position and limit to match the provided {@code parsableByteArray}.
   * Any modifications to the underlying data array will be visible in both instances
   *
   * @param parsableByteArray The {@link ParsableByteArray}.
   */
  public void reset(ParsableByteArray parsableByteArray) {
    reset(parsableByteArray.getData(), parsableByteArray.limit());
    setPosition(parsableByteArray.getPosition() * 8);
  }

  /**
   * Updates the instance to wrap {@code data}, and resets the position to zero.
   *
   * @param data The array to wrap.
   * @param limit The limit in bytes.
   */
  public void reset(byte[] data, int limit) {
    this.data = data;
    byteOffset = 0;
    bitOffset = 0;
    byteLimit = limit;
  }

  /** Returns the number of bits yet to be read. */
  public int bitsLeft() {
    return (byteLimit - byteOffset) * 8 - bitOffset;
  }

  /** Returns the current bit offset. */
  public int getPosition() {
    return byteOffset * 8 + bitOffset;
  }

  /**
   * Returns the current byte offset. Must only be called when the position is byte aligned.
   *
   * @throws IllegalStateException If the position isn't byte aligned.
   */
  public int getBytePosition() {
    Assertions.checkState(bitOffset == 0);
    return byteOffset;
  }

  /**
   * Sets the current bit offset.
   *
   * @param position The position to set.
   */
  public void setPosition(int position) {
    byteOffset = position / 8;
    bitOffset = position - (byteOffset * 8);
    assertValidOffset();
  }

  /** Skips a single bit. */
  public void skipBit() {
    if (++bitOffset == 8) {
      bitOffset = 0;
      byteOffset++;
    }
    assertValidOffset();
  }

  /**
   * Skips bits and moves current reading position forward.
   *
   * @param numBits The number of bits to skip.
   */
  public void skipBits(int numBits) {
    int numBytes = numBits / 8;
    byteOffset += numBytes;
    bitOffset += numBits - (numBytes * 8);
    if (bitOffset > 7) {
      byteOffset++;
      bitOffset -= 8;
    }
    assertValidOffset();
  }

  /**
   * Reads a single bit.
   *
   * @return Whether the bit is set.
   */
  public boolean readBit() {
    boolean returnValue = (data[byteOffset] & (0x80 >> bitOffset)) != 0;
    skipBit();
    return returnValue;
  }

  /**
   * Reads up to 32 bits.
   *
   * @param numBits The number of bits to read.
   * @return An integer whose bottom {@code numBits} bits hold the read data.
   */
  public int readBits(int numBits) {
    if (numBits == 0) {
      return 0;
    }
    int returnValue = 0;
    bitOffset += numBits;
    while (bitOffset > 8) {
      bitOffset -= 8;
      returnValue |= (data[byteOffset++] & 0xFF) << bitOffset;
    }
    returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
    returnValue &= 0xFFFFFFFF >>> (32 - numBits);
    if (bitOffset == 8) {
      bitOffset = 0;
      byteOffset++;
    }
    assertValidOffset();
    return returnValue;
  }

  /**
   * Reads up to 64 bits.
   *
   * @param numBits The number of bits to read.
   * @return A long whose bottom {@code numBits} bits hold the read data.
   */
  public long readBitsToLong(int numBits) {
    if (numBits <= 32) {
      return Util.toUnsignedLong(readBits(numBits));
    }
    return Util.toLong(readBits(numBits - 32), readBits(32));
  }

  /**
   * Reads {@code numBits} bits into {@code buffer}.
   *
   * @param buffer The array into which the read data should be written. The trailing {@code numBits
   *     % 8} bits are written into the most significant bits of the last modified {@code buffer}
   *     byte. The remaining ones are unmodified.
   * @param offset The offset in {@code buffer} at which the read data should be written.
   * @param numBits The number of bits to read.
   */
  public void readBits(byte[] buffer, int offset, int numBits) {
    // Whole bytes.
    int to = offset + (numBits >> 3) /* numBits / 8 */;
    for (int i = offset; i < to; i++) {
      buffer[i] = (byte) (data[byteOffset++] << bitOffset);
      buffer[i] = (byte) (buffer[i] | ((data[byteOffset] & 0xFF) >> (8 - bitOffset)));
    }
    // Trailing bits.
    int bitsLeft = numBits & 7 /* numBits % 8 */;
    if (bitsLeft == 0) {
      return;
    }
    // Set bits that are going to be overwritten to 0.
    buffer[to] = (byte) (buffer[to] & (0xFF >> bitsLeft));
    if (bitOffset + bitsLeft > 8) {
      // We read the rest of data[byteOffset] and increase byteOffset.
      buffer[to] = (byte) (buffer[to] | ((data[byteOffset++] & 0xFF) << bitOffset));
      bitOffset -= 8;
    }
    bitOffset += bitsLeft;
    int lastDataByteTrailingBits = (data[byteOffset] & 0xFF) >> (8 - bitOffset);
    buffer[to] |= (byte) (lastDataByteTrailingBits << (8 - bitsLeft));
    if (bitOffset == 8) {
      bitOffset = 0;
      byteOffset++;
    }
    assertValidOffset();
  }

  /**
   * Aligns the position to the next byte boundary. Does nothing if the position is already aligned.
   */
  public void byteAlign() {
    if (bitOffset == 0) {
      return;
    }
    bitOffset = 0;
    byteOffset++;
    assertValidOffset();
  }

  /**
   * Reads the next {@code length} bytes into {@code buffer}. Must only be called when the position
   * is byte aligned.
   *
   * @see System#arraycopy(Object, int, Object, int, int)
   * @param buffer The array into which the read data should be written.
   * @param offset The offset in {@code buffer} at which the read data should be written.
   * @param length The number of bytes to read.
   * @throws IllegalStateException If the position isn't byte aligned.
   */
  public void readBytes(byte[] buffer, int offset, int length) {
    Assertions.checkState(bitOffset == 0);
    System.arraycopy(data, byteOffset, buffer, offset, length);
    byteOffset += length;
    assertValidOffset();
  }

  /**
   * Skips the next {@code length} bytes. Must only be called when the position is byte aligned.
   *
   * @param length The number of bytes to read.
   * @throws IllegalStateException If the position isn't byte aligned.
   */
  public void skipBytes(int length) {
    Assertions.checkState(bitOffset == 0);
    byteOffset += length;
    assertValidOffset();
  }

  /**
   * Reads the next {@code length} bytes as a UTF-8 string. Must only be called when the position is
   * byte aligned.
   *
   * @param length The number of bytes to read.
   * @return The string encoded by the bytes in UTF-8.
   */
  public String readBytesAsString(int length) {
    return readBytesAsString(length, Charsets.UTF_8);
  }

  /**
   * Reads the next {@code length} bytes as a string encoded in {@link Charset}. Must only be called
   * when the position is byte aligned.
   *
   * @param length The number of bytes to read.
   * @param charset The character set of the encoded characters.
   * @return The string encoded by the bytes in the specified character set.
   */
  public String readBytesAsString(int length, Charset charset) {
    byte[] bytes = new byte[length];
    readBytes(bytes, 0, length);
    return new String(bytes, charset);
  }

  /**
   * Overwrites {@code numBits} from this array using the {@code numBits} least significant bits
   * from {@code value}. Bits are written in order from most significant to least significant. The
   * read position is advanced by {@code numBits}.
   *
   * @param value The integer whose {@code numBits} least significant bits are written into {@link
   *     #data}.
   * @param numBits The number of bits to write.
   */
  public void putInt(int value, int numBits) {
    int remainingBitsToRead = numBits;
    if (numBits < 32) {
      value &= (1 << numBits) - 1;
    }
    int firstByteReadSize = min(8 - bitOffset, numBits);
    int firstByteRightPaddingSize = 8 - bitOffset - firstByteReadSize;
    int firstByteBitmask = (0xFF00 >> bitOffset) | ((1 << firstByteRightPaddingSize) - 1);
    data[byteOffset] = (byte) (data[byteOffset] & firstByteBitmask);
    int firstByteInputBits = value >>> (numBits - firstByteReadSize);
    data[byteOffset] =
        (byte) (data[byteOffset] | (firstByteInputBits << firstByteRightPaddingSize));
    remainingBitsToRead -= firstByteReadSize;
    int currentByteIndex = byteOffset + 1;
    while (remainingBitsToRead > 8) {
      data[currentByteIndex++] = (byte) (value >>> (remainingBitsToRead - 8));
      remainingBitsToRead -= 8;
    }
    int lastByteRightPaddingSize = 8 - remainingBitsToRead;
    data[currentByteIndex] =
        (byte) (data[currentByteIndex] & ((1 << lastByteRightPaddingSize) - 1));
    int lastByteInput = value & ((1 << remainingBitsToRead) - 1);
    data[currentByteIndex] =
        (byte) (data[currentByteIndex] | (lastByteInput << lastByteRightPaddingSize));
    skipBits(numBits);
    assertValidOffset();
  }

  private void assertValidOffset() {
    // It is fine for position to be at the end of the array, but no further.
    Assertions.checkState(
        byteOffset >= 0 && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
  }
}
