// META: global=window,dedicatedworker
// META: script=/webcodecs/utils.js
// META: variant=?adts_aac
// META: variant=?mp4_aac
// META: variant=?mp3
// META: variant=?opus
// META: variant=?pcm_alaw
// META: variant=?pcm_ulaw
// META: variant=?pcm_u8
// META: variant=?pcm_s16
// META: variant=?pcm_s24
// META: variant=?pcm_s32
// META: variant=?pcm_f32
// META: variant=?flac
// META: variant=?vorbis

const ADTS_AAC_DATA = {
  src: 'sfx.adts',
  config: {
    codec: 'mp4a.40.2',
    sampleRate: 48000,
    numberOfChannels: 1,
  },
  chunks: [
    {offset: 0, size: 248}, {offset: 248, size: 280}, {offset: 528, size: 258},
    {offset: 786, size: 125}, {offset: 911, size: 230},
    {offset: 1141, size: 148}, {offset: 1289, size: 224},
    {offset: 1513, size: 166}, {offset: 1679, size: 216},
    {offset: 1895, size: 183}
  ],
  duration: 24000
};

const MP3_DATA = {
  src: 'sfx.mp3',
  config: {
    codec: 'mp3',
    sampleRate: 48000,
    numberOfChannels: 1,
  },
  chunks: [
    {offset: 333, size: 288}, {offset: 621, size: 288},
    {offset: 909, size: 288}, {offset: 1197, size: 288},
    {offset: 1485, size: 288}, {offset: 1773, size: 288},
    {offset: 2061, size: 288}, {offset: 2349, size: 288},
    {offset: 2637, size: 288}, {offset: 2925, size: 288}
  ],
  duration: 24000
};

const MP4_AAC_DATA = {
  src: 'sfx-aac.mp4',
  config: {
    codec: 'mp4a.40.2',
    sampleRate: 48000,
    numberOfChannels: 1,
    description: {offset: 2552, size: 5},
  },
  chunks: [
    {offset: 44, size: 241},
    {offset: 285, size: 273},
    {offset: 558, size: 251},
    {offset: 809, size: 118},
    {offset: 927, size: 223},
    {offset: 1150, size: 141},
    {offset: 1291, size: 217},
    {offset: 1508, size: 159},
    {offset: 1667, size: 209},
    {offset: 1876, size: 176},
  ],
  duration: 21333
};

const OPUS_DATA = {
  src: 'sfx-opus.ogg',
  config: {
    codec: 'opus',
    sampleRate: 48000,
    numberOfChannels: 1,
    description: {offset: 28, size: 19},
  },
  chunks: [
    {offset: 185, size: 450}, {offset: 635, size: 268},
    {offset: 903, size: 285}, {offset: 1188, size: 296},
    {offset: 1484, size: 287}, {offset: 1771, size: 308},
    {offset: 2079, size: 289}, {offset: 2368, size: 286},
    {offset: 2654, size: 296}, {offset: 2950, size: 294}
  ],
  duration: 20000
};

const FLAC_DATA = {
  src: 'sfx.flac',
  config: {
    codec: 'flac',
    sampleRate: 48000,
    numberOfChannels: 1,
    description: { offset: 0, size: 8287 }
  },
  chunks: [
    { offset: 8288, size: 2276 },
    { offset: 10564, size: 2038 },
    { offset: 12602, size: 521 },
  ],
  duration: 20000
};

function pcm(codec, dataOffset) {
  return {
    src: `sfx-${codec}.wav`,
    config: {
      codec: codec,
      sampleRate: 48000,
      numberOfChannels: 1,
    },

    // Chunk are arbitrary and will be generated lazily
    chunks: [],
    offset: dataOffset,
    duration: 0
  }
}

const PCM_ULAW_DATA = pcm("ulaw", 0x5c);
const PCM_ALAW_DATA = pcm("alaw", 0x5c);
const PCM_U8_DATA = pcm("pcm-u8", 0x4e);
const PCM_S16_DATA = pcm("pcm-s16", 0x4e);
const PCM_S24_DATA = pcm("pcm-s24", 0x66);
const PCM_S32_DATA = pcm("pcm-s32", 0x66);
const PCM_F32_DATA = pcm("pcm-f32", 0x72);

