/*
 * Copyright (C) 2017 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 androidx.media3.common.util;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.base.Charsets;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Tests for {@link ParsableBitArray}. */
@RunWith(AndroidJUnit4.class)
public final class ParsableBitArrayTest {

  @Test
  public void readAllBytes() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F);
    ParsableBitArray testArray = new ParsableBitArray(testData);
    byte[] bytesRead = new byte[testData.length];

    testArray.readBytes(bytesRead, 0, testData.length);

    assertThat(bytesRead).isEqualTo(testData);
    assertThat(testArray.getPosition()).isEqualTo(testData.length * 8);
    assertThat(testArray.getBytePosition()).isEqualTo(testData.length);
  }

  @Test
  public void readBitInSameByte() {
    byte[] testData = createByteArray(0, 0b00110000);
    ParsableBitArray testArray = new ParsableBitArray(testData);
    testArray.setPosition(10);

    assertThat(testArray.readBit()).isTrue();
    assertThat(testArray.readBit()).isTrue();
    assertThat(testArray.readBit()).isFalse();
    assertThat(testArray.readBit()).isFalse();
  }

  @Test
  public void readBitInMultipleBytes() {
    byte[] testData = createByteArray(1, 1 << 7);
    ParsableBitArray testArray = new ParsableBitArray(testData);
    testArray.setPosition(6);

    assertThat(testArray.readBit()).isFalse();
    assertThat(testArray.readBit()).isTrue();
    assertThat(testArray.readBit()).isTrue();
    assertThat(testArray.readBit()).isFalse();
  }

  @Test
  public void readBits0Bits() {
    byte[] testData = createByteArray(0x3C);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    int result = testArray.readBits(0);

    assertThat(result).isEqualTo(0);
  }

  @Test
  public void readBitsByteAligned() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F, 0x01);
    ParsableBitArray testArray = new ParsableBitArray(testData);
    testArray.readBits(8);

    int result = testArray.readBits(18);

    assertThat(result).isEqualTo(0xD25F << 2);
    assertThat(testArray.getPosition()).isEqualTo(26);
  }

  @Test
  public void readBitsNonByteAligned() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F);
    ParsableBitArray testArray = new ParsableBitArray(testData);
    testArray.readBits(3);

    int result = testArray.readBits(14);

    assertThat(result).isEqualTo((0x3C & 0b11111) << 9 | 0xD2 << 1 | 0x5F >> 7);
    assertThat(testArray.getPosition()).isEqualTo(17);
  }

  @Test
  public void readBitsNegativeValue() {
    byte[] testData = createByteArray(0xF0, 0, 0, 0);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    int result = testArray.readBits(32);

    assertThat(result).isEqualTo(0xF0000000);
  }

  @Test
  public void readBitsToLong0Bits() {
    byte[] testData = createByteArray(0x3C);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    long result = testArray.readBitsToLong(0);

    assertThat(result).isEqualTo(0);
  }

  @Test
  public void readBitsToLongByteAligned() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F, 0x01, 0xFF, 0x14, 0x60);
    ParsableBitArray testArray = new ParsableBitArray(testData);
    testArray.readBits(8);

    long result = testArray.readBitsToLong(45);

    assertThat(result).isEqualTo(0xD25F01FF14L << 5 | 0x60 >> 3);
    assertThat(testArray.getPosition()).isEqualTo(53);
  }

  @Test
  public void readBitsToLongNonByteAligned() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F, 0x01, 0xFF, 0x14, 0x60);
    ParsableBitArray testArray = new ParsableBitArray(testData);
    testArray.readBits(3);

    long result = testArray.readBitsToLong(53);

    assertThat(result).isEqualTo((0x3CL & 0b11111) << 48 | 0xD25F01FF1460L);
    assertThat(testArray.getPosition()).isEqualTo(56);
  }

  @Test
  public void readBitsToLongNegativeValue() {
    byte[] testData = createByteArray(0xF0, 0, 0, 0, 0, 0, 0, 0);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    long result = testArray.readBitsToLong(64);

    assertThat(result).isEqualTo(0xF000000000000000L);
  }

  @Test
  public void readBitsToByteArray() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F, 0x01, 0xFF, 0x14, 0x60, 0x99);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    int numBytes = testData.length;
    byte[] result = new byte[numBytes];
    // Test read within byte boundaries.
    testArray.readBits(result, 0, 6);
    assertThat(result[0]).isEqualTo((byte) (testData[0] & 0xFC));
    // Test read across byte boundaries.
    testArray.readBits(result, 0, 8);
    assertThat(result[0])
        .isEqualTo((byte) (((testData[0] & 0x03) << 6) | ((testData[1] & 0xFC) >> 2)));
    // Test reading across multiple bytes.
    testArray.readBits(result, 1, numBytes * 8 - 14);
    for (int i = 1; i < numBytes - 1; i++) {
      assertThat(result[i])
          .isEqualTo((byte) (((testData[i] & 0x03) << 6) | ((testData[i + 1] & 0xFC) >> 2)));
    }
    assertThat(result[numBytes - 1]).isEqualTo((byte) ((testData[numBytes - 1] & 0x03) << 6));
    assertThat(testArray.bitsLeft()).isEqualTo(0);
    // Test read last buffer byte across input data bytes.
    testArray.setPosition(31);
    result[3] = 0;
    testArray.readBits(result, 3, 3);
    assertThat(result[3]).isEqualTo((byte) 0xE0);
    // Test read bits in the middle of a input data byte.
    result[0] = 0;
    assertThat(testArray.getPosition()).isEqualTo(34);
    testArray.readBits(result, 0, 3);
    assertThat(result[0]).isEqualTo((byte) 0xE0);
    // Test read 0 bits.
    testArray.setPosition(32);
    result[1] = 0;
    testArray.readBits(result, 1, 0);
    assertThat(result[1]).isEqualTo((byte) 0);
    // Test reading a number of bits divisible by 8.
    testArray.setPosition(0);
    testArray.readBits(result, 0, 16);
    assertThat(result[0]).isEqualTo(testData[0]);
    assertThat(result[1]).isEqualTo(testData[1]);
    // Test least significant bits are unmodified.
    result[1] = (byte) 0xFF;
    testArray.readBits(result, 0, 9);
    assertThat(result[0]).isEqualTo((byte) 0x5F);
    assertThat(result[1]).isEqualTo((byte) 0x7F);
  }

  @Test
  public void skipBytes() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F, 0x01);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    testArray.skipBytes(2);

    assertThat(testArray.readBits(8)).isEqualTo(0x5F);
  }

  @Test
  public void skipBitsByteAligned() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F, 0x01);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    testArray.skipBits(16);

    assertThat(testArray.readBits(8)).isEqualTo(0x5F);
  }

  @Test
  public void skipBitsNonByteAligned() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F, 0x01);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    testArray.skipBits(5);

    assertThat(testArray.readBits(11)).isEqualTo((0x3C & 0b111) << 8 | 0xD2);
  }

  @Test
  public void setPositionByteAligned() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F, 0x01);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    testArray.setPosition(16);

    assertThat(testArray.readBits(8)).isEqualTo(0x5F);
  }

  @Test
  public void setPositionNonByteAligned() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F, 0x01);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    testArray.setPosition(5);

    assertThat(testArray.readBits(11)).isEqualTo((0x3C & 0b111) << 8 | 0xD2);
  }

  @Test
  public void byteAlignFromNonByteAligned() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F, 0x01);
    ParsableBitArray testArray = new ParsableBitArray(testData);
    testArray.setPosition(11);

    testArray.byteAlign();

    assertThat(testArray.getBytePosition()).isEqualTo(2);
    assertThat(testArray.getPosition()).isEqualTo(16);
    assertThat(testArray.readBits(8)).isEqualTo(0x5F);
  }

  @Test
  public void byteAlignFromByteAligned() {
    byte[] testData = createByteArray(0x3C, 0xD2, 0x5F, 0x01);
    ParsableBitArray testArray = new ParsableBitArray(testData);
    testArray.setPosition(16);

    testArray.byteAlign(); // Should be a no-op.

    assertThat(testArray.getBytePosition()).isEqualTo(2);
    assertThat(testArray.getPosition()).isEqualTo(16);
    assertThat(testArray.readBits(8)).isEqualTo(0x5F);
  }

  @Test
  public void readBytesAsStringDefaultsToUtf8() {
    byte[] testData = "a non-åscii strìng".getBytes(Charsets.UTF_8);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    testArray.skipBytes(2);
    assertThat(testArray.readBytesAsString(testData.length - 2)).isEqualTo("non-åscii strìng");
  }

  @Test
  public void readBytesAsStringExplicitCharset() {
    byte[] testData = "a non-åscii strìng".getBytes(Charsets.UTF_16);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    testArray.skipBytes(6);
    assertThat(testArray.readBytesAsString(testData.length - 6, Charsets.UTF_16))
        .isEqualTo("non-åscii strìng");
  }

  @Test
  public void readBytesNotByteAligned() {
    String testString = "test string";
    byte[] testData = testString.getBytes(Charsets.UTF_8);
    ParsableBitArray testArray = new ParsableBitArray(testData);

    testArray.skipBit();
    assertThrows(IllegalStateException.class, () -> testArray.readBytesAsString(2));
  }

  @Test
  public void putBitsWithinByte() {
    ParsableBitArray output = new ParsableBitArray(new byte[4]);
    output.skipBits(1);

    output.putInt(0x3F, 5);

    output.setPosition(0);
    assertThat(output.readBits(8)).isEqualTo(0x1F << 2); // Check that only 5 bits are modified.
  }

  @Test
  public void putBitsAcrossTwoBytes() {
    ParsableBitArray output = new ParsableBitArray(new byte[4]);
    output.setPosition(12);

    output.putInt(0xFF, 8);

    output.setPosition(8);
    assertThat(output.readBits(16)).isEqualTo(0x0FF0);
  }

  @Test
  public void putBitsAcrossMultipleBytes() {
    ParsableBitArray output = new ParsableBitArray(new byte[8]);
    output.setPosition(31); // Writing starts at 31 to test the 30th bit is not modified.

    output.putInt(0xFF146098, 30); // Write only 30 to test the 61st bit is not modified.

    output.setPosition(30);
    assertThat(output.readBits(32)).isEqualTo(0x3F146098 << 1);
  }

  @Test
  public void put32Bits() {
    ParsableBitArray output = new ParsableBitArray(new byte[5]);
    output.setPosition(4);

    output.putInt(0xFF146098, 32);

    output.setPosition(4);
    assertThat(output.readBits(32)).isEqualTo(0xFF146098);
  }

  @Test
  public void putFullBytes() {
    ParsableBitArray output = new ParsableBitArray(new byte[2]);

    output.putInt(0x81, 8);

    output.setPosition(0);
    assertThat(output.readBits(8)).isEqualTo(0x81);
  }

  @Test
  public void noOverwriting() {
    ParsableBitArray output = new ParsableBitArray(createByteArray(0xFF, 0xFF, 0xFF, 0xFF, 0xFF));
    output.setPosition(1);

    output.putInt(0, 30);

    output.setPosition(0);
    assertThat(output.readBits(32)).isEqualTo(0x80000001);
  }

  /** Converts an array of integers in the range [0, 255] into an equivalent byte array. */
  // TODO(internal b/161804035): Use TestUtils when it's available in a dependency we can use here.
  private static byte[] createByteArray(int... bytes) {
    byte[] byteArray = new byte[bytes.length];
    for (int i = 0; i < byteArray.length; i++) {
      Assertions.checkState(0x00 <= bytes[i] && bytes[i] <= 0xFF);
      byteArray[i] = (byte) bytes[i];
    }
    return byteArray;
  }
}
