/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.media;

import android.util.Log;
import androidx.annotation.OptIn;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.BaseRenderer;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.source.SampleStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.mozilla.geckoview.BuildConfig;

@OptIn(markerClass = UnstableApi.class)
public abstract class GeckoHlsRendererBase extends BaseRenderer {
  protected static final int QUEUED_INPUT_SAMPLE_DURATION_THRESHOLD = 1000000; // 1sec
  protected final FormatHolder mFormatHolder = new FormatHolder();
  /*
   *  DEBUG/LOGTAG will be set in the 2 subclass GeckoHlsAudioRenderer and
   *  GeckoHlsVideoRenderer, and we still wants to log message in the base class
   *  GeckoHlsRendererBase, so neither 'static' nor 'final' are applied to them.
   */
  protected boolean DEBUG;
  protected String LOGTAG;
  // Notify GeckoHlsPlayer about renderer's status, i.e. data has arrived.
  protected GeckoHlsPlayer.ComponentEventDispatcher mPlayerEventDispatcher;

  protected ConcurrentLinkedQueue<GeckoHLSSample> mDemuxedInputSamples =
      new ConcurrentLinkedQueue<>();

  protected ByteBuffer mInputBuffer = null;
  protected ArrayList<Format> mFormats = new ArrayList<Format>();
  protected boolean mInitialized = false;
  protected boolean mWaitingForData = true;
  protected boolean mInputStreamEnded = false;
  protected long mFirstSampleStartTime = Long.MIN_VALUE;

  protected abstract void createInputBuffer() throws ExoPlaybackException;

  protected abstract void handleReconfiguration(DecoderInputBuffer bufferForRead);

  protected abstract void handleFormatRead(DecoderInputBuffer bufferForRead)
      throws ExoPlaybackException;

  protected abstract void handleEndOfStream(DecoderInputBuffer bufferForRead);

  protected abstract void handleSamplePreparation(DecoderInputBuffer bufferForRead);

  protected abstract void resetRenderer();

  protected abstract boolean clearInputSamplesQueue();

  protected abstract void notifyPlayerInputFormatChanged(Format newFormat);

  private DecoderInputBuffer mBufferForRead =
      new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
  private final DecoderInputBuffer mNoDataBuffer = DecoderInputBuffer.newNoDataInstance();

  protected void assertTrue(final boolean condition) {
    if (DEBUG && !condition) {
      throw new AssertionError("Expected condition to be true");
    }
  }

  public GeckoHlsRendererBase(
      final int trackType, final GeckoHlsPlayer.ComponentEventDispatcher eventDispatcher) {
    super(trackType);
    mPlayerEventDispatcher = eventDispatcher;
  }

  private boolean isQueuedEnoughData() {
    if (mDemuxedInputSamples.isEmpty()) {
      return false;
    }

    final Iterator<GeckoHLSSample> iter = mDemuxedInputSamples.iterator();
    long firstPTS = 0;
    if (iter.hasNext()) {
      final GeckoHLSSample sample = iter.next();
      firstPTS = sample.info.presentationTimeUs;
    }
    long lastPTS = firstPTS;
    while (iter.hasNext()) {
      final GeckoHLSSample sample = iter.next();
      lastPTS = sample.info.presentationTimeUs;
    }
    return Math.abs(lastPTS - firstPTS) > QUEUED_INPUT_SAMPLE_DURATION_THRESHOLD;
  }

  public Format getFormat(final int index) {
    assertTrue(index >= 0);
    final Format fmt = index < mFormats.size() ? mFormats.get(index) : null;
    if (DEBUG) {
      Log.d(LOGTAG, "getFormat : index = " + index + ", format : " + fmt);
    }
    return fmt;
  }

  public synchronized long getFirstSamplePTS() {
    return mFirstSampleStartTime;
  }

  public synchronized ConcurrentLinkedQueue<GeckoHLSSample> getQueuedSamples(final int number) {
    final ConcurrentLinkedQueue<GeckoHLSSample> samples =
        new ConcurrentLinkedQueue<GeckoHLSSample>();

    GeckoHLSSample sample = null;
    final int queuedSize = mDemuxedInputSamples.size();
    for (int i = 0; i < queuedSize; i++) {
      if (i >= number) {
        break;
      }
      sample = mDemuxedInputSamples.poll();
      samples.offer(sample);
    }

    sample = samples.isEmpty() ? null : samples.peek();
    if (sample == null) {
      if (DEBUG) {
        Log.d(LOGTAG, "getQueuedSamples isEmpty, mWaitingForData = true !");
      }
      mWaitingForData = true;
    } else if (mFirstSampleStartTime == Long.MIN_VALUE) {
      mFirstSampleStartTime = sample.info.presentationTimeUs;
      if (DEBUG) {
        Log.d(LOGTAG, "mFirstSampleStartTime = " + mFirstSampleStartTime);
      }
    }
    return samples;
  }

  protected void handleDrmInitChanged(final Format oldFormat, final Format newFormat) {
    final Object oldDrmInit = oldFormat == null ? null : oldFormat.drmInitData;
    final Object newDrnInit = newFormat.drmInitData;

    // TODO: Notify MFR if the content is encrypted or not.
    if (newDrnInit != oldDrmInit) {
      if (newDrnInit != null) {
      } else {
      }
    }
  }

  protected boolean canReconfigure(final Format oldFormat, final Format newFormat) {
    // Referring to ExoPlayer's MediaCodecBaseRenderer, the default is set
    // to false. Only override it in video renderer subclass.
    return false;
  }

