// META: global=window,dedicatedworker
// META: script=/webcodecs/videoFrame-utils.js

function makeRGBA_2x2() {
  const data = new Uint8Array([
      1,2,3,4,    5,6,7,8,
      9,10,11,12, 13,14,15,16,
  ]);
  const init = {
      format: 'RGBA',
      timestamp: 0,
      codedWidth: 2,
      codedHeight: 2,
  };
  return new VideoFrame(data, init);
}

const NV12_DATA = new Uint8Array([
      1, 2, 3, 4,   // y
      5, 6, 7, 8,
      9, 10, 11, 12 // uv
  ]);

function makeNV12_4x2() {
  const init = {
      format: 'NV12',
      timestamp: 0,
      codedWidth: 4,
      codedHeight: 2,
  };
  return new VideoFrame(NV12_DATA, init);
}

promise_test(async t => {
  const frame = makeI420_4x2();
  frame.close();

  assert_throws_dom('InvalidStateError', () => frame.allocationSize(), 'allocationSize()');

  let data = new Uint8Array(12);
  await promise_rejects_dom(t, 'InvalidStateError', frame.copyTo(data), 'copyTo()');
}, 'Test closed frame.');

promise_test(async t => {
  const destination = new ArrayBuffer(I420_DATA.length);
  await testI420_4x2_copyTo(destination);
}, 'Test copying I420 frame to a non-shared ArrayBuffer');

promise_test(async t => {
  const destination = new Uint8Array(I420_DATA.length);
  await testI420_4x2_copyTo(destination);
}, 'Test copying I420 frame to a non-shared ArrayBufferView');

promise_test(async t => {
  const frame = makeRGBA_2x2();
  const expectedLayout = [
      {offset: 0, stride: 8},
  ];
  const expectedData = new Uint8Array([
      1,2,3,4,    5,6,7,8,
      9,10,11,12, 13,14,15,16,
  ]);
  assert_equals(frame.allocationSize(), expectedData.length, 'allocationSize()');
  const data = new Uint8Array(expectedData.length);
  const layout = await frame.copyTo(data);
  assert_layout_equals(layout, expectedLayout);
  assert_buffer_equals(data, expectedData);
}, 'Test RGBA frame.');

promise_test(async t => {
  const frame = makeNV12_4x2();
  const expectedLayout = [
      {offset: 0, stride: 4},
      {offset: 8, stride: 4},
  ];
  const expectedData = new Uint8Array([
      1,2,3,4,
      5,6,7,8,
      9,10,11,12
  ]);
  assert_equals(frame.allocationSize(), expectedData.length, 'allocationSize()');
  const data = new Uint8Array(expectedData.length);
  const layout = await frame.copyTo(data);
  assert_layout_equals(layout, expectedLayout);
  assert_buffer_equals(data, expectedData);
}, 'Test NV12 frame.');

promise_test(async t => {
  const frame = makeI420_4x2();
  const data = new Uint8Array(11);
  await promise_rejects_js(t, TypeError, frame.copyTo(data));
}, 'Test undersized buffer.');

promise_test(async t => {
  const frame = makeI420_4x2();
  const options = {
    layout: [{offset: 0, stride: 4}],
  };
  assert_throws_js(TypeError, () => frame.allocationSize(options));
  const data = new Uint8Array(12);
  await promise_rejects_js(t, TypeError, frame.copyTo(data, options));
}, 'Test incorrect plane count.');

promise_test(async t => {
  const frame = makeI420_4x2();
  const options = {
      layout: [
          {offset: 4, stride: 4},
          {offset: 0, stride: 2},
          {offset: 2, stride: 2},
      ],
  };
  const expectedData = new Uint8Array([
      9, 10,       // u
      11, 12,      // v
      1, 2, 3, 4,  // y
      5, 6, 7, 8,
  ]);
  assert_equals(frame.allocationSize(options), expectedData.length, 'allocationSize()');
  const data = new Uint8Array(expectedData.length);
  const layout = await frame.copyTo(data, options);
  assert_layout_equals(layout, options.layout);
  assert_buffer_equals(data, expectedData);
}, 'Test I420 stride and offset work.');

