include "primitives.alh"
include "object_operations.alh"
include "modelling.alh"
include "random.alh"
include "conformance_scd.alh"
include "model_management.alh"
include "library.alh"

Element function make_matching_schedule(schedule_model : Element, LHS : String, ignore : Element):
	Element schedule
	Element workset
	Element all_elements
	Element full_all_elements
	Integer required_size
	String new_element
	Integer counter
	String next
	Element tmp
	String elem_id
	Element scheduled

	Element reverse
	reverse = make_reverse_dictionary(schedule_model["model"])

	// Initialize
	schedule = list_create()
	scheduled = set_create()
	workset = set_create()
	all_elements = allAssociationDestinations(schedule_model, LHS, "LHS_contains")
	full_all_elements = set_copy(all_elements)
	required_size = set_len(all_elements)

	// Need to keep adding to the schedule
	while (list_len(schedule) < required_size):

		// workset is empty, but we still need to add to the list
		// Therefore, we pick a random, unbound node, and add it to the workset
		new_element = set_pop(all_elements)
		while (bool_or(set_in(scheduled, new_element), is_edge(schedule_model["model"][new_element]))):
			// Element is not usable, so pick another one
			new_element = set_pop(all_elements)
		set_add(workset, new_element)

		// Handle the workset
		while (set_len(workset) > 0):
			// Still elements in the workset, so pop from these first
			next = set_pop(workset)

			// Check if element might not be already used somewhere
			if (bool_not(set_in(scheduled, next))):
				if (set_in(full_all_elements, next)):
					if (bool_not(set_in(ignore, read_attribute(schedule_model, next, "label")))):
						list_insert(schedule, next, 0)
						set_add(scheduled, next)
					else:
						required_size = required_size - 1
						continue!

					// If it is an edge, we should also add the target and source
					if (is_edge(schedule_model["model"][next])):
						// Add the target/source to the schedule
						set_add(workset, reverse[cast_id(read_edge_src(schedule_model["model"][next]))])
						set_add(workset, reverse[cast_id(read_edge_dst(schedule_model["model"][next]))])

					// Also add all outgoing links
					counter = read_nr_out(schedule_model["model"][next])
					while (counter > 0):
						counter = counter - 1
						elem_id = cast_id(read_out(schedule_model["model"][next], counter))
						if (dict_in(reverse, elem_id)):
							set_add(workset, reverse[elem_id])

					// And incoming links
					counter = read_nr_in(schedule_model["model"][next])
					while (counter > 0):
						counter = counter - 1
						elem_id = cast_id(read_in(schedule_model["model"][next], counter))
						if (dict_in(reverse, elem_id)):
							set_add(workset, reverse[elem_id])

	return schedule!

