/**
 * creates a fast fourier transform analyser
 *
 *
 */
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {AudioBuilder} from '../../../core/audio/AudioBuilder';
import {FFT} from 'tone/build/esm/component/analysis/FFT';
import {AudioNodeAnalyserType} from '../../poly/NodeContext';
import {BaseNodeType} from '../_Base';
import {effectParamsOptions} from './utils/EffectsController';
import {isNumber, isBooleanTrue} from '../../../core/Type';
import {BaseAnalyserAudioNode} from './_BaseAnalyser';
import {Number2} from '../../../types/GlobalTypes';
import {
	convertFrequencyBandsToOctaveBands,
	convertFrequencyBandsToOctaveBandsDivisions2,
} from '../../../core/audio/AudioConversion';
const DEFAULTS = {
	normalRange: false,
	size: 1024,
	smoothing: 0.8,
}; //FFT.getDefaults();

const paramCallback = (node: BaseNodeType) => {
	FFTAudioNode.PARAM_CALLBACK_updateEffect(node as FFTAudioNode);
};

const RANGE_DEFAULT: Number2 = [10000, -10000];

class FFTAudioParamsConfig extends NodeParamsConfig {
	/** @param array size will be 2**sizeExponent */
	sizeExponent = ParamConfig.INTEGER(10, {
		range: [4, 14],
		rangeLocked: [true, true],
	});
	/** @param array size */
	arraySize = ParamConfig.INTEGER(`2**ch('sizeExponent')`, {
		editable: false,
	});
	/** @param a value from between 0 and 1 where 0 represents no time averaging with the last analysis frame */
	smoothing = ParamConfig.FLOAT(DEFAULTS.smoothing, {
		range: [0, 1],
		rangeLocked: [true, true],
		...effectParamsOptions(paramCallback),
	});
	/** @param normalizes the output between 0 and 1. The value will be in decibel otherwise. */
	normalRange = ParamConfig.BOOLEAN(1, effectParamsOptions(paramCallback));
	/** @param groups the FFT frequency bands into octave bands */
	asOctaves = ParamConfig.BOOLEAN(0, {
		// if cook is false, the materials reading the COP/audioAnalyser
		// do not seem to update the texture when this is changed.
		// I should investigate, as this should not be different than changing normalRange.
		//cook:false,
	});
	octaveDivisions = ParamConfig.INTEGER(1, {
		range: [1, 2],
		rangeLocked: [true, true],
		visibleIf: {asOctaves: 1},
		separatorAfter: true,
	});
	/** @param display range param */
	updateRangeParam = ParamConfig.BOOLEAN(1, {
		cook: false,
		callback: (node: BaseNodeType) => {
			FFTAudioNode.PARAM_CALLBACK_updateUpdateRangeParam(node as FFTAudioNode);
		},
	});
	/** @param range value */
	range = ParamConfig.VECTOR2([0, 0], {
		visibleIf: {updateRangeParam: 1},
		editable: false,
		cook: false,
	});
	/** @param accumulated range */
	maxRange = ParamConfig.VECTOR2(RANGE_DEFAULT, {
		visibleIf: {updateRangeParam: 1},
		editable: false,
		cook: false,
	});
	/** @param resetMaxRange */
	resetMaxRange = ParamConfig.BUTTON(null, {
		visibleIf: {updateRangeParam: 1},
		callback: (node) => {
			FFTAudioNode.PARAM_CALLBACK_resetMaxRange(node as FFTAudioNode);
		},
	});
}
const ParamsConfig = new FFTAudioParamsConfig();

export class FFTAudioNode extends BaseAnalyserAudioNode<FFTAudioParamsConfig> {
	override paramsConfig = ParamsConfig;
	static override type() {
		return AudioNodeAnalyserType.FFT;
	}

	override initializeNode() {
		this.io.inputs.setCount(1);
	}

