package kr.util.audio;

import java.util.List;


import kr.gui.GuiModel;
import kr.miditunemodel.Event;
import kr.miditunemodel.EventGroup;
import kr.miditunemodel.MidiTuneModel;
import kr.miditunemodel.NoteEvent;
import kr.util.Listener;
import kr.util.ListenerManager;

/**
 * Represents a song containing a single audio channel 
 */
public class MidiSongAudioClip extends AudioClip implements Listener
{
	private MidiAudioClip ac;
	private MidiTuneModel midiModel;
	private Cache cache = new Cache();
	private int dataLength = -1;
	
	private class Cache
	{
		NoteEvent noteEvent;
		int noteStartDataIndex, noteEndDataIndex; // the start and end of this note
		//in bytes of the data stream
		int lastNoteEndDataIndex; //the end data index of the previous note in bytes
		//of the data stream. This is used to cache the blank area between the notes
		
		public void setData(NoteEvent noteEvent)
		{
			//if we already have the previous notes end index
			if(this.noteEvent == noteEvent.getPrevious())
			{
				setData(noteEvent, noteEndDataIndex);
				return;
			}
			
			//if we don't have the previous notes end data index, we need to find it
			
			//if the first note, there is no previous note
			if(noteEvent.isHead()) setData(noteEvent, 0);
			else
			{
				//grab the previous notes end micros and use that
				NoteEvent prevNoteEvent = (NoteEvent) noteEvent.getPrevious();
				setData(noteEvent, getIndexFromMicros(prevNoteEvent.getEndMicros()));
			}
		}
		
		public void setData(NoteEvent noteEvent, int lastNoteEndDataIndex)
		{
			this.noteEvent = noteEvent;
			if(noteEvent == null)
			{
				return;
			}
			
			this.noteStartDataIndex = getIndexFromMicros(noteEvent.getMicros());
			
			//to prevent static, we need to make sure we stop when the value is zero. This corresponds to the start of the cycle.
			//since we start the note where noteStartDataIndex is zero, we subtract this out before computing the this value and
			//add the start back again when we are finished
			//(NOT NEEDED since MidiAudioClip now handles staticness)
			this.noteEndDataIndex = getIndexFromMicros(noteEvent.getEndMicros());
			/*System.out.println(String.format("orig note length %d, new length %d, adjusted orig length %.2f new orig length %.2f",
					getIndexFromMicros(noteEvent.getEndMicros())-noteStartDataIndex,
					noteEndDataIndex- noteStartDataIndex,
					(getIndexFromMicros(noteEvent.getEndMicros())-noteStartDataIndex) * ac.getFreqRatio(),
					(noteEndDataIndex- noteStartDataIndex) * ac.getFreqRatio()));*/
			this.lastNoteEndDataIndex = lastNoteEndDataIndex;
		}

		public void clear() {
			this.noteEvent = null;
			
		}

		public void updateNote(int dataIndex) {
			//return the notes value
			if(dataIndex >= noteStartDataIndex)
				ac.playMidi((byte)this.noteEvent.getNote());
			else
				ac.playMidi((byte)-1);
		}

		/**
		 * Copies data to the destination buffer, given the info in the cache.
		 * @return the actual number of bytes copied.
		 */
		public int copyTo(int offset, int length, byte[] dest, int destOffset) {
			int bytesCopied = 0;
			
			//int rand = (int)(Math.random()*10000);

			//
			//copy any data in a blank area before the note
			//
			bytesCopied += copyZero(Math.min(length, noteStartDataIndex - offset), dest, destOffset+bytesCopied);
			
			/*if(bytesCopied == 0)
				System.out.println("Bytes not copied "+rand+" zzz "+(offset + bytesCopied < noteStartDataIndex)+" yyyy "+
						String.format("offset is %d, noteStartDataIndex is %d",offset,noteStartDataIndex));*/
			
			//
			//copy the note data itself
			//
			int leftToCopy = Math.min(length - bytesCopied, noteEndDataIndex - offset);
			
			//we copy from the start of the note rather then the offset, so the note
			//starts smoothly
			ac.copyTo(offset + bytesCopied - noteStartDataIndex
					, leftToCopy, dest, destOffset+bytesCopied);
			
			/*if(length - bytesCopied != leftToCopy && Math.abs(dest[destOffset+bytesCopied+leftToCopy-1]) > 50)
			{
				System.out
						.println(
								rand
										+ " We aren't cutting the notes, got "
										+ dest[destOffset + bytesCopied
												+ leftToCopy - 1]
										+ " yyyy "
										+ String
												.format(
														"offset is %d, noteStartDataIndex is %d, byteAt is %d index is %d, sin index is %.2f",
														offset,
														noteStartDataIndex,
														ac
																.byteAt(offset
																		+ bytesCopied
																		- noteStartDataIndex
																		+ leftToCopy
																		- 1),
								offset + bytesCopied - noteStartDataIndex
										+ leftToCopy - 1, (offset + bytesCopied
										- noteStartDataIndex + leftToCopy - 1)
										* ac.getFreqRatio()));
				System.out.println("spacer");
			}*/
			
			return bytesCopied + leftToCopy;
		}

