// META: global=window,dedicatedworker
// META: script=/common/media.js
// META: script=/webcodecs/utils.js

var defaultInit = {
  timestamp: 1234,
  channels: 2,
  sampleRate: 8000,
  frames: 100,
};

function createDefaultAudioData() {
  return make_audio_data(
    defaultInit.timestamp,
    defaultInit.channels,
    defaultInit.sampleRate,
    defaultInit.frames
  );
}

test(t => {
  let local_data = new Float32Array(defaultInit.channels * defaultInit.frames);

  let audio_data_init = {
    timestamp: defaultInit.timestamp,
    data: local_data,
    numberOfFrames: defaultInit.frames,
    numberOfChannels: defaultInit.channels,
    sampleRate: defaultInit.sampleRate,
    format: 'f32-planar',
  }

  let data = new AudioData(audio_data_init);

  assert_equals(data.timestamp, defaultInit.timestamp, 'timestamp');
  assert_equals(data.numberOfFrames, defaultInit.frames, 'frames');
  assert_equals(data.numberOfChannels, defaultInit.channels, 'channels');
  assert_equals(data.sampleRate, defaultInit.sampleRate, 'sampleRate');
  assert_equals(
      data.duration, defaultInit.frames / defaultInit.sampleRate * 1_000_000,
      'duration');
  assert_equals(data.format, 'f32-planar', 'format');

  // Create an Int16 array of the right length.
  let small_data = new Int16Array(defaultInit.channels * defaultInit.frames);

  let wrong_format_init = {...audio_data_init};
  wrong_format_init.data = small_data;

  // Creating `f32-planar` AudioData from Int16 from should throw.
  assert_throws_js(TypeError, () => {
    let data = new AudioData(wrong_format_init);
  }, `AudioDataInit.data needs to be big enough`);

  var members = [
    'timestamp',
    'data',
    'numberOfFrames',
    'numberOfChannels',
    'sampleRate',
    'format',
  ];

  for (const member of members) {
    let incomplete_init = {...audio_data_init};
    delete incomplete_init[member];

    assert_throws_js(
        TypeError, () => {let data = new AudioData(incomplete_init)},
        'AudioData requires \'' + member + '\'');
  }

  let invalid_init = {...audio_data_init};
  invalid_init.numberOfFrames = 0

  assert_throws_js(
      TypeError, () => {let data = new AudioData(invalid_init)},
      'AudioData requires numberOfFrames > 0');

  invalid_init = {...audio_data_init};
  invalid_init.numberOfChannels = 0

  assert_throws_js(
      TypeError, () => {let data = new AudioData(invalid_init)},
      'AudioData requires numberOfChannels > 0');

}, 'Verify AudioData constructors');

test(t => {
  let data = createDefaultAudioData();

  let clone = data.clone();

  // Verify the parameters match.
  assert_equals(data.timestamp, clone.timestamp, 'timestamp');
  assert_equals(data.numberOfFrames, clone.numberOfFrames, 'frames');
  assert_equals(data.numberOfChannels, clone.numberOfChannels, 'channels');
  assert_equals(data.sampleRate, clone.sampleRate, 'sampleRate');
  assert_equals(data.format, clone.format, 'format');

  const data_copyDest = new Float32Array(defaultInit.frames);
  const clone_copyDest = new Float32Array(defaultInit.frames);

  // Verify the data matches.
  for (var channel = 0; channel < defaultInit.channels; channel++) {
    data.copyTo(data_copyDest, {planeIndex: channel});
    clone.copyTo(clone_copyDest, {planeIndex: channel});

    assert_array_equals(
        data_copyDest, clone_copyDest, 'Cloned data ch=' + channel);
  }

  // Verify closing the original data doesn't close the clone.
  data.close();
  assert_equals(data.numberOfFrames, 0, 'data.buffer (closed)');
  assert_not_equals(clone.numberOfFrames, 0, 'clone.buffer (not closed)');

  clone.close();
  assert_equals(clone.numberOfFrames, 0, 'clone.buffer (closed)');

  // Verify closing a closed AudioData does not throw.
  data.close();
}, 'Verify closing and cloning AudioData');

test(t => {
  let data = make_audio_data(
      -10, defaultInit.channels, defaultInit.sampleRate, defaultInit.frames);
  assert_equals(data.timestamp, -10, 'timestamp');
  data.close();
}, 'Test we can construct AudioData with a negative timestamp.');

test(t => {
  var data = new Float32Array([0]);
  let audio_data_init = {
    timestamp: 0,
    data: data,
    numberOfFrames: 1,
    numberOfChannels: 1,
    sampleRate: 44100,
    format: 'f32',
  };
  let audioData = new AudioData(audio_data_init);
  assert_not_equals(data.length, 0, "Input data is copied when constructing an AudioData");
}, 'Test input array is copied on construction');