promise_test(async t => {
  const frame = makeI420_4x2();
  const options = {
      layout: [
          {offset: 9, stride: 5},
          {offset: 1, stride: 3},
          {offset: 5, stride: 3},
      ],
  };
  const expectedData = new Uint8Array([
      0,
      9, 10, 0,       // u
      0,
      11, 12, 0,      // v
      0,
      1, 2, 3, 4, 0,  // y
      5, 6, 7, 8, 0,
  ]);
  assert_equals(frame.allocationSize(options), expectedData.length, 'allocationSize()');
  const data = new Uint8Array(expectedData.length);
  const layout = await frame.copyTo(data, options);
  assert_layout_equals(layout, options.layout);
  assert_buffer_equals(data, expectedData);
}, 'Test I420 stride and offset with padding.');

promise_test(async t => {
  const init = {
    format: 'I420A',
    timestamp: 0,
    codedWidth: 4,
    codedHeight: 2,
  };
  const buf = new Uint8Array([
    1, 2, 3, 4,     // y
    5, 6, 7, 8,
    9, 10,          // u
    11, 12,         // v
    13, 14, 15, 16, // a
    17, 18, 19, 20,
  ]);
  const frame = new VideoFrame(buf, init);
  const options = {
      layout: [
          {offset: 12, stride: 4},
          {offset: 8, stride: 2},
          {offset: 10, stride: 2},
          {offset: 0, stride: 4},
      ],
  };
  const expectedData = new Uint8Array([
      13, 14, 15, 16, // a
      17, 18, 19, 20,
      9, 10,          // u
      11, 12,         // v
      1, 2, 3, 4,     // y
      5, 6, 7, 8,
  ]);
  assert_equals(frame.allocationSize(options), expectedData.length, 'allocationSize()');
  const data = new Uint8Array(expectedData.length);
  const layout = await frame.copyTo(data, options);
  assert_layout_equals(layout, options.layout);
  assert_buffer_equals(data, expectedData);
}, 'Test I420A stride and offset work.');

promise_test(async t => {
  const init = {
    format: 'NV12',
    timestamp: 0,
    codedWidth: 4,
    codedHeight: 2,
  };
  const buf = new Uint8Array([
    1, 2, 3, 4,   // y
    5, 6, 7, 8,
    9, 10, 11, 12 // uv
  ]);
  const frame = new VideoFrame(buf, init);
  const options = {
      layout: [
          {offset: 4, stride: 4},
          {offset: 0, stride: 4},
      ],
  };
  const expectedData = new Uint8Array([
      9, 10, 11, 12, // uv
      1, 2, 3, 4,    // y
      5, 6, 7, 8
  ]);
  assert_equals(frame.allocationSize(options), expectedData.length, 'allocationSize()');
  const data = new Uint8Array(expectedData.length);
  const layout = await frame.copyTo(data, options);
  assert_layout_equals(layout, options.layout);
  assert_buffer_equals(data, expectedData);
}, 'Test NV12 stride and offset work.');

promise_test(async t => {
  const frame = makeI420_4x2();
  const options = {
      layout: [
          {offset: 0, stride: 1},
          {offset: 8, stride: 2},
          {offset: 10, stride: 2},
      ],
  };
  assert_throws_js(TypeError, () => frame.allocationSize(options));
  const data = new Uint8Array(12);
  await promise_rejects_js(t, TypeError, frame.copyTo(data, options));
}, 'Test invalid stride.');

promise_test(async t => {
  const frame = makeI420_4x2();
  const options = {
      layout: [
          {offset: 0, stride: 4},
          {offset: 8, stride: 2},
          {offset: 2 ** 32 - 2, stride: 2},
      ],
  };
  assert_throws_js(TypeError, () => frame.allocationSize(options));
  const data = new Uint8Array(12);
  await promise_rejects_js(t, TypeError, frame.copyTo(data, options));
}, 'Test address overflow.');

