// Utilities for mixing rule testing.
// http://webaudio.github.io/web-audio-api/#channel-up-mixing-and-down-mixing


/**
 * Create an n-channel buffer, with all sample data zero except for a shifted
 * impulse. The impulse position depends on the channel index. For example, for
 * a 4-channel buffer:
 *  channel 0: 1 0 0 0 0 0 0 0
 *  channel 1: 0 1 0 0 0 0 0 0
 *  channel 2: 0 0 1 0 0 0 0 0
 *  channel 3: 0 0 0 1 0 0 0 0
 * @param  {AudioContext} context     Associated AudioContext.
 * @param  {Number} numberOfChannels  Number of channels of test buffer.
 * @param  {Number} frameLength       Buffer length in frames.
 * @return {AudioBuffer}
 */
function createShiftedImpulseBuffer(context, numberOfChannels, frameLength) {
  let shiftedImpulseBuffer =
      context.createBuffer(numberOfChannels, frameLength, context.sampleRate);
  for (let channel = 0; channel < numberOfChannels; ++channel) {
    let data = shiftedImpulseBuffer.getChannelData(channel);
    data[channel] = 1;
  }

  return shiftedImpulseBuffer;
}

/**
 * Create a string that displays the content of AudioBuffer.
 * @param  {AudioBuffer} audioBuffer  AudioBuffer object to stringify.
 * @param  {Number} frameLength       Number of frames to be printed.
 * @param  {Number} frameOffset       Starting frame position for printing.
 * @return {String}
 */
function stringifyBuffer(audioBuffer, frameLength, frameOffset) {
  frameOffset = (frameOffset || 0);

  let stringifiedBuffer = '';
  for (let channel = 0; channel < audioBuffer.numberOfChannels; ++channel) {
    let channelData = audioBuffer.getChannelData(channel);
    for (let i = 0; i < frameLength; ++i)
      stringifiedBuffer += channelData[i + frameOffset] + ' ';
    stringifiedBuffer += '\n';
  }

  return stringifiedBuffer;
}

/**
 * Compute number of channels from the connection.
 * http://webaudio.github.io/web-audio-api/#dfn-computednumberofchannels
 * @param  {String} connections         A string specifies the connection. For
 *                                      example, the string "128" means 3
 *                                      connections, having 1, 2, and 8 channels
 *                                      respectively.
 * @param  {Number} channelCount        Channel count.
 * @param  {String} channelCountMode    Channel count mode.
 * @return {Number}                     Computed number of channels.
 */
function computeNumberOfChannels(connections, channelCount, channelCountMode) {
  if (channelCountMode == 'explicit')
    return channelCount;

  // Must have at least one channel.
  let computedNumberOfChannels = 1;

  // Compute "computedNumberOfChannels" based on all the connections.
  for (let i = 0; i < connections.length; ++i) {
    let connectionNumberOfChannels = parseInt(connections[i]);
    computedNumberOfChannels =
        Math.max(computedNumberOfChannels, connectionNumberOfChannels);
  }

  if (channelCountMode == 'clamped-max')
    computedNumberOfChannels = Math.min(computedNumberOfChannels, channelCount);

  return computedNumberOfChannels;
}

/**
 * Apply up/down-mixing (in-place summing) based on 'speaker' interpretation.
 * @param  {AudioBuffer} input          Input audio buffer.
 * @param  {AudioBuffer} output         Output audio buffer.
 */
function speakersSum(input, output) {
  if (input.length != output.length) {
    throw '[mixing-rules.js] speakerSum(): buffer lengths mismatch (input: ' +
        input.length + ', output: ' + output.length + ')';
  }

  if (input.numberOfChannels === output.numberOfChannels) {
    for (let channel = 0; channel < output.numberOfChannels; ++channel) {
      let inputChannel = input.getChannelData(channel);
      let outputChannel = output.getChannelData(channel);
      for (let i = 0; i < outputChannel.length; i++)
        outputChannel[i] += inputChannel[i];
    }
  } else if (input.numberOfChannels < output.numberOfChannels) {
    processUpMix(input, output);
  } else {
    processDownMix(input, output);
  }
}

/**
 * In-place summing to |output| based on 'discrete' channel interpretation.
 * @param  {AudioBuffer} input          Input audio buffer.
 * @param  {AudioBuffer} output         Output audio buffer.
 */
function discreteSum(input, output) {
  if (input.length != output.length) {
    throw '[mixing-rules.js] speakerSum(): buffer lengths mismatch (input: ' +
        input.length + ', output: ' + output.length + ')';
  }

  let numberOfChannels =
      Math.min(input.numberOfChannels, output.numberOfChannels)

          for (let channel = 0; channel < numberOfChannels; ++channel) {
    let inputChannel = input.getChannelData(channel);
    let outputChannel = output.getChannelData(channel);
    for (let i = 0; i < outputChannel.length; i++)
      outputChannel[i] += inputChannel[i];
  }
}