const VORBIS_DATA = {
  src: 'sfx-vorbis.ogg',
  config: {
    codec: 'vorbis',
    description: [
      2,
      30,
      62,
      {offset: 28, size: 30},
      {offset: 101, size: 62},
      {offset: 163, size: 3771}
    ],
    numberOfChannels: 1,
    sampleRate: 48000,
  },
  chunks: [
    {offset: 3968, size: 44}, {offset: 4012, size: 21},
    {offset: 4033, size: 57}, {offset: 4090, size: 37},
    {offset: 4127, size: 37}, {offset: 4164, size: 107},
    {offset: 4271, size: 172}
  ],
  duration: 21333
};

// Allows mutating `callbacks` after constructing the AudioDecoder, wraps calls
// in t.step().
function createAudioDecoder(t, callbacks) {
  return new AudioDecoder({
    output(frame) {
      if (callbacks && callbacks.output) {
        t.step(() => callbacks.output(frame));
      } else {
        t.unreached_func('unexpected output()');
      }
    },
    error(e) {
      if (callbacks && callbacks.error) {
        t.step(() => callbacks.error(e));
      } else {
        t.unreached_func('unexpected error()');
      }
    }
  });
}

// Create a view of an ArrayBuffer.
function view(buffer, {offset, size}) {
  return new Uint8Array(buffer, offset, size);
}

let CONFIG = null;
let CHUNK_DATA = null;
let CHUNKS = null;
promise_setup(async () => {
  const data = {
    '?adts_aac': ADTS_AAC_DATA,
    '?mp3': MP3_DATA,
    '?mp4_aac': MP4_AAC_DATA,
    '?opus': OPUS_DATA,
    '?pcm_alaw': PCM_ALAW_DATA,
    '?pcm_ulaw': PCM_ULAW_DATA,
    '?pcm_u8': PCM_U8_DATA,
    '?pcm_s16': PCM_S16_DATA,
    '?pcm_s24': PCM_S24_DATA,
    '?pcm_s32': PCM_S32_DATA,
    '?pcm_f32': PCM_F32_DATA,
    '?flac': FLAC_DATA,
    '?vorbis': VORBIS_DATA,
  }[location.search];

  // Don't run any tests if the codec is not supported.
  assert_equals("function", typeof AudioDecoder.isConfigSupported);
  let supported = false;
  try {
    const support = await AudioDecoder.isConfigSupported({
      codec: data.config.codec,
      sampleRate: data.config.sampleRate,
      numberOfChannels: data.config.numberOfChannels
    });
    supported = support.supported;
  } catch (e) {
  }
  assert_implements_optional(supported, data.config.codec + ' unsupported');

  // Fetch the media data and prepare buffers.
  const response = await fetch(data.src);
  const buf = await response.arrayBuffer();

  CONFIG = {...data.config};
  if (data.config.description) {
    // The description for decoding vorbis is expected to be in Xiph extradata format.
    // https://w3c.github.io/webcodecs/vorbis_codec_registration.html#audiodecoderconfig-description
    if (Array.isArray(data.config.description)) {
      const length = data.config.description.reduce((sum, value) => sum + ((typeof value === 'number') ? 1 : value.size), 0);
      const description = new Uint8Array(length);

      data.config.description.reduce((offset, value) => {
          if (typeof value === 'number') {
              description[offset] = value;

              return offset + 1;
          }

          description.set(view(buf, value), offset);

          return offset + value.size;
      }, 0);

      CONFIG.description = description;
    } else {
      CONFIG.description = view(buf, data.config.description);
    }
  }

  CHUNK_DATA = [];
  // For PCM, split in chunks of 1200 bytes and compute the rest
  if (data.chunks.length == 0) {
    let offset = data.offset;
    // 1200 is divisible by 2 and 3 and is a plausible packet length
    // for PCM: this means that there won't be samples split in two packet
    let PACKET_LENGTH = 1200;
    let bytesPerSample = 0;
    switch (data.config.codec) {
      case "pcm-s16": bytesPerSample = 2; break;
      case "pcm-s24": bytesPerSample = 3; break;
      case "pcm-s32": bytesPerSample = 4; break;
      case "pcm-f32": bytesPerSample = 4; break;
      default: bytesPerSample = 1; break;
    }
    while (offset < buf.byteLength) {
      let size = Math.min(buf.byteLength - offset, PACKET_LENGTH);
      assert_equals(size % bytesPerSample, 0);
      CHUNK_DATA.push(view(buf, {offset, size}));
      offset += size;
    }
    data.duration = 1000 * 1000 * PACKET_LENGTH / data.config.sampleRate / bytesPerSample;
  } else {
    CHUNK_DATA = data.chunks.map((chunk, i) => view(buf, chunk));
  }

  CHUNKS = CHUNK_DATA.map((encodedData, i) => new EncodedAudioChunk({
                            type: 'key',
                            timestamp: i * data.duration,
                            duration: data.duration,
                            data: encodedData
                          }));
});

