import java.lang.*;
import java.util.*;
import javax.swing.*;
import javax.sound.midi.*;
import java.awt.event.*;

public class FiniteStateMusicMachine
{		
	public final static int MIDI_NOTE_LENGTH = 2;
	public final static int MIDI_DURATION_LENGTH = 1;
	public final static int MACHINE_OBJECT_LENGTH = 4+MIDI_NOTE_LENGTH+MIDI_DURATION_LENGTH;
	public final static int NUM_OBJECTS_MACHINE = 10;	
	
	private final int MAX_NOTE_DURATION = 2000;
	private final int MIN_NOTE_DURATION = 10;
	
	private final int TEMPO = 250;
	
	private Synthesizer mySynthesizer;
	private Soundbank mySoundbank;
	private Instrument myInstruments[];
	private MidiChannel myMidiChannels[];
	
	private Vector MachineVector = new Vector();
	private String stringRepresentation;
	
	private int currentIndex = 0;
	private int currentState = 0;
	
	// Some MIDI Vars that we may change in string reps
	private int midiChannelNumber = 0;
	private int midiInstrumentNumber = 60;

	// Midi Constants
	final int MIN_NOTE = 10;
	final int MAX_NOTE = 99 + MIN_NOTE; // Should be 127;

	final int NOTES_OCTAVE = 10; // Should be 12....

	private javax.swing.Timer playTimer;
		
	int notesToPlayCounter = 0;
	Vector notesToPlay = new Vector();
	
	public FiniteStateMusicMachine(String thisFiniteMachine) 
	{
				
		// Create the array of machine objects
		if (validateString(thisFiniteMachine))
		{
			stringRepresentation = thisFiniteMachine;
						
			for (int i = 1; i < FiniteStateMusicMachine.NUM_OBJECTS_MACHINE*FiniteStateMusicMachine.MACHINE_OBJECT_LENGTH; i+=FiniteStateMusicMachine.MACHINE_OBJECT_LENGTH)
			{
				// Object and 4 Attributes
				createMachineObject(new MidiNote(new Integer(stringRepresentation.substring(i,i+MIDI_NOTE_LENGTH)).intValue(),new Integer(stringRepresentation.substring(i+MIDI_NOTE_LENGTH,i+MIDI_NOTE_LENGTH+MIDI_DURATION_LENGTH)).intValue(), midiInstrumentNumber),
							new Integer(stringRepresentation.substring(i+MIDI_NOTE_LENGTH+MIDI_DURATION_LENGTH,i+MIDI_NOTE_LENGTH+MIDI_DURATION_LENGTH+1)).intValue(),
							new Integer(stringRepresentation.substring(i+MIDI_NOTE_LENGTH+MIDI_DURATION_LENGTH+1,i+MIDI_NOTE_LENGTH+MIDI_DURATION_LENGTH+2)).intValue(),
							new Integer(stringRepresentation.substring(i+MIDI_NOTE_LENGTH+MIDI_DURATION_LENGTH+2,i+MIDI_NOTE_LENGTH+MIDI_DURATION_LENGTH+3)).intValue(),
							new Integer(stringRepresentation.substring(i+MIDI_NOTE_LENGTH+MIDI_DURATION_LENGTH+3,i+MIDI_NOTE_LENGTH+MIDI_DURATION_LENGTH+4)).intValue());
			}

			playTimer = new javax.swing.Timer(TEMPO, new
				ActionListener()
				{
					public void actionPerformed(ActionEvent Event)
					{
						playNextNote();
			 	        }
			 	});

			playTimer.setRepeats(true);

		}
		else
		{
			System.out.println("This is not a valid machine string representation");
		}		
		
	}

	public static void main(String[] args)
	{
		String theString;
			
		if (args.length > 0)
		{
			theString = args[0];
		}
		else
		{
			Random theRanGen = new Random();
			StringBuffer theRanString = new StringBuffer();

			// Randomly generate 10 machine objects
			theRanString.append(FiniteStateMusicMachine.MACHINE_OBJECT_LENGTH);
			for (int i = 0; i < FiniteStateMusicMachine.NUM_OBJECTS_MACHINE; i++)
			{
				for (int p = 0; p < FiniteStateMusicMachine.MACHINE_OBJECT_LENGTH; p++)
				{
					theRanString.append(theRanGen.nextInt(10));
				}				
			}
			theString = theRanString.toString();				
		}
		
		FiniteStateMusicMachine thisMachine = new FiniteStateMusicMachine(theString);
		System.out.println(thisMachine.playMachine(10));
		//System.exit(1);
	}


