package zero1hd.rhythmbullet.audio;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

import edu.emory.mathcs.jtransforms.fft.FloatFFT_1D;
import zero1hd.rhythmbullet.util.MiniEvents;
import zero1hd.rhythmbullet.util.MiniSender;

public class AudioAnalyzer {
	private boolean containsData;
	private MusicDataPack musicInfo;

	float[] audioPCM;
	float[] spectrum;
	float[] lastSpectrum;
	
	private ExecutorService exec = Executors.newSingleThreadExecutor();
	private Runnable analysisAlgorithm;
	private Runnable thresholdCalculator;
	
	int bassBinBegin;
	int bassBinEnd;
	private FloatArray bassSpectralFlux = new FloatArray();
	private float bassMaxValue;
	private float bassAvg;
	float bassThresholdMultiplier;
	int bassThresholdCalcRange;
	
	int mBinBegin;
	int mBinEnd;
	private FloatArray mSpectralFlux = new FloatArray();
	private float mMaxValue;
	private float mAvg; 
	float mThresholdMultiplier;
	int mThresholdCalcRange;
	
	int umBinBegin;
	int umBinEnd;
	private FloatArray umSpectralFlux = new FloatArray();
	private float umMaxValue;
	private float umAvg; 
	float umThresholdMultiplier;
	int umThresholdCalcRange;

	public MiniSender sender;
	
	private float avgSPB;
	
	int PUID;
	boolean work;
	private int progress;
	private float secondsPerWindow;
	
