package com.krisharris.mpk;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Vector;

import processing.core.PApplet;
import processing.net.Client;
import processing.xml.XMLElement;

/**
 * This class allows you to communicate with a number of Making Things interface
 * boards via the mchelper application
 * <p>
 * In your host PApplet - in processing the top level of your sketch you can
 * define the following functions
 * <p>
 * public void OSCMessage({@link OSCMessage} m)<br>
 * This will be called each time a message is received from any connected board
 * <p>
 * public void boardAdd({@link Board} b)<br>
 * This will be called each time a board is connected
 * <p>
 * public void boardRemove({@link Board} b)<br>
 * This will be called each time a board is removed
 * 
 * @author Kristopf
 */
public class MCProcessingConnect {
	// configuration variables;
	private PApplet parent;
	private Client sock;
	private Vector connectedBoards;
	private Board defaultBoard;

	// communication variables
	private String temp_message;
	private boolean message_done;
	private Method oscmessageHandler;
	private Method boardAddHandler;
	private Method boardRemoveHandler;

	/**
	 * Start & Connect to mchelper at 127.0.0.1:11000
	 * 
	 * @param _parent
	 *            in processing, just pass this
	 */
	public MCProcessingConnect(PApplet _parent) {
		MCProcessingConnect(_parent, "127.0.0.1", 11000);
	}

	/**
	 * Start & Connect to mchelper at a specified IP address, port 11000
	 * 
	 * @param _parent
	 *            in processing, just pass this
	 * @param mchelper_address
	 *            the address mchelper is running on
	 */
	public MCProcessingConnect(PApplet _parent, String mchelper_address) {
		MCProcessingConnect(_parent, mchelper_address, 11000);
	}

	/**
	 * Start & Connect to mchelper at a specified port and address
	 * 
	 * @param _parent
	 *            in processing, just pass this
	 * @param mchelper_address
	 *            the address mchelper is running on
	 * @param port
	 *            the port to connect to
	 */
	public MCProcessingConnect(PApplet _parent, String mchelper_address,
			int port) {
		MCProcessingConnect(_parent, mchelper_address, port);
	}

	/* Connect & Setup */
	private void MCProcessingConnect(PApplet _parent, String mchelper_address,
			int port) {
		parent = _parent;
		message_done = false;
		temp_message = "";
		connectedBoards = new Vector();

		parent.registerDispose(this); // needed so we can dispose of objects
										// properly
		parent.registerPre(this); // needed so we can poll the socket
		parent.registerPost(this); // needed so we can flush the outgoing
									// message queue every frame

		// OSCMessage handler
		Class c = parent.getClass();
		try {
			oscmessageHandler = c.getDeclaredMethod("OSCMessage",
					new Class[] { OSCMessage.class });
			// oscmessageHandler.invoke(parent, new Object[]{new OSCMessage()});
		} catch (Exception e) {
			// failed to find OSCMessage method, ignore...
			parent
					.println("Makeproc: Unable to find OSCMessage(OSCMessage m) in your applet. You will not be able to receive messages from the Make Controller");
		}

		try {
			boardAddHandler = c.getDeclaredMethod("boardAdd",
					new Class[] { Board.class });
		} catch (Exception e) {
			// parent.println("Board add not found");
		}

		try {
			boardRemoveHandler = c.getDeclaredMethod("boardRemove",
					new Class[] { Board.class });
		} catch (Exception e) {

		}

		// Setup the socket
		// Stupid processing socket just explodes if it can't reach the host.
		try {
			sock = new Client(parent, mchelper_address, port);
		} catch (Exception e) {
			parent.println("MCProcessingConnect could not connect to "
					+ mchelper_address + ":" + port);
			// parent.println(e.toString());
			sock = null;
		}
	}

