/*
 *  Copyright 2017 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package org.webrtc;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.chromium.base.test.params.BaseJUnit4RunnerDelegate;
import org.chromium.base.test.params.ParameterAnnotations.ClassParameter;
import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
import org.chromium.base.test.params.ParameterSet;
import org.chromium.base.test.params.ParameterizedRunner;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Unit tests for {@link AndroidVideoDecoder}. */
@RunWith(ParameterizedRunner.class)
@UseRunnerDelegate(BaseJUnit4RunnerDelegate.class)
public final class AndroidVideoDecoderInstrumentationTest {
  @ClassParameter private static List<ParameterSet> CLASS_PARAMS = new ArrayList<>();

  static {
    CLASS_PARAMS.add(new ParameterSet()
                         .value(/* codecName= */ "VP8", false /* useEglContext */)
                         .name("VP8WithoutEglContext"));
    CLASS_PARAMS.add(new ParameterSet()
                         .value(/* codecName= */ "VP8", true /* useEglContext */)
                         .name("VP8WithEglContext"));
    CLASS_PARAMS.add(new ParameterSet()
                         .value(/* codecName= */ "H264", false /* useEglContext */)
                         .name("H264WithoutEglContext"));
    CLASS_PARAMS.add(new ParameterSet()
                         .value(/* codecName= */ "H264", true /* useEglContext */)
                         .name("H264WithEglContext"));
  }

  private final VideoCodecInfo codecType;
  private final boolean useEglContext;

  public AndroidVideoDecoderInstrumentationTest(String codecName, boolean useEglContext) {
    if (codecName.equals("H264")) {
      this.codecType = H264Utils.DEFAULT_H264_BASELINE_PROFILE_CODEC;
    } else {
      this.codecType = new VideoCodecInfo(codecName, new HashMap<>());
    }
    this.useEglContext = useEglContext;
  }

  private static final String TAG = "AndroidVideoDecoderInstrumentationTest";

  private static final int TEST_FRAME_COUNT = 10;
  private static final int TEST_FRAME_WIDTH = 640;
  private static final int TEST_FRAME_HEIGHT = 360;
  private VideoFrame.I420Buffer[] TEST_FRAMES;

  private static final boolean ENABLE_INTEL_VP8_ENCODER = true;
  private static final boolean ENABLE_H264_HIGH_PROFILE = true;
  private static final VideoEncoder.Settings ENCODER_SETTINGS =
      new VideoEncoder.Settings(1 /* core */, TEST_FRAME_WIDTH, TEST_FRAME_HEIGHT, 300 /* kbps */,
          30 /* fps */, 1 /* numberOfSimulcastStreams */, true /* automaticResizeOn */,
          /* capabilities= */ new VideoEncoder.Capabilities(false /* lossNotification */));

  private static final int DECODE_TIMEOUT_MS = 1000;
  private static final VideoDecoder.Settings SETTINGS =
      new VideoDecoder.Settings(1 /* core */, TEST_FRAME_WIDTH, TEST_FRAME_HEIGHT);

  private static class MockDecodeCallback implements VideoDecoder.Callback {
    private BlockingQueue<VideoFrame> frameQueue = new LinkedBlockingQueue<>();

    @Override
    public void onDecodedFrame(VideoFrame frame, Integer decodeTimeMs, Integer qp) {
      assertNotNull(frame);
      frameQueue.offer(frame);
    }

    public void assertFrameDecoded(EncodedImage testImage, VideoFrame.I420Buffer testBuffer) {
      VideoFrame decodedFrame = poll();
      VideoFrame.Buffer decodedBuffer = decodedFrame.getBuffer();
      assertEquals(testImage.encodedWidth, decodedBuffer.getWidth());
      assertEquals(testImage.encodedHeight, decodedBuffer.getHeight());
      // TODO(sakal): Decoder looses the nanosecond precision. This is not a problem in practice
      // because C++ EncodedImage stores the timestamp in milliseconds.
      assertEquals(testImage.captureTimeNs / 1000, decodedFrame.getTimestampNs() / 1000);
      assertEquals(testImage.rotation, decodedFrame.getRotation());
    }

