// Exception
function RuntimeException(msg) {
	this.msg = msg;
}

// InputException
function InputException(msg) {
	RuntimeException.call(this, msg);
}

InputException.prototype = new RuntimeException();

// AssociationException
function AssociationException(msg) {
	RuntimeException.call(this, msg);
}

AssociationException.prototype = new RuntimeException();

// AssociationReferenceException
function AssociationReferenceException(msg) {
	RuntimeException.call(this, msg);
}

AssociationReferenceException.prototype = new RuntimeException();

// ParameterException
function ParameterException(msg) {
	RuntimeException.call(this, msg);
}

ParameterException.prototype = new RuntimeException();

// InputException
function InputException(msg) {
	RuntimeException.call(this, msg);
}

InputException.prototype = new RuntimeException();

// EventQueueEntry
function EventQueueEntry(event, time_offset) {
	this.event = event;
	this.time_offset = time_offset;
}

EventQueueEntry.prototype.decreaseTime = function(offset) {
	this.time_offset -= offset;
};

// EventQueue
function EventQueue() {
	this.event_list = new Array();
}

EventQueue.prototype.add = function(event, time_offset) {
	var entry = new EventQueueEntry(event, time_offset);
	var insert_index = 0;
	var index = this.event_list.length - 1;
	while (index >= 0) {
		if (this.event_list[index].time_offset <= time_offset) {
			insert_index = index + 1;
			break;
		}
		index -= 1;
	}
	this.event_list.splice(insert_index, 0, entry);
};

EventQueue.prototype.decreaseTime = function(offset) {
	for (var event in this.event_list) {
		if (!this.event_list.hasOwnProperty(event)) continue;
		this.event_list[event].decreaseTime(offset);
	}
};

EventQueue.prototype.isEmpty = function() {
	return this.event_list.length === 0;
};

EventQueue.prototype.getEarliestTime = function() {
	if (this.isEmpty()) {
		return Infinity;
	} else {
		return this.event_list[0].time_offset;
	}
};

EventQueue.prototype.popDueEvents = function() {
	if (this.isEmpty() || this.event_list[0].time_offset > 0.0) {
		return new Array();
	}
	var index = 0;
	while (index < this.event_list.length &&
			this.event_list[index].time_offset <= 0.0)
	{
		index++;
	}
	return this.event_list.splice(0, index);
};

// Association
function Association(to_class, min_card, max_card) {
	this.to_class = to_class;
	this.min_card = min_card;
	this.max_card = max_card;
	this.instances = new Object(); /* maps index (as string) to instance */
	this.size = 0;
	this.next_id = 0;
}

Association.prototype.allowedToAdd = function() {
	return (this.max_card === -1 || this.size < this.max_card);
};

Association.prototype.addInstance = function(instance) {
	if (this.allowedToAdd()) {
		var id = this.next_id++;
		this.instances[id] = instance;
		return id;
	} else {
		throw new AssociationException("Not allowed to add the instance to the association.");
	}
};

Association.prototype.getInstance = function(index) {
	var instance = this.instances[index];
	if (instance === undefined) {
		throw new AssociationException("Invalid index for fetching instance(s) from association.");
	}
	return instance;
};

// ObjectManagerBase
function ObjectManagerBase(controller) {
	this.controller = controller;
	this.events = new EventQueue();
	this.instances = new Array();
}

ObjectManagerBase.prototype.addEvent = function(event, time_offset) {
	if (!time_offset) time_offset = 0.0;
	this.events.add(event, time_offset);
};

ObjectManagerBase.prototype.broadcast = function(new_event) {
	for (var i in this.instances) {
		if (!this.instances.hasOwnProperty(i)) continue;
		this.instances[i].addEvent(new_event);
	}
};

ObjectManagerBase.prototype.getWaitTime = function() {
	var smallest_time = this.events.getEarliestTime();
	for (var i in this.instances) {
		if (!this.instances.hasOwnProperty(i)) continue;
		smallest_time = Math.min(smallest_time, this.instances[i].getEarliestEventTime());
	}
	return smallest_time;
};

ObjectManagerBase.prototype.stepAll = function(delta) {
	this.step(delta);
	for (var i in this.instances) {
		if (!this.instances.hasOwnProperty(i)) continue;
		this.instances[i].step(delta);
	}
};

