/**
 * @license
 * Copyright 2017 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * @file Spatially encodes input using weighted spherical harmonics.
 * @author Andrew Allen <bitllama@google.com>
 */
'use strict';
// Internal dependencies.
import Tables from './tables.js';
import Utils from './utils.js';
/**
 * @class Encoder
 * @description Spatially encodes input using weighted spherical harmonics.
 * @param {AudioContext} context
 * Associated {@link
https://developer.mozilla.org/en-US/docs/Web/API/AudioContext AudioContext}.
 * @param {Object} options
 * @param {Number} options.ambisonicOrder
 * Desired ambisonic order. Defaults to
 * {@linkcode Utils.DEFAULT_AMBISONIC_ORDER DEFAULT_AMBISONIC_ORDER}.
 * @param {Number} options.azimuth
 * Azimuth (in degrees). Defaults to
 * {@linkcode Utils.DEFAULT_AZIMUTH DEFAULT_AZIMUTH}.
 * @param {Number} options.elevation
 * Elevation (in degrees). Defaults to
 * {@linkcode Utils.DEFAULT_ELEVATION DEFAULT_ELEVATION}.
 * @param {Number} options.sourceWidth
 * Source width (in degrees). Where 0 degrees is a point source and 360 degrees
 * is an omnidirectional source. Defaults to
 * {@linkcode Utils.DEFAULT_SOURCE_WIDTH DEFAULT_SOURCE_WIDTH}.
 */
