package com.mantz_it.guitartunerlibrary;

import android.os.Vibrator;
import android.util.Log;

import java.util.Locale;

/**
 * <h1>Wear Guitar Tuner - Guitar Tuner</h1>
 *
 * Module:      GuitarTuner.java
 * Description: This class will extract the pitch information from the fft samples
 *              and generate the tuner output which is passed to the callback interface (TunerSurface)
 *
 * @author Dennis Mantz
 *
 * Copyright (C) 2014 Dennis Mantz
 * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */
public class GuitarTuner {
	private static final String LOGTAG = "GuitarTuner";
	private static final int LOW_CUT_OFF_FREQUENCY = 50;	// lowest frequency that will be extracted from the fft data
	private static final int HIGH_CUT_OFF_FREQUENCY = 2500;	// highest frequency that will be extracted from the fft data
	private  float CONCERT_PITCH = 440.0f;		// frequency of the A4 pitch
	private static final int HPS_ORDER = 3;					// order to calculate the harmonic product spectrum
	private static final long[] VIBRATE_PATTERN_UP = {0, 200};							//  ~~~
	private static final long[] VIBRATE_PATTERN_DOWN = {0, 200, 200, 200};				//  ~~~   ~~~
	private static final long[] VIBRATE_PATTERN_TUNED = {0, 100, 100, 100, 100, 100};	//  ~~  ~~  ~~
	private GuitarTunerCallbackInterface callbackInterface;
	private Vibrator vibrator;
	private boolean playNoteOn = false;
	private float[] mag;					// magnitudes of the spectrum
	private float[] hps;					// harmonic product spectrum
	private float updateRate;				// indicates how often processFFTSamples() will be called per second
	private long lastUpdateTimestamp;		// time of the last call to processFFTSamples()
	private float hzPerSample;				// frequency step of one index in mag
	private float strongestFrequency;		// holds the frequency of the strongest (max mag) frequency component (after HPS)
	private float detectedFrequency;		// holds the frequency that was calculated to be the most likely/relevant frequency component
	private float targetFrequency;			// desired frequency to tune to
	private int targetPitchIndex;			// pitch index of the targetFrequency
	private int pitchHoldCounter = 0;		// number of cycles the same pitch was detected in series.
	private float lastDetectedFrequency;	// detected frequency of the last cycle
	private float lastTargetFrequency;		// target frequency of the last cycle
	public boolean valid;					// indicates if the current result is valid
	private boolean vibrate = false;		// on/off switch for the vibration feedback
	private int lowCutOffFrequency = 80;
	/**
	 * constructor
	 *
	 * @param callbackInterface		interface that will get the results of the tuner
	 * @param vibrator				Vibrator instance
	 */
	public GuitarTuner(GuitarTunerCallbackInterface callbackInterface, Vibrator vibrator) {
		this.callbackInterface = callbackInterface;
		this.vibrator = vibrator;
	}

	public void setTargetFrequency(float freq) {
		this.targetFrequency = freq;
	}


	// PASTE THIS METHOD INTO GuitarTuner.java


	/**
	 * This method processes the fft samples from the AudioProcessingEngine and pass the results to
	 * the callback interface.
	 *
	 * @param mag			fft samples (frequency spectrum)
	 * @param sampleRate	samplerate of the audio source
	 * @param updateRate	rate at which the audioProcessingEngine will call this method
	 * @return true if success; false if something went wrong (e.g. the callback interface returned an error)
	 */
// In GuitarTuner.java
// REPLACE the entire method with this one

// In GuitarTuner.java
// REPLACE the entire method with this one

// In GuitarTuner.java
// REPLACE the entire processFFTSamples method with this one.

// In GuitarTuner.java
// REPLACE the entire processFFTSamples method with this one.
// This version correctly fixes the D2 -> D3 octave error.

// In GuitarTuner.java
// FINAL ATTEMPT - REPLACE the entire processFFTSamples method with this one.
// The only change is making the D2 check less strict.

