package kr.gui;

import java.awt.EventQueue;

import javax.swing.SwingUtilities;

import kr.AudioModel;
import kr.TimeKeeper;
import kr.gui.metrodome.MetrodomeGuiModel;
import kr.miditunemodel.EventGuiModel;
import kr.miditunemodel.KREventGuiModel;
import kr.miditunemodel.MidiTuneModel;
import kr.miditunemodel.NoteEventGuiModel;
import kr.util.Listener;
import kr.util.ListenerManager;
import kr.util.audio.AudioClip;
import kr.util.audio.GainAudioClip;
import kr.util.audio.MidiAudioClip;
import kr.util.audio.MixerAudioClip;
import kr.util.audio.MonoAudioClip;
import kr.util.ft.FFTScaleData;

public class GuiModel implements Listener, TimeKeeper
{

	// the micros the last time the model was in stopped state (i.e. isPlaying
	// was false)
	private long lastStoppedNowMicros = 0;

	private double zoomLevel = 1./2000; // in pixels per micro

	private static final double nowLine = .15;

	private boolean isPlaying = false;

	private MonoAudioClip monoVocalsAudioClip;
	
	private TimeKeeper timeKeeper;

	private AudioModel audioModel;

	private MidiTuneModel midiTuneModel;
	private NoteEventGuiModel noteEventGuiModel;
	private MetrodomeGuiModel metrodomeGuiModel;
	private KREventGuiModel krEventGuiModel;


	private MixerAudioClip mixer;

	private boolean isPlaySelection;

	private boolean isPlayLoop;

	private MonoAudioClip subtractedVocalsAudioClip;

	private double playbackRate = 1.;
	
	private boolean viewVocalsFFTFlag;
	
	private FFTScaleData fftScaleData;

	private long selRight = -1;

	private long selLeft = -1;
	
	private long cachedNowMicros;

	//calibrates the sound to the graph
	private long calibration;

	private boolean isPlayingSample;

	public enum MessageType {
		NOW_UPDATED, ZOOM_UPDATED, IS_PLAYING_UPDATED, SELECTION_UPDATED, PLAYING_SPEED_UPDATED,
		VIEW_FFT_SCALE_UPDATED, IS_PLAYING_SAMPLE_CLIP_UPDATED
	}; 
	
	public GuiModel(AudioModel audioModel, MidiTuneModel midiTuneModel)
	{
		this.noteEventGuiModel = new NoteEventGuiModel(midiTuneModel);
		this.krEventGuiModel = new KREventGuiModel(midiTuneModel);
		this.metrodomeGuiModel = new MetrodomeGuiModel(midiTuneModel);
		
		this.audioModel = audioModel;
		this.midiTuneModel = midiTuneModel;
		this.monoVocalsAudioClip = new MonoAudioClip();
		this.subtractedVocalsAudioClip = new MonoAudioClip(true);

		initAudioChannels();

		ListenerManager.inst().registerListener(audioModel, this);
		
		updateAudioChannels();
	} 

	public void updateFixedTime() {
		cachedNowMicros = getActualNowMicros();
		
	}

	public synchronized long getCurrMicros() {
		return cachedNowMicros;
	}

	public synchronized long getActualNowMicros() {
		//if were playing, we add on the time according to the audio player
		//We round the time to what is displayable in pixels. Otherwise, 
		//if we move half a pixel, it can mess up scrolling and cause graphics
		//to be clipped a single vertical row at several places
		return (long)(((long)((isPlaying ? (timeKeeper.getCurrMicros() - calibration)
				: lastStoppedNowMicros) * zoomLevel)) / zoomLevel);
	}

	public double getZoomLevel() {
		return zoomLevel;
	}

	public void setZoomLevel(double zoomLevel) {
		this.zoomLevel = zoomLevel;
	}

	public int calcNowX(int width) {
		return (int) Math.round(nowLine * width);
	}

	/**
	 * Updates now micros and notifies listeners
	 */
	public void updateNowMicros(long newNowMicros) {
		long oldNowMicros = this.lastStoppedNowMicros;
		this.lastStoppedNowMicros = newNowMicros;

		if (newNowMicros == oldNowMicros)
			return;

		// System.out.println("newNowMicros is "+newNowMicros);

		ListenerManager.inst().notify(this, MessageType.NOW_UPDATED,
				oldNowMicros);

	}

	public long pixelsToMicros(int x, int width) {

		return Math.round((x - calcNowX(width)) / getZoomLevel()
				+ getCurrMicros());
	}

	public int microsToPixels(int w, long nowMicros, long micros) {
		int nowX = calcNowX(w);

		return (int) Math.round(getZoomLevel() * (micros - nowMicros) + nowX);
	}