test(t => {
  let audio_data_init = {
    timestamp: 0,
    data: new Float32Array([1,2,3,4,5,6,7,8]),
    numberOfFrames: 4,
    numberOfChannels: 2,
    sampleRate: 44100,
    format: 'f32',
  };
  let audioData = new AudioData(audio_data_init);
  let dest = new Float32Array(8);
  assert_throws_js(
      RangeError, () => audioData.copyTo(dest, {planeIndex: 1}),
      'copyTo from interleaved data with non-zero planeIndex throws');
  audioData.close();
}, 'Test that copyTo throws if copying from interleaved with a non-zero planeIndex');

// Indices to pick a particular specific value in a specific sample-format
const MIN = 0; // Minimum sample value, max amplitude
const MAX = 1; // Maximum sample value, max amplitude
const HALF = 2; // Half the maximum sample value, positive
const NEGATIVE_HALF = 3; // Half the maximum sample value, negative
const BIAS = 4; // Center of the range, silence
const DISCRETE_STEPS = 5; // Number of different value for a type.

function pow2(p) {
  return 2 ** p;
}
// Rounding operations for conversion, currently always floor (round towards
// zero).
let r = Math.floor.bind(this);

const TEST_VALUES = {
  u8: [0, 255, 191, 64, 128, 256],
  s16: [
    -pow2(15),
    pow2(15) - 1,
    r((pow2(15) - 1) / 2),
    r(-pow2(15) / 2),
    0,
    pow2(16),
  ],
  s32: [
    -pow2(31),
    pow2(31) - 1,
    r((pow2(31) - 1) / 2),
    r(-pow2(31) / 2),
    0,
    pow2(32),
  ],
  f32: [-1.0, 1.0, 0.5, -0.5, 0, pow2(24)],
};

const TEST_TEMPLATE = {
  channels: 2,
  frames: 5,
  // Each test is run with an element of the cartesian product of a pair of
  // elements of the set of type in [u8, s16, s32, f32]
  // For each test, this template is copied and the values replaced with the
  // appropriate values for this particular type.
  // For each test, copy this template and replace the number by the appropriate
  // number for this type
  testInput: [MIN, BIAS, MAX, MIN, HALF, NEGATIVE_HALF, BIAS, MAX, BIAS, BIAS],
  testInterleavedResult: [MIN, NEGATIVE_HALF, BIAS, BIAS, MAX, MAX, MIN, BIAS, HALF, BIAS],
  testVectorInterleavedResult: [
    [MIN, MAX, HALF, BIAS, BIAS],
    [BIAS, MIN, NEGATIVE_HALF, MAX, BIAS],
  ],
  testVectorPlanarResult: [
    [MIN, BIAS, MAX, MIN, HALF],
    [NEGATIVE_HALF, BIAS, MAX, BIAS, BIAS],
  ],
};

function isInteger(type) {
  switch (type) {
    case "u8":
    case "s16":
    case "s32":
      return true;
    case "f32":
      return false;
    default:
      throw "invalid type";
  }
}

// This is the complex part: carefully select an acceptable error value
// depending on various factors: expected destination value, source type,
// destination type. This is designed to be strict but reachable with simple
// sample format transformation (no dithering or complex transformation).
function epsilon(expectedDestValue, sourceType, destType) {
  // Strict comparison if not converting
  if (sourceType == destType) {
    return 0.0;
  }
  // There are three cases in which the maximum value cannot be reached, when
  // converting from a smaller integer sample type to a wider integer sample
  // type:
  // - u8 to s16
  // - u8 to s32
  // - s16 to u32
  if (expectedDestValue == TEST_VALUES[destType][MAX]) {
    if (sourceType == "u8" && destType == "s16") {
      return expectedDestValue - 32511; // INT16_MAX - 2 << 7 + 1
    } else if (sourceType == "u8" && destType == "s32") {
      return expectedDestValue - 2130706432; // INT32_MAX - (2 << 23) + 1
    } else if (sourceType == "s16" && destType == "s32") {
      return expectedDestValue - 2147418112; // INT32_MAX - UINT16_MAX
    }
  }
  // Min and bias value are correctly mapped for all integer sample-types
  if (isInteger(sourceType) && isInteger(destType)) {
    if (expectedDestValue == TEST_VALUES[destType][MIN] ||
        expectedDestValue == TEST_VALUES[destType][BIAS]) {
      return 0.0;
    }
  }
  // If converting from float32 to u8 or s16, allow choosing the rounding
  // direction. s32 has higher resolution than f32 in [-1.0,1.0] (24 bits of
  // mantissa)
  if (!isInteger(sourceType) && isInteger(destType) && destType != "s32") {
    return 1.0;
  }
  // In all other cases, expect an accuracy that depends on the source type and
  // the destination type.
  // The resolution of the source type.
  var sourceResolution = TEST_VALUES[sourceType][DISCRETE_STEPS];
  // The resolution of the destination type.
  var destResolution = TEST_VALUES[destType][DISCRETE_STEPS];
  // Computations should be exact if going from high resolution to low resolution.
  if (sourceResolution > destResolution) {
    return 0.0;
  } else {
    // Something that approaches the precision imbalance
    return destResolution / sourceResolution;
  }
}