	// In GuitarTuner.java
// REVERTING to the stable version of processFFTSamples, before the failed octave fixes.
// In GuitarTuner.java
// FINAL, ELEGANT VERSION - REPLACE the entire processFFTSamples method.
// This version uses a fixed 80Hz cutoff, embracing the "octave intelligence" you discovered.

// In GuitarTuner.java
// REPLACE the entire processFFTSamples method with this one.
// This version adds the "Snap to Zero" feature you asked for.

	public boolean processFFTSamples(float[] mag, int sampleRate, float updateRate) {
		this.lastUpdateTimestamp = System.currentTimeMillis();
		this.updateRate = updateRate;
		this.mag = mag;
		hzPerSample = ((float)(sampleRate / 2)) / mag.length;

		// --- START OF IMPROVEMENTS ---

		// STEP 1: ADD A NOISE GATE
		int rawMaxIndex = 0;
		for (int i = 1; i < mag.length; i++) {
			if(mag[rawMaxIndex] < mag[i])
				rawMaxIndex = i;
		}
		final float RAW_NOISE_THRESHOLD_DB = -65.0f;
		if (mag[rawMaxIndex] < RAW_NOISE_THRESHOLD_DB) {
			this.valid = false;
			callbackInterface.process(this);
			return true;
		}

		// STEP 2: SIMPLE, FIXED FILTERING (Based on your insight)
		// Apply a fixed 80Hz low-cut filter.
		for (int i = 0; i < 80 / hzPerSample; i++)
			mag[i] = Float.NEGATIVE_INFINITY;

		// Apply the high-cut filter
		for (int i = (int)(HIGH_CUT_OFF_FREQUENCY / hzPerSample); i < mag.length; i++)
			mag[i] = Float.NEGATIVE_INFINITY;

		// --- END OF IMPROVEMENTS ---

		// Calculate Harmonic Product Spectrum
		if(hps == null || hps.length != mag.length)
			hps = new float[mag.length];
		calcHarmonicProductSpectrum(mag, hps, HPS_ORDER);

		// calculate the max (strongest frequency) of the HPS
		int maxIndex = 0;
		for (int i = 1; i < hps.length; i++) {
			if(hps[maxIndex] < hps[i])
				maxIndex = i;
		}

		// Convert the max index back to a frequency
		strongestFrequency = maxIndex * hzPerSample;
		detectedFrequency = strongestFrequency;

		if (this.targetFrequency <= 0) {
			targetPitchIndex = frequencyToPitchIndex(detectedFrequency);
			this.targetFrequency = pitchIndexToFrequency(targetPitchIndex);
		} else {
			targetPitchIndex = frequencyToPitchIndex(this.targetFrequency);
		}

		// vvvvvv YOUR NEW FEATURE: "SNAP TO ZERO" LOGIC vvvvvv
		// Check if the detected frequency is within the 'tuned' tolerance range.
		if (detectedFrequency > getLowerToleranceBoundaryFrequency(targetPitchIndex) &&
				detectedFrequency < getUpperToleranceBoundaryFrequency(targetPitchIndex)) {
			// If it is, force the detected frequency to be the exact target frequency.
			// This will make the needle "snap" to the center and stop moving.
			detectedFrequency = this.targetFrequency;
			Log.d(LOGTAG, "Snap to Zero: Note is tuned, snapping needle to center.");
		}
		// ^^^^^^ END OF "SNAP TO ZERO" LOGIC ^^^^^^

		valid = detectedFrequency >= pitchIndexToFrequency(0);

		if(detectedFrequency > lastDetectedFrequency*0.99 && detectedFrequency < lastDetectedFrequency*1.01) {
			pitchHoldCounter++;
		} else {
			pitchHoldCounter = 0;
		}

		if(pitchHoldCounter > 2) {
			if(detectedFrequency < getLowerToleranceBoundaryFrequency(targetPitchIndex)) {
				if(vibrate) vibrator.vibrate(VIBRATE_PATTERN_UP, -1);
			} else if(detectedFrequency > getUpperToleranceBoundaryFrequency(targetPitchIndex)) {
				if(vibrate) vibrator.vibrate(VIBRATE_PATTERN_DOWN, -1);
			} else {
				// The vibration for "tuned" will now trigger more reliably as well.
				if(vibrate) vibrator.vibrate(VIBRATE_PATTERN_TUNED, -1);
				if(playNoteOn) {
					callbackInterface.playNoteWhenTuned(targetFrequency);
				}
			}
			pitchHoldCounter = 0;
		}

		boolean success = callbackInterface.process(this);
		lastDetectedFrequency = detectedFrequency;

		if (lastTargetFrequency <= 0) {
			this.targetFrequency = 0;
		}
		lastTargetFrequency = this.targetFrequency;

		return success;
	}