Element function get_possible_bindings(host_model : Element, schedule_model : Element, current_element : String, map : Element):
	Element options
	String src_label
	String dst_label
	String typename
	String original_typename
	Boolean guaranteed_instance

	options = set_create()

	typename = read_type(schedule_model, current_element)
	original_typename = string_substr(typename, 4, string_len(typename))
	guaranteed_instance = False

	// Is a node, so find whether or not there are some connections that are already resolved
	Element ic
	Element oc
	String poll
	String value

	oc = allOutgoingAssociationInstances(schedule_model, current_element, "PreElement")
	while (bool_and(set_len(oc) > 0, set_len(options) < 2)):
		poll = set_pop(oc)
		if (dict_in(map, read_attribute(schedule_model, poll, "label"))):
			// This incoming link is already defined, so we just have one option: the destination of the link we matched
			value = readAssociationSource(host_model, map[read_attribute(schedule_model, poll, "label")])
			if (bool_not(set_in(options, value))):
				set_add(options, value)

	if (set_len(options) < 2):
		ic = allIncomingAssociationInstances(schedule_model, current_element, "PreElement")
		while (bool_and(set_len(ic) > 0, set_len(options) < 2)):
			poll = set_pop(ic)
			if (dict_in(map, read_attribute(schedule_model, poll, "label"))):
				// This incoming link is already defined, so we just have one option: the destination of the link we matched
				value = readAssociationDestination(host_model, map[read_attribute(schedule_model, poll, "label")])
				if (bool_not(set_in(options, value))):
					set_add(options, value)
		
	if (set_len(options) == 0):
		// Is a node and no connections, so we just pick all options
		options = allInstances(host_model, original_typename)
		guaranteed_instance = True

	elif (set_len(options) > 1):
		// Multiple "only" options, which will not work out: no options!
		return set_create()!
	
	if (is_edge(schedule_model["model"][current_element])):
		Boolean src_in
		Boolean dst_in
		// Is an edge, so check for already bound source/target
		src_label = read_attribute(schedule_model, readAssociationSource(schedule_model, current_element), "label")
		dst_label = read_attribute(schedule_model, readAssociationDestination(schedule_model, current_element), "label")
		src_in = set_in(dict_keys(map), src_label)
		dst_in = set_in(dict_keys(map), dst_label)

		guaranteed_instance = True

		if (bool_and(src_in, dst_in)):
			// Source and destination are bound
			if (set_len(options) > 0):
				options = set_overlap(options, allOutgoingAssociationInstances(host_model, map[src_label], original_typename))
			if (set_len(options) > 0):
				options = set_overlap(options, allIncomingAssociationInstances(host_model, map[dst_label], original_typename))

		elif (src_in):
			// Source is bound
			options = set_overlap(options, allOutgoingAssociationInstances(host_model, map[src_label], original_typename))

		elif (dst_in):
			// Destination is bound
			options = set_overlap(options, allIncomingAssociationInstances(host_model, map[dst_label], original_typename))

		else:
			// Neither is bound, so just get all of them
			options = options

	// Filter options further
	Element filtered_options
	String option

	filtered_options = set_create()
	Element bound
	bound = dict_values(map)
	while (set_len(options) > 0):
		option = set_pop(options)

		// Check for detecting same element twice
		if (bool_not(set_in(bound, option))):
			// Option is already present with another label, so skip this!

			if (dict_in(host_model["model"], option)):
				// Check if it conforms to the desired type
				if (bool_not(guaranteed_instance)):
					if (bool_not(is_nominal_instance(host_model, option, original_typename))):
						// Not an actual instance, so skip!
						continue!

				// Check for local (matching) constraints of element
				if (element_eq(read_attribute(schedule_model, current_element, "constraint"), read_root())):
					// No local constraints, so all is well
					set_add(filtered_options, option)
				else:
					// Check local constraints and add only if positive
					Element constraint_function
					Boolean result
					Element func

					constraint_function = read_attribute(schedule_model, current_element, "constraint")
					func = get_func_AL_model(import_node(constraint_function))
					result = func(host_model, option)

					if (result):
						set_add(filtered_options, option)
			else:
				log("ERROR in found option: not in model")

	Element attributes
	String attribute
	Element func
	Boolean result
	Element attributes_copy

	options = filtered_options
	filtered_options = set_create()

	// Check whether all attributes have a satisfied condition
	attributes_copy = dict_keys(getAttributeList(schedule_model, current_element))

	while (set_len(options) > 0):
		option = set_pop(options)
		attributes = set_copy(attributes_copy)

		result = True
		while (set_len(attributes) > 0):
			attribute = set_pop(attributes)
			if (bool_not(string_startswith(attribute, "constraint_"))):
				continue!

			value = read_attribute(schedule_model, current_element, attribute)
			// Attribute might be undefined, so skip if it is
			if (element_neq(value, read_root())):
				func = get_func_AL_model(import_node(value))
				result = func(read_attribute(host_model, option, string_substr(attribute, string_len("constraint_"), string_len(attribute) + 1)))
			else:
				result = True

			if (bool_not(result)):
				break!

		// Check value of last result, which will be True if all passed, or False otherwise
		if (result):
			set_add(filtered_options, option)

	options = filtered_options
	return options!