promise_test(t => {
  return AudioDecoder.isConfigSupported(CONFIG);
}, 'Test isConfigSupported()');

promise_test(t => {
  // Define a valid config that includes a hypothetical 'futureConfigFeature',
  // which is not yet recognized by the User Agent.
  const validConfig = {
    ...CONFIG,
    futureConfigFeature: 'foo',
  };

  // The UA will evaluate validConfig as being "valid", ignoring the
  // `futureConfigFeature` it  doesn't recognize.
  return AudioDecoder.isConfigSupported(validConfig).then((decoderSupport) => {
    // AudioDecoderSupport must contain the following properites.
    assert_true(decoderSupport.hasOwnProperty('supported'));
    assert_true(decoderSupport.hasOwnProperty('config'));

    // AudioDecoderSupport.config must not contain unrecognized properties.
    assert_false(decoderSupport.config.hasOwnProperty('futureConfigFeature'));

    // AudioDecoderSupport.config must contiain the recognized properties.
    assert_equals(decoderSupport.config.codec, validConfig.codec);
    assert_equals(decoderSupport.config.sampleRate, validConfig.sampleRate);
    assert_equals(
        decoderSupport.config.numberOfChannels, validConfig.numberOfChannels);

    if (validConfig.description) {
      // The description must be copied.
      assert_false(
          decoderSupport.config.description === validConfig.description,
          'description is unique');
      assert_array_equals(
          new Uint8Array(decoderSupport.config.description, 0),
          new Uint8Array(validConfig.description, 0), 'description');
    } else {
      assert_false(
          decoderSupport.config.hasOwnProperty('description'), 'description');
    }
  });
}, 'Test that AudioDecoder.isConfigSupported() returns a parsed configuration');

promise_test(async t => {
  const decoder = createAudioDecoder(t);
  decoder.configure(CONFIG);
  assert_equals(decoder.state, 'configured', 'state');
}, 'Test configure()');

promise_test(t => {
  const decoder = createAudioDecoder(t);
  return testClosedCodec(t, decoder, CONFIG, CHUNKS[0]);
}, 'Verify closed AudioDecoder operations');

promise_test(async t => {
  const callbacks = {};
  const decoder = createAudioDecoder(t, callbacks);

  let outputs = 0;
  callbacks.output = frame => {
    outputs++;
    frame.close();
  };

  decoder.configure(CONFIG);
  CHUNKS.forEach(chunk => {
    decoder.decode(chunk);
  });

  await decoder.flush();
  assert_equals(outputs, CONFIG.codec === 'vorbis' ? CHUNKS.length - 1 : CHUNKS.length, 'outputs');
}, 'Test decoding');

promise_test(async t => {
  const callbacks = {};
  const decoder = createAudioDecoder(t, callbacks);

  let outputs = 0;
  callbacks.output = frame => {
    if (outputs === 0) {
      assert_equals(frame.timestamp, -42);
    }
    outputs++;
    frame.close();
  };

  decoder.configure(CONFIG);
  decoder.decode(new EncodedAudioChunk(
      {type: 'key', timestamp: -42, data: CHUNK_DATA[0]}));
  decoder.decode(new EncodedAudioChunk(
      {type: 'key', timestamp: CHUNKS[0].duration - 42, data: CHUNK_DATA[1]}));

  await decoder.flush();
  assert_equals(outputs, CONFIG.codec === 'vorbis' ? 1 : 2, 'outputs');
}, 'Test decoding a with a negative timestamp');