  protected void prepareReconfiguration() {
    // Referring to ExoPlayer's MediaCodec related renderers, only video
    // renderer handles this.
  }

  protected void updateCSDInfo(final Format format) {
    // do nothing.
  }

  protected void onInputFormatChanged(final Format newFormat) throws ExoPlaybackException {
    Format oldFormat;
    try {
      oldFormat = mFormats.get(mFormats.size() - 1);
    } catch (final IndexOutOfBoundsException e) {
      oldFormat = null;
    }
    if (DEBUG) {
      Log.d(LOGTAG, "[onInputFormatChanged] old : " + oldFormat + " => new : " + newFormat);
    }
    mFormats.add(newFormat);
    handleDrmInitChanged(oldFormat, newFormat);

    if (mInitialized && canReconfigure(oldFormat, newFormat)) {
      prepareReconfiguration();
    } else {
      resetRenderer();
      maybeInitRenderer();
    }

    updateCSDInfo(newFormat);
    notifyPlayerInputFormatChanged(newFormat);
  }

  protected void maybeInitRenderer() throws ExoPlaybackException {
    if (mInitialized || mFormats.size() == 0) {
      return;
    }
    if (DEBUG) {
      Log.d(LOGTAG, "Initializing ... ");
    }
    try {
      createInputBuffer();
      mInitialized = true;
    } catch (final OutOfMemoryError e) {
      throw ExoPlaybackException.createForRenderer(
          e,
          getName(),
          getIndex(),
          mFormats.isEmpty() ? null : getFormat(mFormats.size() - 1),
          C.FORMAT_HANDLED,
          null,
          false,
          ExoPlaybackException.ERROR_CODE_DECODER_INIT_FAILED);
    }
  }

  /*
   * The place we get demuxed data from HlsMediaSource(ExoPlayer).
   * The data will then be converted to GeckoHLSSample and deliver to
   * GeckoHlsDemuxerWrapper for further use.
   * If the return value is ture, that means a GeckoHLSSample is queued
   * successfully. We can try to feed more samples into queue.
   * If the return value is false, that means we might encounter following
   * situation 1) not initialized 2) input stream is ended 3) queue is full.
   * 4) format changed. 5) exception happened.
   */
  protected synchronized boolean feedInputBuffersQueue() throws ExoPlaybackException {
    if (!mInitialized || mInputStreamEnded || isQueuedEnoughData()) {
      // Need to reinitialize the renderer or the input stream has ended
      // or we just reached the maximum queue size.
      return false;
    }

    mBufferForRead.data = mInputBuffer;
    if (mBufferForRead.data != null) {
      mBufferForRead.clear();
    }

    handleReconfiguration(mBufferForRead);

    // Read data from HlsMediaSource
    int result = C.RESULT_NOTHING_READ;
    try {
      result = readSource(mFormatHolder, mBufferForRead, 0);
    } catch (final Exception e) {
      Log.e(LOGTAG, "[feedInput] Exception when readSource :", e);
      return false;
    }

    if (result == C.RESULT_NOTHING_READ) {
      return false;
    }

    if (result == C.RESULT_FORMAT_READ) {
      handleFormatRead(mBufferForRead);
      return true;
    }

    // We've read a buffer.
    if (mBufferForRead.isEndOfStream()) {
      if (DEBUG) {
        Log.d(LOGTAG, "Now we're at the End Of Stream.");
      }
      handleEndOfStream(mBufferForRead);
      return false;
    }

    mBufferForRead.flip();

    handleSamplePreparation(mBufferForRead);

    maybeNotifyDataArrived();
    return true;
  }

  private void maybeNotifyDataArrived() {
    if (mWaitingForData && isQueuedEnoughData()) {
      if (DEBUG) {
        Log.d(LOGTAG, "onDataArrived");
      }
      mPlayerEventDispatcher.onDataArrived(getTrackType());
      mWaitingForData = false;
    }
  }

  private void readFormat() throws ExoPlaybackException {
    mNoDataBuffer.clear();
    final int result =
        readSource(mFormatHolder, mNoDataBuffer, /* readFlags= */ SampleStream.FLAG_REQUIRE_FORMAT);
    if (result == C.RESULT_FORMAT_READ) {
      onInputFormatChanged(mFormatHolder.format);
    }
  }

  @Override
  protected void onDisabled() {
    mFormats.clear();
    resetRenderer();
  }

  @Override
  public boolean isReady() {
    return mFormats.size() != 0;
  }

  @Override
  public boolean isEnded() {
    return mInputStreamEnded;
  }

  @Override
  protected synchronized void onPositionReset(
      final long positionUs, final boolean joining, final boolean sampleStreamIsResetToKeyFrame) {
    if (DEBUG) {
      Log.d(LOGTAG, "onPositionReset : positionUs = " + positionUs);
    }
    mInputStreamEnded = false;
    if (mInitialized) {
      clearInputSamplesQueue();
    }
  }

  /*
   * This is called by ExoPlayerImplInternal.java.
   * ExoPlayer checks the status of renderer, i.e. isReady() / isEnded(), and
   * calls renderer.render by passing its wall clock time.
   */
  @Override
  public void render(final long positionUs, final long elapsedRealtimeUs)
      throws ExoPlaybackException {
    if (BuildConfig.DEBUG_BUILD) {
      Log.d(LOGTAG, "positionUs = " + positionUs + ", mInputStreamEnded = " + mInputStreamEnded);
    }
    if (mInputStreamEnded) {
      return;
    }
    if (mFormats.size() == 0) {
      readFormat();
    }

    maybeInitRenderer();
    while (feedInputBuffersQueue()) {
      // Do nothing
    }
  }
}