    public VideoFrame poll() {
      try {
        VideoFrame frame = frameQueue.poll(DECODE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        assertNotNull("Timed out waiting for the frame to be decoded.", frame);
        return frame;
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
  }

  private static VideoFrame.I420Buffer[] generateTestFrames() {
    VideoFrame.I420Buffer[] result = new VideoFrame.I420Buffer[TEST_FRAME_COUNT];
    for (int i = 0; i < TEST_FRAME_COUNT; i++) {
      result[i] = JavaI420Buffer.allocate(TEST_FRAME_WIDTH, TEST_FRAME_HEIGHT);
      // TODO(sakal): Generate content for the test frames.
    }
    return result;
  }

  private final EncodedImage[] encodedTestFrames = new EncodedImage[TEST_FRAME_COUNT];
  private EglBase14 eglBase;

  private VideoDecoderFactory createDecoderFactory(EglBase.Context eglContext) {
    return new HardwareVideoDecoderFactory(eglContext);
  }

  private @Nullable VideoDecoder createDecoder() {
    VideoDecoderFactory factory =
        createDecoderFactory(useEglContext ? eglBase.getEglBaseContext() : null);
    return factory.createDecoder(codecType);
  }

  private void encodeTestFrames() {
    VideoEncoderFactory encoderFactory = new HardwareVideoEncoderFactory(
        eglBase.getEglBaseContext(), ENABLE_INTEL_VP8_ENCODER, ENABLE_H264_HIGH_PROFILE);
    VideoEncoder encoder = encoderFactory.createEncoder(codecType);
    HardwareVideoEncoderTest.MockEncoderCallback encodeCallback =
        new HardwareVideoEncoderTest.MockEncoderCallback();
    assertEquals(VideoCodecStatus.OK, encoder.initEncode(ENCODER_SETTINGS, encodeCallback));

    long lastTimestampNs = 0;
    for (int i = 0; i < TEST_FRAME_COUNT; i++) {
      lastTimestampNs += TimeUnit.SECONDS.toNanos(1) / ENCODER_SETTINGS.maxFramerate;
      VideoEncoder.EncodeInfo info = new VideoEncoder.EncodeInfo(
          new EncodedImage.FrameType[] {EncodedImage.FrameType.VideoFrameDelta});
      HardwareVideoEncoderTest.testEncodeFrame(
          encoder, new VideoFrame(TEST_FRAMES[i], 0 /* rotation */, lastTimestampNs), info);
      encodedTestFrames[i] = encodeCallback.poll();
    }

    assertEquals(VideoCodecStatus.OK, encoder.release());
  }

  @Before
  public void setUp() {
    NativeLibrary.initialize(new NativeLibrary.DefaultLoader(), TestConstants.NATIVE_LIBRARY);

    TEST_FRAMES = generateTestFrames();

    eglBase = EglBase.createEgl14(EglBase.CONFIG_PLAIN);
    eglBase.createDummyPbufferSurface();
    eglBase.makeCurrent();

    encodeTestFrames();
  }

  @After
  public void tearDown() {
    eglBase.release();
  }

  @Test
  @SmallTest
  public void testInitialize() {
    VideoDecoder decoder = createDecoder();
    assertEquals(VideoCodecStatus.OK, decoder.initDecode(SETTINGS, null /* decodeCallback */));
    assertEquals(VideoCodecStatus.OK, decoder.release());
  }

  @Test
  @SmallTest
  public void testDecode() {
    VideoDecoder decoder = createDecoder();
    MockDecodeCallback callback = new MockDecodeCallback();
    assertEquals(VideoCodecStatus.OK, decoder.initDecode(SETTINGS, callback));

    for (int i = 0; i < TEST_FRAME_COUNT; i++) {
      assertEquals(VideoCodecStatus.OK,
          decoder.decode(encodedTestFrames[i],
              new VideoDecoder.DecodeInfo(false /* isMissingFrames */, 0 /* renderTimeMs */)));
      callback.assertFrameDecoded(encodedTestFrames[i], TEST_FRAMES[i]);
    }

    assertEquals(VideoCodecStatus.OK, decoder.release());
  }
}
