/*
 * 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 androidx.media3.exoplayer.mediacodec;

import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.Handler;
import android.os.PersistableBundle;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.decoder.CryptoInfo;
import java.io.IOException;
import java.nio.ByteBuffer;

/**
 * Abstracts {@link MediaCodec} operations.
 *
 * <p>{@code MediaCodecAdapter} offers a common interface to interact with a {@link MediaCodec}
 * regardless of the mode the {@link MediaCodec} is operating in.
 */
@UnstableApi
public interface MediaCodecAdapter {
  /** Configuration parameters for a {@link MediaCodecAdapter}. */
  final class Configuration {

    /**
     * Creates a configuration for audio decoding.
     *
     * @param codecInfo See {@link #codecInfo}.
     * @param mediaFormat See {@link #mediaFormat}.
     * @param format See {@link #format}.
     * @param crypto See {@link #crypto}.
     * @return The created instance.
     */
    public static Configuration createForAudioDecoding(
        MediaCodecInfo codecInfo,
        MediaFormat mediaFormat,
        Format format,
        @Nullable MediaCrypto crypto) {
      return new Configuration(
          codecInfo, mediaFormat, format, /* surface= */ null, crypto, /* flags= */ 0);
    }

    /**
     * Creates a configuration for video decoding.
     *
     * @param codecInfo See {@link #codecInfo}.
     * @param mediaFormat See {@link #mediaFormat}.
     * @param format See {@link #format}.
     * @param surface See {@link #surface}.
     * @param crypto See {@link #crypto}.
     * @return The created instance.
     */
    public static Configuration createForVideoDecoding(
        MediaCodecInfo codecInfo,
        MediaFormat mediaFormat,
        Format format,
        @Nullable Surface surface,
        @Nullable MediaCrypto crypto) {
      return new Configuration(codecInfo, mediaFormat, format, surface, crypto, /* flags= */ 0);
    }

    /** Information about the {@link MediaCodec} being configured. */
    public final MediaCodecInfo codecInfo;
    /** The {@link MediaFormat} for which the codec is being configured. */
    public final MediaFormat mediaFormat;
    /** The {@link Format} for which the codec is being configured. */
    public final Format format;
    /**
     * For video decoding, the output where the object will render the decoded frames. This must be
     * null if the codec is not a video decoder, or if it is configured for {@link ByteBuffer}
     * output.
     */
    @Nullable public final Surface surface;
    /** For DRM protected playbacks, a {@link MediaCrypto} to use for decryption. */
    @Nullable public final MediaCrypto crypto;
    /** See {@link MediaCodec#configure}. */
    public final int flags;

    private Configuration(
        MediaCodecInfo codecInfo,
        MediaFormat mediaFormat,
        Format format,
        @Nullable Surface surface,
        @Nullable MediaCrypto crypto,
        int flags) {
      this.codecInfo = codecInfo;
      this.mediaFormat = mediaFormat;
      this.format = format;
      this.surface = surface;
      this.crypto = crypto;
      this.flags = flags;
    }
  }

  /** A factory for {@link MediaCodecAdapter} instances. */
  interface Factory {

    /** Default factory used in most cases. */
    Factory DEFAULT = new DefaultMediaCodecAdapterFactory();

    /** Creates a {@link MediaCodecAdapter} instance. */
    MediaCodecAdapter createAdapter(Configuration configuration) throws IOException;
  }

  /**
   * Listener to be called when an output frame has rendered on the output surface.
   *
   * @see MediaCodec.OnFrameRenderedListener
   */
  interface OnFrameRenderedListener {
    void onFrameRendered(MediaCodecAdapter codec, long presentationTimeUs, long nanoTime);
  }

  /**
   * Returns the next available input buffer index from the underlying {@link MediaCodec} or {@link
   * MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists.
   *
   * @throws IllegalStateException If the underlying {@link MediaCodec} raised an error.
   */
  int dequeueInputBufferIndex();