	private AudioDataPackage pack;
	public AudioAnalyzer(MusicDataPack audiofile) {
		sender = new MiniSender();
		
		analysisAlgorithm = () -> {
			progress = 0;
			int tasksDone = 0;
			long totalTasks = musicInfo.getSampleCount()/musicInfo.getReadWindowSize();
			
			bassThresholdMultiplier = 1.5f;
			mThresholdMultiplier = 1.4f;
			umThresholdMultiplier = 1.4f;
			
			bassBinBegin = 0;
			bassBinEnd = 12;
			
			mBinBegin = 50;
			mBinEnd = 250;
			
			umBinBegin = 350;
			umBinEnd = 513;
			
			bassThresholdCalcRange = thresholdRangeCalc(0.27f);
			mThresholdCalcRange = thresholdRangeCalc(0.4f);
			umThresholdCalcRange = thresholdRangeCalc(0.4f);
			
			Gdx.app.debug("Read freq", String.valueOf(musicInfo.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("Total tasks", String.valueOf(totalTasks));
			
			Gdx.app.debug("Threshold Calc Range UM", String.valueOf(umThresholdCalcRange));
			Gdx.app.debug("Threshold Calc Range M", String.valueOf(umThresholdCalcRange));
			Gdx.app.debug("Threshold Calc Range Bass", String.valueOf(bassThresholdCalcRange));
			
			FloatFFT_1D fft = new FloatFFT_1D(musicInfo.getReadWindowSize());
			int seedDigit = 0;
			
			while (musicInfo.readSamples(audioPCM) > 0 && work) {
				
				fft.realForward(audioPCM);
				
				//Building a PUID (Pseudo unique ID)
				if (tasksDone == (seedDigit*totalTasks/9)) {
					float avg = 0;
					for (int frame = 0; frame < spectrum.length; frame++) {
						avg += spectrum[frame];
					}
					avg /= spectrum.length;
					if (avg < 0) {
						avg *= -1f;
					}
					PUID +=(int) Math.pow(10, 9-seedDigit) * ((int)(avg*1000f)-(int)(avg*100f)*10);
					seedDigit ++;
				}
				
				System.arraycopy(spectrum, 0, lastSpectrum, 0, spectrum.length);
				System.arraycopy(audioPCM, 0, spectrum, 0, spectrum.length);
				
				float fluxVal;
				//bass detection
				fluxVal = 0;
				for (int i = bassBinBegin; i < bassBinEnd; i++) {
					fluxVal += ((spectrum[i] - lastSpectrum[i])) < 0
							? 0 : (spectrum[i] - lastSpectrum[i]);
				}
				bassSpectralFlux.add(fluxVal);
				
				//m detection
				fluxVal = 0;
				for (int i = mBinBegin; i < mBinEnd; i++) {
					fluxVal += ((spectrum[i] - lastSpectrum[i])) < 0
							? 0 : (spectrum[i] - lastSpectrum[i]);
				}
				mSpectralFlux.add(fluxVal);
				
				//um detection
				fluxVal = 0;
				for (int i = umBinBegin; i < umBinEnd; i++) {
					fluxVal += ((spectrum[i] - lastSpectrum[i])) < 0
							? 0 : (spectrum[i] - lastSpectrum[i]);
				}
				umSpectralFlux.add(fluxVal);
				tasksDone++;
				progress = (int) (100f*tasksDone/totalTasks);
				sender.send(MiniEvents.ANALYZER_ITERATED);
			}
			
			if (work) {
				Gdx.app.debug("Audio Analyzer", "Done getting spectral flux.");
				Gdx.app.debug("Audio Analyzer", "window count: " + bassSpectralFlux.size);
				containsData = true;
				Gdx.app.debug("Audio Analyzer", "USING SEED: " + PUID);
				progress = 100;
				sender.send(MiniEvents.SPECTRAL_FLUX_DONE);
			}
		};
		
		thresholdCalculator = () -> {
			Gdx.app.debug("Audio Analyzer", "beginning threshold calc.");
			FloatArray bassThreshold = new FloatArray();
			FloatArray mThreshold = new FloatArray();
			FloatArray umThreshold = new FloatArray();
			//threshold calculation
			for (int i = 0; i < umSpectralFlux.size && work; i++) {
				int start = Math.max(0, i - umThresholdCalcRange/2);
				int end = Math.min(umSpectralFlux.size - 1, i + umThresholdCalcRange/2);
				
				float average = 0;
				for (int j = start; j <= end; j++) {
					average += bassSpectralFlux.get(j);
				}
				average /= (end - start);
				bassThreshold.add(average * bassThresholdMultiplier);
				
				average = 0;
				for (int j = start; j <= end; j++) {
					average+= mSpectralFlux.get(j);
				}
				average /= (end - start);
				mThreshold.add(average*mThresholdMultiplier);
				
				average = 0;
				for (int j = start; j <= end; j++) {
					average+= umSpectralFlux.get(j);
				}
				average /= (end - start);
				umThreshold.add(average*umThresholdMultiplier);
			}
			
			Gdx.app.debug("Audio Analyzer", "Threshold calculated.");

			//pruning data
			float prunnedCurrentVal;
			FloatArray bassPrunned = new FloatArray();
			FloatArray mPrunned = new FloatArray();
			FloatArray umPrunned = new FloatArray();
			for (int i = 0; i < umSpectralFlux.size && work; i++) {
				prunnedCurrentVal = bassSpectralFlux.get(i) - bassThreshold.get(i);
				if (prunnedCurrentVal >= 0) {
					bassPrunned.add(prunnedCurrentVal);
				} else {
					bassPrunned.add(0);
				}
				
				prunnedCurrentVal = mSpectralFlux.get(i) - mThreshold.get(i);
				if (prunnedCurrentVal >= 0 ) {
					mPrunned.add(prunnedCurrentVal);
				} else {
					mPrunned.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.");

			secondsPerWindow = musicInfo.getReadWindowSize()/musicInfo.getSampleRate();
			//peak detection
			
			int lastID = 0;
			float bassBeats = 0;
			float mBeats = 0;
			float umBeats = 0;
			avgSPB = -1f;
			FloatArray bassPeaks = new FloatArray();
			FloatArray mPeaks = new FloatArray();
			FloatArray umPeaks = new FloatArray();
			for (int i = 0; i < umPrunned.size-1 && work; i++) {
				bassPeaks.add((bassPrunned.get(i) > bassPrunned.get(i+1) ? bassPrunned.get(i) : 0f));
				if (bassPeaks.get(i) > bassMaxValue) {
					bassMaxValue = bassPeaks.get(i);
				}
				
				mPeaks.add((mPrunned.get(i) > mPrunned.get(i+1) ? mPrunned.get(i) : 0f));
				if (mPeaks.get(i) > mMaxValue) {
					mMaxValue = mPeaks.get(i);
				}
				
				umPeaks.add((umPrunned.get(i) > umPrunned.get(i+1) ? umPrunned.get(i) : 0f));
				if (umPeaks.get(i) > umMaxValue) {
					umMaxValue = umPeaks.get(i);
				}
				
				

				if (avgSPB != -1) {
					if (bassPeaks.get(i) == 0) {
						avgSPB ++;
					} else {
						lastID = i;
					}
				} else if (bassPeaks.get(i) != 0) {
					avgSPB = 0;
				}

				if (bassPeaks.get(i) != 0) {
					bassAvg += bassPeaks.get(i);
					bassBeats++;
				}
				
				if (mPeaks.get(i) != 0) {
					mAvg += mPeaks.get(i);
					mBeats++;
				}
				
				if (umPeaks.get(i) != 0) {
					umAvg += umPeaks.get(i);
					umBeats++;
				}
				
			}
			
			//then we minus one from the beats so it actually works out
			avgSPB -= bassPrunned.size-lastID;
			avgSPB *= secondsPerWindow;
			avgSPB /= bassBeats;
			Gdx.app.debug("Audio Analyzer", "Avg SPB: " + avgSPB);

			bassAvg /= bassBeats;
			mAvg /= mBeats;
			umAvg /= umBeats;
			Gdx.app.debug("Audio Analyzer", "Avg bass: " + bassAvg);
			Gdx.app.debug("Audio Analyzer", "Avg M: " + mAvg);
			Gdx.app.debug("Audio Analyzer", "Avg UM: " + umAvg);
			
			pack = new AudioDataPackage();
			pack.setBassData(bassPeaks, bassMaxValue, bassAvg);
			pack.setmData(mPeaks, mMaxValue, mAvg);
			pack.setUmData(umPeaks, umMaxValue, umAvg);
			
			pack.setPUID(PUID);
			pack.setAvgSPB(avgSPB);
			pack.setSecPerWin(secondsPerWindow);
			pack.setMusicInfo(musicInfo);
			
			if (work) {
				Gdx.app.debug("Audio Analyzer", "data cleaned and ready for map gen.");
				sender.send(MiniEvents.MUSIC_DATA_CLEANED);
			}
		};
		
		audioPCM = new float[audiofile.getReadWindowSize()];
		spectrum = new float[(audiofile.getReadWindowSize()/2)+1];
		lastSpectrum = new float[(audiofile.getReadWindowSize()/2)+1];
		this.musicInfo = audiofile;
		work = true;
		
		exec.submit(analysisAlgorithm);
	}
	
	public void runThresholdCleaning(float rangeModifier) {
		this.bassThresholdMultiplier -= rangeModifier;
		this.umThresholdMultiplier -= rangeModifier;
		work = true;
		
		if (containsData) {
			exec.submit(thresholdCalculator);
		} else {
			throw new NullPointerException("Either you didn't start the spectral flux gen, or you didn't let it finish.");
		}
	}
	
	public void runThresholdCleaning() {
		work = true;
		if (containsData) {
			exec.submit(thresholdCalculator);
		} else {
			throw new NullPointerException("Either you didn't start the spectral flux gen, or you didn't let it finish.");
		}
	}
	
	private int thresholdRangeCalc(float durationOfRange) {
		return (int) (durationOfRange/(musicInfo.getReadWindowSize()/musicInfo.getSampleRate()));
	}
	
	public boolean containsData() {
		return containsData;
	}
	
	public synchronized int getProgress() {
		return progress;
	}
	
	public void stop() {
		work = false;
		exec.shutdown();
	}
	public synchronized AudioDataPackage getAudioDataPackage() {
		if (pack != null) {
			exec.shutdown();
			return pack;
		} else {
			throw new NullPointerException("Pack isn't filled yet... You made a mistake somewhere!");
		}
	}
}
