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

function makeI420_4x2() {
  const init = {
      format: 'I420',
      timestamp: 0,
      codedWidth: 4,
      codedHeight: 2,
  };
  return new VideoFrame(I420_DATA, init);
}

function testBufferConstructedI420Frame(bufferType) {
  let fmt = 'I420';
  let vfInit = {format: fmt, timestamp: 1234, codedWidth: 4, codedHeight: 2};

  let buffer;
  if (bufferType == 'SharedArrayBuffer' ||
      bufferType == 'Uint8Array(SharedArrayBuffer)') {
    buffer = new SharedArrayBuffer(I420_DATA.length);
  } else {
    assert_true(bufferType == 'ArrayBuffer' ||
                bufferType == 'Uint8Array(ArrayBuffer)');
    buffer = new ArrayBuffer(I420_DATA.length);
  }
  let bufferView = new Uint8Array(buffer);
  bufferView.set(I420_DATA);
  let data = bufferType.startsWith('Uint8Array') ? bufferView : buffer;

  let frame = new VideoFrame(data, vfInit);
  assert_equals(frame.format, fmt, 'plane format');
  assert_equals(frame.colorSpace.primaries, 'bt709', 'color primaries');
  assert_equals(frame.colorSpace.transfer, 'bt709', 'color transfer');
  assert_equals(frame.colorSpace.matrix, 'bt709', 'color matrix');
  assert_false(frame.colorSpace.fullRange, 'color range');
  frame.close();

  let y = {offset: 0, stride: 4};
  let u = {offset: 8, stride: 2};
  let v = {offset: 10, stride: 2};

  assert_throws_js(TypeError, () => {
    let y = {offset: 0, stride: 1};
    let frame = new VideoFrame(data, {...vfInit, layout: [y, u, v]});
  }, 'y stride too small');
  assert_throws_js(TypeError, () => {
    let u = {offset: 8, stride: 1};
    let frame = new VideoFrame(data, {...vfInit, layout: [y, u, v]});
  }, 'u stride too small');
  assert_throws_js(TypeError, () => {
    let v = {offset: 10, stride: 1};
    let frame = new VideoFrame(data, {...vfInit, layout: [y, u, v]});
  }, 'v stride too small');
  assert_throws_js(TypeError, () => {
    let frame = new VideoFrame(data.slice(0, 8), vfInit);
  }, 'data too small');
}

function assert_buffer_equals(actual, expected) {
  assert_true(expected instanceof Uint8Array, 'actual instanceof Uint8Array');

  if (actual instanceof ArrayBuffer ||
      (typeof(SharedArrayBuffer) != 'undefined' &&
        actual instanceof SharedArrayBuffer)) {
    actual = new Uint8Array(actual);
  } else {
    assert_true(actual instanceof Uint8Array,
        'expected instanceof Uint8Array, ArrayBuffer, or SharedArrayBuffer');
  }

  assert_equals(actual.length, expected.length, 'buffer length');
  for (let i = 0; i < actual.length; i++) {
    if (actual[i] != expected[i]) {
      assert_equals(actual[i], expected[i], 'buffer contents at index ' + i);
    }
  }
}

function assert_layout_equals(actual, expected) {
  assert_equals(actual.length, expected.length, 'layout planes');
  for (let i = 0; i < actual.length; i++) {
    assert_object_equals(actual[i], expected[i], 'plane ' + i + ' layout');
  }
}

async function testI420_4x2_copyTo(destination) {
  const frame = makeI420_4x2();
  const expectedLayout = [
      {offset: 0, stride: 4},
      {offset: 8, stride: 2},
      {offset: 10, stride: 2},
  ];
  const expectedData = new Uint8Array([
      1, 2, 3, 4,  // y
      5, 6, 7, 8,
      9, 10,       // u
      11, 12       // v
  ]);

  assert_equals(frame.allocationSize(), expectedData.length, 'allocationSize()');
  const layout = await frame.copyTo(destination);
  assert_layout_equals(layout, expectedLayout);
  assert_buffer_equals(destination, expectedData);
}

function verifyTimestampRequiredToConstructFrame(imageSource) {
  assert_throws_js(
      TypeError,
      () => new VideoFrame(imageSource),
      'timestamp required to construct VideoFrame from this source');
  let validFrame = new VideoFrame(imageSource, {timestamp: 0});
  validFrame.close();
}

// Fills a visible region within `data` with the given YUV values.
function fillYUV(data, codedWidth, codedHeight, visibleRect, v_y, v_u, v_v) {
  for (let y = 0; y < codedHeight; y++) {
    for (let x = 0; x < codedWidth; x++) {
      data[y * codedWidth + x] =
          (x >= visibleRect.x && y < visibleRect.y + visibleRect.height) ? v_y
                                                                         : 0;
    }
  }
  const uvWidth = codedWidth / 2;
  const uvHeight = codedHeight / 2;
  const uvPlaneSize = uvWidth * uvHeight;
  const uvOffset = codedWidth * codedHeight;
  const uvX = visibleRect.x / 2;
  const uvMaxY = (visibleRect.y + visibleRect.height) / 2;
  for (let y = 0; y < uvHeight; y++) {
    for (let x = 0; x < uvWidth; x++) {
      if (x >= uvX && y < uvMaxY) {
        data[uvOffset + y * uvWidth + x] = v_u;
        data[uvOffset + uvPlaneSize + y * uvWidth + x] = v_v;
      } else {
        data[uvOffset + y * uvWidth + x] =
            data[uvOffset + uvPlaneSize + y * uvWidth + x] = 128;
      }
    }
  }
}
