/*
 * Copyright (C) 2018 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.extractor.ts;

import static java.lang.Math.min;

import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;

/**
 * A seeker that supports seeking within TS stream using binary search.
 *
 * <p>This seeker uses the first and last PCR values within the stream, as well as the stream
 * duration to interpolate the PCR value of the seeking position. Then it performs binary search
 * within the stream to find a packets whose PCR value is within {@link #SEEK_TOLERANCE_US} from the
 * target PCR.
 */
/* package */ final class TsBinarySearchSeeker extends BinarySearchSeeker {

  private static final long SEEK_TOLERANCE_US = 100_000;
  private static final int MINIMUM_SEARCH_RANGE_BYTES = 5 * TsExtractor.TS_PACKET_SIZE;

  public TsBinarySearchSeeker(
      TimestampAdjuster pcrTimestampAdjuster,
      long streamDurationUs,
      long inputLength,
      int pcrPid,
      int timestampSearchBytes) {
    super(
        new DefaultSeekTimestampConverter(),
        new TsPcrSeeker(pcrPid, pcrTimestampAdjuster, timestampSearchBytes),
        streamDurationUs,
        /* floorTimePosition= */ 0,
        /* ceilingTimePosition= */ streamDurationUs + 1,
        /* floorBytePosition= */ 0,
        /* ceilingBytePosition= */ inputLength,
        /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE,
        MINIMUM_SEARCH_RANGE_BYTES);
  }

  /**
   * A {@link TimestampSeeker} implementation that looks for a given PCR timestamp at a given
   * position in a TS stream.
   *
   * <p>Given a PCR timestamp, and a position within a TS stream, this seeker will peek up to {@link
   * #timestampSearchBytes} from that stream position, look for all packets with PID equal to
   * PCR_PID, and then compare the PCR timestamps (if available) of these packets to the target
   * timestamp.
   */
  private static final class TsPcrSeeker implements TimestampSeeker {

    private final TimestampAdjuster pcrTimestampAdjuster;
    private final ParsableByteArray packetBuffer;
    private final int pcrPid;
    private final int timestampSearchBytes;

    public TsPcrSeeker(
        int pcrPid, TimestampAdjuster pcrTimestampAdjuster, int timestampSearchBytes) {
      this.pcrPid = pcrPid;
      this.pcrTimestampAdjuster = pcrTimestampAdjuster;
      this.timestampSearchBytes = timestampSearchBytes;
      packetBuffer = new ParsableByteArray();
    }

    @Override
    public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
        throws IOException {
      long inputPosition = input.getPosition();
      int bytesToSearch = (int) min(timestampSearchBytes, input.getLength() - inputPosition);

      packetBuffer.reset(bytesToSearch);
      input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch);

      return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
    }

    private TimestampSearchResult searchForPcrValueInBuffer(
        ParsableByteArray packetBuffer, long targetPcrTimeUs, long bufferStartOffset) {
      int limit = packetBuffer.limit();

      long startOfLastPacketPosition = C.POSITION_UNSET;
      long endOfLastPacketPosition = C.POSITION_UNSET;
      long lastPcrTimeUsInRange = C.TIME_UNSET;

      while (packetBuffer.bytesLeft() >= TsExtractor.TS_PACKET_SIZE) {
        int startOfPacket =
            TsUtil.findSyncBytePosition(packetBuffer.getData(), packetBuffer.getPosition(), limit);
        int endOfPacket = startOfPacket + TsExtractor.TS_PACKET_SIZE;
        if (endOfPacket > limit) {
          break;
        }
        long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, startOfPacket, pcrPid);
        if (pcrValue != C.TIME_UNSET) {
          long pcrTimeUs = pcrTimestampAdjuster.adjustTsTimestamp(pcrValue);
          if (pcrTimeUs > targetPcrTimeUs) {
            if (lastPcrTimeUsInRange == C.TIME_UNSET) {
              // First PCR timestamp is already over target.
              return TimestampSearchResult.overestimatedResult(pcrTimeUs, bufferStartOffset);
            } else {
              // Last PCR timestamp < target timestamp < this timestamp.
              return TimestampSearchResult.targetFoundResult(
                  bufferStartOffset + startOfLastPacketPosition);
            }
          } else if (pcrTimeUs + SEEK_TOLERANCE_US > targetPcrTimeUs) {
            long startOfPacketInStream = bufferStartOffset + startOfPacket;
            return TimestampSearchResult.targetFoundResult(startOfPacketInStream);
          }

          lastPcrTimeUsInRange = pcrTimeUs;
          startOfLastPacketPosition = startOfPacket;
        }
        packetBuffer.setPosition(endOfPacket);
        endOfLastPacketPosition = endOfPacket;
      }

      if (lastPcrTimeUsInRange != C.TIME_UNSET) {
        long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;
        return TimestampSearchResult.underestimatedResult(
            lastPcrTimeUsInRange, endOfLastPacketPositionInStream);
      } else {
        return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
      }
    }

    @Override
    public void onSeekFinished() {
      packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
    }
  }
}