	private boolean validateString(String toValidate)
	{
		boolean returnValue = true;
		
		/* 
		String Format:
			0-9 Machine Length
			for 1 to Machine Length
				0-9 Machine Attribute
		*/
		
		// Check Machine Length, must be greater than MachineObject.minMachineLength.
		if (new Integer(toValidate.substring(0,1)).intValue() == FiniteStateMusicMachine.MACHINE_OBJECT_LENGTH)
		{
			// Check String Length - Always 10 Objects
			if ((toValidate.length()-1) / FiniteStateMusicMachine.MACHINE_OBJECT_LENGTH == FiniteStateMusicMachine.NUM_OBJECTS_MACHINE)
			{
				
			}
			else
			{
				returnValue = false;
			}
		}
		else
		{
			returnValue = false;
		}
		
		
		return returnValue;
	}
   	

	public String toString()
	{
		return stringRepresentation;
	}

   	public Object createMachineObject(Object thisObject, int stateOneNextObjectIndex, int stateOneNextObjectState, int stateTwoNextObjectIndex, int stateTwoNextObjectState)
   	{
		MachineObject currentObject = new MachineObject(thisObject, stateOneNextObjectIndex, stateOneNextObjectState, stateTwoNextObjectIndex, stateTwoNextObjectState);
		MachineVector.add(currentObject);
		
		return currentObject;
   	}

	public void stopMachine()
	{
		playTimer.stop();
		notesToPlay.removeAllElements();
		notesToPlayCounter = 0;
		
		// Not sure I want to do all of this here
		mySynthesizer.close();
//		mySynthesizer.dispose();
//		playTimer.dispose();
	}

	public String playMachine(int numNotesToPlay)
	{

		// Initialize Midi
		try
		{
		
			// Get the default Synthesizer
			if (mySynthesizer == null)
			{
				if ((mySynthesizer = MidiSystem.getSynthesizer()) == null) 
				{
					System.out.println("Synthesizer is null");
					return null;
				}
			} 
			mySynthesizer.open();  // Open the Synth
			
			// Get Default Soundbank
			mySoundbank = mySynthesizer.getDefaultSoundbank();
			
			// Get Instruments array
			if (mySoundbank != null)
			{
				myInstruments = mySoundbank.getInstruments();
				mySynthesizer.loadInstrument(myInstruments[midiInstrumentNumber]);
				
			}
			else
			{
				System.out.println("Soundbank is null");
			}
		
			// Get the MidiChannels
			myMidiChannels = mySynthesizer.getChannels();
			myMidiChannels[midiChannelNumber].programChange(midiInstrumentNumber);
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}     		
		
		
		
		StringBuffer playedString = new StringBuffer();

		int startingIndex = 0;		
		while (startingIndex < 10)
		{
			currentIndex = startingIndex;
			int startingState = 0;			
			while (startingState < 2)
			{
				currentState = startingState;
				
				MidiNote firstNote = (MidiNote) ((MachineObject) MachineVector.elementAt(currentIndex)).returnObject();
				notesToPlay.add(firstNote);
				playedString.append(firstNote.toString());
		
				int counter = 0;
				while (counter < numNotesToPlay)
				{
					MidiNote newNote = (MidiNote) returnNextObject();

					notesToPlay.add(newNote);
					playedString.append(newNote.toString());			
					
					counter++;			
				}
				playedString.append("\n");
			
				startingState++;
			}
			
			startingIndex++;
		}

		playTimer.start();
		playTimer.setRepeats(true);
		
		return playedString.toString();

	}

	public void playNextNote()
	{
		if (notesToPlayCounter < notesToPlay.size())
		{
			((MidiNote) notesToPlay.elementAt(notesToPlayCounter)).playNote();	
			notesToPlayCounter++;
		}
		else
		{
			notesToPlayCounter = 0;
		}
	}

	
	public Object returnNextObject()
	{
		currentState = ((Integer) ((MachineObject) MachineVector.elementAt(currentIndex)).returnNextObjectState(currentState)).intValue();
		currentIndex = ((Integer) ((MachineObject) MachineVector.elementAt(currentIndex)).returnNextObjectIndex(currentState)).intValue();
		MidiNote newObject = (MidiNote) ((MachineObject) MachineVector.elementAt(currentIndex)).returnObject();
		return newObject;
	}
	
