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

/**
 * Class representing a Distortion effect.
 * 
 * A Distortion effect alters an audio waveform by adding a large amount of gain to the audio
 * signal, normally to the point of clipping the signal. This creates a distorted, gritty feeling,
 * most commonly used with electrical instruments.
 * 
 * @extends EffectBase
 */
export class Distortion extends EffectBase {

   // Effect-specific private variables
   /** @type {GainNode} */
   #outputNode;
   /** @type {BiquadFilterNode} */
   #preBandpassNode;
   /** @type {WaveShaperNode} */
   #distortionNode;
   /** @type {number} */
   #intensityValue;

   // Parameter limits
   static minTone = 0;
   static maxTone = 22050;
   static minIntensity = 0;
   static maxIntensity = 1;

   /**
    * Constructs a new {@link Distortion} effect object.
    */
   constructor(audioContext) {
      super(audioContext);
      this.#outputNode = new GainNode(audioContext);
      this.#preBandpassNode = new BiquadFilterNode(audioContext, { type: 'lowpass' });
      this.#distortionNode = new WaveShaperNode(audioContext);
      this.#preBandpassNode.connect(this.#distortionNode).connect(this.#outputNode);
   }

   /**
    * 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: 'tone', type: 'number', validValues: [Distortion.minTone, Distortion.maxTone], defaultValue: 3000 },
         { name: 'intensity', type: 'number', validValues: [Distortion.minIntensity, Distortion.maxIntensity], defaultValue: 0.5 }
      ];
   }

   async load() {
      const driveValue = 0.5, n = 22050, deg = Math.PI / 180;
      const k = driveValue * 100, curve = new Float32Array(n);
      this.#preBandpassNode.frequency.value = 3000;
      for (let i = 0; i < n; ++i) {
         const x = i * 2 / n - 1;
         curve[i] = (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x));
      }
      this.#distortionNode.curve = curve;
      this.#outputNode.gain.value = driveValue;
      this.#intensityValue = 0.5;
   }

   /**
    * Updates the {@link Distortion} 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} tone - Low-pass cutoff frequency in Hz for filtering before distortion between [0, 22050]
    * @param {number} intensity - Ratio of distorted-to-original sound as a percentage between [0, 1]
    * @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({ tone, intensity }, updateTime, timeConstant) {
      if ((tone == null) && (intensity == null))
         throw new WebAudioApiErrors.WebAudioValueError('Cannot update the Distortion effect without at least one of the following parameters: "tone, intensity"');
      const timeToUpdate = (updateTime == null) ? this.audioContext.currentTime : updateTime;
      const timeConstantTarget = (timeConstant == null) ? 0.0 : timeConstant;
      if (tone != null)
         this.#preBandpassNode.frequency.setTargetAtTime(tone, timeToUpdate, timeConstantTarget);
      if (intensity != null) {
         const n = 22050, deg = Math.PI / 180;
         const k = intensity * 100, curve = new Float32Array(n);
         for (let i = 0; i < n; ++i) {
            const x = i * 2 / n - 1;
            curve[i] = (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x));
         }
         this.#distortionNode.curve = curve;
         const gainOffset = (intensity < 0.5) ? (Math.exp(2.3 * (0.5 - intensity)) - 0.5) : (0.5 + (0.2 * (0.5 - intensity)));
         this.#outputNode.gain.setTargetAtTime(gainOffset, timeToUpdate, timeConstantTarget);
         this.#intensityValue = intensity;
      }
      return true;
   }

   currentParameterValues() {
      return {
         tone: this.#preBandpassNode.frequency.value,
         intensity: this.#intensityValue
      };
   }

   getInputNode() {
      return this.#preBandpassNode;
   }

   getOutputNode() {
      return this.#outputNode;
   }
}