/*
 * 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 androidx.media3.extractor.mkv;

import static com.google.common.truth.Truth.assertThat;

import androidx.media3.extractor.ExtractorInput;
import androidx.media3.test.utils.FakeExtractorInput;
import androidx.media3.test.utils.TestUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Tests {@link DefaultEbmlReader}. */
@RunWith(AndroidJUnit4.class)
public class DefaultEbmlReaderTest {

  @Test
  public void masterElement() throws IOException {
    ExtractorInput input = createTestInput(0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x85, 0x81, 0x01);
    TestProcessor expected = new TestProcessor();
    expected.startMasterElement(TestProcessor.ID_EBML, 5, 4);
    expected.integerElement(TestProcessor.ID_DOC_TYPE_READ_VERSION, 1);
    expected.endMasterElement(TestProcessor.ID_EBML);
    assertEvents(input, expected.events);
  }

  @Test
  public void masterElementEmpty() throws IOException {
    ExtractorInput input = createTestInput(0x18, 0x53, 0x80, 0x67, 0x80);
    TestProcessor expected = new TestProcessor();
    expected.startMasterElement(TestProcessor.ID_SEGMENT, 5, 0);
    expected.endMasterElement(TestProcessor.ID_SEGMENT);
    assertEvents(input, expected.events);
  }

  @Test
  public void unsignedIntegerElement() throws IOException {
    // 0xFE is chosen because for signed integers it should be interpreted as -2
    ExtractorInput input = createTestInput(0x42, 0xF7, 0x81, 0xFE);
    TestProcessor expected = new TestProcessor();
    expected.integerElement(TestProcessor.ID_EBML_READ_VERSION, 254);
    assertEvents(input, expected.events);
  }

  @Test
  public void unsignedIntegerElementLarge() throws IOException {
    ExtractorInput input =
        createTestInput(0x42, 0xF7, 0x88, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);
    TestProcessor expected = new TestProcessor();
    expected.integerElement(TestProcessor.ID_EBML_READ_VERSION, Long.MAX_VALUE);
    assertEvents(input, expected.events);
  }

  @Test
  public void unsignedIntegerElementTooLargeBecomesNegative() throws IOException {
    ExtractorInput input =
        createTestInput(0x42, 0xF7, 0x88, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);
    TestProcessor expected = new TestProcessor();
    expected.integerElement(TestProcessor.ID_EBML_READ_VERSION, -1);
    assertEvents(input, expected.events);
  }

  @Test
  public void stringElement() throws IOException {
    ExtractorInput input = createTestInput(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x31, 0x32, 0x33);
    TestProcessor expected = new TestProcessor();
    expected.stringElement(TestProcessor.ID_DOC_TYPE, "Abc123");
    assertEvents(input, expected.events);
  }

  @Test
  public void stringElementWithZeroPadding() throws IOException, InterruptedException {
    ExtractorInput input = createTestInput(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x00, 0x00, 0x00);
    TestProcessor expected = new TestProcessor();
    expected.stringElement(TestProcessor.ID_DOC_TYPE, "Abc");
    assertEvents(input, expected.events);
  }

  @Test
  public void stringElementEmpty() throws IOException {
    ExtractorInput input = createTestInput(0x42, 0x82, 0x80);
    TestProcessor expected = new TestProcessor();
    expected.stringElement(TestProcessor.ID_DOC_TYPE, "");
    assertEvents(input, expected.events);
  }

  @Test
  public void floatElementFourBytes() throws IOException {
    ExtractorInput input = createTestInput(0x44, 0x89, 0x84, 0x3F, 0x80, 0x00, 0x00);
    TestProcessor expected = new TestProcessor();
    expected.floatElement(TestProcessor.ID_DURATION, 1.0);
    assertEvents(input, expected.events);
  }