// Fill the template above with the values for a particular type
function get_type_values(type) {
  let cloned = structuredClone(TEST_TEMPLATE);
  cloned.testInput = Array.from(
    cloned.testInput,
    idx => TEST_VALUES[type][idx]
  );
  cloned.testInterleavedResult = Array.from(
    cloned.testInterleavedResult,
    idx => TEST_VALUES[type][idx]
  );
  cloned.testVectorInterleavedResult = Array.from(
    cloned.testVectorInterleavedResult,
    c => {
      return Array.from(c, idx => {
        return TEST_VALUES[type][idx];
      });
    }
  );
  cloned.testVectorPlanarResult = Array.from(
    cloned.testVectorPlanarResult,
    c => {
      return Array.from(c, idx => {
        return TEST_VALUES[type][idx];
      });
    }
  );
  return cloned;
}

function typeToArrayType(type) {
  switch (type) {
    case "u8":
      return Uint8Array;
    case "s16":
      return Int16Array;
    case "s32":
      return Int32Array;
    case "f32":
      return Float32Array;
    default:
      throw "Unexpected";
  }
}

function arrayTypeToType(array) {
  switch (array.constructor) {
    case Uint8Array:
      return "u8";
    case Int16Array:
      return "s16";
    case Int32Array:
      return "s32";
    case Float32Array:
      return "f32";
    default:
      throw "Unexpected";
  }
}

function check_array_equality(values, expected, sourceType, message, assert_func) {
  if (values.length != expected.length) {
    throw "Array not of the same length";
  }
  for (var i = 0; i < values.length; i++) {
    var eps = epsilon(expected[i], sourceType, arrayTypeToType(values));
    assert_func(
      Math.abs(expected[i] - values[i]) <= eps,
      `Got ${values[i]} but expected result ${
        expected[i]
      } at index ${i} when converting from ${sourceType} to ${arrayTypeToType(
        values
      )}, epsilon ${eps}`
    );
  }
  assert_func(
    true,
    `${values} is equal to ${expected} when converting from ${sourceType} to ${arrayTypeToType(
      values
    )}`
  );
}

function conversionTest(sourceType, destinationType) {
  test(function (t) {
    var test = get_type_values(sourceType);
    var result = get_type_values(destinationType);

    var sourceArrayCtor = typeToArrayType(sourceType);
    var destArrayCtor = typeToArrayType(destinationType);

    let data = new AudioData({
      timestamp: defaultInit.timestamp,
      data: new sourceArrayCtor(test.testInput),
      numberOfFrames: test.frames,
      numberOfChannels: test.channels,
      sampleRate: defaultInit.sampleRate,
      format: sourceType,
    });

    // All conversions can be supported, but conversion of any type to f32-planar
    // MUST be supported.
    var assert_func = destinationType == "f32" ? assert_true : assert_implements_optional;
    let dest = new destArrayCtor(data.numberOfFrames);
    data.copyTo(dest, { planeIndex: 0, format: destinationType + "-planar" });
    check_array_equality(
      dest,
      result.testVectorInterleavedResult[0],
      sourceType,
      "interleaved channel 0",
      assert_func
    );
    data.copyTo(dest, { planeIndex: 1, format: destinationType + "-planar" });
    check_array_equality(
      dest,
      result.testVectorInterleavedResult[1],
      sourceType,
      "interleaved channel 0",
      assert_func
    );
    let destInterleaved = new destArrayCtor(data.numberOfFrames * data.numberOfChannels);
    data.copyTo(destInterleaved, { planeIndex: 0, format: destinationType });
    check_array_equality(
      destInterleaved,
      result.testInput,
      sourceType,
      "copyTo from interleaved to interleaved (conversion only)",
      assert_implements_optional
    );

    data = new AudioData({
      timestamp: defaultInit.timestamp,
      data: new sourceArrayCtor(test.testInput),
      numberOfFrames: test.frames,
      numberOfChannels: test.channels,
      sampleRate: defaultInit.sampleRate,
      format: sourceType + "-planar",
    });

    data.copyTo(dest, { planeIndex: 0, format: destinationType + "-planar" });
    check_array_equality(
      dest,
      result.testVectorPlanarResult[0],
      sourceType,
      "planar channel 0",
      assert_func,
    );
    data.copyTo(dest, { planeIndex: 1, format: destinationType + "-planar" });
    check_array_equality(
      dest,
      result.testVectorPlanarResult[1],
      sourceType,
      "planar channel 1",
      assert_func
    );
    // Copy to interleaved from planar: all channels are copied
    data.copyTo(destInterleaved, {planeIndex: 0, format: destinationType});
    check_array_equality(
      destInterleaved,
      result.testInterleavedResult,
      sourceType,
      "planar to interleaved",
      assert_func
    );
  }, `Test conversion of ${sourceType} to ${destinationType}`);
}

const TYPES = ["u8", "s16", "s32", "f32"];
 TYPES.forEach(sourceType => {
   TYPES.forEach(destinationType => {
    conversionTest(sourceType, destinationType);
  });
});