class Encoder {
    constructor(context, options) {
        // Public variables.
        /**
         * Mono (1-channel) input {@link
         * https://developer.mozilla.org/en-US/docs/Web/API/AudioNode AudioNode}.
         * @member {AudioNode} input
         * @memberof Encoder
         * @instance
         */
        /**
         * Ambisonic (multichannel) output {@link
         * https://developer.mozilla.org/en-US/docs/Web/API/AudioNode AudioNode}.
         * @member {AudioNode} output
         * @memberof Encoder
         * @instance
         */
        // Use defaults for undefined arguments.
        if (options == undefined) {
            options = {};
        }
        if (options.ambisonicOrder == undefined) {
            options.ambisonicOrder = Utils.DEFAULT_AMBISONIC_ORDER;
        }
        if (options.azimuth == undefined) {
            options.azimuth = Utils.DEFAULT_AZIMUTH;
        }
        if (options.elevation == undefined) {
            options.elevation = Utils.DEFAULT_ELEVATION;
        }
        if (options.sourceWidth == undefined) {
            options.sourceWidth = Utils.DEFAULT_SOURCE_WIDTH;
        }
        this._context = context;
        // Create I/O nodes.
        this.input = context.createGain();
        this._channelGain = [];
        this._merger = undefined;
        this.output = context.createGain();
        // Set initial order, angle and source width.
        this.setAmbisonicOrder(options.ambisonicOrder);
        this._azimuth = options.azimuth;
        this._elevation = options.elevation;
        this.setSourceWidth(options.sourceWidth);
    }
    /**
     * Set the desired ambisonic order.
     * @param {Number} ambisonicOrder Desired ambisonic order.
     */
    setAmbisonicOrder(ambisonicOrder) {
        this._ambisonicOrder = Encoder.validateAmbisonicOrder(ambisonicOrder);
        this.input.disconnect();
        for (let i = 0; i < this._channelGain.length; i++) {
            this._channelGain[i].disconnect();
        }
        if (this._merger != undefined) {
            this._merger.disconnect();
        }
        delete this._channelGain;
        delete this._merger;
        // Create audio graph.
        let numChannels = (this._ambisonicOrder + 1) * (this._ambisonicOrder + 1);
        this._merger = this._context.createChannelMerger(numChannels);
        this._channelGain = new Array(numChannels);
        for (let i = 0; i < numChannels; i++) {
            this._channelGain[i] = this._context.createGain();
            this.input.connect(this._channelGain[i]);
            this._channelGain[i].connect(this._merger, 0, i);
        }
        this._merger.connect(this.output);
    }
    /**
     * Set the direction of the encoded source signal.
     * @param {Number} azimuth
     * Azimuth (in degrees). Defaults to
     * {@linkcode Utils.DEFAULT_AZIMUTH DEFAULT_AZIMUTH}.
     * @param {Number} elevation
     * Elevation (in degrees). Defaults to
     * {@linkcode Utils.DEFAULT_ELEVATION DEFAULT_ELEVATION}.
     */
    setDirection(azimuth, elevation) {
        // Format input direction to nearest indices.
        if (azimuth == undefined || isNaN(azimuth)) {
            azimuth = Utils.DEFAULT_AZIMUTH;
        }
        if (elevation == undefined || isNaN(elevation)) {
            elevation = Utils.DEFAULT_ELEVATION;
        }
        // Store the formatted input (for updating source width).
        this._azimuth = azimuth;
        this._elevation = elevation;
        // Format direction for index lookups.
        azimuth = Math.round(azimuth % 360);
        if (azimuth < 0) {
            azimuth += 360;
        }
        elevation = Math.round(Math.min(90, Math.max(-90, elevation))) + 90;
        // Assign gains to each output.
        this._channelGain[0].gain.value = Tables.MAX_RE_WEIGHTS[this._spreadIndex][0];
        for (let i = 1; i <= this._ambisonicOrder; i++) {
            let degreeWeight = Tables.MAX_RE_WEIGHTS[this._spreadIndex][i];
            for (let j = -i; j <= i; j++) {
                let acnChannel = (i * i) + i + j;
                let elevationIndex = i * (i + 1) / 2 + Math.abs(j) - 1;
                let val = Tables.SPHERICAL_HARMONICS[1][elevation][elevationIndex];
                if (j != 0) {
                    let azimuthIndex = Tables.SPHERICAL_HARMONICS_MAX_ORDER + j - 1;
                    if (j < 0) {
                        azimuthIndex = Tables.SPHERICAL_HARMONICS_MAX_ORDER + j;
                    }
                    val *= Tables.SPHERICAL_HARMONICS[0][azimuth][azimuthIndex];
                }
                this._channelGain[acnChannel].gain.value = val * degreeWeight;
            }
        }
    }
    /**
     * Set the source width (in degrees). Where 0 degrees is a point source and 360
     * degrees is an omnidirectional source.
     * @param {Number} sourceWidth (in degrees).
     */
    setSourceWidth(sourceWidth) {
        // The MAX_RE_WEIGHTS is a 360 x (Tables.SPHERICAL_HARMONICS_MAX_ORDER+1)
        // size table.
        this._spreadIndex = Math.min(359, Math.max(0, Math.round(sourceWidth)));
        this.setDirection(this._azimuth, this._elevation);
    }
}
/**
 * Validate the provided ambisonic order.
 * @param {Number} ambisonicOrder Desired ambisonic order.
 * @return {Number} Validated/adjusted ambisonic order.
 * @private
 */
Encoder.validateAmbisonicOrder = ambisonicOrder => {
    if (isNaN(ambisonicOrder) || ambisonicOrder == undefined) {
        Utils.log('Error: Invalid ambisonic order', options.ambisonicOrder, '\nUsing ambisonicOrder=1 instead.');
        ambisonicOrder = 1;
    }
    else if (ambisonicOrder < 1) {
        Utils.log('Error: Unable to render ambisonic order', options.ambisonicOrder, '(Min order is 1)', '\nUsing min order instead.');
        ambisonicOrder = 1;
    }
    else if (ambisonicOrder > Tables.SPHERICAL_HARMONICS_MAX_ORDER) {
        Utils.log('Error: Unable to render ambisonic order', options.ambisonicOrder, '(Max order is', Tables.SPHERICAL_HARMONICS_MAX_ORDER, ')\nUsing max order instead.');
        options.ambisonicOrder = Tables.SPHERICAL_HARMONICS_MAX_ORDER;
    }
    return ambisonicOrder;
};
export default Encoder;
