// Helpers for Web Audio tests

function expectException(func, exceptionCode) {
  var threw = false;
  try {
    func();
  } catch (ex) {
    threw = true;
    ok(ex instanceof DOMException, "Expect a DOM exception");
    is(ex.code, exceptionCode, "Expect the correct exception code");
  }
  ok(threw, "The exception was thrown");
}

function expectNoException(func) {
  var threw = false;
  try {
    func();
  } catch (ex) {
    threw = true;
  }
  ok(!threw, "An exception was not thrown");
}

function expectTypeError(func) {
  var threw = false;
  try {
    func();
  } catch (ex) {
    threw = true;
    ok(ex instanceof TypeError, "Expect a TypeError");
  }
  ok(threw, "The exception was thrown");
}

function expectRejectedPromise(that, func, exceptionName) {
  var promise = that[func]();

  ok(promise instanceof Promise, "Expect a Promise");

  promise.then(function(res) {
    ok(false, "Promise resolved when it should have been rejected.");
  }).catch(function(err) {
    is(err.name, exceptionName, "Promise correctly reject with " + exceptionName);
  });
}

// For a given frequency and AnalyserNode, return the index of the bin that
// measures this frequency.
function binIndexForFrequency(frequency, analyser) {
  return 1 + Math.round(frequency *
                        analyser.fftSize /
                        analyser.context.sampleRate);
}

function fuzzyCompare(a, b) {
  return Math.abs(a - b) < 9e-3;
}

function compareChannels(buf1, buf2,
                        /*optional*/ length,
                        /*optional*/ sourceOffset,
                        /*optional*/ destOffset,
                        /*optional*/ skipLengthCheck) {
  if (!skipLengthCheck) {
    is(buf1.length, buf2.length, "Channels must have the same length");
  }
  sourceOffset = sourceOffset || 0;
  destOffset = destOffset || 0;
  if (length == undefined) {
    length = buf1.length - sourceOffset;
  }
  var difference = 0;
  var maxDifference = 0;
  var firstBadIndex = -1;
  for (var i = 0; i < length; ++i) {
    if (!fuzzyCompare(buf1[i + sourceOffset], buf2[i + destOffset])) {
      difference++;
      maxDifference = Math.max(maxDifference, Math.abs(buf1[i + sourceOffset] - buf2[i + destOffset]));
      if (firstBadIndex == -1) {
        firstBadIndex = i;
      }
    }
  };

  is(difference, 0, "maxDifference: " + maxDifference +
     ", first bad index: " + firstBadIndex +
     " with test-data offset " + sourceOffset + " and expected-data offset " +
     destOffset + "; corresponding values " + buf1[firstBadIndex + sourceOffset] +
     " and " + buf2[firstBadIndex + destOffset] + " --- differences");
}

function compareBuffers(got, expected) {
  if (got.numberOfChannels != expected.numberOfChannels) {
    is(got.numberOfChannels, expected.numberOfChannels,
       "Correct number of buffer channels");
    return;
  }
  if (got.length != expected.length) {
    is(got.length, expected.length,
       "Correct buffer length");
    return;
  }
  if (got.sampleRate != expected.sampleRate) {
    is(got.sampleRate, expected.sampleRate,
       "Correct sample rate");
    return;
  }

  for (var i = 0; i < got.numberOfChannels; ++i) {
    compareChannels(got.getChannelData(i), expected.getChannelData(i),
                    got.length, 0, 0, true);
  }
}

/**
 * Compute the root mean square (RMS,
 * <http://en.wikipedia.org/wiki/Root_mean_square>) of a channel of a slice
 * (defined by `start` and `end`) of an AudioBuffer.
 *
 * This is useful to detect that a buffer is noisy or silent.
 */
function rms(audiobuffer, channel = 0, start = 0, end = audiobuffer.length) {
  var buffer= audiobuffer.getChannelData(channel);
  var rms = 0;
  for (var i = start; i < end; i++) {
    rms += buffer[i] * buffer[i];
  }

  rms /= buffer.length;
  rms = Math.sqrt(rms);
  return rms;
}

function getEmptyBuffer(context, length) {
  return context.createBuffer(gTest.numberOfChannels, length, context.sampleRate);
}

