package zero1hd.polyjet.audio;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;

import edu.emory.mathcs.jtransforms.fft.FloatFFT_1D;
import zero1hd.polyjet.util.MiniEvents;
import zero1hd.polyjet.util.MiniListener;

public class AudioAnalyzer {
	private boolean containsData;
	private boolean finalized;
	FloatFFT_1D fft;
	public AudioData audiofile;

	float[] audioPCM;
	float[] spectrum;
	float[] lastSpectrum;
	float[] fftData;
	
	Runnable analysisAlgorithm;
	Runnable thresholdCalculator;
	
	int bassBinBegin;
	int bassBinEnd;
	private FloatArray bassSpectralFlux = new FloatArray();
	private FloatArray bassThreshold = new FloatArray();
	private FloatArray bassPrunned = new FloatArray();
	private FloatArray bassPeaks = new FloatArray();
	private float bassMaxValue;

	int UMBinBegin;
	int UMBinEnd;
	private FloatArray UMSpectralFlux = new FloatArray();
	private FloatArray UMThreshold = new FloatArray();
	private FloatArray UMPrunned = new FloatArray();
	private FloatArray UMPeaks = new FloatArray();
	private float UMMaxValue;
	
	private FloatArray overlappedBeats = new FloatArray();
	
	float bassThresholdMultiplier;
	float UMThresholdMultiplier;
	int UMThresholdCalcRange;
	int bassThresholdCalcRange;
	
	private Array<MiniListener> listeners;
	
	private volatile int progress;
	public AudioAnalyzer() {
		listeners = new Array<>();
		
		analysisAlgorithm = new Runnable() {

			@Override
			public void run() {
				progress = 0;
				int tasksDone = 0;
				int totalTasks = audiofile.getSampleCount()/audiofile.getReadWindowSize();
				
				bassThresholdMultiplier = 1.5f;
				UMThresholdMultiplier = 2f;
				
				bassBinBegin = binCalculator(60);
				bassBinEnd = binCalculator(800);
				
				UMBinBegin = binCalculator(1500);
				UMBinEnd = binCalculator(3000);
				
				UMThresholdCalcRange = thresholdRangeCalc(0.5f);
				bassThresholdCalcRange = thresholdRangeCalc(0.7f);
				
				Gdx.app.debug("Read freq", String.valueOf(audiofile.getFormat().getSampleRate()));
				Gdx.app.debug("Using following bin ranges", "\nBass freq begin: " + bassBinBegin + "\nBass freq end: " + bassBinEnd + "\nMain freq begin: " + UMBinBegin + "\nMain freq end: " + UMBinEnd);

				
				Gdx.app.debug("Threshold Calc Range UM", String.valueOf(UMThresholdCalcRange));
				Gdx.app.debug("Threshold Calc Range Bass", String.valueOf(bassThresholdCalcRange));
				
				
				fft = new FloatFFT_1D(audiofile.getReadWindowSize());
				while (audiofile.readSamples(audioPCM) > 0) {
					tasksDone++;
					
					fft.realForward(audioPCM);
					
					System.arraycopy(spectrum, 0, lastSpectrum, 0, spectrum.length);
					System.arraycopy(audioPCM, 0, spectrum, 0, spectrum.length);
					
					float fluxVal = 0;
					//bass detection
					fluxVal = 0;
					for (int i = bassBinBegin; i < bassBinEnd; i++) {
						fluxVal += ((int) (spectrum[i] - lastSpectrum[i])) > 0
								? (int) (spectrum[i] - lastSpectrum[i]) : 0;
					}
					bassSpectralFlux.add(fluxVal);
					
					//main detection
					fluxVal = 0;
					for (int i = UMBinBegin; i < UMBinEnd; i++) {
						fluxVal += ((int) (spectrum[i] - lastSpectrum[i])) > 0
								? (int) (spectrum[i] - lastSpectrum[i]) : 0;
					}
					UMSpectralFlux.add(fluxVal);
					
					progress = (int) (100f*tasksDone/totalTasks);
				}

				Gdx.app.debug("Audio Analyzer", "Done getting spectral flux.");

				shrinkData();
				containsData = true;
				
				send(MiniEvents.SPECTRAL_FLUX_DONE);
			}
		};
		
		thresholdCalculator = new Runnable() {
			
			@Override
			public void run() {
				
				//threshold calculation
				for (int i = 0; i < UMSpectralFlux.size; i++) {
					int UMStart = Math.max(0, i - UMThresholdCalcRange/2);
					int UMEnd = Math.min(UMSpectralFlux.size - 1, i + UMThresholdCalcRange/2);
					
					int bassStart = Math.max(0, i - bassThresholdCalcRange/2);
					int bassEnd = Math.min(UMSpectralFlux.size - 1, i + bassThresholdCalcRange/2);
					
					float average = 0;
					for (int j = bassStart; j <= bassEnd; j++) {
						average += bassSpectralFlux.get(j);
					}
					average /= (bassEnd - bassStart);
					bassThreshold.add(average * bassThresholdMultiplier);
					
					average = 0;
					for (int j = UMStart; j <= UMEnd; j++) {
						average+= UMSpectralFlux.get(j);
					}
					average /= (UMEnd - UMStart);
					UMThreshold.add(average*UMThresholdMultiplier);
				}
				
				Gdx.app.debug("Audio Analyzer", "Threshold calculated.");

				
				//pruning data
				float prunnedCurrentVal;
				for (int i = 0; i < UMSpectralFlux.size; i++) {
					prunnedCurrentVal = bassSpectralFlux.get(i) - bassThreshold.get(i);
					
					if (prunnedCurrentVal >= 0) {
						bassPrunned.add(prunnedCurrentVal);
					} else {
						bassPrunned.add(0);
					}
					
					prunnedCurrentVal = UMSpectralFlux.get(i) - UMThreshold.get(i);
					
					if (prunnedCurrentVal >= 0 ) {
						UMPrunned.add(prunnedCurrentVal);
					} else {
						UMPrunned.add(0);
					}
				}
				Gdx.app.debug("Audio Analyzer", "Data prunned.");

				//peak detection
				for (int i = 0; i < UMPrunned.size-1; i++) {
					bassPeaks.add((bassPrunned.get(i) > bassPrunned.get(i+1) ? bassPrunned.get(i) : 0));
					if (bassPeaks.get(i) > bassMaxValue) {
						bassMaxValue = bassPeaks.get(i);
					}
					
					UMPeaks.add((UMPrunned.get(i) > UMPrunned.get(i+1) ? UMPrunned.get(i) : 0));
					if (UMPeaks.get(i) > UMMaxValue) {
						UMMaxValue = UMPeaks.get(i);
					}
				}
				
				Gdx.app.debug("Audio Analyzer", "Found all peaks.");
				
				//overlapping beats
				for (int i = 0; i < UMPeaks.size; i++) {
					if (bassPeaks.get(i) != 0 && UMPeaks.get(i) != 0) {
						overlappedBeats.add(bassPeaks.get(i)+UMPeaks.get(i)/2);
					} else {
						overlappedBeats.add(0);
					}
				}
				
				Gdx.app.debug("Audio Analyzer", "overlapped beats checked.");
				
				finalized = true;
			}
		};
		
	}
	
