// HTTPMidiScratchExtension.java
// Copyright (c) MIT Media Laboratory, 2013
// John Maloney
//
// This Scratch Extension helper runs an HTTP server that allows Scratch extension
// blocks to control a software or hardware music synthesizer. Only a subset of MIDI is
// supported: noteOn, noteOff, programChange, controllerChange, and pitchBend.
//
// For a summary of the MIDI protocol, see http://www.midi.org/techspecs/midimessages.php

import java.net.*;
import java.io.*;
import java.util.*;
import javax.sound.midi.*;

public class HTTPMidiScratchExtension {
		
	private static final int PORT = 17801; // HTTPMidiScratchExtension port
		
	private static InputStream sockIn;
	private static OutputStream sockOut;
		
	private static Receiver midiOut;
	private static Synthesizer synth;
	private static Receiver synthOut;
	private static Boolean useSynth = true;
	private static long baseTime = new Date().getTime();
	
	public static void main(String[] args) throws IOException, InvalidMidiDataException, MidiUnavailableException {
		InetAddress addr = InetAddress.getLocalHost();
		System.out.println("HTTPExtensionTest helper app started on " + addr.toString());

		initMIDI();
		ServerSocket serverSock = new ServerSocket(PORT);
		while (true) {
			Socket sock = serverSock.accept();
			sockIn = sock.getInputStream();
			sockOut = sock.getOutputStream();
			try {
				handleRequest();
			} catch (Exception e) {
				e.printStackTrace(System.err);
				sendResponse("unknown server error");
			}
			sock.close();
		}
	}
	
	private static void initMIDI() throws InvalidMidiDataException, MidiUnavailableException {
		// Initialize the MIDI synth.

		MidiSystem.getReceiver(); // ensure that the MIDI system is available
		midiOut = MidiSystem.getReceiver();
		synth = MidiSystem.getSynthesizer();
		synth.open();
		synthOut = synth.getReceiver();
	}

	private static void handleRequest() throws IOException, InvalidMidiDataException, MidiUnavailableException {
		String httpBuf = "";
		int i;
		
		// read data until the first HTTP header line is complete (i.e. a '\n' is seen)
		while ((i = httpBuf.indexOf('\n')) < 0) {
			byte[] buf = new byte[5000];
			int bytes_read = sockIn.read(buf, 0, buf.length);
			if (bytes_read < 0) {
				System.out.println("Socket closed; no HTTP header.");
				return;
			}
			httpBuf += new String(Arrays.copyOf(buf, bytes_read));
		}
		
		String header = httpBuf.substring(0, i);
		if (header.indexOf("GET ") != 0) {
			System.out.println("This server only handles HTTP GET requests.");
			return;
		}
		i = header.indexOf("HTTP/1");
		if (i < 0) {
			System.out.println("Bad HTTP GET header.");
			return;
		}
		header = header.substring(5, i - 1);
		if (header.equals("favicon.ico")) return; // igore browser favicon.ico requests
		else if (header.equals("crossdomain.xml")) sendPolicyFile();
		else if (header.length() == 0) doHelp();
		else doCommand(header);
	}
	
	private static void sendPolicyFile() {
		// Send a Flash null-teriminated cross-domain policy file.
		String policyFile =
		"<cross-domain-policy>\n" +
		"  <allow-access-from domain=\"*\" to-ports=\"" + PORT + "\"/>\n" +
		"</cross-domain-policy>\n\0";
		sendResponse(policyFile);
	}
	
	private static void sendResponse(String s) {
		String crlf = "\r\n";
		String httpResponse = "HTTP/1.1 200 OK" + crlf;
		httpResponse += "Content-Type: text/html; charset=ISO-8859-1" + crlf;
		httpResponse += "Access-Control-Allow-Origin: *" + crlf;
		httpResponse += crlf;
		httpResponse += s + crlf;
		try {
			byte[] outBuf = httpResponse.getBytes();
			sockOut.write(outBuf, 0, outBuf.length);
		} catch (Exception ignored) { }
	}
	