	override cook(inputContents: AudioBuilder[]) {
		const audioBuilder = inputContents[0];

		this._resetEffect();
		const fft = this._effect();
		this._updateOnTickHook();

		const inputNode = audioBuilder.audioNode();
		if (inputNode) {
			inputNode.connect(fft);
		}
		audioBuilder.setAudioNode(fft);

		this.setAudioBuilder(audioBuilder);
	}
	getAnalyserValue() {
		if (this.__effect__) {
			const fftValues = this.__effect__.getValue();

			if (isBooleanTrue(this.pv.asOctaves)) {
				// if (isBooleanTrue(this.pv.normalRange)) {
				// const timeConstant = this.__effect__.getFrequencyOfIndex(1);
				// const mult = 2 ** this.pv.sizeExponent;
				// console.log('mult', mult);
				// const valuesCount = fftValues.length;
				// for (let i = 0; i < valuesCount; i++) {
				// 	fftValues[i] *= mult;
				// }
				// }
				const octaveValues = this._octaveValues();
				if (this._octaveDivisions() == 1) {
					convertFrequencyBandsToOctaveBands(fftValues, octaveValues);
				} else {
					convertFrequencyBandsToOctaveBandsDivisions2(fftValues, octaveValues, this._octaveDivisions());
				}
				return octaveValues;
			} else {
				return fftValues;
			}
		}
	}
	private __octaveValues: Float32Array | undefined;
	private _octaveDivisions() {
		return 2 ** (this.pv.octaveDivisions - 1);
	}
	private _octaveValues() {
		const requiredSize = isBooleanTrue(this.pv.asOctaves)
			? this.pv.sizeExponent * this._octaveDivisions()
			: this.pv.sizeExponent;
		// const requiredSize = this.pv.sizeExponent;
		if (this.__octaveValues && this.__octaveValues.length != requiredSize) {
			this.__octaveValues = undefined;
		}
		return (this.__octaveValues = this.__octaveValues || new Float32Array(requiredSize));
	}
	private __effect__: FFT | undefined;
	private _effect() {
		return (this.__effect__ = this.__effect__ || this._createEffect());
	}
	private _createEffect() {
		return new FFT({
			size: this._FFTSize(),
			normalRange: isBooleanTrue(this.pv.normalRange),
			smoothing: this.pv.smoothing,
		});
	}
	private _resetEffect() {
		if (this.__effect__) {
			this.__effect__.dispose();
			this.__effect__ = undefined;
		}
	}
	static PARAM_CALLBACK_updateEffect(node: FFTAudioNode) {
		node._updateEffect();
	}
	private _updateEffect() {
		const effect = this._effect();
		effect.normalRange = isBooleanTrue(this.pv.normalRange);
		effect.smoothing = this.pv.smoothing;
	}
	private _FFTSize() {
		return 2 ** this.pv.sizeExponent;
	}

	/*
	 * UPDATE RANGE PARAM
	 */
	static PARAM_CALLBACK_updateUpdateRangeParam(node: FFTAudioNode) {
		node._updateRangeParam();
		node._updateOnTickHook();
	}
	static PARAM_CALLBACK_resetMaxRange(node: FFTAudioNode) {
		node.p.maxRange.set(RANGE_DEFAULT);
	}

	private _updateRangeParam() {
		if (!this.__effect__) {
			return;
		}
		const values = this.getAnalyserValue();
		if (!values) {
			return;
		}
		const min = Math.min(...values);
		const max = Math.max(...values);
		this.p.range.set([min, max]);

		if (min < this.pv.maxRange.x && isNumber(min) && isFinite(min)) {
			this.p.maxRange.x.set(min);
		}
		if (max > this.pv.maxRange.y && isNumber(max) && isFinite(max)) {
			this.p.maxRange.y.set(max);
		}
	}
	/*
	 * REGISTER TICK CALLBACK
	 */
	private _updateOnTickHook() {
		if (isBooleanTrue(this.pv.updateRangeParam)) {
			this._registerOnTickHook();
		} else {
			this._unRegisterOnTickHook();
		}
	}
	private async _registerOnTickHook() {
		if (this.scene().registeredBeforeTickCallbacks().has(this._tickCallbackName())) {
			return;
		}
		this.scene().registerOnBeforeTick(this._tickCallbackName(), this._updateRangeParam.bind(this));
	}
	private async _unRegisterOnTickHook() {
		this.scene().unRegisterOnBeforeTick(this._tickCallbackName());
	}
	private _tickCallbackName() {
		return `audio/FFT-${this.graphNodeId()}`;
	}
}
