__next_objid=1;
function objectId(obj) {
    if (obj==null) return null;
    if (obj.__obj_id==null) obj.__obj_id=__next_objid++;
    return obj.__obj_id;
}

start_time = undefined
function time() {
    return (new Date).getTime() - start_time;
}

// 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(scheduled_time, the_event) {
	this.scheduled_time = scheduled_time;
	this.the_event = the_event;
}

// EventQueue
function EventQueue() {
	this.event_list = new Array();
    this.sort_function = function(a, b) {
        return a.scheduled_time - b.scheduled_time
    }
}

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].scheduled_time;
	}
};

EventQueue.prototype.add = function(the_event) {
	this.event_list.push(the_event)
    this.event_list.sort(this.sort_function)
    return objectId(the_event)
};

EventQueue.prototype.remove = function(event_id) {
    this.event_list = this.event_list.filter(function(el) {objectId(el) != event_id}).sort()
}

EventQueue.prototype.pop = function() {
	return this.event_list.shift().the_event
};

// 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();
	this.regex = /^([a-zA-Z_]\w*)(?:\[(\d+)\])?$/;
}

ObjectManagerBase.prototype.addEvent = function(the_event, time_offset) {
	if (time_offset === undefined) time_offset = 0;
	this.events.add(new EventQueueEntry(this.controller.simulated_time + time_offset, the_event));
};

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

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

ObjectManagerBase.prototype.stepAll = function() {
	this.step();
	for (var i in this.instances) {
		if (!this.instances.hasOwnProperty(i)) continue;
        var instance = this.instances[i];
        if (instance.active && (instance.getEarliestEventTime() <= this.controller.simulated_time || instance.eventlessTransitions())) {
            instance.step();
        }
	}
};

ObjectManagerBase.prototype.step = function() {
	while (this.events.getEarliestTime() <= this.controller.simulated_time) {
		this.handleEvent(this.events.pop());
	}
};

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 === "disassociate_instance") {
		this.handleDisassociateEvent(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.length == 0) {
		throw new AssociationReferenceException("Empty association reference.");
	}
	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 = this.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();
	}
    source.addEvent(new Event("instance_started", undefined, [parameters[1]]))
};

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

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.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.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);
	}
    source.addEvent(new Event("instance_associated", undefined, [parameters[1], parameters[2]]))
};

ObjectManagerBase.prototype.handleDisassociateEvent = function(parameters) {
	if (parameters.length !== 2) {
		throw new ParameterException("The disassociate 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;
		// delete association from source instance
		var association_to_remove = instances[i].ref.associations[instances[i].assoc_name];
		if (instances[i].assoc_index === -1) {
			// empty instances object
			association_to_remove.instances = new Object();
		} else {
			// remove property from instances object
			delete association_to_remove.instances[instances[i].assoc_index];
		}
	}
	source.addEvent(new Event("instance_disassociated", undefined, [parameters[1]]));
};

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) {
    if (port === undefined) port = "";
    if (parameters === undefined) parameters = [];
	this.name = name;
	this.port = port;
	this.parameters = parameters;
}

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

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

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

// ControllerBase
function ControllerBase(object_manager) {
	this.object_manager = object_manager;
    
	this.private_port_counter = 0;
    
    // keep track of input ports
	this.input_ports = new Object();
	this.input_queue = new EventQueue();
    
    // keep track of output ports
	this.output_ports = new Array();
	this.output_listeners = new Array();

    this.simulated_time = null;
}

ControllerBase.prototype.getSimulatedTime = function() {
    return this.simulated_time
}

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

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

ControllerBase.prototype.broadcast = function(new_event, time_offset) {
    if (time_offset === undefined) time_offset = 0
	this.object_manager.broadcast(undefined, new_event, time_offset);
};

ControllerBase.prototype.start = function() {
    start_time = (new Date()).getTime();
    this.simulated_time = 0;
	this.object_manager.start();
};

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

ControllerBase.prototype.addInput = function(input_event_list, time_offset) {
    if (time_offset === undefined) time_offset = 0
	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(new EventQueueEntry((this.simulated_time === null ? 0 : time()) + time_offset, input_event_list[e]));
	}
};

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