	/**
	 * Converts micros to absolute pixels. This will be pixels from the 
	 * beginning of the clip, rather then pixels from the beginning of the window
	 */
	public int microsToAbsPixels(long micros) {
		return (int) Math.round(getZoomLevel() * micros);
	}
	
	
	public long getStartMicros(int width) {
		return pixelsToMicros(0, width);
	}

	public long getEndMicros(int width) {
		return pixelsToMicros(width, width);
	}

	public void startPlaying() {
		if (isPlaying)
			return;

		isPlaying = true;
		isPlaySelection = false;
		isPlayLoop = false;

		ListenerManager.inst().notify(this, MessageType.IS_PLAYING_UPDATED);
	}

	public boolean isPlaying() {
		return isPlaying;
	}

	public synchronized void stopPlaying() {
		if (!isPlaying)
			return;
		
		lastStoppedNowMicros = timeKeeper.getCurrMicros();
		isPlaying = false;
		SwingUtilities.invokeLater(new Runnable() { 
			public void run() {
				ListenerManager.inst().notify(GuiModel.this, MessageType.IS_PLAYING_UPDATED);
				}
			});
		//ListenerManager.inst().notify(this, MessageType.NOW_UPDATED);
	}

	/**
	 * This always returns the current time in micros, within the clip, when the
	 * system was in a stopped state. If the system is currently in a stopped
	 * state, it returns the current time.
	 */
	public long getLastStoppedMicros() {
		return lastStoppedNowMicros;
	}

	public double getPixelsToMicrosRatio() {
		return zoomLevel;
	}

	public TimeKeeper getTimeKeeper() {
		return timeKeeper;
	}

	public void setTimeKeeper(TimeKeeper timeKeeper) {
		this.timeKeeper = timeKeeper;
	}

	public AudioClip getAudioOut() {
		return mixer;
	}

	public void notify(Object source, Object type, Object... values) {
		if(source == audioModel)
		{
			if(type == AudioModel.MessageType.BKG_CLIP_UPDATED
					|| type == AudioModel.MessageType.VOCALS_CLIP_UPDATED
				)
			{
				updateAudioChannels();
				
				if(type == AudioModel.MessageType.VOCALS_CLIP_UPDATED)
				{
					setViewVocalsFFTFlag(false);
				}
			}
		}
	}
	
	public boolean useSpectrumAnalyzerScale()
	{
		return true;
	}
	
	private void initAudioChannels()
	{
		mixer = new MixerAudioClip(2);//2 channels

		mixer.addLine(null, "Bkg", false);
		mixer.addLine(null, "Vocals", false);
		mixer.addLine(null, "Tune", false);
		mixer.addLine(null, "UI", false);
		mixer.addLine(null, "Test", true);
		
		updateAudioChannels();
	}


	private void updateAudioChannels() {
		/*MidiAudioClip hack = new MidiAudioClip(48000);
		
		hack.playMidi((byte)60);
		monoVocalsAudioClip.setSource(hack);*/
		
		monoVocalsAudioClip.setSource(audioModel.getVocalsChannel().getClip());
		subtractedVocalsAudioClip.setSource(audioModel.getVocalsChannel().getClip());
		
		mixer.replaceClip(1, audioModel.getBkgChannel().getClip());
		mixer.replaceClip(2, audioModel.getVocalsChannel().getClip());
		mixer.replaceClip(3, midiTuneModel.getSynthAudioClip());
		mixer.replaceClip(4, noteEventGuiModel.getRealTimeMidiAudioClip()); 
		if(!isPlaying) turnOffSampledSources();
		else turnOnSampledSources();
	}

	
	public void playAudioClip(AudioClip ac) {
		mixer.replaceClip(5, ac);
		isPlayingSample = true;
		updateMixerForSampledAudioClip();
		ListenerManager.inst().notify(this, MessageType.IS_PLAYING_SAMPLE_CLIP_UPDATED);
	}
	
	private void updateMixerForSampledAudioClip()
	{
		if(isPlayingSample)
		{
			//turn on only the sample channel
			
			for(int i = 1; i < 5; i++)
			{
				mixer.updateOnFlag(false, i);
			}
			
			mixer.updateOnFlag(true, 5);
		}
		else
		{
			mixer.updateOnFlag(false, 5);
			updateAudioChannels();
		}
	}
	
	public void stopPlayingAudioClip()
	{
		isPlayingSample = false;
		updateMixerForSampledAudioClip();
		ListenerManager.inst().notify(this, MessageType.IS_PLAYING_SAMPLE_CLIP_UPDATED);		
	}