	private static void doCommand(String cmdAndArgs) throws IOException, InvalidMidiDataException, MidiUnavailableException {
		// Essential: handle commands understood by this server
//		if (!cmdAndArgs.equals("poll")) System.out.println("cmd: " + cmdAndArgs);
		String response = "okay";
		String[] parts = cmdAndArgs.split("/");
		String cmd = parts[0];
		if (cmd.equals("midiReset")) {
			midiReset();
		} else if (cmd.equals("noteOn")) {
			noteOn(asInt(parts[1]), asInt(parts[2]), asInt(parts[3])); // key, vel, chan
		} else if (cmd.equals("noteOff")) {
			noteOn(asInt(parts[1]), 0, asInt(parts[2])); // key, chan (noteOn with zero velocity turns the note off)
		} else if (cmd.equals("controller")) {
			controlChange(asInt(parts[1]), asInt(parts[2]), asInt(parts[3])); // controllerNum, value, chan
		} else if (cmd.equals("pitchBend")) {
			pitchBend(asInt(parts[1]), asInt(parts[2])); // bend, chan
		} else if (cmd.equals("program")) {
			programChange(asInt(parts[1]), asInt(parts[2])); // instrument, chan
		} else if (cmd.equals("poll") || cmd.equals("update-poll")) { // Note: "update-poll" is only for backward compatibility with an old version
			long msecs = new Date().getTime() - baseTime;
			response = "time " + msecs + "\n";
		} else if (cmd.equals("useJavaSynth")) {
			useSynth = (parts[1] == "true");
			System.out.println("useJavaSynth: " + useSynth);
		} else {
			response = "unknown command: " + cmd;
		}
		sendResponse(response);
	}

	private static int asInt(String s) { return Integer.parseInt(s); }
	
	private static void controlChange(int controllerNum, int value, int chan) throws InvalidMidiDataException {
		ShortMessage msg = new ShortMessage();
		msg.setMessage(ShortMessage.CONTROL_CHANGE, chan, controllerNum, value);
		midiOut.send(msg, -1);
		if (useSynth) synthOut.send(msg, -1);
	}
	
	private static void midiReset() throws InvalidMidiDataException, MidiUnavailableException {
		int chan;
		for (chan = 0; chan < 16; chan++) {
			controlChange(121, 0, chan); // reset controllers to default values
			controlChange(123, 0, chan); // turn all notes off
		}
	}
	
	private static void noteOn(int key, int velocity, int chan) throws InvalidMidiDataException, MidiUnavailableException {
		ShortMessage msg = new ShortMessage();
		msg.setMessage(ShortMessage.NOTE_ON, chan, key, velocity);
		midiOut.send(msg, -1);
		if (useSynth) synthOut.send(msg, -1);
	}
	
	private static void pitchBend(int bend, int chan) throws InvalidMidiDataException, MidiUnavailableException {
		ShortMessage msg = new ShortMessage();
		msg.setMessage(ShortMessage.PITCH_BEND, chan, bend & 0x7F, (bend >> 7) & 0x7F);
		midiOut.send(msg, -1);
		if (useSynth) synthOut.send(msg, -1);
	}
	
	private static void programChange(int instrument, int chan) throws InvalidMidiDataException, MidiUnavailableException {
		ShortMessage msg = new ShortMessage();
		msg.setMessage(ShortMessage.PROGRAM_CHANGE, chan, instrument, 0);
		midiOut.send(msg, -1);
		if (useSynth) synthOut.send(msg, -1);
	}
	
	private static void doHelp() {
		// Optional: return a list of commands understood by this server
		String help = "Scratch MIDI Extension<br><br>";
		help += "midiReset - Reset the MIDI system.<br>";
		help += "noteOn/<key>/<velocity>/<channel> - Send a noteOn command<br>";
		help += "noteOff/<key>/<channel> - Send a noteOff command<br>";
		help += "controller/<controller>/<value>/<channel> - Send a controller command<br>";
		help += "pitchBend/<bend>/<channel> - Send a pitch bend<br>";
		help += "useJavaSynth/<boolean> - Enable or disable the Java MIDI synth<br>";
		sendResponse(help);
	}
	
}