promise_test(async t => {
  const callbacks = {};
  const decoder = createAudioDecoder(t, callbacks);

  let outputs = 0;
  callbacks.output = frame => {
    if (outputs === 0) {
      assert_equals(frame.timestamp, 42);
    }
    outputs++;
    frame.close();
  };

  decoder.configure(CONFIG);
  decoder.decode(new EncodedAudioChunk(
      {type: 'key', timestamp: 42, data: CHUNK_DATA[0]}));
  decoder.decode(new EncodedAudioChunk(
      {type: 'key', timestamp: CHUNKS[0].duration + 42, data: CHUNK_DATA[1]}));

  await decoder.flush();
  assert_equals(outputs, CONFIG.codec === 'vorbis' ? 1 : 2, 'outputs');
}, 'Test decoding a with a positive timestamp');

promise_test(async t => {
  const callbacks = {};
  const decoder = createAudioDecoder(t, callbacks);

  let outputs = 0;
  callbacks.output = frame => {
    outputs++;
    frame.close();
  };

  decoder.configure(CONFIG);
  decoder.decode(CHUNKS[0]);
  decoder.decode(CHUNKS[1]);

  await decoder.flush();
  assert_equals(outputs, CONFIG.codec === 'vorbis' ? 1 : 2, 'outputs');

  decoder.decode(CHUNKS[2]);
  await decoder.flush();
  assert_equals(outputs, CONFIG.codec === 'vorbis' ? 2 : 3, 'outputs');
}, 'Test decoding after flush');

promise_test(async t => {
  const callbacks = {};
  const decoder = createAudioDecoder(t, callbacks);

  decoder.configure(CONFIG);
  decoder.decode(CHUNKS[0]);
  decoder.decode(CHUNKS[1]);
  const flushDone = decoder.flush();

  // Wait for the first output, then reset.
  let outputs = 0;
  await new Promise(resolve => {
    callbacks.output = frame => {
      outputs++;
      assert_equals(outputs, 1, 'outputs');
      decoder.reset();
      frame.close();
      resolve();
    };
  });

  // Flush should have been synchronously rejected.
  await promise_rejects_dom(t, 'AbortError', flushDone);

  assert_equals(outputs, 1, 'outputs');
}, 'Test reset during flush');

promise_test(async t => {
  const callbacks = {};
  const decoder = createAudioDecoder(t, callbacks);

  // No decodes yet.
  assert_equals(decoder.decodeQueueSize, 0);

  decoder.configure(CONFIG);

  // Still no decodes.
  assert_equals(decoder.decodeQueueSize, 0);

  let lastDequeueSize = Infinity;
  decoder.ondequeue = () => {
    assert_greater_than(lastDequeueSize, 0, "Dequeue event after queue empty");
    assert_greater_than(lastDequeueSize, decoder.decodeQueueSize,
                        "Dequeue event without decreased queue size");
    lastDequeueSize = decoder.decodeQueueSize;
  };

  for (let chunk of CHUNKS)
    decoder.decode(chunk);

  assert_greater_than_equal(decoder.decodeQueueSize, 0);
  assert_less_than_equal(decoder.decodeQueueSize, CHUNKS.length);

  await decoder.flush();
  // We can guarantee that all decodes are processed after a flush.
  assert_equals(decoder.decodeQueueSize, 0);
  // Last dequeue event should fire when the queue is empty.
  assert_equals(lastDequeueSize, 0);

  // Reset this to Infinity to track the decline of queue size for this next
  // batch of decodes.
  lastDequeueSize = Infinity;

  for (let chunk of CHUNKS)
    decoder.decode(chunk);

  assert_greater_than_equal(decoder.decodeQueueSize, 0);
  decoder.reset();
  assert_equals(decoder.decodeQueueSize, 0);
}, 'AudioDecoder decodeQueueSize test');