Element function full_match(host_model : Element, schedule_model : Element, current : String, single_ok : Boolean):
	Element NACs
	Element NACs_backup
	String LHS
	String NAC
	Element mappings
	Element mapping
	Element result
	Element final_mappings
	Boolean allowed

	final_mappings = set_create()

	// First match the LHS part itself to get initial mappings
	LHS = set_pop(allAssociationDestinations(schedule_model, current, "LHSLink"))
	mappings = match(host_model, schedule_model, LHS, dict_create())

	// Got a list of all possible mappings, now filter based on NACs
	NACs_backup = allAssociationDestinations(schedule_model, current, "NACLink")

	// For each possible mapping, we check all NACs!
	while (set_len(mappings) > 0):
		mapping = set_pop(mappings)
		allowed = True
		NACs = set_copy(NACs_backup)
		while (set_len(NACs) > 0):
			NAC = set_pop(NACs)
			result = match(host_model, schedule_model, NAC, mapping)
			if (set_len(result) > 0):
				// NAC could be matched, and therefore is not allowed
				allowed = False
				break!
		
		if (allowed):
			set_add_node(final_mappings, mapping)

			if (single_ok):
				break!

	return final_mappings!

Element function match(host_model : Element, schedule_model : Element, LHS : String, initial_mapping : Element):
	// Match the schedule_model to the host_model, returning all possible mappings from schedule_model elements to host_model elements

	// Make the schedule first
	Element schedule
	schedule = make_matching_schedule(schedule_model, LHS, dict_keys(initial_mapping))

	// Now follow the schedule, incrementally building all mappings
	Element mappings
	Element new_mappings
	Element new_map
	String current_element
	Element map
	String option
	Element options

	mappings = set_create()
	set_add_node(mappings, initial_mapping)
	while (list_len(schedule) > 0):
		current_element = list_pop(schedule, list_len(schedule) - 1)
		//log("Binding element with label " + cast_value(read_attribute(schedule_model, current_element, "label")))
		new_mappings = dict_create()

		while (set_len(mappings) > 0):
			map = set_pop(mappings)
			options = get_possible_bindings(host_model, schedule_model, current_element, map)

			while (set_len(options) > 0):
				option = set_pop(options)
				new_map = dict_copy(map)
				dict_add_fast(new_map, read_attribute(schedule_model, current_element, "label"), option)
				set_add_node(new_mappings, new_map)

		mappings = new_mappings
		//log("Remaining options: " + cast_value(set_len(mappings)))

		if (set_len(mappings) == 0):
			// Stop because we have no more options remaining!
			return set_create()!

	// Finished, so try the global constraint
	String constraint
	Element func
	Boolean result

	new_mappings = dict_create()
	constraint = read_attribute(schedule_model, LHS, "constraint")
	if (element_neq(constraint, read_root())):
		while (set_len(mappings) > 0):
			map = set_pop(mappings)
			func = get_func_AL_model(import_node(constraint))
			result = func(host_model, map)

			if (result):
				set_add_node(new_mappings, map)
		mappings = new_mappings

	return mappings!