ObjectManagerBase.prototype.step = function(delta) {
	this.events.decreaseTime(delta);
	var due = this.events.popDueEvents();
	for (var e in due) {
		this.handleEvent(due[e].event);
	}
};

ObjectManagerBase.prototype.start = function() {
	for (var i in this.instances) {
		if (!this.instances.hasOwnProperty(i)) continue;
		this.instances[i].start();
	}
};

ObjectManagerBase.prototype.handleEvent = function(e) {
	if (e.name === "narrow_cast") {
		this.handleNarrowCastEvent(e.parameters);
	} else if (e.name === "broad_cast") {
		this.handleBroadcastEvent(e.parameters);
	} else if (e.name === "create_instance") {
		this.handleCreateEvent(e.parameters);
	} else if (e.name === "associate_instance") {
		this.handleAssociateEvent(e.parameters);
	} else if (e.name === "start_instance") {
		this.handleStartInstanceEvent(e.parameters);
	} else if (e.name === "delete_instance") {
		this.handleDeleteInstanceEvent(e.parameters);
	}
};

ObjectManagerBase.prototype.processAssociationReference = function(input_string) {
	//if (input_string === "") {
		//throw new AssociationReferenceException("Empty association reference.");
	//}
	var regex = /^([a-zA-Z_]\w*)(?:\[(\d+)\])?$/;
	var path_string = input_string.split('/');
	var result = new Array();
	if (input_string !== "") {
		for (var p in path_string) {
			if (!path_string.hasOwnProperty(p)) continue;
			var m = regex.exec(path_string[p]);
			if (m) {
				var name = m[1];
				var index = m[2];
				if (!index) {
					index = -1;
				}
				result.push({name:name,index:index});
			} else {
				throw new AssociationReferenceException("Invalid entry in association reference.");
			}
		}
	}
	return result;
};

ObjectManagerBase.prototype.handleStartInstanceEvent = function(parameters) {
	if (parameters.length !== 2) {
		throw new ParameterException("The start instance event needs 2 parameters.");
	}
	var source = parameters[0];
	var traversal_list = this.processAssociationReference(parameters[1]);
	var instances = this.getInstances(source, traversal_list);
	for (var i in instances) {
		if (!instances.hasOwnProperty(i)) continue;
		instances[i].instance.start();
	}
};

ObjectManagerBase.prototype.handleDeleteInstanceEvent = function(parameters) {
	if (parameters.length !== 2) {
		throw new ParameterException("The delete instance event needs 2 parameters.");
	}
	var source = parameters[0];
	var traversal_list = this.processAssociationReference(parameters[1]);
	var instances = this.getInstances(source, traversal_list);
	for (var i in instances) {
		if (!instances.hasOwnProperty(i)) continue;
		instances[i].instance.stop();
		if (instances[i].instance.destructor)
			instances[i].instance.destructor();
		// delete association from source instance
		var association_to_remove = instances[i].ref.associations[instances[i].assoc_name];
		if (instances[i].assoc_index === -1) {
			/*for (var x in association_to_remove.instances) {
				if (!association_to_remove.instances.hasOwnProperty(x)) continue;
				association_to_remove.instances = new Object();
				//association_to_remove.instances[x] = null;
			}*/
			// empty instances object
			association_to_remove.instances = new Object();
			//association_to_remove.instances = new Array();
		} else {
			//association_to_remove.instances[instances[i].assoc_index] = null;
			// remove property from instances object
			delete association_to_remove.instances[instances[i].assoc_index];
		}
		// also remove instance from OM's list of instances
		index = this.instances.indexOf(instances[i].instance);
		this.instances.splice(index,1);
        
        source.addEvent(new Event("instance_deleted", undefined, [parameters[1]]));
	}
};

ObjectManagerBase.prototype.handleBroadcastEvent = function(parameters) {
	if (parameters.length !== 1) {
		throw new ParameterException("The broadcast event needs 1 parameter.");
	}
	this.broadcast(parameters[0]);
};

