
	/*
	author:
	  mikolalysenko (https://www.npmjs.org/~mikolalysenko)

	license:
	  MIT

	source:
	  https://github.com/mikolalysenko/double-bits
	  https://github.com/mikolalysenko/nextafter
	*/

	(function(){

	var hasTypedArrays = false;
	if(typeof Float64Array !== "undefined") {
	  var DOUBLE_VIEW = new Float64Array(1);
	  var UINT_VIEW   = new Uint32Array(DOUBLE_VIEW.buffer);
	  DOUBLE_VIEW[0] = 1.0;
	  hasTypedArrays = true;
	  if(UINT_VIEW[1] === 0x3ff00000) {
		//Use little endian
		var pack = function(lo, hi) {
		  UINT_VIEW[0] = lo;
		  UINT_VIEW[1] = hi;
		  return DOUBLE_VIEW[0];
		};
		var lo = function(n) {
		  DOUBLE_VIEW[0] = n;
		  return UINT_VIEW[0];
		};
		var hi = function(n) {
		  DOUBLE_VIEW[0] = n;
		  return UINT_VIEW[1];
		};
	  } else if(UINT_VIEW[0] === 0x3ff00000) {
		//Use big endian
		var pack = function(lo, hi) {
		  UINT_VIEW[1] = lo;
		  UINT_VIEW[0] = hi;
		  return DOUBLE_VIEW[0];
		};
		var lo = function(n) {
		  DOUBLE_VIEW[0] = n;
		  return UINT_VIEW[1];
		};
		var hi = function(n) {
		  DOUBLE_VIEW[0] = n;
		  return UINT_VIEW[0];
		};
	  } else {
		hasTypedArrays = false;
	  }
	}
	if(!hasTypedArrays) {
	  var buffer = new Buffer(8);
	  var pack = function(lo, hi) {
		buffer.writeUInt32LE(lo, 0, true);
		buffer.writeUInt32LE(hi, 4, true);
		return buffer.readDoubleLE(0, true);
	  };
	  var lo = function(n) {
		buffer.writeDoubleLE(n, 0, true);
		return buffer.readUInt32LE(0, true);
	  };
	  var hi = function(n) {
		buffer.writeDoubleLE(n, 0, true);
		return buffer.readUInt32LE(4, true);
	  };
	}

	var SMALLEST_DENORM = Math.pow(2, -1074);
	var UINT_MAX = (-1)>>>0;

	nextafter = function(x, y) {
	  if(isNaN(x) || isNaN(y)) {
		return NaN;
	  }
	  if(x === y) {
		return x;
	  }
	  if(x === 0) {
		if(y < 0) {
		  return -SMALLEST_DENORM;
		} else {
		  return SMALLEST_DENORM;
		}
	  }
	  var h = hi(x);
	  var l = lo(x);
	  if((y > x) === (x > 0)) {
		if(l === UINT_MAX) {
		  h += 1;
		  l = 0;
		} else {
		  l += 1;
		}
	  } else {
		if(l === 0) {
		  l = UINT_MAX;
		  h -= 1;
		} else {
		  l -= 1;
		}
	  }
	  return pack(l, h);
	}

	})();


function time() {
	return (new Date).getTime()/1000.0;
}