Void function rewrite(host_model : Element, schedule_model : Element, RHS : String, mapping : Element):
	// Rewrite the host model based on the mapping combined with the RHS
	Element LHS_labels
	Element RHS_labels
	Element RHS_elements
	Element remaining
	String elem
	String label
	Element labels_to_remove
	Element labels_to_add
	String typename
	String original_typename
	String src
	String dst
	Element new_mapping
	String new_name
	Element RHS_map
	String tmp
	String value
	String action
	Element original_RHS_labels

	Element reverse
	reverse = make_reverse_dictionary(schedule_model["model"])

	LHS_labels = dict_keys(mapping)
	RHS_labels = set_create()
	RHS_map = dict_create()

	RHS_elements = allAssociationDestinations(schedule_model, RHS, "RHS_contains")
	while (set_len(RHS_elements) > 0):
		tmp = set_pop(RHS_elements)
		label = read_attribute(schedule_model, tmp, "label")
		set_add(RHS_labels, label)
		dict_add_fast(RHS_map, label, tmp)

	remaining = set_overlap(LHS_labels, RHS_labels)
	original_RHS_labels = set_copy(RHS_labels)
	while (set_len(remaining) > 0):
		elem = set_pop(remaining)
		set_remove(LHS_labels, elem)
		set_remove(RHS_labels, elem)

	labels_to_remove = LHS_labels
	labels_to_add = set_to_list(RHS_labels)

	new_mapping = dict_copy(mapping)

	while (list_len(labels_to_add) > 0):
		// Add the elements linked to these labels
		label = list_pop(labels_to_add, list_len(labels_to_add) - 1)
		if (is_edge(schedule_model["model"][RHS_map[label]])):
			// Edge
			src = read_attribute(schedule_model, reverse[cast_id(read_edge_src(schedule_model["model"][RHS_map[label]]))], "label")
			dst = read_attribute(schedule_model, reverse[cast_id(read_edge_dst(schedule_model["model"][RHS_map[label]]))], "label")

			// First check whether both source and destination are already created
			if (bool_and(dict_in(new_mapping, src), dict_in(new_mapping, dst))):
				// Both are present, so we can make the link
				typename = read_type(schedule_model, RHS_map[label])
				original_typename = string_substr(typename, 5, string_len(typename))
				new_name = instantiate_link(host_model, original_typename, "", new_mapping[src], new_mapping[dst])
				dict_add_fast(new_mapping, label, new_name)
			else:
				// Delay this a bit, until all are bound
				list_insert(labels_to_add, label, 0)
		else:
			// Node
			// Create the node and add it
			typename = read_type(schedule_model, RHS_map[label])
			original_typename = string_substr(typename, 5, string_len(typename))
			new_name = instantiate_node(host_model, original_typename, "")
			dict_add_fast(new_mapping, label, new_name)

	Element attributes
	String attribute
	Element result
	Element func

	while (set_len(original_RHS_labels) > 0):
		// Perform actions
		label = set_pop(original_RHS_labels)

		// Do all attribute actions that are defined
		attributes = dict_keys(getAttributeList(schedule_model, RHS_map[label]))
		while (set_len(attributes) > 0):
			attribute = set_pop(attributes)
			if (bool_not(string_startswith(attribute, "value_"))):
				continue!

			value = read_attribute(schedule_model, RHS_map[label], attribute)
			if (element_neq(value, read_root())):
				func = get_func_AL_model(import_node(value))
				result = func(host_model, new_mapping[label], mapping)

				if (element_eq(result, read_root())):
					unset_attribute(host_model, new_mapping[label], string_substr(attribute, string_len("value_"), string_len(attribute) + 1))
				elif (has_value(result)):
					// New value defined, so assign!
					instantiate_attribute(host_model, new_mapping[label], string_substr(attribute, string_len("value_"), string_len(attribute) + 1), result)
				elif (is_error(result)):
					log("Error in evaluation of attribute " + attribute)
					log("On element with label " + label)
				else:
					// Try to interpret as code
					instantiate_attribute_code(host_model, new_mapping[label], string_substr(attribute, string_len("value_"), string_len(attribute) + 1), result)

		// Do the global action of each element
		action = read_attribute(schedule_model, RHS_map[label], "action")
		if (element_neq(action, read_root())):
			Element func
			func = get_func_AL_model(import_node(action))
			func(host_model, new_mapping[label], mapping)

	while (set_len(labels_to_remove) > 0):
		// Remove the elements linked to these labels
		label = set_pop(labels_to_remove)
		model_delete_element(host_model, mapping[label])
		dict_delete(new_mapping, label)

	// Execute global action (whatever it may be)
	action = read_attribute(schedule_model, RHS, "action")
	if (element_neq(action, read_root())):
		Element func
		func = get_func_AL_model(import_node(action))
		func(host_model, new_mapping)

	return!