	/**
	 * Called internally by processing.
	 */
	public void pre() {
		while (sock.available() > 0) {// read until there's no more data, or
										// we see a stop bit
			char read = (char) sock.read();
			// parent.print(read);
			if (read > 0) {
				temp_message += read;
			} else {
				message_done = true;
			}
		}

		if (message_done) {// iff there's a whole message done (ncoming) this
							// frame, deal with it.;
			XMLElement incomingXML;
			incomingXML = new XMLElement();
			try {
				incomingXML.parseString("<BODY>" + temp_message + "</BODY>");// I
																				// add
																				// body
																				// tags
																				// because
																				// sometimes
																				// I
																				// can
																				// get
																				// more
																				// than
																				// one
																				// parent
																				// tag
																				// per
																				// packet
																				// and
																				// the
																				// xml
																				// class
																				// can't
																				// see
																				// sibbling
																				// elements.
				for (int i = 0; i < incomingXML.getChildCount(); i++) {
					XMLElement xml = incomingXML.getChild(i);
					String type = xml.getName();// type of message we're
												// receiveing
					if (type.equals("OSCPACKET")) { // OSC (board i/o)
													// information
						// parent.println(temp_message);
						handleOSCPacket(xml);

					} else if (type.equals("BOARD_ARRIVAL")) { // board
																// announce
																// event
						Board b = new Board(xml.getChild(0).getStringAttribute(
								"LOCATION", ""), xml.getChild(0)
								.getStringAttribute("TYPE", ""), xml
								.getChild(0).getStringAttribute("NAME", ""),
								xml.getChild(0).getIntAttribute("SERIALNUMBER"));
						connectedBoards.add(b);
						b.sendMessage("/system/name", null);
						b.sendMessage("/system/serialnumber", null);
						invokeBoardAddHandler(b);
						if (defaultBoard == null) {
							defaultBoard = b;
							parent.println("Connected Default Board: "
									+ b.location);
						}// fi defaultBoard==null

					} else if (type.equalsIgnoreCase("BOARD_REMOVAL")) { // board
																			// remove
																			// event?
						Board b = findBoardByAddress(xml.getChild(0)
								.getStringAttribute("LOCATION"));
						if (b == defaultBoard)
							defaultBoard = null;

						invokeBoardRemoveHandler(b);
						connectedBoards.remove(b);
					} else {
						parent.println("Unhandled Message:" + type);
					}// fi type
				}// fi for incomingXML
				message_done = false;
				temp_message = "";
			} catch (Exception e) {
				parent.print("XML receiving exception occoured: "
						+ e.toString());
			} // weird things seem to sometimes happen with the xml. Ignore
				// it.
		}// fi message_done

	}

	/**
	 * Called internally by processing.
	 */
	public void post() {
		// flush each board's message queue
		for (int i = 0; i < connectedBoards.size(); i++) {
			Board b = (Board) connectedBoards.get(i);
			if (b != null && b.messageQueue.size() > 0) {
				// Construct an OSC packet to this board.
				XMLElement oscpacket = new XMLElement();
				oscpacket.setName("OSCPACKET");
				oscpacket.setAttribute("ADDRESS", b.location);
				oscpacket.setIntAttribute("PORT", 0);
				oscpacket.setIntAttribute("TIME", 0);
				for (int j = 0; j < b.messageQueue.size(); j++) {
					XMLElement oscmessage = new XMLElement();
					OSCMessage msg = (OSCMessage) b.messageQueue.get(j);
					if (b.cache_messages)
						b.OSCMessage(msg);

					oscmessage.setName("MESSAGE");
					oscmessage.setAttribute("NAME", msg.message);

					// if arguements were passed, add them to the oscmessage
					if (msg.args != null && msg.args.size() > 0) {
						for (int k = 0; k < msg.args.size(); k++) {
							Object o = msg.args.get(k);
							XMLElement attr = new XMLElement();
							attr.setName("ARGUMENT");
							attr.setAttribute("VALUE", o.toString());
							if (o instanceof Integer) {
								attr.setAttribute("TYPE", "i");
							} else if (o instanceof Float) {
								attr.setAttribute("TYPE", "f");
							} else {
								attr.setAttribute("TYPE", "s");
							}
							oscmessage.addChild(attr);
						}// end for
					}// end if
					oscpacket.addChild(oscmessage);
				}// end for
				writePacket(oscpacket.toString());
				b.messageQueue = new Vector();
			}// end if messageQueue
		}// end for connectedBoards
	}

	/**
	 * Send an OSC Message
	 * 
	 * @param message
	 *            the OSC message to send to the default board
	 * @param args
	 *            a vector containing Integers, Floats and Strings to be sent as
	 *            the argument(s) can be null.
	 */
	public void sendMessage(String message, Vector args) {
		sendMessage(defaultBoard, message, args);
	}

