// TODO:
// * Keep track of established connections.
Ingen : Model {
	classvar <>program = "om", <>patchLoader = "om_patch_loader";
	classvar <>oscURL, <nodeTypeMap, <>uiClass;
	var <addr;
	var <>loadIntoJack = true;
	var allocator, requestResponders, requestHandlers, notificationResponders;
	var creatingNode, newNodeEnd;
	var <registered = false, <booting = false;
	var <root;
	var <plugins, pluginParentEvent;
	var onNewPatch;
	*initClass {
		Class.initClassTree(Event);
		Class.initClassTree(NetAddr);
		Event.parentEvents.default[\omcmd] = \noteOn;
		Event.parentEvents.default[\omEventFunctions] = (
			noteOn:  #{ arg midinote=60, amp=0.1;
				[midinote, asInteger((amp * 127).clip(0, 127)) ] },
			noteOff: #{ arg midinote=60; [ midinote ] },
			setPortValue: #{|portValue=0| [portValue] }
		);
		Event.parentEvents.default.eventTypes[\om]=#{|server|
			var freqs, lag, dur, sustain, strum, target, bndl, omcmd;
			freqs = ~freq = ~freq.value + ~detune;
			if (freqs.isKindOf(Symbol).not) {
				~amp = ~amp.value;
				strum = ~strum;
				lag = ~lag + server.latency;
				sustain = ~sustain = ~sustain.value;
				omcmd = ~omcmd;
				target = ~target;
				~midinote = ~midinote.value;
				~portValue = ~portValue.value;
				bndl = ~omEventFunctions[omcmd].valueEnvir.asCollection;
				bndl = bndl.flop;
				bndl.do {|msgArgs, i|
					var latency;
					latency = i * strum + lag;
					if(latency == 0.0) {
						target.performList(omcmd, msgArgs)
					} {
						thisThread.clock.sched(latency, {
							target.performList(omcmd, msgArgs);
						})
					};
					if(omcmd === \noteOn) {
						thisThread.clock.sched(sustain + latency, { 
							target.noteOff(msgArgs[0])
						})
					}
				}
			}
		};
		oscURL="osc.udp://"++NetAddr.localAddr.ip++$:++NetAddr.localAddr.port;
		nodeTypeMap = IdentityDictionary[
			\Internal -> IngenInternalNode,
			\LADSPA   -> IngenLADSPANode,
			\DSSI     -> IngenDSSINode
		];
		uiClass = IngenEmacsUI
	}
	*new { | netaddr |
		^super.new.init(netaddr)
	}
	gui { ^uiClass.new(this) }
	init { |netaddr|
		addr = netaddr ? NetAddr("127.0.0.1", 16180);
		onNewPatch = IdentityDictionary.new;
		allocator = StackNumberAllocator(0,1024);
		requestHandlers = IdentityDictionary.new;
		requestResponders = [
			"response/ok" -> {|id|
				requestHandlers.removeAt(id).value; allocator.free(id) },
			"response/error" -> {|id,text|
				requestHandlers.removeAt(id);
				allocator.free(id);
				("Ingen"+text).error }
		].collect({|a|
			var func = a.value;
			OSCresponder(addr, "/om/"++a.key, {|time,resp,msg|
				func.value(*msg[1..])
			})
		});
		notificationResponders = [
			"new_patch" -> {|path,poly|
				var func = onNewPatch.removeAt(path);
				if (func.notNil) {
					func.value(this.getPatch(path,false).prSetPoly(poly))
				}
			},
			"metadata/update" -> {|path,key,value|
				this.getObject(path).metadata.prSetMetadata(key, value) },
			"new_node" -> {|path,poly,type,lib,label|
				var patchPath, nodeName, patch, node;
				var lastSlash = path.asString.inject(nil,{|last,char,i|
					if(char==$/,i,last)
				});
				if (lastSlash.notNil) {
					patchPath = path.asString.copyFromStart(lastSlash-1);
					nodeName = path.asString.copyToEnd(lastSlash+1);
					patch = this.getPatch(patchPath);
					if (patch.notNil) {
						if (patch.hasNode(nodeName).not) {
							node = nodeTypeMap[type].new
							(nodeName, patch, poly, label, lib);
							creatingNode = node;
							patch.nodes[nodeName.asSymbol] = node;
							patch.changed(\newNode, node);
						} {
							if (patch.getNode(nodeName).class != nodeTypeMap[type]) {
								("Ingen sent an existng node with differing type"+path).warn
							}
						}
					} {
						("Ingen tried to create node in non-existing patch"+patchPath).warn
					}
				} {
					("Invalid path in node creation"+path).warn
				}
			},
			"new_node_end" -> {
				newNodeEnd.value(creatingNode);
				newNodeEnd = nil;
				creatingNode = nil },
			"new_port" -> {|path,type,dir,hint,def,min,max|
				var basePath, portName, parent, port;
				var lastSlash = path.asString.inject(nil,{|last,char,i|
					if(char==$/,i,last)
				});
				if (lastSlash.notNil) {
					basePath = path.asString.copyFromStart(lastSlash-1);
					portName = path.asString.copyToEnd(lastSlash+1);
					parent = this.getNode(basePath) ? this.getPatch(basePath);
					if (parent.notNil) {
						if (parent.hasPort(portName).not) {
							port = IngenPort.new(portName, parent, type, dir, hint, def, min, max);
							parent.ports[portName.asSymbol] = port;
							parent.changed(\newPort, port)
						} {
							if (parent.getPort(portName).porttype != type) {
								("Ingen tried to create an already existing port with differing type"
									+path).warn
							}
						}
					} {
						("Ingen tried to create port on non-existing object"+basePath).warn
					}
				} {
					("Invalid path in port creation"+path).warn
				}
			},
			"control_change" -> {|path,value|
				this.getPort(path).prSetValue(value) },
			"patch_enabled" -> {|path| this.getPatch(path).prSetEnabled(true) },
			"patch_disabled" -> {|path| this.getPatch(path).prSetEnabled(false) },
			"plugin" -> {|lib,label,name,type|
				plugins.add(Event.new(4,nil,pluginParentEvent).putAll(
					(type:type, lib:lib, label:label, name:name))) },
			"node_removal" -> {|path|
				var node = this.getNode(path);
				if (node.notNil) {
					node.parent.nodes.removeAt(node.name.asSymbol).free
				} {
					("Ingen attempting to remove non-existing node"+path).warn
				}
			},
			"port_removal" -> {|path|
				var port = this.getPort(path);
				if (port.notNil) {
					port.parent.ports.removeAt(port.name.asSymbol).free
				} {
					("Ingen attempting to remove non-existing port"+path).warn
				}
			},
			"patch_destruction" -> {|path|
				var patch = this.getPatch(path);
				if (patch.notNil) {
					patch.parent.patches.removeAt(patch.name.asSymbol).free
				} {
					("Ingen attempting to remove non-existing patch"+path).warn
				}
			},
			"program_add" -> {|path,bank,program,name|
				var node = this.getNode(path);
				if (node.respondsTo(\prProgramAdd)) {
					node.prProgramAdd(bank,program,name)
				} {
					("Ingen tried to add program info to"+node).warn
				}
			}
		].collect({|a|
			var func = a.value;
			OSCresponder(addr, "/om/"++a.key, {|time,resp,msg|
				func.value(*msg[1..])
			})
		});
		pluginParentEvent = Event.new(2,nil,nil).putAll((
			engine:this,
			new:{|self,path,poly=1,handler|self.engine.createNode(path?("/"++self.name),self.type,self.lib,self.label,poly,created:handler)}
		));
	}
	*waitForBoot {|func| ^this.new.waitForBoot(func) }
	waitForBoot {|func|
		var r, id = 727;
		requestHandlers[id] = {
			r.stop;
			booting=false;
			this.changed(\running, true);
			func.value(this)
		};
		if (booting.not) {this.boot};
		r = Routine.run {
			50.do {
				0.1.wait;
				addr.sendMsg("/om/ping", id)
			};
			requestHandlers.removeAt(id);
			"Ingen engine boot failed".error;
		}
	}
	getPatch {|path, mustExist=true|
		var elements, currentPatch;
		if (path.class == Array) { elements = path
		} { elements = path.asString.split($/) };
		elements.do{|elem|
			if (elem=="") {
				currentPatch = root
			} {
				currentPatch = currentPatch.getPatch(elem,mustExist);
				if (currentPatch.isNil) { ^nil }
			}
		};
		^currentPatch;
	}
	getNode {|path|
		var basePath, nodeName, patch;
		if (path.class == Array) { basePath = path
		} { basePath = path.asString.split($/) };
		nodeName = basePath.pop;
		patch = this.getPatch(basePath,true);
		if (patch.notNil) {
			^patch.getNode(nodeName)
		};
		^nil
	}
	getPort {|path|
		var basePath, portName, node, patch;
		basePath = path.asString.split($/);
		portName = basePath.pop;
		node = this.getNode(basePath.copy);
		if (node.notNil) { ^node.getPort(portName) };
		patch = this.getPatch(basePath,true);
		if (patch.notNil) { ^patch.getPort(portName) };
		^nil
	}
	getObject {|path|
		var patch,node,port;
		patch = this.getPatch(path,true);
		if (patch.notNil) { ^patch };
		node = this.getNode(path);
		if (node.notNil) { ^node };
		port = this.getPort(path,true);
		if (port.notNil) { ^port };
		^nil
	}
	at {|path|^this.getObject(path.asString)}
	*boot {|func|
		^Ingen.new.waitForBoot {|e|
			e.activate {
				e.register {
					e.loadPlugins {
						e.requestPlugins {
							e.requestAllObjects {
								func.value(e)
							}
						}
					}
				}
			}
		}
	}
	boot {
		requestResponders.do({|resp| resp.add});
		booting = true;
		if (addr.addr == 2130706433) {
			if (loadIntoJack) {
				("jack_load"+"-i"+addr.port+"Ingen"+"om").unixCmd
			} {
				(program+"-p"+addr.port).unixCmd
			}
		} {
			"You have to manually boot Ingen now".postln
		}
	}
	loadPatch {|patchPath| (patchLoader + patchPath).unixCmd }
	activate { | handler |
		this.sendReq("engine/activate", {
			root = IngenPatch("",nil,this);
			this.changed(\newPatch, root);
			handler.value
		})
	}
	register { | handler |
		this.sendReq("engine/register_client", {
			registered=true;
			notificationResponders.do({|resp| resp.add});
			this.changed(\registered, registered);
			handler.value(this)
		})
	}
	unregister { | handler |
		this.sendReq("engine/unregister_client", {
			registered=false;
			notificationResponders.do({|resp| resp.remove});
			this.changed(\registered, registered);
			handler.value(this)
		})
	}
	registered_ {|flag|
		if (flag and: registered.not) {
			this.register
		} {
			if (flag.not and: registered) {
				this.unregister
			}
		}
	}
	loadPlugins { | handler | this.sendReq("engine/load_plugins", handler) }
	requestPlugins {|handler|
		var startTime = Main.elapsedTime;
		plugins = Set.new;
		this.sendReq("request/plugins", {
			("Received info about"+plugins.size+"plugins in"+(Main.elapsedTime-startTime)+"seconds").postln;
			this.changed(\plugins, plugins);
			handler.value(this);
		})
	}
	requestAllObjects { |handler|
		this.sendReq("request/all_objects", handler)
	}
	createPatch { | path, poly=1, handler |
		onNewPatch[path.asSymbol] = handler;
		this.sendReq("synth/create_patch", nil, path.asString, poly.asInteger)
	}
	createNode { | path, type='LADSPA', lib, label, poly=1, created, handler |
		newNodeEnd = created;
		this.sendReq("synth/create_node",handler,path,type,lib,label,poly)
	}
	createAudioInput { | path, handler |
		this.createNode(path,"Internal","","audio_input",0,handler)
	}
	createAudioOutput {|path,handler|
		this.createNode(path,"Internal","","audio_output",0,handler)
	}
	createMIDIInput {|path,handler|
		this.createNode(path,"Internal","","midi_input",1,handler)
	}
	createMIDIOutput {|path,handler|
		this.createNode(path,"Internal","","midi_output",1,handler)
	}
	createNoteIn {|path| this.createNode(path,"Internal","","note_in") }
	connect {|fromPath,toPath,handler|
		this.sendReq("synth/connect",handler,fromPath.asString,toPath.asString)
	}
	disconnect { | fromPath, toPath, handler |
		this.sendReq("synth/disconnect",handler,fromPath.asString,toPath.asString)
	}
	disconnectAll { | path, handler |
		this.sendReq("synth/disconnect_all",handler,path);
	}
	sendReq { | path, handler...args |
		var id = allocator.alloc;
		requestHandlers[id] = handler;
		addr.sendMsg("/om/"++path, id, *args)
	}
	quit {
		if (loadIntoJack) {
			("jack_unload"+"Ingen").unixCmd;
			booting=false;
			requestResponders.do(_.remove);
			notificationResponders.do(_.remove);
			this.changed(\running, false);
		} {
			this.sendReq("engine/quit", {
				booting=false;
				requestResponders.do(_.remove);
				notificationResponders.do(_.remove);
				this.changed(\running, false);
			})
		}
	}
	ping {| n=1, func |
		var id, result, start;
		id = allocator.alloc;
		result = 0;
		requestHandlers[id] = {
			var end;
			end = Main.elapsedTime;
			result=max((end-start).postln,result);
			n=n-1;
			if (n > 0) {
				start = Main.elapsedTime;
				addr.sendMsg("/om/ping", id)
			} {
				allocator.free(id);
				func.value(result)
			}
		};
		start = Main.elapsedTime;
		addr.sendMsg("/om/ping", id)
	}
	setPortValue {|path, val| this.getPort(path.asString).value=val	}
	jackConnect {|path, jackPort|
		this.getPort(path).jackConnect(jackPort)
	}
	noteOn {|path, note, vel|
		var patch,node;
		patch = this.getPatch(path,true);
		if (patch.notNil) { patch.noteOn(note,vel) };
		node = this.getNode(path);
		if (node.notNil) { node.noteOn(note,vel) };
	}
	noteOff {|path, note|
		var patch,node;
		patch = this.getPatch(path,true);
		if (patch.notNil) { patch.noteOff(note) };
		node = this.getNode(path);
		if (node.notNil) { node.noteOff(note) };
	}
	matchPlugins{ | label, lib, name, type |
		^plugins.select{ |p|
			label.matchItem(p.label) and: {
				lib.matchItem(p.lib) and: {
					name.matchItem(p.name) and: {
						type.matchItem(p.type)
					}
				}
			}
		}
	}
	dssiMsg {|path,reqType="program" ...args|
		addr.sendMsg("/dssi"++path++$/++reqType,*args)
	}
}