  @Test
  public void floatElementEightBytes() throws IOException {
    ExtractorInput input =
        createTestInput(0x44, 0x89, 0x88, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
    TestProcessor expected = new TestProcessor();
    expected.floatElement(TestProcessor.ID_DURATION, -2.0);
    assertEvents(input, expected.events);
  }

  @Test
  public void binaryElement() throws IOException {
    ExtractorInput input =
        createTestInput(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08);
    TestProcessor expected = new TestProcessor();
    expected.binaryElement(
        TestProcessor.ID_SIMPLE_BLOCK,
        8,
        createTestInput(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08));
    assertEvents(input, expected.events);
  }

  private static void assertEvents(ExtractorInput input, List<String> expectedEvents)
      throws IOException {
    DefaultEbmlReader reader = new DefaultEbmlReader();
    TestProcessor output = new TestProcessor();
    reader.init(output);

    // We expect the number of successful reads to equal the number of expected events.
    for (int i = 0; i < expectedEvents.size(); i++) {
      assertThat(reader.read(input)).isTrue();
    }
    // The next read should be unsuccessful.
    assertThat(reader.read(input)).isFalse();
    // Check that we really did get to the end of input.
    assertThat(input.readFully(new byte[1], 0, 1, true)).isFalse();

    assertThat(output.events).containsExactlyElementsIn(expectedEvents).inOrder();
  }

  /**
   * Helper to build an {@link ExtractorInput} from byte data.
   *
   * @param data Zero or more integers with values between {@code 0x00} and {@code 0xFF}.
   * @return An {@link ExtractorInput} from which the data can be read.
   */
  private static ExtractorInput createTestInput(int... data) {
    return new FakeExtractorInput.Builder()
        .setData(TestUtil.createByteArray(data))
        .setSimulateUnknownLength(true)
        .build();
  }

  /** An {@link EbmlProcessor} that records each event callback. */
  private static final class TestProcessor implements EbmlProcessor {

    // Element IDs
    private static final int ID_EBML = 0x1A45DFA3;
    private static final int ID_EBML_READ_VERSION = 0x42F7;
    private static final int ID_DOC_TYPE = 0x4282;
    private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;

    private static final int ID_SEGMENT = 0x18538067;
    private static final int ID_DURATION = 0x4489;
    private static final int ID_SIMPLE_BLOCK = 0xA3;

    private final List<String> events = new ArrayList<>();

    @Override
    public @EbmlProcessor.ElementType int getElementType(int id) {
      switch (id) {
        case ID_EBML:
        case ID_SEGMENT:
          return EbmlProcessor.ELEMENT_TYPE_MASTER;
        case ID_EBML_READ_VERSION:
        case ID_DOC_TYPE_READ_VERSION:
          return EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT;
        case ID_DOC_TYPE:
          return EbmlProcessor.ELEMENT_TYPE_STRING;
        case ID_SIMPLE_BLOCK:
          return EbmlProcessor.ELEMENT_TYPE_BINARY;
        case ID_DURATION:
          return EbmlProcessor.ELEMENT_TYPE_FLOAT;
        default:
          return EbmlProcessor.ELEMENT_TYPE_UNKNOWN;
      }
    }

    @Override
    public boolean isLevel1Element(int id) {
      return false;
    }

    @Override
    public void startMasterElement(int id, long contentPosition, long contentSize) {
      events.add(
          formatEvent(
              id, "start contentPosition=" + contentPosition + " contentSize=" + contentSize));
    }

    @Override
    public void endMasterElement(int id) {
      events.add(formatEvent(id, "end"));
    }

    @Override
    public void integerElement(int id, long value) {
      events.add(formatEvent(id, "integer=" + value));
    }

    @Override
    public void floatElement(int id, double value) {
      events.add(formatEvent(id, "float=" + value));
    }

    @Override
    public void stringElement(int id, String value) {
      events.add(formatEvent(id, "string=" + value));
    }

    @Override
    public void binaryElement(int id, int contentSize, ExtractorInput input) throws IOException {
      byte[] bytes = new byte[contentSize];
      input.readFully(bytes, 0, contentSize);
      events.add(formatEvent(id, "bytes=" + Arrays.toString(bytes)));
    }

    private static String formatEvent(int id, String event) {
      return "[" + Integer.toHexString(id) + "] " + event;
    }
  }
}