	private class MidiNote
	{
		private javax.swing.Timer noteTimer;
		//private javax.swing.Timer[] noteTimers = new noteTimers[10];

		private int noteLength;		
		private int noteNumber;
		private int instrumentNumber;
		private int noteDuration;
		private int defaultVelocity = 100;
		private int octaveNumber = 4;
		
		MidiNote(int thisNoteNumber, int thisDurationNumber, int thisInstrumentNumber)
		{
			noteNumber = thisNoteNumber+MIN_NOTE;
			instrumentNumber = thisInstrumentNumber;			
			noteDuration = thisDurationNumber;

			noteTimer = new javax.swing.Timer(((MAX_NOTE_DURATION-MIN_NOTE_DURATION)/(MIDI_DURATION_LENGTH*10))*(noteDuration+1), new
				ActionListener()
				{
					public void actionPerformed(ActionEvent Event)
					{
						stopNote();
			 	        }
			 	});
		}
		
		public void stopNote()
		{
        		//myMidiChannels[midiChannelNumber].allNotesOff();
        		//myMidiChannels[midiChannelNumber].noteOff(noteNumber+octaveNumber*12, defaultVelocity);
			myMidiChannels[midiChannelNumber].noteOff(noteNumber, defaultVelocity);
			noteTimer.stop();
		}
		
		public void playNote()
		{
			// Play the instrument selected on the channel
			//myMidiChannels[midiChannelNumber].noteOn(noteNumber+octaveNumber*12, defaultVelocity);			myMidiChannels[midiChannelNumber].noteOn(noteNumber+octaveNumber*12, defaultVelocity);
			//if different myMidiChannels[midiChannelNumber].programChange(instrumentNumber);
			myMidiChannels[midiChannelNumber].noteOn(noteNumber, defaultVelocity);
			noteTimer.start();
		}
		
		public String toString()
		{
			return new Integer(noteNumber).toString() + new Integer(noteDuration).toString();
		}
	}
   					
	private class MachineObject
	{		
		private int stateOneNextObjectIndex;
		private int stateTwoNextObjectIndex;
		private int stateOneNextObjectState;
		private int stateTwoNextObjectState;
		
		private Object thisObject;

		private int attributeOne;
		private int attributeTwo;
		private int attributeThree;

		
		MachineObject(Object thisObject, int stateOneNextObjectIndex, int stateOneNextObjectState, int stateTwoNextObjectIndex, int stateTwoNextObjectState)		
		{
			this.thisObject = thisObject;
			setNextObjectIndex(stateOneNextObjectIndex, stateOneNextObjectState,1);
			setNextObjectIndex(stateTwoNextObjectIndex, stateTwoNextObjectState,2);
		}

		Object returnObject()
		{
			return thisObject;
		}

		Integer returnNextObjectIndex(int currentState)
		{
			Integer returnInteger;
			
			if (currentState % 2 == 0)
			{
				returnInteger = new Integer(stateTwoNextObjectIndex);
			}
			else 
			{
				returnInteger = new Integer(stateOneNextObjectIndex);
			}
			
			return returnInteger;
		}
		
		Integer returnNextObjectState(int currentState)
		{
			Integer returnInteger;
			
			if (currentState % 2 == 0)
			{
				returnInteger = new Integer(stateTwoNextObjectState);
			}
			else 
			//if (currentState % 2 == 1)
			{
				returnInteger = new Integer(stateOneNextObjectState);
			}
			
			return returnInteger;
		}
		
		void setNextObjectIndex(int stateNextObjectIndex, int nextObjectState, int stateNumber)
		{
			
			if (stateNumber % 2 == 0)
			{
				stateTwoNextObjectIndex = stateNextObjectIndex;
				stateTwoNextObjectState = nextObjectState;
			}
			else
			{	
				stateOneNextObjectIndex = stateNextObjectIndex;
				stateOneNextObjectState = nextObjectState;
			}
			
		}		
	}
							
}