// Use a power of two to eliminate round-off when converting frames to time and
// vice versa.
let sampleRate = 32768;

let numberOfChannels = 1;

// Time step when each panner node starts.  Make sure it starts on a frame
// boundary.
let timeStep = Math.floor(0.001 * sampleRate) / sampleRate;

// Length of the impulse signal.
let pulseLengthFrames = Math.round(timeStep * sampleRate);

// How many panner nodes to create for the test
let nodesToCreate = 100;

// Be sure we render long enough for all of our nodes.
let renderLengthSeconds = timeStep * (nodesToCreate + 1);

// These are global mostly for debugging.
let context;
let impulse;
let bufferSource;
let panner;
let position;
let time;

let renderedBuffer;
let renderedLeft;
let renderedRight;

function createGraph(context, nodeCount, positionSetter) {
  bufferSource = new Array(nodeCount);
  panner = new Array(nodeCount);
  position = new Array(nodeCount);
  time = new Array(nodeCount);
  // Angle between panner locations.  (nodeCount - 1 because we want
  // to include both 0 and 180 deg.
  let angleStep = Math.PI / (nodeCount - 1);

  if (numberOfChannels == 2) {
    impulse = createStereoImpulseBuffer(context, pulseLengthFrames);
  } else
    impulse = createImpulseBuffer(context, pulseLengthFrames);

  for (let k = 0; k < nodeCount; ++k) {
    bufferSource[k] = context.createBufferSource();
    bufferSource[k].buffer = impulse;

    panner[k] = context.createPanner();
    panner[k].panningModel = 'equalpower';
    panner[k].distanceModel = 'linear';

    let angle = angleStep * k;
    position[k] = {angle: angle, x: Math.cos(angle), z: Math.sin(angle)};
    positionSetter(panner[k], position[k].x, 0, position[k].z);

    bufferSource[k].connect(panner[k]);
    panner[k].connect(context.destination);

    // Start the source
    time[k] = k * timeStep;
    bufferSource[k].start(time[k]);
  }
}

function createTestAndRun(
    context, should, nodeCount, numberOfSourceChannels, positionSetter) {
  numberOfChannels = numberOfSourceChannels;

  createGraph(context, nodeCount, positionSetter);

  return context.startRendering().then(buffer => checkResult(buffer, should));
}

// Map our position angle to the azimuth angle (in degrees).
//
// An angle of 0 corresponds to an azimuth of 90 deg; pi, to -90 deg.
function angleToAzimuth(angle) {
  return 90 - angle * 180 / Math.PI;
}

// The gain caused by the EQUALPOWER panning model
function equalPowerGain(angle) {
  let azimuth = angleToAzimuth(angle);

  if (numberOfChannels == 1) {
    let panPosition = (azimuth + 90) / 180;

    let gainL = Math.cos(0.5 * Math.PI * panPosition);
    let gainR = Math.sin(0.5 * Math.PI * panPosition);

    return {left: gainL, right: gainR};
  } else {
    if (azimuth <= 0) {
      let panPosition = (azimuth + 90) / 90;

      let gainL = 1 + Math.cos(0.5 * Math.PI * panPosition);
      let gainR = Math.sin(0.5 * Math.PI * panPosition);

      return {left: gainL, right: gainR};
    } else {
      let panPosition = azimuth / 90;

      let gainL = Math.cos(0.5 * Math.PI * panPosition);
      let gainR = 1 + Math.sin(0.5 * Math.PI * panPosition);

      return {left: gainL, right: gainR};
    }
  }
}

function checkResult(renderedBuffer, should) {
  renderedLeft = renderedBuffer.getChannelData(0);
  renderedRight = renderedBuffer.getChannelData(1);

  // The max error we allow between the rendered impulse and the
  // expected value.  This value is experimentally determined.  Set
  // to 0 to make the test fail to see what the actual error is.
  let maxAllowedError = 1.1597e-6;

  let success = true;

  // Number of impulses found in the rendered result.
  let impulseCount = 0;

  // Max (relative) error and the index of the maxima for the left
  // and right channels.
  let maxErrorL = 0;
  let maxErrorIndexL = 0;
  let maxErrorR = 0;
  let maxErrorIndexR = 0;

  // Number of impulses that don't match our expected locations.
  let timeCount = 0;

  // Locations of where the impulses aren't at the expected locations.
  let timeErrors = new Array();

  for (let k = 0; k < renderedLeft.length; ++k) {
    // We assume that the left and right channels start at the same instant.
    if (renderedLeft[k] != 0 || renderedRight[k] != 0) {
      // The expected gain for the left and right channels.
      let pannerGain = equalPowerGain(position[impulseCount].angle);
      let expectedL = pannerGain.left;
      let expectedR = pannerGain.right;

      // Absolute error in the gain.
      let errorL = Math.abs(renderedLeft[k] - expectedL);
      let errorR = Math.abs(renderedRight[k] - expectedR);

      if (Math.abs(errorL) > maxErrorL) {
        maxErrorL = Math.abs(errorL);
        maxErrorIndexL = impulseCount;
      }
      if (Math.abs(errorR) > maxErrorR) {
        maxErrorR = Math.abs(errorR);
        maxErrorIndexR = impulseCount;
      }

      // Keep track of the impulses that didn't show up where we
      // expected them to be.
      let expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate);
      if (k != expectedOffset) {
        timeErrors[timeCount] = {actual: k, expected: expectedOffset};
        ++timeCount;
      }
      ++impulseCount;
    }
  }

  should(impulseCount, 'Number of impulses found').beEqualTo(nodesToCreate);

  should(
      timeErrors.map(x => x.actual),
      'Offsets of impulses at the wrong position')
      .beEqualToArray(timeErrors.map(x => x.expected));

  should(maxErrorL, 'Error in left channel gain values')
      .beLessThanOrEqualTo(maxAllowedError);

  should(maxErrorR, 'Error in right channel gain values')
      .beLessThanOrEqualTo(maxAllowedError);
}