IngenMetadata {
	var object, dict;
	*new {|obj|^super.new.metadataInit(obj)}
	metadataInit {|obj|
		dict=Dictionary.new;
		object=obj
	}
	put {|key,val|
		object.engine.sendReq("metadata/set", nil,
			object.path, key.asString, val.asString)
	}
	at {|key|^dict.at(key.asSymbol)}
	prSetMetadata {|key,val|
		dict.put(key,val);
		object.changed(\metadata, key, val)
	}
}

IngenObject : Model {
	var <name, <parent, <metadata;
	*new {|name, parent|
		^super.new.initIngenObject(name,parent);
	}
	initIngenObject {|argName, argParent|
		name = argName;
		parent = argParent;
		metadata=IngenMetadata(this)
	}
	path { ^parent.notNil.if({ parent.path ++ $/ ++ name }, name).asString }
	depth { ^parent.notNil.if({ parent.depth + 1 }, 0) }
	engine { ^parent.engine }
}

IngenPort : IngenObject {
	var <porttype, <direction, <spec, <value, <connections;
	*new {|name,parent,type,dir,hint,def,min,max|
		^super.new(name,parent).initPort(type,dir,hint,def,min,max)
	}
	initPort {|type,dir,hint,def,min,max|
		porttype = type;
		direction = dir;
		spec = ControlSpec(min, max,
			if (hint == 'LOGARITHMIC', 'exp', 'lin'),
			if (hint == 'INTEGER', 1, 0),
			def);
		connections = IdentityDictionary.new;
	}
	jackConnect {|jackPort|
		if (porttype.asSymbol != \AUDIO
			|| direction.asSymbol!=\OUTPUT) {
			Error("Not a audio output port").throw
		};
		("jack_connect" + "Ingen:"++(this.path) + jackPort).unixCmd
	}
	value_ {|val|
		if (porttype == \CONTROL and: {direction == \INPUT}) {
			this.engine.sendReq("synth/set_port_value", nil, this.path, val)
		} {
			Error("Not a input control port"+this.path).throw
		}
	}
	connectTo {|destPort|
		if (this.direction!=destPort.direction) {
			this.engine.connect(this,destPort)
		} {
			Error("Unable to connect ports with same direction").throw
		}
	}

	// Setters for OSC responders
	prSetValue {|argValue|
		if (value != argValue) {
			value = argValue;
			this.changed(\value, value)
		}
	}
}