ObjectManagerBase.prototype.handleCreateEvent = function(parameters) {
	if (parameters.length < 2) {
		throw new ParameterException("The create event needs at least 2 parameters.");
	}
	var source = parameters[0];
	var association_name = parameters[1];
	var association = source.associations[association_name];
	if (!association) {
		throw new ParameterException("No such association: " + association_name);
	}
	if (association.allowedToAdd()) {
		// allow subclasses to be instantiated
		if (parameters.length === 2) {
			var class_name = association.to_class;
			var creation_parameters = [];
		} else /* 3 or more parameters*/ {
			// 3rd parameter is class name
			var class_name = parameters[2];
			// parameters after 3rd parameter are creation parameters
			var creation_parameters = parameters.slice(3);
		}
		var new_instance = this.createInstance(class_name, creation_parameters);
		if (new_instance === undefined) {
			throw new ParameterException("Creating instance: no such class: " + class_name);
		}
		var index = association.addInstance(new_instance);
		// add parent association to created instance
		// if a parent association is defined in the class diagram
		var parent_association = new_instance.associations["parent"];
		if (parent_association !== undefined) {
			parent_association.addInstance(source);
		}
		// TODO: maybe change order of Event constructor parameters such that we don't have to
		//       explicitly set the port to 'undefined'?
		source.addEvent(new Event("instance_created", undefined, [association_name+"["+index+"]"]));
	} else {
		source.addEvent(new Event("instance_creation_error", undefined, [association_name]));
	}
};

ObjectManagerBase.prototype.handleAssociateEvent = function(parameters) {
	if (parameters.length !== 3) {
		throw new ParameterException("The associate_instance event needs 3 parameters.");
	}
	var source = parameters[0];
	var source_list = parameters[1];
	var traversal_list = this.processAssociationReference(source_list);
	var to_copy_list = this.getInstances(source, traversal_list);
	if (to_copy_list.length !== 1) {
		throw new AssociationReferenceException("Invalid source association reference.");
	}
	var wrapped_to_copy_instance = to_copy_list[0].instance;
	var dest_list = this.processAssociationReference(parameters[2]);
	if (dest_list.length === 0) {
		throw new AssociationReferenceException("Invalid destination association reference.");
	}
	var last = dest_list.pop();
	if (last.index !== -1) {
		throw new AssociationReferenceException("Last association name in association reference could not be accompanied by an index.");
	}
	var instances = this.getInstances(source, dest_list);
	for (var i in instances) {
		if (!instances.hasOwnProperty(i)) continue;
		instances[i].instance.associations[last.name].addInstance(wrapped_to_copy_instance);
	}
};

ObjectManagerBase.prototype.handleNarrowCastEvent = function(parameters) {
	if (parameters.length !== 3) {
		throw new ParameterException("The narrow_cast event needs 3 parameters.");
	}
	var source = parameters[0];
	var traversal_list = this.processAssociationReference(parameters[1]);
	var cast_event = parameters[2];
	var instances = this.getInstances(source, traversal_list);
	for (var i in instances) {
		if (!instances.hasOwnProperty(i)) continue;
		instances[i].instance.addEvent(cast_event);
	}
};

ObjectManagerBase.prototype.getInstances = function(source, traversal_list) {
	var currents = [{
		instance : source,
		ref : null,
		assoc_name : null,
		assoc_index : null
	}];
	for (var t in traversal_list) {
		if (!traversal_list.hasOwnProperty(t)) continue;
		var name = traversal_list[t].name;
		var index = traversal_list[t].index;
		nexts = new Array();
		for (var c in currents) {
			if (!currents.hasOwnProperty(c)) continue;
			var association = currents[c].instance.associations[name];
			if (index >= 0) {
				nexts.push({
					instance : association.getInstance(index),
					ref : currents[c].instance,
					assoc_name : name,
					assoc_index : index
				});
			} else if (index === -1) {
				for (var i in association.instances) {
					if (!association.instances.hasOwnProperty(i)) continue;
					nexts.push({
						instance: association.instances[i],
						ref: currents[c].instance,
						assoc_name : name,
						assoc_index : index
					});
				}
				//nexts = nexts.concat(association.instances);
			} else {
				throw new AssociationReferenceException("Incorrect index in association reference.");
			}
		}
		currents = nexts;
	}
	return currents;
};

ObjectManagerBase.prototype.instantiate = function(to_class, construct_params) {
	// pure virtual
};

ObjectManagerBase.prototype.createInstance = function(to_class, construct_params) {
	var instance = this.instantiate(to_class, construct_params);
	this.instances.push(instance);
	return instance;
};

// Event
function Event(name, port, parameters) {
	this.name = name;
	this.port = port;
	this.parameters = parameters;
}