ControllerBase.prototype.handleInput = function() {
	var event_time = this.input_queue.getEarliestTime();
	while (event_time <= this.simulated_time) {
		var e = this.input_queue.pop();
        input_port = this.input_ports[e.port];
        e.port = input_port.virtual_name
        var target_instance = input_port.instance;
        if (target_instance === undefined) {
            this.broadcast(e, event_time - this.simulated_time);
        } else {
            target_instance.addEvent(e, event_time - this.simulated_time);
        }
        event_time = this.input_queue.getEarliestTime();
	}
};

ControllerBase.prototype.outputEvent = function(the_event) {
	for (var l in this.output_listeners) {
		if (!this.output_listeners.hasOwnProperty(l)) continue;
		this.output_listeners[l].add(the_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);
};

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

GameLoopControllerBase.prototype = new ControllerBase();

GameLoopControllerBase.prototype.update = function(delta) {
	this.handleInput();
    earliest_event_time = this.getEarliestEventTime();
    if (earliest_event_time > time()) {
        this.simulated_time = earliest_event_time;
        this.object_manager.stepAll();
    }
};

// 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_print = 0.0;
}

EventLoop.prototype.schedule = function(f, wait_time, behind) {
    if (behind === undefined) behind = false
	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.scheduled_id = this.schedule_callback(f, wait_time, behind)
    }
};

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

// EventLoopControllerBase
function EventLoopControllerBase(object_manager, event_loop, finished_callback, behind_schedule_callback) {
	ControllerBase.call(this, object_manager);
	this.event_loop = event_loop;
	this.finished_callback = finished_callback;
    this.behind_schedule_callback = behind_schedule_callback;
	this.last_print_time = 0;
}

EventLoopControllerBase.prototype = new ControllerBase();

EventLoopControllerBase.prototype.addInput = function(input_event, time_offset) {
	ControllerBase.prototype.addInput.call(this, input_event, time_offset);
	this.event_loop.clear();
    this.simulated_time = ControllerBase.prototype.getEarliestEventTime.call(this);
    this.run();
};

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() {
    var start_time = time();
	while (true) {
		// clear existing timeout
		this.event_loop.clear();
		// simulate
		this.handleInput();
		this.object_manager.stepAll();
		// schedule next timeout
		var earliest_event_time = ControllerBase.prototype.getEarliestEventTime.call(this);
		if (earliest_event_time == Infinity) {
            if (this.finished_callback != undefined) this.finished_callback(); // TODO: This is not necessarily correct (keep_running necessary?)
            return;
		}
        var now = time();
        if (now - start_time > 10 || earliest_event_time - now > 0) {
            this.event_loop.schedule(this.run.bind(this), earliest_event_time - now, now - start_time > 10);
            if (now - earliest_event_time > 10 && now - this.last_print_time >= 1000) {
                console.log('running ' + (now - earliest_event_time) + ' ms behind schedule');
                this.last_print_time = now;
            }
            this.simulated_time = earliest_event_time;
            return;
        } else {
            this.simulated_time = earliest_event_time;
        }
	}
};

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

JsEventLoop.prototype = new EventLoop();

// 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;
};

// State
function State(state_id, name, obj) {
    this.state_id = state_id;
    this.name = name;
    this.obj = obj;
    
    this.ancestors = new Array();
    this.descendants = new Array();
    this.children = new Array();
    this.my_parent = null;
    this.enter = null;
    this.exit = null;
    this.default_state = null;
    this.transitions = new Array();
    this.my_history = new Array();
    this.has_eventless_transitions = false;
}

State.prototype.getEffectiveTargetStates = function() {
    var targets = [this];
    if (this.default_state != null) {
        Array.prototype.push.apply(targets, this.default_state.getEffectiveTargetStates());
    }
    return targets;
}

State.prototype.fixTree = function() {
    for (let c of this.children) {
        if (c instanceof HistoryState) {
            this.my_history.push(c);
        }
        c.my_parent = this;
        c.ancestors.push(this);
        Array.prototype.push.apply(c.ancestors, this.ancestors);
        c.fixTree();
    }
    Array.prototype.push.apply(this.descendants, this.children)
    for (let c of this.children) {
        Array.prototype.push.apply(this.descendants, c.descendants)
    }
}

State.prototype.addChild = function(child) {
    this.children.push(child);
}