	/**
	 * calculates the harmonic product spectrum from an array of magnitudes (in dB)
	 * @param mag		magnitude array (in dB)
	 * @param hps		result array (will be overwritten with the result)
	 * @param order		order of the product; 1 = up to the first harmonic ...
	 */
	/**
	 * This is the standard, correct, and stable implementation of the
	 * additive Harmonic Product Spectrum algorithm. This version is more robust
	 * and works correctly with the noisy, high-fidelity 48kHz audio from a phone's
	 * built-in microphone, fixing the unstable needle and incorrect tuning.
	 */
	private void calcHarmonicProductSpectrum(float[] mag, float[] hps, int order) {
		if(mag.length != hps.length) {
			Log.e(LOGTAG, "calcHarmonicProductSpectrum: mag[] and hps[] have to be of the same length!");
			throw new IllegalArgumentException("mag[] and hps[] have to be of the same length");
		}

		// 1. Initialize HPS with the original magnitude spectrum.
		// We use System.arraycopy for efficiency.
		System.arraycopy(mag, 0, hps, 0, mag.length);

		// 2. Add the downsampled spectrum for each harmonic.
		// This is the standard, correct loop that starts from the 2nd harmonic.
		// It adds the log-magnitudes of the harmonics to reinforce the fundamental peak.
		for (int harmonic = 2; harmonic <= order; harmonic++) {
			for (int i = 0; i < hps.length / harmonic; i++) {
				hps[i] += mag[i * harmonic];
			}
		}
	}


	// Add this method anywhere in the GuitarTuner class
	public void setLowCutOffFrequency(int freq) {
		this.lowCutOffFrequency = freq;
		Log.d(LOGTAG, "Low cut-off frequency set to " + freq + " Hz");
	}
	/**
	 * converts a frequency (float) into an pitch index ( 0 is A0, 1 is A0#, 2 is B0, 3 is C1, ...).
	 * This will round the frequency to the closest pitch index.
	 *
	 * @param frequency		frequency in Hz
	 * @return pitch index
	 */
	public int frequencyToPitchIndex(float frequency) {
		float A1 = CONCERT_PITCH / 8;
		return Math.round((float) (12 * Math.log(frequency / A1) / Math.log(2)));
	}
	public void setConcertPitch(float newConcertPitch) {
		if (newConcertPitch > 0) {
			this.CONCERT_PITCH = newConcertPitch;
			Log.i(LOGTAG, "Concert pitch changed to " + newConcertPitch + " Hz");
		}
	}
	/**
	 * returns the corresponding frequency (in Hz) for a given pitch index
	 * @param index			pitch index ( 0 is A0, 1 is A0#, 2 is B0, 3 is C1, ...)
	 * @return frequency in Hz
	 */
	public float pitchIndexToFrequency(int index) {
		float A1 = CONCERT_PITCH / 8;
		return (float) (A1 * Math.pow(2, index/12f));
	}