// ControllerBase
function ControllerBase(object_manager, keep_running, finished_callback) {
	this.object_manager = object_manager;
	this.keep_running = keep_running;
	this.finished_callback = finished_callback;
	this.input_ports = new Object(); /* maps port name to pair of (private name, instance) */
	this.private_port_counter = 0;
	this.input_queue = new EventQueue();
	this.output_ports = new Array();
	this.output_listeners = new Array();
}

ControllerBase.prototype.addInputPort = function(virtual_name, instance) {
	if (instance === undefined) {
		var port_name = virtual_name; // "public" port
	} else {
		var port_name = "private_" + (this.private_port_counter++) + /*"_" + instance.class_name +*/ "_" + virtual_name;
	}
	this.input_ports[port_name] = {
		virtual_name: virtual_name,
		instance: instance
	};
	return port_name;
};

ControllerBase.prototype.removeInputPort = function(name) {
	delete this.input_ports[name];
};

ControllerBase.prototype.addOutputPort = function(port_name) {
	this.output_ports.push(port_name);
};

ControllerBase.prototype.broadcast = function(new_event) {
	this.object_manager.broadcast(new_event);
};

ControllerBase.prototype.start = function() {
	this.object_manager.start();
};

ControllerBase.prototype.stop = function() {
};

ControllerBase.prototype.addInput = function(input_event, time_offset) {
	if (input_event.name === "") {
		throw new InputException("Input event can't have an empty name.");
	}
	var input_port = this.input_ports[input_event.port];
	if (input_port === undefined) {
		throw new InputException("Input port mismatch.");
	}
	this.input_queue.add(input_event, time_offset);
};

ControllerBase.prototype.outputEvent = function(event) {
	for (var l in this.output_listeners) {
		if (!this.output_listeners.hasOwnProperty(l)) continue;
		this.output_listeners[l].add(event);
	}
};

ControllerBase.prototype.addOutputListener = function(ports) {
	var listener = new OutputListener(ports);
	this.output_listeners.push(listener);
	return listener;
};

ControllerBase.prototype.addMyOwnOutputListener = function(listener) {
	this.output_listeners.push(listener);
};

ControllerBase.prototype.addEventList = function(event_list) {
	for (var e in event_list) {
		if (!event_list.hasOwnProperty(e)) continue;
		var entry = event_list[e];
		this.addInput(entry.event, entry.time_offset);
	}
};

// GameLoopControllerBase
function GameLoopControllerBase(object_manager, keep_running, finished_callback) {
	ControllerBase.call(this, object_manager, keep_running, finished_callback);
}

GameLoopControllerBase.prototype = new ControllerBase();

GameLoopControllerBase.prototype.update = function(delta) {
	this.input_queue.decreaseTime(delta);
	var due = this.input_queue.popDueEvents();
	for (var e in due) {
		if (!due.hasOwnProperty(e)) continue;
		this.broadcast(due[e].event);
	}
	this.object_manager.stepAll(delta);
};

function TimeoutId(id, delay) {
	this.id = id;
	this.delay = delay;
}

// JsEventLoopControllerBase
function JsEventLoopControllerBase(object_manager, keep_running, finished_callback) {
	ControllerBase.call(this, object_manager, keep_running, finished_callback);
	this.running = false;
	this.next_timeout = null;
	this.last_simulation_time = null;
}

JsEventLoopControllerBase.prototype = new ControllerBase();

JsEventLoopControllerBase.prototype.handleInput = function(delta) {
	this.input_queue.decreaseTime(delta);
	var due = this.input_queue.popDueEvents();
	for (var e in due) {
		if (!due.hasOwnProperty(e)) continue;
		var input_port = this.input_ports[due[e].event.port];
		// rename input port
		due[e].event.port = input_port.virtual_name;
		var target_instance = input_port.instance;
		if (target_instance === undefined) {
			this.broadcast(due[e].event);
		} else {
			target_instance.addEvent(due[e].event);
		}
	}
};

JsEventLoopControllerBase.prototype.addInput = function(input_event, time_offset) {
	if (this.last_simulation_time && this.next_timeout) {
		var waited = (new Date).getTime() - this.last_simulation_time;
		var remaining = this.next_timeout.delay - waited;
	} else {
		var waited = 0.0;
		var remaining = 0.0;
	}
	var interleave = time_offset < remaining;
	if (this.next_timeout) {
		var additional_offset = waited;
	} else {
		var additional_offset = 0.0;
	}
	ControllerBase.prototype.addInput.call(this, input_event, time_offset + additional_offset);
	if (this.running && (interleave || !this.next_timeout)) {
		this.run(); // adjust timeout
	}
};

