import * as WebAudioApiErrors from '../modules/Errors.mjs';
import { EffectBase } from './EffectBase.mjs';

/**
 * Class representing an Equalization effect.
 * 
 * An Equalizer allows for the volume of an audio signal to be adjusted piecewise according to
 * any number of discrete frequency ranges. Both the size and quantity of frequency ranges are
 * user-definable.
 * 
 * @extends EffectBase
 */
export class Equalization extends EffectBase {

   // Effect-specific private variables
   /** @type {BiquadFilterNode[]} */
   #equalizerNodes;

   // Parameter limits
   static minGain = -40;
   static maxGain = 40;
   static minCutoff = 1000;
   static maxCutoff = 22050;

   /**
    * Constructs a new {@link Equalization} effect object.
    */
   constructor(audioContext) {
      super(audioContext);
      this.#equalizerNodes = [
         new BiquadFilterNode(audioContext, { type: 'lowshelf', frequency: audioContext.sampleRate / 4, gain: 0 }),
         new BiquadFilterNode(audioContext, { type: 'highshelf', frequency: audioContext.sampleRate / 4, gain: 0 })
      ];
   }

   /**
    * Returns a list of all available parameters for manipulation in the `effectOptions` parameter
    * of the {@link EffectBase#update update()} function for this {@link Effect}.
    * 
    * @returns {EffectParameter[]} List of effect-specific parameters for use in the effect's {@link EffectBase#update update()} function
    * @see {@link EffectParameter}
    */
   static getParameters() {
      return [
         { name: 'frequencyBandUpperCutoffs', type: 'Array<number>', validValues: [Equalization.minCutoff, Equalization.maxCutoff], defaultValue: 0 },
         { name: 'frequencyBandGains', type: 'Array<number>', validValues: [Equalization.minGain, Equalization.maxGain], defaultValue: 0 },
      ];
   }

   async load() {
      this.#equalizerNodes[0].connect(this.#equalizerNodes[1]);
   }

   /**
    * Updates the {@link Equalization} effect according to the specified parameters at the
    * specified time.
    * 
    * Note that the `updateTime` parameter can be omitted to immediately cause the requested
    * changes to take effect.
    * 
    * @param {number[]} frequencyBandUpperCutoffs - Upper frequency cutoffs in Hz for each band in the equalizer between [0, 22050]
    * @param {number[]} frequencyBandGains - Gains in dB for each frequency band in the equalizer between [-40, 40]
    * @param {number} [updateTime] - Global API time at which to update the effect
    * @param {number} [timeConstant] - Time constant defining an exponential approach to the target
    * @returns {Promise<boolean>} Whether the effect update was successfully applied
    */
   async update({ frequencyBandUpperCutoffs, frequencyBandGains }, updateTime, timeConstant) {
      if ((frequencyBandUpperCutoffs == null) || (frequencyBandGains == null))
         throw new WebAudioApiErrors.WebAudioValueError('Cannot update the Equalization effect without both of the following parameters: "frequencyBandUpperCutoffs, frequencyBandGains"');
      if (frequencyBandUpperCutoffs.length != frequencyBandGains.length)
         throw new WebAudioApiErrors.WebAudioValueError('Frequency cutoff array and frequency gain array must have the same size');
      for (const cutoff of frequencyBandUpperCutoffs) {
         if (cutoff < Equalization.minCutoff)
            throw new WebAudioApiErrors.WebAudioValueError(`Frequency upper cutoff value cannot be less than ${Equalization.minCutoff}`);
         else if (cutoff > Equalization.maxCutoff)
            throw new WebAudioApiErrors.WebAudioValueError(`Frequency upper cutoff value cannot be greater than ${Equalization.maxCutoff}`);
      }
      for (const gain of frequencyBandGains) {
         if (gain < Equalization.minGain)
            throw new WebAudioApiErrors.WebAudioValueError(`Gain value cannot be less than ${Equalization.minGain}`);
         else if (gain > Equalization.maxGain)
            throw new WebAudioApiErrors.WebAudioValueError(`Gain value cannot be greater than ${Equalization.maxGain}`);
      }
      for (let i = 1; i < frequencyBandUpperCutoffs.length; ++i) {
         if (frequencyBandUpperCutoffs[i] <= frequencyBandUpperCutoffs[i-1])
            throw new WebAudioApiErrors.WebAudioValueError('Frequency band upper cutoffs must be monotonically increasing within the array');
      }
      const timeToUpdate = (updateTime == null) ? this.audioContext.currentTime : updateTime;
      const timeConstantTarget = (timeConstant == null) ? 0.0 : timeConstant;

      // Ensure the correct number of equalization bands are present
      if (frequencyBandUpperCutoffs.length < this.#equalizerNodes.length) {
         this.#equalizerNodes[0].connect(this.#equalizerNodes[this.#equalizerNodes.length - frequencyBandUpperCutoffs.length + 1]);
         for (const removedNode of this.#equalizerNodes.splice(1, this.#equalizerNodes.length - frequencyBandUpperCutoffs.length))
            removedNode.disconnect();
      }
      else if (frequencyBandUpperCutoffs.length > this.#equalizerNodes.length) {
         const lastNode = this.#equalizerNodes.splice(-1, 1)[0];
         this.#equalizerNodes[this.#equalizerNodes.length - 1].disconnect();
         while ((this.#equalizerNodes.length + 1) < frequencyBandUpperCutoffs.length) {
            const newNode = new BiquadFilterNode(this.audioContext, { type: 'peaking' });
            this.#equalizerNodes[this.#equalizerNodes.length - 1].connect(newNode);
            this.#equalizerNodes.push(newNode);
         }
         this.#equalizerNodes[this.#equalizerNodes.length - 1].connect(lastNode);
         this.#equalizerNodes.push(lastNode);
      }

      // Update the parameters for each equalization band
      this.#equalizerNodes[0].frequency.setTargetAtTime(frequencyBandUpperCutoffs[0], timeToUpdate, timeConstantTarget);
      this.#equalizerNodes[0].gain.setTargetAtTime(frequencyBandGains[0], timeToUpdate, timeConstantTarget);
      this.#equalizerNodes[this.#equalizerNodes.length-1].frequency.setTargetAtTime(frequencyBandUpperCutoffs[frequencyBandUpperCutoffs.length-2], timeToUpdate, timeConstantTarget);
      this.#equalizerNodes[this.#equalizerNodes.length-1].gain.setTargetAtTime(frequencyBandGains[frequencyBandGains.length-1], timeToUpdate, timeConstantTarget);
      for (let i = 1; i < this.#equalizerNodes.length - 1; ++i) {
         const centerFrequency = 0.5 * (frequencyBandUpperCutoffs[i] + frequencyBandUpperCutoffs[i-1]);
         this.#equalizerNodes[i].frequency.setTargetAtTime(centerFrequency, timeToUpdate, timeConstantTarget);
         this.#equalizerNodes[i].Q.setTargetAtTime(centerFrequency / (frequencyBandUpperCutoffs[i] - frequencyBandUpperCutoffs[i-1]), timeToUpdate, timeConstantTarget);
         this.#equalizerNodes[i].gain.setTargetAtTime(frequencyBandGains[i], timeToUpdate, timeConstantTarget);
      }
      return true;
   }

   currentParameterValues() {
      const cutoffs = [], gains = [];
      for (const element of this.#equalizerNodes) {
         cutoffs.push(element.frequency.value);
         gains.push(element.gain.value);
      }
      return {
         frequencyBandUpperCutoffs: cutoffs,
         frequencyBandGains: gains
      };
   }

   getInputNode() {
      return this.#equalizerNodes[0];
   }

   getOutputNode() {
      return this.#equalizerNodes[this.#equalizerNodes.length - 1];
   }
}