/**
 * Perform up-mix by in-place summing to |output| buffer.
 * @param  {AudioBuffer} input          Input audio buffer.
 * @param  {AudioBuffer} output         Output audio buffer.
 */
function processUpMix(input, output) {
  let numberOfInputChannels = input.numberOfChannels;
  let numberOfOutputChannels = output.numberOfChannels;
  let i, length = output.length;

  // Up-mixing: 1 -> 2, 1 -> 4
  //   output.L += input
  //   output.R += input
  //   output.SL += 0 (in the case of 1 -> 4)
  //   output.SR += 0 (in the case of 1 -> 4)
  if ((numberOfInputChannels === 1 && numberOfOutputChannels === 2) ||
      (numberOfInputChannels === 1 && numberOfOutputChannels === 4)) {
    let inputChannel = input.getChannelData(0);
    let outputChannel0 = output.getChannelData(0);
    let outputChannel1 = output.getChannelData(1);
    for (i = 0; i < length; i++) {
      outputChannel0[i] += inputChannel[i];
      outputChannel1[i] += inputChannel[i];
    }

    return;
  }

  // Up-mixing: 1 -> 5.1
  //   output.L += 0
  //   output.R += 0
  //   output.C += input
  //   output.LFE += 0
  //   output.SL += 0
  //   output.SR += 0
  if (numberOfInputChannels == 1 && numberOfOutputChannels == 6) {
    let inputChannel = input.getChannelData(0);
    let outputChannel2 = output.getChannelData(2);
    for (i = 0; i < length; i++)
      outputChannel2[i] += inputChannel[i];

    return;
  }

  // Up-mixing: 2 -> 4, 2 -> 5.1
  //   output.L += input.L
  //   output.R += input.R
  //   output.C += 0 (in the case of 2 -> 5.1)
  //   output.LFE += 0 (in the case of 2 -> 5.1)
  //   output.SL += 0
  //   output.SR += 0
  if ((numberOfInputChannels === 2 && numberOfOutputChannels === 4) ||
      (numberOfInputChannels === 2 && numberOfOutputChannels === 6)) {
    let inputChannel0 = input.getChannelData(0);
    let inputChannel1 = input.getChannelData(1);
    let outputChannel0 = output.getChannelData(0);
    let outputChannel1 = output.getChannelData(1);
    for (i = 0; i < length; i++) {
      outputChannel0[i] += inputChannel0[i];
      outputChannel1[i] += inputChannel1[i];
    }

    return;
  }

  // Up-mixing: 4 -> 5.1
  //   output.L += input.L
  //   output.R += input.R
  //   output.C += 0
  //   output.LFE += 0
  //   output.SL += input.SL
  //   output.SR += input.SR
  if (numberOfInputChannels === 4 && numberOfOutputChannels === 6) {
    let inputChannel0 = input.getChannelData(0);    // input.L
    let inputChannel1 = input.getChannelData(1);    // input.R
    let inputChannel2 = input.getChannelData(2);    // input.SL
    let inputChannel3 = input.getChannelData(3);    // input.SR
    let outputChannel0 = output.getChannelData(0);  // output.L
    let outputChannel1 = output.getChannelData(1);  // output.R
    let outputChannel4 = output.getChannelData(4);  // output.SL
    let outputChannel5 = output.getChannelData(5);  // output.SR
    for (i = 0; i < length; i++) {
      outputChannel0[i] += inputChannel0[i];
      outputChannel1[i] += inputChannel1[i];
      outputChannel4[i] += inputChannel2[i];
      outputChannel5[i] += inputChannel3[i];
    }

    return;
  }

  // All other cases, fall back to the discrete sum.
  discreteSum(input, output);
}

/**
 * Perform down-mix by in-place summing to |output| buffer.
 * @param  {AudioBuffer} input          Input audio buffer.
 * @param  {AudioBuffer} output         Output audio buffer.
 */