State.prototype.addTransition = function(transition) {
    this.transitions.push(transition);
}

State.prototype.setEnter = function(enter) {
    this.enter = enter.bind(this.obj);
}

State.prototype.setExit = function(exit) {
    this.exit = exit.bind(this.obj);
}

// HistoryState
function HistoryState(state_id, name, obj) {
    State.call(this, state_id, name, obj);
}

HistoryState.prototype = new State();

// ShallowHistoryState
function ShallowHistoryState(state_id, name, obj) {
    HistoryState.call(this, state_id, name, obj);
}

ShallowHistoryState.prototype = new HistoryState();

ShallowHistoryState.prototype.getEffectiveTargetStates = function() {
    if (this.state_id in this.obj.history_values) {
        var targets = [];
        for (let hv of this.obj.history_values[this.state_id]) {
            Array.prototype.push.apply(targets, hv.getEffectiveTargetStates());
        }
        return targets;
    } else {
        // TODO: is it correct that in this case, the parent itself is also entered?
        return this.my_parent.getEffectiveTargetStates();
    }
}

// DeepHistoryState
function DeepHistoryState(state_id, name, obj) {
    HistoryState.call(this, state_id, name, obj);
}

DeepHistoryState.prototype = new HistoryState();

DeepHistoryState.prototype.getEffectiveTargetStates = function() {
    if (this.state_id in this.obj.history_values) {
        return this.obj.history_values[this.state_id];
    } else {
        // TODO: is it correct that in this case, the parent itself is also entered?
        return this.my_parent.getEffectiveTargetStates();
    }
}

// ParallelState
function ParallelState(state_id, obj) {
    State.call(this, state_id, obj);
}

ParallelState.prototype = new State();

ParallelState.prototype.getEffectiveTargetStates = function() {
    var targets = [this];
    for (let c of this.children) {
        if (!(c instanceof HistoryState)) {
            Array.prototype.push.apply(targets, c.getEffectiveTargetStates());
        }
    }
    return targets;
}

// Transition
function Transition(obj, source, targets) {
    this.guard = null;
    this.action = null;
    this.trigger = null;
    this.source = source;
    this.targets = targets;
    this.obj = obj;
    this.enabled_event = null; // the event that enabled this transition
    this.optimize();
}

Transition.prototype.isEnabled = function(events) {
    if (this.trigger === null) {
        this.enabled_event = null;
        return (this.guard === null || this.guard([]));
    } else {
        for (var i in events) {
            the_event = events[i];
            if ((this.trigger.name == the_event.name && (!this.trigger.port || this.trigger.port == the_event.port)) && (this.guard === null || this.guard(the_event.parameters))) {
                this.enabled_event = the_event;
                return true;
            }
        }
    }
}

Transition.prototype.fire = function() {
    // exit states...
    var targets = this.__getEffectiveTargetStates();
    var exit_set = this.__exitSet(targets);
    for (let s of exit_set) {
        for (let h of s.my_history) {
            var f = function(s0) {return s0.ancestors.length > 0 && s0.my_parent == s;}
            if (h instanceof DeepHistoryState) {
                f = function(s0) {return s0.descendants.length == 0 && s.descendants.indexOf(s0) >= 0;}
            }
            this.obj.history_values[h.state_id] = this.obj.configuration.filter(f);
        }
    }
    for (let s of exit_set) {
        if (s.exit != null) {
            s.exit();
        }
    }
    this.obj.configuration = this.obj.configuration.filter(function(el) {return exit_set.indexOf(el) < 0;})
    
    // combo state changed area
    this.obj.combo_step.changed.push(this.lca);
    for (let d of this.lca.descendants) {
        this.obj.combo_step.changed.push(d);
    }
    
    // execute transition action(s)
    if (this.action != null) {
        var params = [];
        if (this.enabled_event != null) params = this.enabled_event.parameters;
        this.action(params);
    }
    
    // enter states...
    for (let s of this.__enterSet(targets)) {
        this.obj.configuration.push(s);
        if (s.enter != null) {
            s.enter();
        }
    }
    
    this.obj.configuration = this.obj.configuration.sort(function(a, b) {return a.state_id - b.state_id})
    this.enabled_event = null;
}