JsEventLoopControllerBase.prototype.start = function() {
	ControllerBase.prototype.start.call(this);
	this.running = true;
	this.run();
};

JsEventLoopControllerBase.prototype.stop = function() {
	this.run(); // update timeouts
	if (this.next_timeout) {
		window.clearTimeout(this.next_timeout.id);
	}
	this.running = false;
	ControllerBase.prototype.stop.call(this);
};

JsEventLoopControllerBase.prototype.getWaitTime = function() {
	var wait_time = Math.min(this.object_manager.getWaitTime(), this.input_queue.getEarliestTime());
	return wait_time;
};

JsEventLoopControllerBase.prototype.run = function() {
		// clear previous timeout
		if (this.next_timeout) {
			window.clearTimeout(this.next_timeout.id);
			this.next_timeout = null;
		}
		// calculate last time since simulation
		if (this.last_simulation_time) {
			var simulation_duration = (new Date).getTime() - this.last_simulation_time;
		} else {
			var simulation_duration = 0.0;
		}
		// simulate
		this.handleInput(simulation_duration);
		this.object_manager.stepAll(simulation_duration);
		// keep time
		this.last_simulation_time = (new Date).getTime();
		// set next timeout
		var wait_time = this.getWaitTime();
		if (wait_time !== Infinity) {
			var actual_wait_time = wait_time - ((new Date).getTime() - this.last_simulation_time);
			if (actual_wait_time < 0.0)
				actual_wait_time = 0.0;
			// wait actual_wait_time
			//console.log("waiting " + actual_wait_time + " ms");
			this.next_timeout = new TimeoutId(window.setTimeout(this.run.bind(this), actual_wait_time), actual_wait_time);
		} else {
			// wait forever
			//console.log("waiting forever");
			this.last_simulation_time = null;
			if (this.finished_callback) {
				this.finished_callback();
			}
		}
};

// OutputListener
function OutputListener(port_names) {
	this.port_names = port_names;
	this.queue = new Array(); // TODO: optimize!
}

OutputListener.prototype.add = function(event) {
	if (this.port_names.length === 0
		|| this.port_names.indexOf(event.port) !== -1)
	{
		this.queue.push(event);
	}
};

OutputListener.prototype.fetch = function(timeout) {
	return this.queue.shift();
};

// RuntimeClassBase
function RuntimeClassBase() {
	this.active = false;
	this.state_changed = false;
	this.events = new EventQueue();
	this.timers = null;
}

RuntimeClassBase.prototype.addEvent = function(event, time_offset) {
	if (!time_offset) time_offset = 0.0;
	this.events.add(event, time_offset);
};

RuntimeClassBase.prototype.getEarliestEventTime = function() {
	if (this.timers) {
		var minimum = Infinity;
		for (var t in this.timers) {
			if (!this.timers.hasOwnProperty(t)) continue;
			minimum = Math.min(minimum, this.timers[t]);
		}
		return Math.min(this.events.getEarliestTime(), minimum);
	}
	return this.events.getEarliestTime();
};


RuntimeClassBase.prototype.step = function(delta) {
	if (!this.active) {
		return;
	}
	this.events.decreaseTime(delta);
	if (this.timers) {
		var next_timers = new Object();
		for (var t in this.timers) {
			if (!this.timers.hasOwnProperty(t)) continue;
			var time_left = this.timers[t] - delta;
			if (time_left <= 0.0) {
				this.addEvent(new Event("_" + t + "after"), time_left);
			} else {
				next_timers[t] = time_left;
			}
		}
		this.timers = next_timers;
	}

	this.microstep();
	while (this.state_changed) {
		this.microstep();
	}
};

RuntimeClassBase.prototype.microstep = function() {
	var due = this.events.popDueEvents();
	if (due.length === 0) {
		this.transition();
	} else {
		for (var e in due) {
			if (!due.hasOwnProperty(e)) continue;
			this.transition(due[e].event);
		}
	}
};

RuntimeClassBase.prototype.transition = function(event) {
	// pure virtual
};

RuntimeClassBase.prototype.start = function() {
	this.active = true;
};

RuntimeClassBase.prototype.stop = function() {
	this.active = false;
};