function processDownMix(input, output) {
  let numberOfInputChannels = input.numberOfChannels;
  let numberOfOutputChannels = output.numberOfChannels;
  let i, length = output.length;

  // Down-mixing: 2 -> 1
  //   output += 0.5 * (input.L + input.R)
  if (numberOfInputChannels === 2 && numberOfOutputChannels === 1) {
    let inputChannel0 = input.getChannelData(0);  // input.L
    let inputChannel1 = input.getChannelData(1);  // input.R
    let outputChannel0 = output.getChannelData(0);
    for (i = 0; i < length; i++)
      outputChannel0[i] += 0.5 * (inputChannel0[i] + inputChannel1[i]);

    return;
  }

  // Down-mixing: 4 -> 1
  //   output += 0.25 * (input.L + input.R + input.SL + input.SR)
  if (numberOfInputChannels === 4 && numberOfOutputChannels === 1) {
    let inputChannel0 = input.getChannelData(0);  // input.L
    let inputChannel1 = input.getChannelData(1);  // input.R
    let inputChannel2 = input.getChannelData(2);  // input.SL
    let inputChannel3 = input.getChannelData(3);  // input.SR
    let outputChannel0 = output.getChannelData(0);
    for (i = 0; i < length; i++) {
      outputChannel0[i] += 0.25 *
          (inputChannel0[i] + inputChannel1[i] + inputChannel2[i] +
           inputChannel3[i]);
    }

    return;
  }

  // Down-mixing: 5.1 -> 1
  //   output += sqrt(1/2) * (input.L + input.R) + input.C
  //            + 0.5 * (input.SL + input.SR)
  if (numberOfInputChannels === 6 && numberOfOutputChannels === 1) {
    let inputChannel0 = input.getChannelData(0);  // input.L
    let inputChannel1 = input.getChannelData(1);  // input.R
    let inputChannel2 = input.getChannelData(2);  // input.C
    let inputChannel4 = input.getChannelData(4);  // input.SL
    let inputChannel5 = input.getChannelData(5);  // input.SR
    let outputChannel0 = output.getChannelData(0);
    let scaleSqrtHalf = Math.sqrt(0.5);
    for (i = 0; i < length; i++) {
      outputChannel0[i] +=
          scaleSqrtHalf * (inputChannel0[i] + inputChannel1[i]) +
          inputChannel2[i] + 0.5 * (inputChannel4[i] + inputChannel5[i]);
    }

    return;
  }

  // Down-mixing: 4 -> 2
  //   output.L += 0.5 * (input.L + input.SL)
  //   output.R += 0.5 * (input.R + input.SR)
  if (numberOfInputChannels == 4 && numberOfOutputChannels == 2) {
    let inputChannel0 = input.getChannelData(0);    // input.L
    let inputChannel1 = input.getChannelData(1);    // input.R
    let inputChannel2 = input.getChannelData(2);    // input.SL
    let inputChannel3 = input.getChannelData(3);    // input.SR
    let outputChannel0 = output.getChannelData(0);  // output.L
    let outputChannel1 = output.getChannelData(1);  // output.R
    for (i = 0; i < length; i++) {
      outputChannel0[i] += 0.5 * (inputChannel0[i] + inputChannel2[i]);
      outputChannel1[i] += 0.5 * (inputChannel1[i] + inputChannel3[i]);
    }

    return;
  }

  // Down-mixing: 5.1 -> 2
  //   output.L += input.L + sqrt(1/2) * (input.C + input.SL)
  //   output.R += input.R + sqrt(1/2) * (input.C + input.SR)
  if (numberOfInputChannels == 6 && numberOfOutputChannels == 2) {
    let inputChannel0 = input.getChannelData(0);    // input.L
    let inputChannel1 = input.getChannelData(1);    // input.R
    let inputChannel2 = input.getChannelData(2);    // input.C
    let inputChannel4 = input.getChannelData(4);    // input.SL
    let inputChannel5 = input.getChannelData(5);    // input.SR
    let outputChannel0 = output.getChannelData(0);  // output.L
    let outputChannel1 = output.getChannelData(1);  // output.R
    let scaleSqrtHalf = Math.sqrt(0.5);
    for (i = 0; i < length; i++) {
      outputChannel0[i] += inputChannel0[i] +
          scaleSqrtHalf * (inputChannel2[i] + inputChannel4[i]);
      outputChannel1[i] += inputChannel1[i] +
          scaleSqrtHalf * (inputChannel2[i] + inputChannel5[i]);
    }

    return;
  }

  // Down-mixing: 5.1 -> 4
  //   output.L += input.L + sqrt(1/2) * input.C
  //   output.R += input.R + sqrt(1/2) * input.C
  //   output.SL += input.SL
  //   output.SR += input.SR
  if (numberOfInputChannels === 6 && numberOfOutputChannels === 4) {
    let inputChannel0 = input.getChannelData(0);    // input.L
    let inputChannel1 = input.getChannelData(1);    // input.R
    let inputChannel2 = input.getChannelData(2);    // input.C
    let inputChannel4 = input.getChannelData(4);    // input.SL
    let inputChannel5 = input.getChannelData(5);    // input.SR
    let outputChannel0 = output.getChannelData(0);  // output.L
    let outputChannel1 = output.getChannelData(1);  // output.R
    let outputChannel2 = output.getChannelData(2);  // output.SL
    let outputChannel3 = output.getChannelData(3);  // output.SR
    let scaleSqrtHalf = Math.sqrt(0.5);
    for (i = 0; i < length; i++) {
      outputChannel0[i] += inputChannel0[i] + scaleSqrtHalf * inputChannel2[i];
      outputChannel1[i] += inputChannel1[i] + scaleSqrtHalf * inputChannel2[i];
      outputChannel2[i] += inputChannel4[i];
      outputChannel3[i] += inputChannel5[i];
    }

    return;
  }

  // All other cases, fall back to the discrete sum.
  discreteSum(input, output);
}