/**
 * This function assumes that the test file defines a single gTest variable with
 * the following properties and methods:
 *
 * + numberOfChannels: optional property which specifies the number of channels
 *                     in the output.  The default value is 2.
 * + createGraph: mandatory method which takes a context object and does
 *                everything needed in order to set up the Web Audio graph.
 *                This function returns the node to be inspected.
 * + createGraphAsync: async version of createGraph.  This function takes
 *                     a callback which should be called with an argument
 *                     set to the node to be inspected when the callee is
 *                     ready to proceed with the test.  Either this function
 *                     or createGraph must be provided.
 * + createExpectedBuffers: optional method which takes a context object and
 *                          returns either one expected buffer or an array of
 *                          them, designating what is expected to be observed
 *                          in the output.  If omitted, the output is expected
 *                          to be silence.  All buffers must have the same
 *                          length, which must be a bufferSize supported by
 *                          ScriptProcessorNode.  This function is guaranteed
 *                          to be called before createGraph.
 * + length: property equal to the total number of frames which we are waiting
 *           to see in the output, mandatory if createExpectedBuffers is not
 *           provided, in which case it must be a bufferSize supported by
 *           ScriptProcessorNode (256, 512, 1024, 2048, 4096, 8192, or 16384).
 *           If createExpectedBuffers is provided then this must be equal to
 *           the number of expected buffers * the expected buffer length.
 *
 * + skipOfflineContextTests: optional. when true, skips running tests on an offline
 *                            context by circumventing testOnOfflineContext.
 */
function runTest()
{
  function done() {
    SimpleTest.finish();
  }

  SimpleTest.waitForExplicitFinish();
  function runTestFunction () {
    if (!gTest.numberOfChannels) {
      gTest.numberOfChannels = 2; // default
    }

    var testLength;

    function runTestOnContext(context, callback, testOutput) {
      if (!gTest.createExpectedBuffers) {
        // Assume that the output is silence
        var expectedBuffers = getEmptyBuffer(context, gTest.length);
      } else {
        var expectedBuffers = gTest.createExpectedBuffers(context);
      }
      if (!(expectedBuffers instanceof Array)) {
        expectedBuffers = [expectedBuffers];
      }
      var expectedFrames = 0;
      for (var i = 0; i < expectedBuffers.length; ++i) {
        is(expectedBuffers[i].numberOfChannels, gTest.numberOfChannels,
           "Correct number of channels for expected buffer " + i);
        expectedFrames += expectedBuffers[i].length;
      }
      if (gTest.length && gTest.createExpectedBuffers) {
        is(expectedFrames, gTest.length, "Correct number of expected frames");
      }

      if (gTest.createGraphAsync) {
        gTest.createGraphAsync(context, function(nodeToInspect) {
          testOutput(nodeToInspect, expectedBuffers, callback);
        });
      } else {
        testOutput(gTest.createGraph(context), expectedBuffers, callback);
      }
    }

    function testOnNormalContext(callback) {
      function testOutput(nodeToInspect, expectedBuffers, callback) {
        testLength = 0;
        var sp = context.createScriptProcessor(expectedBuffers[0].length, gTest.numberOfChannels, 0);
        nodeToInspect.connect(sp);
        sp.onaudioprocess = function(e) {
          var expectedBuffer = expectedBuffers.shift();
          testLength += expectedBuffer.length;
          compareBuffers(e.inputBuffer, expectedBuffer);
          if (expectedBuffers.length == 0) {
            sp.onaudioprocess = null;
            callback();
          }
        };
      }
      var context = new AudioContext();
      runTestOnContext(context, callback, testOutput);
    }

    function testOnOfflineContext(callback, sampleRate) {
      function testOutput(nodeToInspect, expectedBuffers, callback) {
        nodeToInspect.connect(context.destination);
        context.oncomplete = function(e) {
          var samplesSeen = 0;
          while (expectedBuffers.length) {
            var expectedBuffer = expectedBuffers.shift();
            is(e.renderedBuffer.numberOfChannels, expectedBuffer.numberOfChannels,
               "Correct number of input buffer channels");
            for (var i = 0; i < e.renderedBuffer.numberOfChannels; ++i) {
              compareChannels(e.renderedBuffer.getChannelData(i),
                             expectedBuffer.getChannelData(i),
                             expectedBuffer.length,
                             samplesSeen,
                             undefined,
                             true);
            }
            samplesSeen += expectedBuffer.length;
          }
          callback();
        };
        context.startRendering();
      }

      var context = new OfflineAudioContext(gTest.numberOfChannels, testLength, sampleRate);
      runTestOnContext(context, callback, testOutput);
    }

    testOnNormalContext(function() {
      if (!gTest.skipOfflineContextTests) {
        testOnOfflineContext(function() {
          testOnOfflineContext(done, 44100);
        }, 48000);
      } else {
        done();
      }
    });
  };

  if (document.readyState !== 'complete') {
    addLoadEvent(runTestFunction);
  } else {
    runTestFunction();
  }
}
