/*
 * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source;

import androidx.annotation.Nullable;
import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.CryptoInfo;
import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput.CryptoData;
import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue.SampleExtrasHolder;
import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocation;
import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;

/** A queue of media sample data. */
/* package */ class SampleDataQueue {

  private static final int INITIAL_SCRATCH_SIZE = 32;

  private final Allocator allocator;
  private final int allocationLength;
  private final ParsableByteArray scratch;

  // References into the linked list of allocations.
  private AllocationNode firstAllocationNode;
  private AllocationNode readAllocationNode;
  private AllocationNode writeAllocationNode;

  // Accessed only by the loading thread (or the consuming thread when there is no loading thread).
  private long totalBytesWritten;

  public SampleDataQueue(Allocator allocator) {
    this.allocator = allocator;
    allocationLength = allocator.getIndividualAllocationLength();
    scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
    firstAllocationNode = new AllocationNode(/* startPosition= */ 0, allocationLength);
    readAllocationNode = firstAllocationNode;
    writeAllocationNode = firstAllocationNode;
  }

  // Called by the consuming thread, but only when there is no loading thread.

  /** Clears all sample data. */
  public void reset() {
    clearAllocationNodes(firstAllocationNode);
    firstAllocationNode = new AllocationNode(0, allocationLength);
    readAllocationNode = firstAllocationNode;
    writeAllocationNode = firstAllocationNode;
    totalBytesWritten = 0;
    allocator.trim();
  }

  /**
   * Discards sample data bytes from the write side of the queue.
   *
   * @param totalBytesWritten The reduced total number of bytes written after the samples have been
   *     discarded, or 0 if the queue is now empty.
   */
  public void discardUpstreamSampleBytes(long totalBytesWritten) {
    this.totalBytesWritten = totalBytesWritten;
    if (this.totalBytesWritten == 0
        || this.totalBytesWritten == firstAllocationNode.startPosition) {
      clearAllocationNodes(firstAllocationNode);
      firstAllocationNode = new AllocationNode(this.totalBytesWritten, allocationLength);
      readAllocationNode = firstAllocationNode;
      writeAllocationNode = firstAllocationNode;
    } else {
      // Find the last node containing at least 1 byte of data that we need to keep.
      AllocationNode lastNodeToKeep = firstAllocationNode;
      while (this.totalBytesWritten > lastNodeToKeep.endPosition) {
        lastNodeToKeep = lastNodeToKeep.next;
      }
      // Discard all subsequent nodes.
      AllocationNode firstNodeToDiscard = lastNodeToKeep.next;
      clearAllocationNodes(firstNodeToDiscard);
      // Reset the successor of the last node to be an uninitialized node.
      lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength);
      // Update writeAllocationNode and readAllocationNode as necessary.
      writeAllocationNode =
          this.totalBytesWritten == lastNodeToKeep.endPosition
              ? lastNodeToKeep.next
              : lastNodeToKeep;
      if (readAllocationNode == firstNodeToDiscard) {
        readAllocationNode = lastNodeToKeep.next;
      }
    }
  }

  // Called by the consuming thread.

  /** Rewinds the read position to the first sample in the queue. */
  public void rewind() {
    readAllocationNode = firstAllocationNode;
  }

  /**
   * Reads data from the rolling buffer to populate a decoder input buffer.
   *
   * @param buffer The buffer to populate.
   * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
   */
  public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {
    // Read encryption data if the sample is encrypted.
    if (buffer.isEncrypted()) {
      readEncryptionData(buffer, extrasHolder);
    }
    // Read sample data, extracting supplemental data into a separate buffer if needed.
    if (buffer.hasSupplementalData()) {
      // If there is supplemental data, the sample data is prefixed by its size.
      scratch.reset(4);
      readData(extrasHolder.offset, scratch.data, 4);
      int sampleSize = scratch.readUnsignedIntToInt();
      extrasHolder.offset += 4;
      extrasHolder.size -= 4;

      // Write the sample data.
      buffer.ensureSpaceForWrite(sampleSize);
      readData(extrasHolder.offset, buffer.data, sampleSize);
      extrasHolder.offset += sampleSize;
      extrasHolder.size -= sampleSize;

      // Write the remaining data as supplemental data.
      buffer.resetSupplementalData(extrasHolder.size);
      readData(extrasHolder.offset, buffer.supplementalData, extrasHolder.size);
    } else {
      // Write the sample data.
      buffer.ensureSpaceForWrite(extrasHolder.size);
      readData(extrasHolder.offset, buffer.data, extrasHolder.size);
    }
  }

  /**
   * Advances the read position to the specified absolute position.
   *
   * @param absolutePosition The new absolute read position. May be {@link C#POSITION_UNSET}, in
   *     which case calling this method is a no-op.
   */
  public void discardDownstreamTo(long absolutePosition) {
    if (absolutePosition == C.POSITION_UNSET) {
      return;
    }
    while (absolutePosition >= firstAllocationNode.endPosition) {
      // Advance firstAllocationNode to the specified absolute position. Also clear nodes that are
      // advanced past, and return their underlying allocations to the allocator.
      allocator.release(firstAllocationNode.allocation);
      firstAllocationNode = firstAllocationNode.clear();
    }
    if (readAllocationNode.startPosition < firstAllocationNode.startPosition) {
      // We discarded the node referenced by readAllocationNode. We need to advance it to the first
      // remaining node.
      readAllocationNode = firstAllocationNode;
    }
  }

  // Called by the loading thread.

  public long getTotalBytesWritten() {
    return totalBytesWritten;
  }

  public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
      throws IOException, InterruptedException {
    length = preAppend(length);
    int bytesAppended =
        input.read(
            writeAllocationNode.allocation.data,
            writeAllocationNode.translateOffset(totalBytesWritten),
            length);
    if (bytesAppended == C.RESULT_END_OF_INPUT) {
      if (allowEndOfInput) {
        return C.RESULT_END_OF_INPUT;
      }
      throw new EOFException();
    }
    postAppend(bytesAppended);
    return bytesAppended;
  }

  public void sampleData(ParsableByteArray buffer, int length) {
    while (length > 0) {
      int bytesAppended = preAppend(length);
      buffer.readBytes(
          writeAllocationNode.allocation.data,
          writeAllocationNode.translateOffset(totalBytesWritten),
          bytesAppended);
      length -= bytesAppended;
      postAppend(bytesAppended);
    }
  }

  // Private methods.

  /**
   * Reads encryption data for the current sample.
   *
   * <p>The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link
   * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same
   * value is added to {@link SampleExtrasHolder#offset}.
   *
   * @param buffer The buffer into which the encryption data should be written.
   * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
   */
  private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {
    long offset = extrasHolder.offset;

    // Read the signal byte.
    scratch.reset(1);
    readData(offset, scratch.data, 1);
    offset++;
    byte signalByte = scratch.data[0];
    boolean subsampleEncryption = (signalByte & 0x80) != 0;
    int ivSize = signalByte & 0x7F;

    // Read the initialization vector.
    CryptoInfo cryptoInfo = buffer.cryptoInfo;
    if (cryptoInfo.iv == null) {
      cryptoInfo.iv = new byte[16];
    } else {
      // Zero out cryptoInfo.iv so that if ivSize < 16, the remaining bytes are correctly set to 0.
      Arrays.fill(cryptoInfo.iv, (byte) 0);
    }
    readData(offset, cryptoInfo.iv, ivSize);
    offset += ivSize;

    // Read the subsample count, if present.
    int subsampleCount;
    if (subsampleEncryption) {
      scratch.reset(2);
      readData(offset, scratch.data, 2);
      offset += 2;
      subsampleCount = scratch.readUnsignedShort();
    } else {
      subsampleCount = 1;
    }

    // Write the clear and encrypted subsample sizes.
    @Nullable int[] clearDataSizes = cryptoInfo.numBytesOfClearData;
    if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
      clearDataSizes = new int[subsampleCount];
    }
    @Nullable int[] encryptedDataSizes = cryptoInfo.numBytesOfEncryptedData;
    if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
      encryptedDataSizes = new int[subsampleCount];
    }
    if (subsampleEncryption) {
      int subsampleDataLength = 6 * subsampleCount;
      scratch.reset(subsampleDataLength);
      readData(offset, scratch.data, subsampleDataLength);
      offset += subsampleDataLength;
      scratch.setPosition(0);
      for (int i = 0; i < subsampleCount; i++) {
        clearDataSizes[i] = scratch.readUnsignedShort();
        encryptedDataSizes[i] = scratch.readUnsignedIntToInt();
      }
    } else {
      clearDataSizes[0] = 0;
      encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset);
    }

    // Populate the cryptoInfo.
    CryptoData cryptoData = extrasHolder.cryptoData;
    cryptoInfo.set(
        subsampleCount,
        clearDataSizes,
        encryptedDataSizes,
        cryptoData.encryptionKey,
        cryptoInfo.iv,
        cryptoData.cryptoMode,
        cryptoData.encryptedBlocks,
        cryptoData.clearBlocks);

    // Adjust the offset and size to take into account the bytes read.
    int bytesRead = (int) (offset - extrasHolder.offset);
    extrasHolder.offset += bytesRead;
    extrasHolder.size -= bytesRead;
  }

  /**
   * Reads data from the front of the rolling buffer.
   *
   * @param absolutePosition The absolute position from which data should be read.
   * @param target The buffer into which data should be written.
   * @param length The number of bytes to read.
   */
  private void readData(long absolutePosition, ByteBuffer target, int length) {
    advanceReadTo(absolutePosition);
    int remaining = length;
    while (remaining > 0) {
      int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));
      Allocation allocation = readAllocationNode.allocation;
      target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy);
      remaining -= toCopy;
      absolutePosition += toCopy;
      if (absolutePosition == readAllocationNode.endPosition) {
        readAllocationNode = readAllocationNode.next;
      }
    }
  }

  /**
   * Reads data from the front of the rolling buffer.
   *
   * @param absolutePosition The absolute position from which data should be read.
   * @param target The array into which data should be written.
   * @param length The number of bytes to read.
   */
  private void readData(long absolutePosition, byte[] target, int length) {
    advanceReadTo(absolutePosition);
    int remaining = length;
    while (remaining > 0) {
      int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));
      Allocation allocation = readAllocationNode.allocation;
      System.arraycopy(
          allocation.data,
          readAllocationNode.translateOffset(absolutePosition),
          target,
          length - remaining,
          toCopy);
      remaining -= toCopy;
      absolutePosition += toCopy;
      if (absolutePosition == readAllocationNode.endPosition) {
        readAllocationNode = readAllocationNode.next;
      }
    }
  }

  /**
   * Advances the read position to the specified absolute position.
   *
   * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced.
   */
  private void advanceReadTo(long absolutePosition) {
    while (absolutePosition >= readAllocationNode.endPosition) {
      readAllocationNode = readAllocationNode.next;
    }
  }

  /**
   * Clears allocation nodes starting from {@code fromNode}.
   *
   * @param fromNode The node from which to clear.
   */
  private void clearAllocationNodes(AllocationNode fromNode) {
    if (!fromNode.wasInitialized) {
      return;
    }
    // Bulk release allocations for performance (it's significantly faster when using
    // DefaultAllocator because the allocator's lock only needs to be acquired and released once)
    // [Internal: See b/29542039].
    int allocationCount =
        (writeAllocationNode.wasInitialized ? 1 : 0)
            + ((int) (writeAllocationNode.startPosition - fromNode.startPosition)
                / allocationLength);
    Allocation[] allocationsToRelease = new Allocation[allocationCount];
    AllocationNode currentNode = fromNode;
    for (int i = 0; i < allocationsToRelease.length; i++) {
      allocationsToRelease[i] = currentNode.allocation;
      currentNode = currentNode.clear();
    }
    allocator.release(allocationsToRelease);
  }

  /**
   * Called before writing sample data to {@link #writeAllocationNode}. May cause {@link
   * #writeAllocationNode} to be initialized.
   *
   * @param length The number of bytes that the caller wishes to write.
   * @return The number of bytes that the caller is permitted to write, which may be less than
   *     {@code length}.
   */
  private int preAppend(int length) {
    if (!writeAllocationNode.wasInitialized) {
      writeAllocationNode.initialize(
          allocator.allocate(),
          new AllocationNode(writeAllocationNode.endPosition, allocationLength));
    }
    return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten));
  }

  /**
   * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced.
   *
   * @param length The number of bytes that were written.
   */
  private void postAppend(int length) {
    totalBytesWritten += length;
    if (totalBytesWritten == writeAllocationNode.endPosition) {
      writeAllocationNode = writeAllocationNode.next;
    }
  }

  /** A node in a linked list of {@link Allocation}s held by the output. */
  private static final class AllocationNode {

    /** The absolute position of the start of the data (inclusive). */
    public final long startPosition;
    /** The absolute position of the end of the data (exclusive). */
    public final long endPosition;
    /** Whether the node has been initialized. Remains true after {@link #clear()}. */
    public boolean wasInitialized;
    /** The {@link Allocation}, or {@code null} if the node is not initialized. */
    @Nullable public Allocation allocation;
    /**
     * The next {@link AllocationNode} in the list, or {@code null} if the node has not been
     * initialized. Remains set after {@link #clear()}.
     */
    @Nullable public AllocationNode next;

    /**
     * @param startPosition See {@link #startPosition}.
     * @param allocationLength The length of the {@link Allocation} with which this node will be
     *     initialized.
     */
    public AllocationNode(long startPosition, int allocationLength) {
      this.startPosition = startPosition;
      this.endPosition = startPosition + allocationLength;
    }

    /**
     * Initializes the node.
     *
     * @param allocation The node's {@link Allocation}.
     * @param next The next {@link AllocationNode}.
     */
    public void initialize(Allocation allocation, AllocationNode next) {
      this.allocation = allocation;
      this.next = next;
      wasInitialized = true;
    }

    /**
     * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to
     * the specified absolute position.
     *
     * @param absolutePosition The absolute position.
     * @return The corresponding offset into the allocation's data.
     */
    public int translateOffset(long absolutePosition) {
      return (int) (absolutePosition - startPosition) + allocation.offset;
    }

    /**
     * Clears {@link #allocation} and {@link #next}.
     *
     * @return The cleared next {@link AllocationNode}.
     */
    public AllocationNode clear() {
      allocation = null;
      AllocationNode temp = next;
      next = null;
      return temp;
    }
  }
}