  /**
   * Returns the next available output buffer index from the underlying {@link MediaCodec}. If the
   * next available output is a MediaFormat change, it will return {@link
   * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} and you should call {@link #getOutputFormat()} to get
   * the format. If there is no available output, this method will return {@link
   * MediaCodec#INFO_TRY_AGAIN_LATER}.
   *
   * @throws IllegalStateException If the underlying {@link MediaCodec} raised an error.
   */
  int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo);

  /**
   * Gets the {@link MediaFormat} that was output from the {@link MediaCodec}.
   *
   * <p>Call this method if a previous call to {@link #dequeueOutputBufferIndex} returned {@link
   * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
   */
  MediaFormat getOutputFormat();

  /**
   * Returns a writable ByteBuffer object for a dequeued input buffer index.
   *
   * @see MediaCodec#getInputBuffer(int)
   */
  @Nullable
  ByteBuffer getInputBuffer(int index);

  /**
   * Returns a read-only ByteBuffer for a dequeued output buffer index.
   *
   * @see MediaCodec#getOutputBuffer(int)
   */
  @Nullable
  ByteBuffer getOutputBuffer(int index);

  /**
   * Submit an input buffer for decoding.
   *
   * <p>The {@code index} must be an input buffer index that has been obtained from a previous call
   * to {@link #dequeueInputBufferIndex()}.
   *
   * @see MediaCodec#queueInputBuffer
   */
  void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags);

  /**
   * Submit an input buffer that is potentially encrypted for decoding.
   *
   * <p>The {@code index} must be an input buffer index that has been obtained from a previous call
   * to {@link #dequeueInputBufferIndex()}.
   *
   * <p>This method behaves like {@link MediaCodec#queueSecureInputBuffer}, with the difference that
   * {@code info} is of type {@link CryptoInfo} and not {@link android.media.MediaCodec.CryptoInfo}.
   *
   * @see MediaCodec#queueSecureInputBuffer
   */
  void queueSecureInputBuffer(
      int index, int offset, CryptoInfo info, long presentationTimeUs, int flags);

  /**
   * Returns the buffer to the {@link MediaCodec}. If the {@link MediaCodec} was configured with an
   * output surface, setting {@code render} to {@code true} will first send the buffer to the output
   * surface. The surface will release the buffer back to the codec once it is no longer
   * used/displayed.
   *
   * @see MediaCodec#releaseOutputBuffer(int, boolean)
   */
  void releaseOutputBuffer(int index, boolean render);

  /**
   * Updates the output buffer's surface timestamp and sends it to the {@link MediaCodec} to render
   * it on the output surface. If the {@link MediaCodec} is not configured with an output surface,
   * this call will simply return the buffer to the {@link MediaCodec}.
   *
   * @see MediaCodec#releaseOutputBuffer(int, long)
   */
  @RequiresApi(21)
  void releaseOutputBuffer(int index, long renderTimeStampNs);

  /** Flushes the adapter and the underlying {@link MediaCodec}. */
  void flush();

  /** Releases the adapter and the underlying {@link MediaCodec}. */
  void release();

  /**
   * Registers a callback to be invoked when an output frame is rendered on the output surface.
   *
   * @see MediaCodec#setOnFrameRenderedListener
   */
  @RequiresApi(23)
  void setOnFrameRenderedListener(OnFrameRenderedListener listener, Handler handler);

  /**
   * Dynamically sets the output surface of a {@link MediaCodec}.
   *
   * @see MediaCodec#setOutputSurface(Surface)
   */
  @RequiresApi(23)
  void setOutputSurface(Surface surface);

  /**
   * Communicate additional parameter changes to the {@link MediaCodec} instance.
   *
   * @see MediaCodec#setParameters(Bundle)
   */
  @RequiresApi(19)
  void setParameters(Bundle params);

  /**
   * Specifies the scaling mode to use, if a surface was specified when the codec was created.
   *
   * @see MediaCodec#setVideoScalingMode(int)
   */
  void setVideoScalingMode(@C.VideoScalingMode int scalingMode);

  /** Whether the adapter needs to be reconfigured before it is used. */
  boolean needsReconfiguration();

  /**
   * Returns metrics data about the current codec instance.
   *
   * @see MediaCodec#getMetrics()
   */
  @RequiresApi(26)
  PersistableBundle getMetrics();
}
