var sampleRate = 48000.0;

var numberOfChannels = 1;

// Time step when each panner node starts.
var timeStep = 0.001;

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

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

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

// These are global mostly for debugging.
var context;
var impulse;
var bufferSource;
var panner;
var position;
var time;
      
var renderedBuffer;
var renderedLeft;
var renderedRight;

function createGraph(context, nodeCount) {
    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.
    var angleStep = Math.PI / (nodeCount - 1);

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

    for (var 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";

        var angle = angleStep * k;
        position[k] = {angle : angle, x : Math.cos(angle), z : Math.sin(angle)};
        panner[k].setPosition(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, nodeCount, numberOfSourceChannels) {
    numberOfChannels = numberOfSourceChannels;

    createGraph(context, nodeCount);

    context.oncomplete = checkResult;
    context.startRendering();
}

// 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) {
    var azimuth = angleToAzimuth(angle);

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

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

        return { left : gainL, right : gainR };
    } else {
        if (azimuth <= 0) {
            var panPosition = (azimuth + 90) / 90;
    
            var gainL = 1 + Math.cos(0.5 * Math.PI * panPosition);
            var gainR = Math.sin(0.5 * Math.PI * panPosition);
    
            return { left : gainL, right : gainR };
        } else {
            var panPosition = azimuth / 90;
    
            var gainL = Math.cos(0.5 * Math.PI * panPosition);
            var gainR = 1 + Math.sin(0.5 * Math.PI * panPosition);
    
            return { left : gainL, right : gainR };
        }
    }
}

function checkResult(event) {
    renderedBuffer = event.renderedBuffer;
    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.
    var maxAllowedError = 1.3e-6;
  
    var success = true;

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

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

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

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

    for (var 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.
            var pannerGain = equalPowerGain(position[impulseCount].angle);
            var expectedL = pannerGain.left;
            var expectedR = pannerGain.right;

            // Absolute error in the gain.
            var errorL = Math.abs(renderedLeft[k] - expectedL);
            var 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.
            var expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate);
            if (k != expectedOffset) {
                timeErrors[timeCount] = { actual : k, expected : expectedOffset};
                ++timeCount;
            }
            ++impulseCount;
        }
    }

    if (impulseCount == nodesToCreate) {
        testPassed("Number of impulses matches the number of panner nodes.");
    } else {
        testFailed("Number of impulses is incorrect.  (Found " + impulseCount + " but expected " + nodesToCreate + ")");
        success = false;
    }

    if (timeErrors.length > 0) {
        success = false;
        testFailed(timeErrors.length + " timing errors found in " + nodesToCreate + " panner nodes.");
        for (var k = 0; k < timeErrors.length; ++k) {
            testFailed("Impulse at sample " + timeErrors[k].actual + " but expected " + timeErrors[k].expected);
        }
    } else {
        testPassed("All impulses at expected offsets.");
    }

    if (maxErrorL <= maxAllowedError) {
        testPassed("Left channel gain values are correct.");
    } else {
        testFailed("Left channel gain values are incorrect.  Max error = " + maxErrorL + " at time " + time[maxErrorIndexL] + " (threshold = " + maxAllowedError + ")");
        success = false;
    }

    if (maxErrorR <= maxAllowedError) {
        testPassed("Right channel gain values are correct.");
    } else {
        testFailed("Right channel gain values are incorrect.  Max error = " + maxErrorR + " at time " + time[maxErrorIndexR] + " (threshold = " + maxAllowedError + ")");
        success = false;
    }

    if (success) {
        testPassed("EqualPower panner test passed");
    } else {
        testFailed("EqualPower panner test failed");
    }

    finishJSTest();
}