	public void turnOnSampledSources() {
		mixer.updateOnFlag(true, 1);
		mixer.updateOnFlag(true, 2);
		mixer.updateOnFlag(true, 3);
		mixer.updateOnFlag(false, 4);
	}

	public void turnOffSampledSources() {
		mixer.updateOnFlag(false, 1);
		mixer.updateOnFlag(false, 2);
		mixer.updateOnFlag(false, 3);
		mixer.updateOnFlag(true, 4);
	}

	public NoteEventGuiModel getNoteEventGuiModel() {
		return noteEventGuiModel;
	}

	public KREventGuiModel getKREventGuiModel() {
		return krEventGuiModel;
	}

	public MixerAudioClip getMixer() {
		return mixer;
	}

	public long getSelectionEnd() {
		return Math.max(selLeft, selRight);
	}

	public long getSelectionStart() {
		return Math.min(selLeft, selRight);
	}

	public void startPlayingLoop() {
		if (isPlaying)
			return;

		isPlaying = true;
		isPlaySelection = true;
		isPlayLoop = true;

		ListenerManager.inst().notify(this, MessageType.IS_PLAYING_UPDATED);
	
	}

	public boolean isPlaySelection() {
		return isPlaySelection;
	}

	public boolean isPlayLoop() {
		return isPlayLoop;
	}

	public MonoAudioClip getMonoVocalsAudioClip() {
		return monoVocalsAudioClip;
	}

	public AudioClip getSubtractedVocalsAudioClip() {
		return subtractedVocalsAudioClip;
	}

	public MetrodomeGuiModel getMetrodomeGuiModel() {
		return metrodomeGuiModel;
	}

	public void setPlaybackRate(double playbackRate) {
		this.playbackRate = playbackRate;
		ListenerManager.inst().notify(this, MessageType.PLAYING_SPEED_UPDATED);
	}

	public double getPlaybackRate() {
		return playbackRate;
	}

	public boolean isViewVocalsFFTFlag() {
		return viewVocalsFFTFlag;
	}

	public void setViewVocalsFFTFlag(final boolean viewVocalsFFTFlag) {
		if(viewVocalsFFTFlag && fftScaleData == null)
		{
			final double [] freqs = new double[MidiTuneModel.MAX_NOTE - MidiTuneModel.MIN_NOTE + 1];
			
			for(int i = 0; i < freqs.length; i++)
			{
				freqs[i] = MidiAudioClip.getFreqForMidi(i + MidiTuneModel.MIN_NOTE);
			}

//			if(useMaster)
//			{
//				FFTScaleData monoMaster = new FFTScaleData(freqs, 4096);
//				FFTScaleData subtractedMaster = new FFTScaleData(freqs, 4096);
//				MonoAudioClip addClip = new MonoAudioClip(false);
//				MonoAudioClip subClip = new MonoAudioClip(true);
//				addClip.setSource(audioModel.getMasterClip());
//				subClip.setSource(audioModel.getMasterClip());
//				
//				System.out.println("Analyzing addition clip");
//				monoMaster.analyze(addClip);
//				System.out.println("Analyzing subtraction clip");
//				subtractedMaster.analyze(subClip);
//				
//				SubtractScaleData ssd = new SubtractScaleData(monoMaster, subtractedMaster);
//				
//				scaleUI.setScaleData(ssd);
	//
//			}
//			else
//			{
			new Thread() { 

			public void run() {
				fftScaleData = new FFTScaleData(freqs, 4096, 4096 * 2);

				fftScaleData.analyze(getMonoVocalsAudioClip());

				GuiModel.this.viewVocalsFFTFlag = viewVocalsFFTFlag;
				//fftScaleData.analyzeStereo(audioModel.getVocalsChannel().getClip());
				ListenerManager.inst().notifyGui(GuiModel.this, MessageType.VIEW_FFT_SCALE_UPDATED);
			}}.start();
		}
		else
		{
			this.viewVocalsFFTFlag = viewVocalsFFTFlag;
			ListenerManager.inst().notify(this, MessageType.VIEW_FFT_SCALE_UPDATED);
		}
	}

	public FFTScaleData getFFTScaleData() {
		return fftScaleData;
	}

	public void setSelectionRight(long r) {
		selRight = r;
		ListenerManager.inst().notify(this, MessageType.SELECTION_UPDATED);
	}

	public void setSelectionLeft(long l) {
		selLeft = l;		
		ListenerManager.inst().notify(this, MessageType.SELECTION_UPDATED);
	}

	public int getAudioRate() {
		
		return 0;
	}

	public void setCalibrationMicros(long calibration) {
		this.calibration = calibration;
	}

	public long getCalibrationMicros() {
		return calibration;
	}

	public boolean isPlayingSampleClip() {
		
		return isPlayingSample;
	}

}