promise_test(async t => {
  const frame = makeI420_4x2();
  const options = {
      rect: frame.codedRect,
  };
  const expectedLayout = [
      {offset: 0, stride: 4},
      {offset: 8, stride: 2},
      {offset: 10, stride: 2},
  ];
  const expectedData = new Uint8Array([
      1, 2, 3, 4, 5, 6, 7, 8,  // y
      9, 10,                   // u
      11, 12                   // v
  ]);
  assert_equals(frame.allocationSize(options), expectedData.length, 'allocationSize()');
  const data = new Uint8Array(expectedData.length);
  const layout = await frame.copyTo(data, options);
  assert_layout_equals(layout, expectedLayout);
  assert_buffer_equals(data, expectedData);
}, 'Test codedRect.');

promise_test(async t => {
  const frame = makeI420_4x2();
  const options = {
      rect: {x: 0, y: 0, width: 4, height: 0},
  };
  assert_throws_js(TypeError, () => frame.allocationSize(options));
  const data = new Uint8Array(12);
  await promise_rejects_js(t, TypeError, frame.copyTo(data, options));
}, 'Test empty rect.');

promise_test(async t => {
  const frame = makeI420_4x2();
  const options = {
      rect: {x: 2, y: 0, width: 2, height: 2},
  };
  const expectedLayout = [
      {offset: 0, stride: 2},
      {offset: 4, stride: 1},
      {offset: 5, stride: 1},
  ];
  const expectedData = new Uint8Array([
      3, 4,  // y
      7, 8,
      10,    // u
      12     // v
  ]);
  assert_equals(frame.allocationSize(options), expectedData.length, 'allocationSize()');
  const data = new Uint8Array(expectedData.length);
  const layout = await frame.copyTo(data, options);
  assert_layout_equals(layout, expectedLayout);
  assert_buffer_equals(data, expectedData);
}, 'Test left crop.');

promise_test(async t => {
  const frame = makeI420_4x2();
  const options = {
      rect: {x: 0, y: 0, width: 4, height: 4},
  };
  assert_throws_js(TypeError, () => frame.allocationSize(options));
  const data = new Uint8Array(12);
  await promise_rejects_js(t, TypeError, frame.copyTo(data, options));
}, 'Test invalid rect.');

promise_test(async t => {
  let init = {
    format: 'I420',
    timestamp: 1234,
    codedWidth: 8,
    codedHeight: 16,
    visibleRect: {
      x: 2,
      y: 2,
      width: 4,
      height: 8,
    },
    colorSpace: {
      primaries: 'smpte170m',
      transfer: 'smpte170m',
      matrix: 'smpte170m',
      fullRange: false,
    }
  };

  // Define YUV values for BT.601 red.
  const redY = 76;
  const redU = 84;
  const redV = 255;

  const ySize = init.codedWidth * init.codedHeight;
  const uvSize = ySize / 4;
  let data = new Uint8Array(ySize + 2 * uvSize);
  fillYUV(data, init.codedWidth, init.codedHeight, init.visibleRect, redY, redU,
          redV);

  let frame = new VideoFrame(data, init);
  assert_equals(frame.codedWidth, init.visibleRect.width);
  assert_equals(frame.codedHeight, init.visibleRect.height);
  assert_equals(frame.visibleRect.x, 0);
  assert_equals(frame.visibleRect.y, 0);
  assert_equals(frame.visibleRect.width, init.visibleRect.width);
  assert_equals(frame.visibleRect.height, init.visibleRect.height);
  assert_equals(frame.displayWidth, init.visibleRect.width);
  assert_equals(frame.displayHeight, init.visibleRect.height);

  let options = {rect: frame.visibleRect};
  let copied_data = new Uint8Array(frame.allocationSize(options));
  await frame.copyTo(copied_data, options);

  // We could write a bunch of code to carefully slice out the visible region
  // of `data`, but it's simpler and works better with testharness.js operators
  // to just create a fresh packed version for direct comparison.
  let packed_data = new Uint8Array(frame.allocationSize(options));
  fillYUV(packed_data, frame.codedWidth, frame.codedHeight, frame.visibleRect,
          redY, redU, redV);

  assert_array_equals(copied_data, packed_data, `Copied frame data incorrect.`);
}, 'copyTo from byte data with non-default visibleRect');