	public void setPlayNote(boolean playNoteOn) {
		this.playNoteOn = playNoteOn;
	}
	/**
	 * returns the corresponding human readable pitch letter for a given pitch index
	 * @param index			pitch index ( 0 is A0, 1 is A0#, 2 is B0, 3 is C1, ...)
	 * @return pitch letter (e.g. "a0" or "c1#")
	 */
	public String pitchLetterFromIndex(int index) {
		String letters;
		int octaveNumber = ((index+9) / 12) + 1;
		switch(index%12) {
			case 0:  letters = "A" + octaveNumber; break;
			case 1:  letters = "A" + octaveNumber + "#"; break;
			case 2:
				if(Locale.getDefault().getLanguage().equals("en"))
					letters = "B" + octaveNumber;
				else
					letters = "H" + octaveNumber;
				break;
			case 3:  letters = "C" + octaveNumber; break;
			case 4:  letters = "C" + octaveNumber + "#"; break;
			case 5:  letters = "D" + octaveNumber; break;
			case 6:  letters = "D" + octaveNumber + "#"; break;
			case 7:  letters = "E" + octaveNumber; break;
			case 8:  letters = "F" + octaveNumber; break;
			case 9:  letters = "F" + octaveNumber + "#"; break;
			case 10: letters = "G" + octaveNumber; break;
			case 11: letters = "G" + octaveNumber + "#"; break;
			default: letters = "err";
		}
		return letters;
	}

	/**
	 * calculates the lowest frequency that would still be considered as 'tuned' to the given
	 * pitch index.
	 *
	 * @param pitchIndex		pitch index ( 0 is A0, 1 is A0#, 2 is B0, 3 is C1, ...)
	 * @return the lower boundary frequency (in Hz) for a tuned note
	 */
	public float getLowerToleranceBoundaryFrequency(int pitchIndex) {
		float frequency = pitchIndexToFrequency(pitchIndex);
		float nextLowerFrequency = pitchIndexToFrequency(pitchIndex-1);
		return frequency - 0.05f * (frequency-nextLowerFrequency);
	}

	/**
	 * calculates the highest frequency that would still be considered as 'tuned' to the given
	 * pitch index.
	 *
	 * @param pitchIndex		pitch index ( 0 is A0, 1 is A0#, 2 is B0, 3 is C1, ...)
	 * @return the upper boundary frequency (in Hz) for a tuned note
	 */
	public float getUpperToleranceBoundaryFrequency(int pitchIndex) {
		float frequency = pitchIndexToFrequency(pitchIndex);
		float nextUpperFrequency = pitchIndexToFrequency(pitchIndex+1);
		return frequency + 0.05f * (nextUpperFrequency-frequency);
	}

	/**
	 * @return true if the detectedFrequency is 'tuned' to the current targetPitch
	 */
	public boolean isTuned() {
		return detectedFrequency > getLowerToleranceBoundaryFrequency(targetPitchIndex)
				&& detectedFrequency < getUpperToleranceBoundaryFrequency(targetPitchIndex);
	}

	public float getStrongestFrequency() {
		return strongestFrequency;
	}

	public float[] getMag() {
		return mag;
	}

	public float[] getHPS() {
		return hps;
	}

	public float getUpdateRate() {
		return updateRate;
	}

	public long getLastUpdateTimestamp() {
		return lastUpdateTimestamp;
	}

	public float getHzPerSample() {
		return hzPerSample;
	}

	public float getDetectedFrequency() {
		return detectedFrequency;
	}

	public float getTargetFrequency() {
		return targetFrequency;
	}

	public int getTargetPitchIndex() {
		return targetPitchIndex;
	}

	public boolean isValid() {
		return valid;
	}

	public boolean isVibrate() {
		return vibrate;
	}

	public void setVibrate(boolean vibrate) {
		this.vibrate = vibrate;
	}

	public float getLastDetectedFrequency() {
		return lastDetectedFrequency;
	}

	public float getLastTargetFrequency() {
		return lastTargetFrequency;
	}

	public interface GuitarTunerCallbackInterface {
		public boolean process(GuitarTuner guitarTuner);
		void playNoteWhenTuned(float frequency);
	}

}
