import javax.sound.midi.*;
import javax.sound.midi.MidiMessage.*;
import java.io.RandomAccessFile;

class BIN2BOOT
{
	private static final int MIDI_CONTROL_CHANNEL          = 9;
	private static final int MIDI_DATA_CHANNEL             = 0;

	private static final int CONTROL_DEVICE_READY          = 0xD1;
	private static final int CONTROL_ENTER_PROG_MODE       = 0xDC;
	private static final int CONTROL_LEAVE_PROG_MODE       = 0xDF;
	private static final int CONTROL_GET_PAGE_SIZE         = 0x01;

	public static void main(String[] args)
	{
		if (args.length != 1)
		{
			System.out.println("BIN2BOOT - USB-MIDI bootloader");
			System.out.println("  Usage: java BIN2BOOT {input}.bin");
		}	
	
		RandomAccessFile inFile = null;
		
		try
		{
			inFile = new RandomAccessFile(args[0], "r");
		}
		catch (Exception e)
		{
			System.out.println("Could not open input file!");
			return;
		}

		MidiDevice  currDevice = null;
		Receiver    midiOut    = null;
		Transmitter midiIn     = null;
		MIDIMessageReceiver midiInMessages = new MIDIMessageReceiver();
		
		try
		{
			MidiDevice.Info[] infos = MidiSystem.getMidiDeviceInfo();
			
			for (MidiDevice.Info info : infos)
			{
				currDevice = MidiSystem.getMidiDevice(info);
			
				if (!(currDevice instanceof Sequencer) && !(currDevice instanceof Synthesizer))
				{
					if (info.getName().indexOf("LUFA") == -1)
					  continue;
					  
					System.out.println(" MIDI Device: " + info.getName());
				
					currDevice.open();
				
					if (currDevice.getMaxReceivers() != 0)
					{
						midiOut = currDevice.getReceiver();
						break;
					}

					if (currDevice.getMaxTransmitters() != 0)
					{
						midiIn  = currDevice.getTransmitter();
						midiIn.setReceiver(midiInMessages);
					}
				}
			}
			
			if ((midiOut == null) || (midiIn == null))
			{
				System.out.println("Could not find suitable MIDI device!");
				return;
			}			
		}
		catch (Exception e)
		{
			System.out.println("Could not enumerate MIDI devices!");
			return;
		}
		
		System.out.println("PROGRAMMING FILE...");

		ProgramFirmware(inFile, midiOut, midiInMessages);

		System.out.println("DONE.");

		try
		{
			midiOut.close();
			midiIn.close();
			currDevice.close();
			inFile.close();
		}
		catch (Exception e)
		{
			System.out.println("ERROR: Could not close open resources.");		
		}
	}
	
	private static void ProgramFirmware(RandomAccessFile inFile, Receiver midiOut, MIDIMessageReceiver midiInMessages)
	{
		try
		{
			System.out.println("Entering Programming Mode...");
			sendByteViaMIDI(midiOut, MIDI_CONTROL_CHANNEL, CONTROL_ENTER_PROG_MODE);
			
			int[] messageData;
			do
			{
				messageData = receiveByteViaMIDI(midiInMessages);
			}
			while ((messageData[0] != MIDI_CONTROL_CHANNEL) && (messageData[1] != CONTROL_DEVICE_READY));
			
			System.out.println("Getting Page Size...");
			sendByteViaMIDI(midiOut, MIDI_CONTROL_CHANNEL, CONTROL_GET_PAGE_SIZE);

			int nextByte = inFile.read();
			while (nextByte != -1)
			{
				sendByteViaMIDI(midiOut, 9, nextByte);

				if ((inFile.getFilePointer() % (inFile.length() / 100)) == 0)
					System.out.println("  LOADING: " + (int)(inFile.getFilePointer() / (inFile.length() / 100.0)) + "%...");

				nextByte = inFile.read();
			}

			sendByteViaMIDI(midiOut, MIDI_CONTROL_CHANNEL, CONTROL_LEAVE_PROG_MODE);
		}
		catch (Exception e)
		{
			System.out.println("ERROR: Could not send data to device.");		
		}
	}

	private static void sendByteViaMIDI(Receiver midiOut, int channel, int data)
	{
		ShortMessage sendMessage = new ShortMessage();
		
		try
		{
			sendMessage.setMessage(ShortMessage.NOTE_ON, channel, (data & 0x7F), (((data & 0x80) == 0x80) ? 64 : 32));
			midiOut.send(sendMessage, -1);

//			try {Thread.sleep(1);} catch (Exception e) {}

			sendMessage.setMessage(ShortMessage.NOTE_OFF, channel, (data & 0x7F), (((data & 0x80) == 0x80) ? 64 : 32));
			midiOut.send(sendMessage, -1);
		}
		catch (Exception e)
		{
			System.out.println("ERROR: Could not send MIDI note press.");
		}
	}

	private static int[] receiveByteViaMIDI(MIDIMessageReceiver midiInReceiver)
	{
		byte[] messageData;
		
		do
		{
			while (!(midiInReceiver.hasReceived()));
			messageData = midiInReceiver.receive().getMessage();
		}
		while ((messageData[0] & 0xF0) != ShortMessage.NOTE_ON);
	
		int channel = (messageData[0] & 0x0F);
		int data    = (messageData[1] | ((messageData[2] == 64) ? 0x80 : 0x00));
		
		int[] formattedMessageData = new int[2];
		formattedMessageData[0] = channel;
		formattedMessageData[1] = data;
	
		return formattedMessageData;
	}
}