Transition.prototype.__getEffectiveTargetStates = function() {
    var targets = []
    for (let target of this.targets) {
        for (let e_t of target.getEffectiveTargetStates()) {
            if (targets.indexOf(e_t) < 0) {
                targets.push(e_t);
            }
        }
    }
    return targets;
}

Transition.prototype.__exitSet = function(targets) {
    var exit_set = this.lca.descendants.slice(0).filter(function(obj) {
        return function(s) {
            return obj.obj.configuration.indexOf(s) >= 0;
        }
    }(this));
    exit_set.reverse();
    return exit_set;
}

Transition.prototype.__enterSet = function*(targets) {
    var target = targets[0];
    var reversed_ancestors = target.ancestors.slice(0).reverse();
    for (let a of reversed_ancestors) {
        if (this.source.ancestors.indexOf(a) >= 0) {
            continue;
        } else {
            yield a;
        }
    }
    for (let target of targets) {
        yield target;
    }
}

Transition.prototype.setGuard = function(guard) {
    this.guard = guard.bind(this.obj);
}

Transition.prototype.setAction = function(action) {
    this.action = action.bind(this.obj);
}

Transition.prototype.setTrigger = function(trigger) {
    this.trigger = trigger;
    if (this.trigger === null) {
        this.source.has_eventless_transitions = true;
    }
}

Transition.prototype.optimize = function() {
    // the least-common ancestor can be computed statically
    var target = this.targets[0];
    if (target.ancestors.indexOf(this.source) >= 0) {
        this.lca = this.source
    } else {
        this.lca = this.source.my_parent;
        if (this.source.my_parent != target.my_parent) { // external
            for (let a of this.source.ancestors) {
                if (target.ancestors.indexOf(a) >= 0) {
                    this.lca = a;
                    break;
                }
            }
        }
    }
}

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

	this.controller = controller;
	this.inports = new Object();
	this.timers = new Object();
    this.states = new Object();

	this.semantics = new DefaultStatechartSemantics();
}

RuntimeClassBase.prototype.eventlessTransitions = function() {
    for (let s of this.configuration) {
        if (s.has_eventless_transitions) return true;
    }
    return false;
}

