package ddf.minim;

import java.util.ArrayList;
import java.util.HashMap;

import ddf.minim.ugens.Instrument;

/**
 * 
 * @author ddf
 * @invisible
 */

public class NoteManager
{
	// we use this do our timing, basically
	private float sampleRate;
	private float tempo;
	private float noteOffset;
	private float durationFactor;
	private int   now;
	// our events are stored in a map.
	// the keys in this map are the "now" that the events should
	// occur at and the values are a list of events that occur
	// at that time.
	private HashMap<Integer, ArrayList<NoteEvent>> events;
	// are we paused?
	// pausing is important because if we're going to queue up 
	// a large number of notes, we want to make sure their timestamps
	// are accurate. this won't be possible if the note manager
	// is sending events because of ticks from the audio output.
	private boolean paused;
	
	private interface NoteEvent
	{
		void send();
	}
	
	private class NoteOnEvent implements NoteEvent
	{
		private Instrument instrument;
		private float duration;
		
		public NoteOnEvent(Instrument i, float dur)
		{
			instrument = i;
			duration = dur;
		}
		
		public void send()
		{
			instrument.noteOn(duration);
		}
	}
	
	private class NoteOffEvent implements NoteEvent
	{
		private Instrument instrument;
		
		public NoteOffEvent(Instrument i)
		{
			instrument = i;
		}
		
		public void send()
		{
			instrument.noteOff();
		}
	}
	
	public NoteManager( float sampleRate )
	{
		this.sampleRate = sampleRate;
		events = new HashMap<Integer, ArrayList<NoteEvent>>();
		tempo = 60f;
		noteOffset = 0.0f;
		durationFactor = 1.0f;
		now = 0;
		paused = false;
	}
	
	// events are always specified as happening some period of time from now.
	// but we store them as taking place at a specific time, rather than a relative time.
	public synchronized void addEvent(float startTime, float duration, Instrument instrument)
	{
		int on = now + (int)(sampleRate * ( startTime + noteOffset ) * 60f/tempo);
		Integer onAt = new Integer( on );
		
		float actualDuration = duration * durationFactor * 60f/tempo;
		
		if ( events.containsKey(onAt) )
		{
			ArrayList<NoteEvent> eventsAtOn = events.get(onAt);
			eventsAtOn.add( new NoteOnEvent(instrument, actualDuration) );
		}
		else
		{
			ArrayList<NoteEvent> eventsAtOn = new ArrayList<NoteEvent>();
			eventsAtOn.add( new NoteOnEvent(instrument, actualDuration) );
			events.put(onAt, eventsAtOn);
		}
		
		Integer offAt = new Integer( on + (int)(sampleRate * actualDuration) );
		
		if ( events.containsKey(offAt) )
		{
			ArrayList<NoteEvent> eventsAtOff = events.get(offAt);
			eventsAtOff.add( new NoteOffEvent(instrument) );
		}
		else
		{
			ArrayList<NoteEvent> eventsAtOff = new ArrayList<NoteEvent>();
			eventsAtOff.add( new NoteOffEvent(instrument) );
			events.put(offAt, eventsAtOff);
		}
		
	}
	
	public void setTempo(float tempo)
	{
		this.tempo = tempo;
	}
	
	public float getTempo()
	{
		return tempo;
	}
	
	public void setNoteOffset(float noteOffset)
	{
		this.noteOffset = noteOffset;
	}
	
	public float getNoteOffset()
	{
		return noteOffset;
	}
	
	public void setDurationFactor(float durationFactor)
	{
		this.durationFactor = durationFactor;
	}
	
	public float getDurationFactor()
	{
		return durationFactor;
	}
	
	public void pause()
	{
		paused = true;
	}
	
	public void resume()
	{
		paused = false;
	}
	
	synchronized public void tick()
	{
		if ( paused == false )
		{
			// find the events we should trigger now.
			Integer Now = new Integer(now);
			
			if ( events.containsKey(Now) )
			{
				ArrayList<NoteEvent> eventsToSend = events.get(Now);
				// ddf: change this to a for loop from an iterator so that
				// 		this list can be safely concurrently modified.
				for( int i = 0; i < eventsToSend.size(); ++i )
				{
					eventsToSend.get(i).send();
				}
				// remove this list because we've sent all the events
				events.remove(Now);
			}
			
			// increment our now
			++now;
		}
	}
}