IngenNode : IngenObject { // Abstract class
	var <ports, <polyphonic;
	*new {|name,parent,poly|
		^super.new(name,parent).initNode(poly)
	}
	initNode {|argPoly|
		polyphonic = argPoly;
		ports = IdentityDictionary.new;
	}
	hasPort {|name| ^ports[name.asSymbol].notNil }
	getPort {|name| ^ports[name.asSymbol] }
	portArray {|type=\AUDIO,dir=\OUTPUT|
		var result = Array.new(8);
		ports.do {|port|
			if (port.porttype==type and: {port.direction==dir}) {
				result=result.add(port)
			}
		};
		^result
	}
	audioOut {
		^this.portArray(\AUDIO, \OUTPUT)
	}
	audioIn {
		^this.portArray(\AUDIO, \INPUT)
	}
}

IngenInternalNode : IngenNode {
	var <pluginlabel;
	*new {|name,parent,poly,label|
		^super.new(name,parent,poly).initInternalNode(label)
	}
	initInternalNode {|label|
		pluginlabel = label
	}
	noteOn { |note,vel|
		if (pluginlabel == \note_in or:
			{pluginlabel == \trigger_in and: {this.value == note}}) {
			this.engine.sendReq("synth/note_on", nil, this.path, note, vel)
		} {
			Error("Not a trigger_in or note_in node").throw
		}
	}
	noteOff {|note|
		if (pluginlabel == \note_in or:
			{pluginlabel == \trigger_in and: {this.value == note}}) {
			this.engine.sendReq("synth/note_off", nil, this.path, note)
		} {
			Error("Not a trigger_in or note_in node").throw
		}
	}
}