	/**
	 * Send an OSC Message imediately, without adding it to the queue
	 * 
	 * @param dest
	 *            The board to send the message to
	 * @param message
	 *            The OSC message to send
	 * @param args
	 *            (Can be null): a vector containing Integers, Floats and
	 *            Strings to be sent as the argument(s)
	 */
	public void sendMessage(Board dest, String message, Vector args) {
		if (dest != null) {
			XMLElement oscpacket = new XMLElement();
			XMLElement oscmessage = new XMLElement();
			oscpacket.setName("OSCPACKET");
			oscpacket.setAttribute("ADDRESS", dest.location);
			oscpacket.setIntAttribute("PORT", 0);
			oscpacket.setIntAttribute("TIME", 0);

			oscmessage.setName("MESSAGE");
			oscmessage.setAttribute("NAME", message);

			// if arguements were passed, add them to the oscmessage
			if (args != null && args.size() > 0) {
				for (int i = 0; i < args.size(); i++) {
					Object o = args.get(i);
					XMLElement attr = new XMLElement();
					attr.setName("ARGUMENT");
					attr.setAttribute("VALUE", o.toString());
					if (o instanceof Integer) {
						attr.setAttribute("TYPE", "i");
					} else if (o instanceof Float) {
						attr.setAttribute("TYPE", "f");
					} else {
						attr.setAttribute("TYPE", "s");
					}
					oscmessage.addChild(attr);
				}
			}
			oscpacket.addChild(oscmessage);

			writePacket(oscpacket.toString());

		}
	}

	/*
	 * Decode an XML Packet containing exactly one <OSCPACKET>...</OSCPACKET>
	 * and send out appropreate messages It strikes me this should return an
	 * oscmessage....
	 */

	private void handleOSCPacket(XMLElement oscpacket) {
		Vector messages = new Vector(); // I think this is unneeded........

		for (int i = 0; i < oscpacket.getChildCount(); i++) {// <MESSAGE>s
			XMLElement msg = oscpacket.getChild(i);
			// parent.println("Parsing "+msg.toString());
			OSCMessage oscmsg = new OSCMessage(msg.getStringAttribute("NAME")); // create
																				// a
																				// new
																				// OSCMessage
																				// with
																				// the
																				// name
																				// of
																				// this
																				// message
			oscmsg.from = oscpacket.getStringAttribute("ADDRESS");
			Board b = findBoardByAddress(oscmsg.from); // Find the board
														// related to this osc
														// packet
			for (int j = 0; j < msg.getChildCount(); j++) {// <ARGUMENT>s
				XMLElement arg = msg.getChild(j);
				String type = arg.getStringAttribute("TYPE");
				if (type.equals("i")) {
					oscmsg.args.add(new Integer(arg.getIntAttribute("VALUE")));
				} else if (type.equals("f")) {
					oscmsg.args.add(new Float(arg.getFloatAttribute("VALUE")));
				} else {
					oscmsg.args.add(arg.getAttribute("VALUE"));
				}
			}
			// messages.add(oscmsg);
			// notify the board of messages
			if (b != null) {
				b.OSCMessage(oscmsg);
			}
			invokeOSCHandler(oscmsg);
		}
	}

	/**
	 * Called Internaly by processing. disposes of socket
	 */
	public void dispose() {
		sock.stop();
		connectedBoards = new Vector();
		defaultBoard = null;

	}

	/**
	 * Returns a Vector of boards mchelper knows about.
	 */
	public Vector getConnectedBoards() {
		return connectedBoards;
	}

	/**
	 * Returns the default board
	 */
	public Board getDefaultBoard() {
		return defaultBoard;
	}

	private void writePacket(String s) {
		// parent.println("Sending Packet:");
		// parent.println(s);
		sock.write(s); // write out the xml
		sock.write(0); // write stop bit
	}

	private void invokeOSCHandler(OSCMessage args) {
		if (oscmessageHandler != null) {
			try {
				oscmessageHandler.invoke(parent, new Object[] { args });
			} catch (Exception e) {
				// It failed, set variable to null so it won't happen again
				oscmessageHandler = null;
			}
		}
	}

	private void invokeBoardAddHandler(Board b) {
		if (boardAddHandler != null) {
			try {
				boardAddHandler.invoke(parent, new Object[] { b });
			} catch (Exception e) {
				// It failed, set variable to null so it won't happen again
				boardAddHandler = null;
			}
		}
	}

	private void invokeBoardRemoveHandler(Board b) {
		if (boardRemoveHandler != null) {
			try {
				boardRemoveHandler.invoke(parent, new Object[] { b });
			} catch (Exception e) {
				// It failed, set variable to null so it won't happen again
				boardRemoveHandler = null;
			}
		}
	}

	/*
	 * finds a board by it's address. returns null if board cannot be found.
	 */
	private Board findBoardByAddress(String address) {
		for (int i = 0; i < connectedBoards.size(); i++) {
			Board b;
			b = (Board) connectedBoards.get(i);
			if (b != null) {
				if (b.location.equals(address)) {
					return b;
				}
			}
		}
		return null;
	}
}