	public void resetVars() {
		bassSpectralFlux.clear();
		bassThreshold.clear();
		bassPrunned.clear();
		bassPeaks.clear();
		
		UMSpectralFlux.clear();
		UMThreshold.clear();
		UMPrunned.clear();
		UMPeaks.clear();
		
		overlappedBeats.clear();
		
		containsData = false;
	}
	
	public void shrinkData() {
		bassSpectralFlux.shrink();
		bassThreshold.shrink();
		bassPrunned.shrink();
		bassPeaks.shrink();
		
		UMSpectralFlux.shrink();
		UMThreshold.shrink();
		UMPrunned.shrink();
		UMPeaks.shrink();
		
		overlappedBeats.shrink();
	}
	
	public void startAnalyticalThread(AudioData audiofile) {
		audioPCM = new float[audiofile.getReadWindowSize()];
		fftData = new float[audiofile.getReadWindowSize()];
		spectrum = new float[(audiofile.getReadWindowSize()/2)+1];
		lastSpectrum = new float[(audiofile.getReadWindowSize()/2)+1];
		this.audiofile = audiofile;
		
		Thread analyticalThread = new Thread(analysisAlgorithm);
		analyticalThread.start();
	}
	
	public void runThresholdCleaning(float baseThresholdMultiplier, float UMThresholdMultiplier) {
		this.bassThresholdMultiplier = baseThresholdMultiplier;
		this.UMThresholdMultiplier = UMThresholdMultiplier;
		
		Thread thresholdClean = new Thread(thresholdCalculator);
		thresholdClean.start();
	}
	
	public void runThresholdCleaning() {
		Thread thresholdClean = new Thread(thresholdCalculator);
		thresholdClean.start();
	}
	
	public FloatArray getBassPeaks() {
		return bassPeaks;
	}
	public FloatArray getUMPeaks() {
		return UMPeaks;
	}
	
	private int binCalculator(int frequency) {
		float totalBins = audiofile.getReadWindowSize()/2 +1;
		float frequencyRange = audiofile.getFormat().getSampleRate()/2;
		
		//Formula derived from: (22050b/513)+(22050/513/2) = F
		//let b be the bin
		//let F be the frequency.
		return (int) (totalBins*(frequency - (frequencyRange/totalBins*2))/frequencyRange);
	}
	
	private int thresholdRangeCalc(float durationOfRange) {
		float timePerWindow = (float)audiofile.getReadWindowSize()/audiofile.getFormat().getSampleRate();
		return (int) (durationOfRange/timePerWindow);
	}
	
	public float getBassMaxValue() {
		return bassMaxValue;
	}
	
	public float getUMMaxValue() {
		return UMMaxValue;
	}
	
	public int getReadIndex() {
		if (audiofile.getReadIndex() < UMPeaks.size) {
			return audiofile.getReadIndex();
		} else {
			return 0;
		}
	}
	
	public boolean containsData() {
		return containsData;
	}
	
	public synchronized int getProgress() {
		return progress;
	}
	
	public boolean isFinalized() {
		return finalized;
	}
	
	public void addListener(MiniListener listener) {
		listeners.add(listener);
	}
	
	public void send(MiniEvents ID) {
		while (listeners.iterator().hasNext()) {
			listeners.iterator().next().handle(ID);
		}
	}
}