IngenLADSPANode : IngenNode {
	var <pluginlabel, <libname;
	*new {|name, parent, poly, label, lib|
		^super.new(name,parent,poly).initLADSPANode(label,lib)
	}
	initLADSPANode {|label,lib|
		pluginlabel = label;
		libname = lib
	}
}

IngenDSSINode : IngenLADSPANode {
	var programs;
	*new {|name,parent,poly,label,lib|
		^super.new(name,parent,poly,label,lib).initDSSI
	}
	initDSSI {
		programs = Set.new;
	}
	program {|bank, prog|
		this.engine.dssiMsg(this.path,"program",bank.asInteger,prog.asInteger)
	}
	prProgramAdd {|bank,program,name|
		var info = (bank:bank, program:program, name:name);
		if (programs.includes(info).not) {
			programs.add(info);
			this.changed(\programAdded, info)
		}
	}
}

IngenPatch : IngenNode {
	var <nodes, <patches, <poly, <enabled;
	var om;
	*new {|name,parent,engine|
		^super.new(name,parent).initPatch(engine);
	}
	initPatch {|argEngine|
		nodes = IdentityDictionary.new;
		patches = IdentityDictionary.new;
		om = argEngine
	}
	hasNode {|name|
		^nodes[name.asSymbol].notNil
	}
	getNode {|name|
		^nodes[name.asSymbol]
	}
	hasPatch {|name|
		^patches[name.asSymbol].notNil
	}
	getPatch {|name,mustExist=false|
		if (this.hasPatch(name).not) {
			if (mustExist) { ^nil };
			patches[name.asSymbol] = IngenPatch(name,this);
		};
		^patches[name.asSymbol]
	}
	engine {
		if (om.notNil) { ^om } { ^parent.engine };
	}
	connect {|fromPort, toPort|
		this.engine.connect(this.path++$/++fromPort, this.path++$/++toPort)
	}
	dumpX {
		(String.fill(this.depth,$ )++"*"+this.path).postln;
		nodes.do{|node|
			(String.fill(node.depth,$ )++"**"+node.path).postln;
			node.ports.do{|port|
				(String.fill(port.depth,$ )++"***"+port.path).postln };
		};
		ports.do{|port| (String.fill(port.depth,$ )++"***"+port.path).postln };
		patches.do(_.dump)
	}
	noteOn {|note,vel|
		var targetNode;
		this.nodes.do{|node|
			if (node.class == IngenInternalNode) {
				node.pluginlabel.switch(
					\trigger_in, {
						if (node.ports['Note Number'].value == note) {
							targetNode = node;
						}
					},
					\note_in, {
						targetNode = node;
					}
				)
			}
		};
		if (targetNode.notNil) {
			targetNode.noteOn(note, vel)
		} {
			Error("Unable to find trigger_in for note "++note).throw
		}
	}
	noteOff {|note|
		var targetNode;
		this.nodes.do{|node|
			if (node.class == IngenInternalNode) {
				node.pluginlabel.switch(
					\trigger_in, {
						if (node.ports['Note Number'].value == note) {
							targetNode = node;
						}
					},
					\note_in, {
						targetNode = node;
					}
				)
			}
		};
		if (targetNode.notNil) {
			targetNode.noteOff(note)
		} {
			Error("Unable to find node for note_off "++note).throw
		}
	}
	newPatch {|name, poly=1, handler|
		this.engine.createPatch(this.path++$/++name, poly, handler)
	}
	newNode {|name, poly=1, type, lib, label, fullname, handler|
		var candidates = this.engine.matchPlugins(label,lib,fullname,type);
		if (candidates.size == 1) {
			candidates.pop.new(this.path++"/"++name, poly, handler)
		} {
			if (candidates.size==0) {
				Error("Plugin not found").throw
			} {
				Error("Plugin info not unique").throw
			}
		}
	}

	// Setters for OSC responders
	prSetPoly {|argPoly| poly = argPoly }
	prSetEnabled {|flag|
		if (flag != enabled) {
			enabled = flag;
			this.changed(\enabled, flag)
		}
	}
}


IngenEmacsUI {
	var engine, window, bootBtn;
	*new {|engine| ^super.newCopyArgs(engine).init }
	init {
		window = EmacsBuffer("*Ingen -"+engine.addr.ip++$:++engine.addr.port);
		bootBtn = EmacsButton(window, ["Boot","Quit"], {|value|
			if (value==1) {
				engine.boot
			} {
				engine.quit
			}
		}).value=engine.registered.binaryValue;
		engine.addDependant(this);
		window.front;
	}
	update {|who, what  ... args|
		Emacs.message(who.asString+what+args);
		if (what == \newPatch or: {what == \newNode or: {what == \newPort}}) {
			args[0].addDependant(this)
		};
	}
}