// 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_list, time_offset) {
	this.event_list = event_list;
	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_list, time_offset) {
	var entry = new EventQueueEntry(event_list, 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() {
	var result = new Array();
	if (this.isEmpty() || this.event_list[0].time_offset > 0.0) {
		return result;
	}
	var index = 0;
	while (index < this.event_list.length && this.event_list[index].time_offset <= 0.0) {
		result.push(this.event_list[index].event_list);
		index++;
	}
	this.event_list.splice(0, index);
	return result;
};

// 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.allowedToRemove = function() {
	return (this.min_card === -1 || this.size > this.min_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.removeInstance = function(instance) {
	if (this.allowedToRemove()) {
		delete this.instances[this.instances_to_ids[instance]]
	} else {
		throw new AssociationException("Not allowed to remove 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]);
	}
};

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();
		instances[i].instance.user_defined_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) {
	this.object_manager = object_manager;
	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_list, time_offset) {
	if (!(input_event_list instanceof Array))
		input_event_list = [input_event_list];
	for (var e in input_event_list) {
		if (!input_event_list.hasOwnProperty(e)) continue;
		if (input_event_list[e].name === "") {
			throw new InputException("Input event can't have an empty name.");
		}
		var input_port = this.input_ports[input_event_list[e].port];
		if (input_port === undefined) {
			throw new InputException("Input port mismatch, no such port: " + input_event_list[e].port + ".");
		}
	}
	this.input_queue.add(input_event_list, time_offset);
};

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

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

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) {
	ControllerBase.call(this, object_manager);
}

GameLoopControllerBase.prototype = new ControllerBase();

GameLoopControllerBase.prototype.update = function(delta) {
	this.handleInput(delta);
	this.object_manager.stepAll(delta);
};

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

// EventLoop

// parameters:
//  schedule - a callback scheduling another callback in the event loop
//      this callback should take 2 parameters: (callback, timeout) and return an ID
//  clear - a callback that clears a scheduled callback
//      this callback should take an ID that was returned by 'schedule'
function EventLoop(schedule, clear) {
	this.schedule_callback = schedule;
	this.clear_callback = clear;
	this.scheduled_id = null;
	this.last_time = null;
	this.next_wakeup = null;
	this.last_print = 0.0;
}

EventLoop.prototype.getScheduledTimeout = function() {
	if (this.last_time && this.next_wakeup) {
		return this.next_wakeup - this.last_time;
	} else {
		return Infinity;
	}
};

EventLoop.prototype.schedule = function(f, wait_time) {
	if (this.scheduled_id) {
		// if the following error occurs, it is probably due to a flaw in the logic of EventLoopControllerBase
		throw new RuntimeException("EventLoop class intended to maintain at most 1 scheduled callback.");
	}
	if (wait_time === Infinity) {
		this.last_time = null;
		this.next_wakeup = null;
	} else {
		var now = time();
		if (this.last_time === null) {
			this.last_time = now;
		}
		this.next_wakeup = this.last_time + wait_time;
		if (this.next_wakeup - this.last_time < wait_time) { // compensate floating-point imprecision
			this.next_wakeup = nextafter(this.next_wakeup, Infinity);
		}
		var remaining = Math.max(this.next_wakeup - now, 0.0);
		
		this.scheduled_id = this.schedule_callback(f, remaining*1000.0);
	}
};

EventLoop.prototype.clear = function() {
	if (this.scheduled_id) {
		this.clear_callback(this.scheduled_id);
		this.scheduled_id = null;
	}
};

EventLoop.prototype.nextDelta = function() {
	var now = time();
	if (this.next_wakeup) {
		var simulated_now = this.next_wakeup;
	} else {
		var simulated_now = now;
	}
	if (now - this.last_print > 1.0) {
		var behind_schedule = now - simulated_now;
		if (behind_schedule > 0.1) {
			console.log("Warning: running " + behind_schedule*1000.0 + " ms behind schedule");
			this.last_print = now
		}
	}
	if (this.last_time) {
		var delta = simulated_now - this.last_time;
	} else {
		var delta = 0.0;
	}
	this.last_time = simulated_now;
	this.next_wakeup = null;
	return delta;
};

EventLoop.prototype.elapsed = function() {
	if (this.last_time) {
		return time() - this.last_time;
	} else {
		return 0.0;
	}
};

// JsEventLoop
function JsEventLoop() {
	EventLoop.call(this, window.setTimeout.bind(window), window.clearTimeout.bind(window));
}

JsEventLoop.prototype = new EventLoop();

// EventLoopControllerBase
function EventLoopControllerBase(object_manager, event_loop, finished_callback) {
	ControllerBase.call(this, object_manager);
	this.event_loop = event_loop;
	this.finished_callback = finished_callback;
	this.last_simulation_time = null;
	this.behind_schedule = 0.0; // If we cannot process events in time, this value will become the negative amount of seconds that we are 'behind schedule'. If this value is nonzero, then in the next 'run' we will act as if we are 'in the past', to make sure newly scheduled events still have the right timeout in wallclock time. This will make models run behind schedule if there is not enough CPU time available, but we will catch up soon as more CPU time becomes available.
}

EventLoopControllerBase.prototype = new ControllerBase();

EventLoopControllerBase.prototype.addInput = function(input_event, time_offset) {
	var elapsed = this.event_loop.elapsed();
	var controller_timeout = time_offset + elapsed;
	ControllerBase.prototype.addInput.call(this, input_event, controller_timeout);
	if (controller_timeout < this.event_loop.getScheduledTimeout()) {
		// added event's timeout is sooner than existing timeout -> re-schedule
		this.event_loop.clear();
		this.event_loop.schedule(this.run.bind(this), controller_timeout);
	}
};

EventLoopControllerBase.prototype.start = function() {
	ControllerBase.prototype.start.call(this);
	this.run();
};

EventLoopControllerBase.prototype.stop = function() {
	this.event_loop.clear();
	ControllerBase.prototype.stop.call(this);
};

EventLoopControllerBase.prototype.run = function() {
	// clear existing timeout
	this.event_loop.clear();
	// calculate last time since simulation
	var delta = this.event_loop.nextDelta();
	// simulate
	this.handleInput(delta);
	this.object_manager.stepAll(delta);
	// set next timeout
	var wait_time = this.getWaitTime();
	this.event_loop.schedule(this.run.bind(this), wait_time);
	if (wait_time === Infinity) {
		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();
};

// Enum-like construct holding statechart semantic options.
StatechartSemantics = {
	// Big Step Maximality
	TakeOne : 0,
	TakeMany : 1,
	// Concurrency
	Single : 0,
	Many : 1, // not yet implemented
	// Preemption (unsupported)
	NonPreemptive : 0,
	Preemptive : 1,
	// Internal Event Lifeline
	Queue : 0,
	NextSmallStep : 1,
	NextComboStep : 2,
	// Input Event Lifeline
	Whole : 0,
	FirstSmallStep : 1,
	FirstComboStep : 2,
	// Priority
	SourceParent : 0,
	SourceChild : 1
};

var DefaultStatechartSemantics = function() {
	this.big_step_maximality = this.TakeMany;
	this.concurrency = this.Single
	this.internal_event_lifeline = this.Queue;
	this.input_event_lifeline = this.FirstComboStep;
	this.priority = this.SourceParent;
};

// RuntimeClassBase
function RuntimeClassBase(controller) {
	this.active = false;
	this.is_stable = true;
	this.events = new EventQueue();

	this.controller = controller;

	this.timers = null;
	this.inports = new Object();

	this.semantics = new DefaultStatechartSemantics();
}

RuntimeClassBase.prototype.start = function() {
	this.current_state = new Object();
	this.history_state = new Object();
	this.timers = new Object();

	this.big_step = new BigStepState();
	this.combo_step = new ComboStepState();
	this.small_step = new SmallStepState();

	this.active = true;
	this.is_stable = false;

	this.initializeStatechart();
	this.processBigStepOutput();
};

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

RuntimeClassBase.prototype.addEvent = function(event_list, time_offset) {
	if (!time_offset) time_offset = 0.0;
	if (!(event_list instanceof Array)) {
		event_list = [event_list];
	}
	this.events.add(event_list, time_offset);
};

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

RuntimeClassBase.prototype.processBigStepOutput = function() {
	var o = this.big_step.getOutputEvents();
	var om = this.big_step.getOutputEventsOM();
	for (var e in o) {
		if (!o.hasOwnProperty(e)) continue;
		this.controller.outputEvent(o[e]);
	}
	for (var e in om) {
		if (!om.hasOwnProperty(e)) continue;
		this.controller.object_manager.addEvent(om[e]);
	}
};

RuntimeClassBase.prototype.step = function(delta) {
	if (!this.active) {
		return;
	}

	// decrease event queue time
	this.events.decreaseTime(delta);

	// decrease timers time
	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;

	// execute big step(s)
	var due = this.events.popDueEvents();
	if (due.length === 0 && !this.is_stable) {
		due = [[]]; // object was not stable after last big step -> execute 1 big step with no input events
	}
	for (var e in due) {
		if (!due.hasOwnProperty(e)) continue;
		this.is_stable = !this.bigStep(due[e]);
		this.processBigStepOutput();
	}
};

RuntimeClassBase.prototype.inState = function(nodes) {
	for (var c in this.current_state) {
		if (!this.current_state.hasOwnProperty(c)) continue;
		var new_nodes = new Array();
		for (var n in nodes) {
			if (!nodes.hasOwnProperty(n)) continue;
			if (this.current_state[c].indexOf(nodes[n]) === -1) {
				new_nodes.push(nodes[n]);
			}
		}
		nodes = new_nodes;
		if (nodes.length === 0) {
			return true;
		}
	}
	return false;
};

RuntimeClassBase.prototype.bigStep = function(input_events) {
	//console.log("new big step");
	//console.log("input events = " + JSON.stringify(input_events));
	this.big_step.next(input_events);
	this.small_step.reset();
	this.combo_step.reset();
	while (this.comboStep()) {
		this.big_step.setStepped();
		if (this.semantics.big_step_maximality === StatechartSemantics.TakeOne)
			break;
	}
	return this.big_step.hasStepped();
};

RuntimeClassBase.prototype.comboStep = function() {
	//console.log("new combo step");
	this.combo_step.next();
	while (this.smallStep()) {
		this.combo_step.setStepped();
		if (++this.big_step.transitions > 1000) {
			console.log("maximum number of big step transitions exceded");
			return false;
		}
	}
	return this.combo_step.hasStepped();
};

RuntimeClassBase.prototype.smallStep = function() {
	if (this.small_step.hasStepped()) {
		this.small_step.next();
	}
	this.generateCandidates();
	if (this.small_step.hasCandidates()) {
		//console.log("new small step");
		if (this.semantics.concurrency === StatechartSemantics.Single) {
			var cand = this.small_step.getCandidates()[0];
			cand.transition.call(this, cand.parameters);
		} else if (this.semantics.concurrency === StatechartSemantics.Many) {
			// TODO: implement
		}
		this.small_step.setStepped();
	}
	return this.small_step.hasStepped();
};

RuntimeClassBase.prototype.getEnabledEvents = function() {
	var result = this.small_step.getCurrentEvents().concat(this.combo_step.getCurrentEvents());
	if (this.semantics.input_event_lifeline === StatechartSemantics.Whole ||
		(!this.big_step.hasStepped() &&
			(this.semantics.input_event_lifeline === StatechartSemantics.FirstComboStep ||
				(!this.combo_step.hasStepped() &&
					this.semantics.input_event_lifeline === StatechartSemantics.FirstSmallStep)))) {
		result = result.concat(this.big_step.getInputEvents());
	}
	return result;
};

RuntimeClassBase.prototype.raiseInternalEvent = function(event) {
	if (this.semantics.internal_event_lifeline === StatechartSemantics.NextSmallStep) {
		this.small_step.addNextEvent(event);
	} else if (this.semantics.internal_event_lifeline === StatechartSemantics.NextComboStep) {
		this.combo_step.addNextEvent(event);
	} else if (this.semantics.internal_event_lifeline === StatechartSemantics.Queue) {
		this.events.add([event], 0.0);
	}
};

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

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

// BigStepState
var BigStepState = function() {
	this.input_events = new Array();
	this.output_events_port = new Array();
	this.output_events_om = new Array();
	this.has_stepped = true;
	this.transitions = 0;
};

BigStepState.prototype.next = function(input_events) {
	this.input_events = input_events;
	this.output_events_port = new Array();
	this.output_events_om = new Array();
	this.has_stepped = false;
	this.transitions = 0;
};

BigStepState.prototype.getInputEvents = function() {
	return this.input_events;
};

BigStepState.prototype.getOutputEvents = function() {
	return this.output_events_port;
};

BigStepState.prototype.getOutputEventsOM = function() {
	return this.output_events_om;
};

BigStepState.prototype.outputEvent = function(event) {
	this.output_events_port.push(event);
};

BigStepState.prototype.outputEventOM = function(event) {
	this.output_events_om.push(event);
};

BigStepState.prototype.setStepped = function() {
	this.has_stepped = true;
};

BigStepState.prototype.hasStepped = function() {
	return this.has_stepped;
};

// ComboStepState
var ComboStepState = function() {
	this.current_events = new Array();
	this.next_events = new Array();
	this.changed = new Array();
	this.has_stepped = true;
};

ComboStepState.prototype.reset = function() {
	this.current_events = new Array();
	this.next_events = new Array();
};

ComboStepState.prototype.next = function() {
	this.current_events = this.next_events;
	this.next_events = new Array();
	this.changed = new Array();
	this.has_stepped = false;
};

ComboStepState.prototype.addNextEvent = function(event) {
	this.next_events.push(event);
};

ComboStepState.prototype.getCurrentEvents = function(event) {
	return this.current_events;
};

ComboStepState.prototype.setArenaChanged = function(arena) {
	this.changed.push(arena);
};

ComboStepState.prototype.isArenaChanged = function(arena) {
	return (this.changed.indexOf(arena) !== -1);
};

ComboStepState.prototype.isStable = function() {
	return (this.changed.length === 0);
};

ComboStepState.prototype.setStepped = function() {
	this.has_stepped = true;
};

ComboStepState.prototype.hasStepped = function() {
	return this.has_stepped;
};

// SmallStepState
var SmallStepState = function() {
	this.current_events = new Array();
	this.next_events = new Array();
	this.candidates = new Array();
	this.has_stepped = true;
};

SmallStepState.prototype.reset = function() {
	this.current_events = new Array();
	this.next_events = new Array();
};

SmallStepState.prototype.next = function() {
	this.current_events = this.next_events;
	this.next_events = new Array();
	this.candidates = new Array();
	this.has_stepped = false;
};

SmallStepState.prototype.addNextEvent = function(event) {
	this.next_events.push(event);
};

SmallStepState.prototype.getCurrentEvents = function() {
	return this.current_events;
};

SmallStepState.prototype.addCandidate = function(t, p) {
	this.candidates.push({transition: t, parameters: p});
};

SmallStepState.prototype.getCandidates = function() {
	return this.candidates;
};

SmallStepState.prototype.hasCandidates = function() {
	return (this.candidates.length > 0);
};

SmallStepState.prototype.setStepped = function() {
	this.has_stepped = true;
};

SmallStepState.prototype.hasStepped = function() {
	return this.has_stepped;
};