Element function transform(host_model : Element, schedule_model : Element):
	// Find initial model
	Element all_composites
	String composite
	String current
	Element merged
	Element original_mm

	// Now start transforming for real
	all_composites = allInstances(schedule_model, "Composite")
	if (set_len(all_composites) == 1):
		// Only one, so it is easy
		current = set_pop(all_composites)
	else:
		// Filter out those that are themselves contained
		while (set_len(all_composites) > 0):
			composite = set_pop(all_composites)
			if (set_len(allIncomingAssociationInstances(schedule_model, composite, "Contains")) == 0):
				// Isn't contained in any, so this is the root model!
				current = composite

	if (transform_composite(host_model, schedule_model, current)):
		// Success, so return True if it is in-place, or the new model if it is out-place
		return True!
	else:
		return False!

Boolean function transform_composite(host_model : Element, schedule_model : Element, composite : String):
	String current
	String typename
	Boolean result

	current = set_pop(allAssociationDestinations(schedule_model, composite, "Initial"))
	while (is_nominal_instance(schedule_model, current, "Rule")):
		// Still a rule that we must execute
		typename = read_type(schedule_model, current)
		if (typename == "Atomic"):
			result = transform_atomic(host_model, schedule_model, current)
		elif (typename == "Query"):
			result = transform_query(host_model, schedule_model, current)
		elif (typename == "Composite"):
			result = transform_composite(host_model, schedule_model, current)
		elif (typename == "ForAll"):
			result = transform_forall(host_model, schedule_model, current)

		Element result_set
		if (result):
			result_set = allAssociationDestinations(schedule_model, current, "OnSuccess")
		else:
			result_set = allAssociationDestinations(schedule_model, current, "OnFailure")

		if (set_len(result_set) == 0):
			log("ERROR: no next rule found for execution result " + cast_string(result))
		elif (set_len(result_set) > 1):
			log("WARNING: multiple next rules found for execution result " + cast_string(result))
			log("Picking one at random...")
		current = set_pop(result_set)

	// No longer a rule, so it is either success or failure
	if (is_nominal_instance(schedule_model, current, "Success")):
		return True!
	else:
		return False!

Boolean function transform_atomic(host_model : Element, schedule_model : Element, current : String):
	// Execute the atomic transformation
	Element mappings
	Element mapping
	mappings = full_match(host_model, schedule_model, current, True)

	if (set_len(mappings) > 0):
		// Pick one!
		mapping = random_choice(set_to_list(mappings))
		String RHS
		RHS = set_pop(allAssociationDestinations(schedule_model, current, "RHSLink"))
		rewrite(host_model, schedule_model, RHS, mapping)
		return True!
	else:
		return False!

Boolean function transform_forall(host_model : Element, schedule_model : Element, current : String):
	// Execute the atomic transformation
	Element mappings
	String RHS
	Element mapping
	Boolean result

	//log("Executing rule: " + current)

	mappings = full_match(host_model, schedule_model, current, False)

	if (set_len(mappings) > 0):
		result = True
	else:
		result = False

	//log("Matches in forall: " + cast_string(set_len(mappings)))
	while (set_len(mappings) > 0):
		mapping = set_pop(mappings)
		RHS = set_pop(allAssociationDestinations(schedule_model, current, "RHSLink"))
		rewrite(host_model, schedule_model, RHS, mapping)

	return result!

Boolean function transform_query(host_model : Element, schedule_model : Element, current : String):
	// Execute the transformation
	Element mappings
	Element mapping
	mappings = full_match(host_model, schedule_model, current, True)

	if (set_len(mappings) > 0):
		return True!
	else:
		return False!