		public int copyZero(int length, byte[] dest, int destOffset) {
			int c = 0;
			while(c < length)
			{
				/*if(bytesCopied == 0)
					System.out.println("Bytes copied "+rand);*/
				dest[destOffset + c] = 0;
				c++;
			}

			return c;
		}
	} //class Cache
	
	public MidiSongAudioClip(int rate, MidiTuneModel midiModel)
	{
		this.ac = new MidiAudioClip(rate);
		this.midiModel = midiModel;
		ListenerManager.inst().registerListener(midiModel.getNoteEventGroup(), this);
		
		clearCache();
	}
	
	@Override
	public int getNumChannels() {
		return 1;
	}

	@Override
	public int length() {
		return dataLength;
	}

	@Override
	public int getRate() {
		return ac.getRate();
	}

	private int hackIndex = -1;
	
	@Override
	public byte byteAt(int dataIndex) {
		//we assume that we usually are getting bytes in order they occur in the stream.
		
		boolean triedNext = false; //we will try the next note to this one
		
		//while we have a cache
		while(cache.noteEvent != null)
		{
			if(dataIndex >= cache.lastNoteEndDataIndex && dataIndex < cache.noteEndDataIndex)
			{
				cache.updateNote(dataIndex);
				return ac.byteAt(dataIndex);
			}
			else if(dataIndex >= cache.noteEndDataIndex) //if we have gone beyond the end of the currently
				//cached note, see if the index belongs to the next note
			{
				if(cache.noteEvent.isTail()) //if at the end already, set the note to zero
				{
					ac.playMidi((byte) -1);
					return ac.byteAt(dataIndex);
				}
				cache.setData((NoteEvent) cache.noteEvent.getNext());
			}
			else break;
		}
		
		//
		//create a new cache
		//
		long dataIndexMicros = getMicrosFromIndex(dataIndex); 
		NoteEvent noteEvent = midiModel.getNoteEventGroup().getNextEvent(null, dataIndexMicros);
		
		if(noteEvent == null)
		{
			ac.playMidi((byte) -1);
			return ac.byteAt(dataIndex);
		}
		
		if(!noteEvent.isHead() && ((Event) noteEvent.getPrevious()).getEndMicros() > dataIndexMicros)
			cache.setData((NoteEvent) noteEvent.getPrevious());
		else
			cache.setData(noteEvent);
		cache.updateNote(dataIndex);
		
		//if at a blank spot, return zero
		return ac.byteAt(dataIndex);
	}

	@Override
	public void copyTo(int offset, int length, byte[] dest, int destOffset) {
		//we assume that we usually are getting bytes in order they occur in the stream.
		
		
		int endOffset = offset+ length;
		
		//while we still have data left to copy
		while(offset < endOffset)
		{
			while(cache.noteEvent != null && offset < endOffset)
			{
				//if the current note cache can copy some data
				if(offset >= cache.lastNoteEndDataIndex && offset < cache.noteEndDataIndex)
				{
					int bytesCopied = cache.copyTo(offset, endOffset - offset, dest, destOffset);
					offset += bytesCopied;
					destOffset += bytesCopied;
					continue;
				}
				else if(offset >= cache.noteEndDataIndex) //if we have gone beyond the end of the currently
					//cached note, see if the index belongs to the next note
				{
					if(cache.noteEvent.isTail()) //if at the end already, return 0
					{
						cache.copyZero(endOffset - offset, dest, destOffset);
						return;
					}
					cache.setData((NoteEvent) cache.noteEvent.getNext());
				}
				else break;
			}

			long dataIndexMicros = getMicrosFromIndex(offset); 
			NoteEvent noteEvent = midiModel.getNoteEventGroup().getNextEvent(null, dataIndexMicros);
			
			if(noteEvent == null)
			{
				cache.copyZero(endOffset - offset, dest, destOffset);
				return;
			}
			
			if(!noteEvent.isHead() && ((Event) noteEvent.getPrevious()).getEndMicros() > dataIndexMicros)
				cache.setData((NoteEvent) noteEvent.getPrevious());
			else
				cache.setData(noteEvent);

		}//while all the data isn't copied yet
		
	}

	public void notify(Object source, Object type, Object... values) {
		if(source == midiModel.getNoteEventGroup())
		{
			if(type ==  EventGroup.MessageType.EVENTS_CHANGED)
			{
				clearCache();
			}
		}		
	}
 
	/**
	 * Clears cache and reinitializes incase the model has changed.
	 */
	private void clearCache() {
		//find the length of the data based on the events. Add .1 secs so that if the note finishes in mid cycle, it
		//will have a chance to complete before we end the song
		if(midiModel.getNoteEventGroup().getNumEvents() > 0)
			dataLength = getIndexFromMicros(midiModel.getNoteEventGroup().getEvents().getLast().getEndMicros())+100000; 
		else
			dataLength = 0;
		cache.clear();
	}
	
	
}