RuntimeClassBase.prototype.start = function() {
    this.configuration = new Array();
    
	this.current_state = new Object();
	this.history_values = new Object();
	this.timers = new Object();
    this.timers_to_add = new Object();

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

	this.active = true;
	this.__set_stable(false);

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

RuntimeClassBase.prototype.sccd_yield = function() {
    return Math.max(0, (time() - this.controller.simulated_time) / 1000.0);
}

RuntimeClassBase.prototype.getSimulatedTime = function() {
    return this.controller.simulated_time;
}

RuntimeClassBase.prototype.updateConfiguration = function(states) {
    this.configuration = states.slice(0);
};

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

RuntimeClassBase.prototype.addTimer = function(index, timeout) {
    this.timers_to_add[index] = new EventQueueEntry(this.controller.simulated_time + Math.trunc(timeout * 1000), new Event("_" + index + "after"));
};

RuntimeClassBase.prototype.removeTimer = function(index) {
    if (index in this.timers_to_add) delete this.timers_to_add[index];
	this.events.remove(this.timers[index]);
    delete this.timers[index];
};

RuntimeClassBase.prototype.addEvent = function(event_list, time_offset) {
	if (time_offset == undefined) time_offset = 0;
    var event_time = this.controller.simulated_time + time_offset;
    if (event_time < this.earliest_event_time) {
        this.earliest_event_time = event_time;
    }
	if (!(event_list instanceof Array)) {
		event_list = [event_list];
	}
    for (i in event_list) {
        if (!event_list.hasOwnProperty(i)) continue;
        this.events.add(new EventQueueEntry(this.controller.simulated_time + time_offset, event_list[i]));
    }
};

RuntimeClassBase.prototype.getEarliestEventTime = function() {
	return this.earliest_event_time;
};

RuntimeClassBase.prototype.processBigStepOutput = function() {
	var o = this.big_step.output_events_port;
	var om = this.big_step.output_events_om;
	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.__set_stable = function(is_stable) {
    this.is_stable = is_stable;
    // self.earliest_event_time keeps track of the earliest time this instance will execute a transition
    if (!this.is_stable) {
        this.earliest_event_time = 0.0;
    } else if (!this.active) {
        this.earliest_event_time = Infinity;
    } else {
        this.earliest_event_time = this.events.getEarliestTime();
    }
}

RuntimeClassBase.prototype.step = function(delta) {
	var is_stable = false;
    while (!is_stable) {
        var due = [];
        if (this.events.getEarliestTime() <= this.controller.simulated_time) {
            due = [this.events.pop()];
        }
        is_stable = !this.bigStep(due);
        this.processBigStepOutput();
    }
    for (key in this.timers_to_add) {
        if (!this.timers_to_add.hasOwnProperty(key)) continue;
        this.timers[key] = this.events.add(this.timers_to_add[key]);
    }
	this.timers_to_add = new Object();
    this.__set_stable(true);
};

RuntimeClassBase.prototype.inState = function(state_strings) {
    var state_ids = [];
    for (let state_string of state_strings) {
        state_ids.push(this.states[state_string].state_id);
    }
    for (let state_id of state_ids) {
        var found = false;
        for (let s of this.configuration) {
            if (s.state_id == state_id) {
                found = true;
                break;
            }
        }
        if (!found) return false;
    }
    return true;
};

RuntimeClassBase.prototype.bigStep = function(input_events) {
	this.big_step.next(input_events);
	this.small_step.reset();
	this.combo_step.reset();
	while (this.comboStep()) {
		this.big_step.has_stepped = true;
		if (this.semantics.big_step_maximality === StatechartSemantics.TakeOne)
			break;
	}
	return this.big_step.has_stepped;
};

RuntimeClassBase.prototype.comboStep = function() {
	this.combo_step.next();
	while (this.smallStep()) {
		this.combo_step.has_stepped = true;
	}
	return this.combo_step.has_stepped;
};

// generate transition candidates for current small step
RuntimeClassBase.prototype.generateCandidates = function() {
    var enabledEvents = this.getEnabledEvents();
    var enabledTransitions = new Array();
    for (let s of this.configuration) {
        if (this.combo_step.changed.indexOf(s) < 0) {
            for (let t of s.transitions) {
                if (t.isEnabled(enabledEvents)) {
                    enabledTransitions.push(t);
                }
            }
        }
    }
    return enabledTransitions
};

RuntimeClassBase.prototype.smallStep = function() {
    var __younger_than = 
        function(x, y) {
            if (y.source.ancestors.indexOf(x.source) >= 0) {
                return 1;
            } else if (x.source.ancestors.indexOf(y.source) >= 0) {
                return -1;
            } else {
                throw new Exception("These items have no relation with each other.");
            }
        };
    
	if (this.small_step.has_stepped) {
		this.small_step.next();
	}
	var candidates = this.generateCandidates();
	if (candidates.length > 0) {
        to_skip = new Set();
        conflicting = new Array();
        for (var i in candidates) {
            c1 = candidates[i];
            if (!to_skip.has(c1)) {
                conflict = [c1];
                for (var j in candidates.slice(candidates.indexOf(c1))) {
                    c2 = candidates[j];
                    if (c1.source.ancestors.indexOf(c2.source) >= 0 || c2.source.ancestors.indexOf(c1.source) >= 0) {
                        conflict.push(c2);
                        to_skip.add(c2);
                    }
                }
                conflicting.push(conflict.sort(__younger_than));
            }
        }
        if (this.semantics.concurrency === StatechartSemantics.Single) {
            var candidate = conflicting[0];
            if (this.semantics.priority === StatechartSemantics.SourceParent) {
                candidate[candidate.length - 1].fire();
            } else {
                candidate[0].fire();
            }
        } else if (this.semantics.concurrency === StatechartSemantics.Many) {
            // TODO: Implement
        }
        this.small_step.has_stepped = true;
    }
	return this.small_step.has_stepped;
};

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

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

RuntimeClassBase.prototype.initializeStatechart = function() {
	this.updateConfiguration(this.default_targets);
    for (let state of this.default_targets) {
        if (state.enter != null) {
            state.enter();
        }
    }
};

// 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.outputEvent = function(event) {
	this.output_events_port.push(event);
};

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

// 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.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);
};

// 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.addCandidate = function(t, p) {
	this.candidates.push({transition: t, parameters: p});
};

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

