include "primitives.alh"
include "io.alh"
include "object_operations.alh"
include "constructors.alh"
include "metamodels.alh"
include "library.alh"
include "modelling.alh"
include "utils.alh"

Element function model_fuse(models : Element):
	Element new_model
	Element tagged_model
	String model_name
	Element model
	Element keys
	Element second_keys
	String key
	Element selected_MM
	String type
	Element reverse
	Integer prev_length

	// Read out some data first
	if (set_len(models) == 0):
		log("Cannot fuse empty set of models!")
		return read_root()!

	tagged_model = set_read(models)
	new_model = instantiate_model(tagged_model[1]["metamodel"])

	// Do the iteration
	while (set_len(models) > 0):
		tagged_model = set_pop(models)
		model_name = string_join(list_read(tagged_model, 0), "/")
		model = list_read(tagged_model, 1)

		reverse = make_reverse_dictionary(model["model"])

		// Add all elements from 'model', but prepend it with the 'model_name'
		keys = dict_keys(model["model"])
		second_keys = set_create()
		prev_length = -1

		while (set_len(keys) > 0):
			key = set_pop(keys)
			type = read_type(model, key)

			if (is_edge(model["model"][key])):
				String src
				String dst
				src = string_join(model_name, reverse[cast_id(read_edge_src(model["model"][key]))])
				dst = string_join(model_name, reverse[cast_id(read_edge_dst(model["model"][key]))])
				if (bool_and(dict_in(new_model["model"], src), dict_in(new_model["model"], dst))):
					instantiate_link(new_model, type, model_name + key, src, dst)
				else:
					set_add(second_keys, key)
			elif (has_value(model["model"][key])):
				instantiate_value(new_model, type, model_name + key, model["model"][key])
			else:
				instantiate_node(new_model, type, model_name + key)

			if (set_len(keys) == 0):
				if (set_len(second_keys) == prev_length):
					log("Cannot terminate model_fuse due to infinite recursion!")
					return read_root()!

				keys = second_keys
				second_keys = set_create()
				prev_length = set_len(keys)

	return new_model!

Element function model_copy(src_model : Element):
	Element dst_model
	Element name
	Element keys
	Element second_keys
	String type
	Integer prev_length
	Element reverse

	reverse = make_reverse_dictionary(src_model["model"])

	dst_model = instantiate_model(src_model["metamodel"])
	keys = dict_keys(src_model["model"])
	second_keys = set_create()
	prev_length = -1

	while (set_len(keys) > 0):
		name = set_pop(keys)

		if (is_edge(src_model["model"][name])):
			// Is an edge, so potentially queue it
			String src
			String dst

			src = reverse[cast_id(read_edge_src(src_model["model"][name]))]
			dst = reverse[cast_id(read_edge_dst(src_model["model"][name]))]

			type = read_type(src_model, name)

			if (bool_and(dict_in(dst_model["model"], src), dict_in(dst_model["model"], dst))):
				// All present, so create the link between them
				instantiate_link(dst_model, type, name, src, dst)
			else:
				set_add(second_keys, name)

		elif (has_value(src_model["model"][name])):
			// Has a value, so copy that as well
			type = read_type(src_model, name)
			instantiate_value(dst_model, type, name, src_model["model"][name])

		else:
			// Is a node
			type = read_type(src_model, name)
			instantiate_node(dst_model, type, name)

		if (set_len(keys) == 0):
			if (set_len(second_keys) == prev_length):
				log("Cannot terminate model_copy due to infinite recursion!")
				return read_root()!

			keys = second_keys
			second_keys = set_create()
			prev_length = set_len(keys)

	return dst_model!

Element function model_join(models : Element, metamodel : Element, tracability_model : Element):
	Element new_model
	Element tagged_model
	String retyping_key
	Element elem_map
	Element model
	Element keys
	String new_name
	Element second_keys
	String src
	String dst
	String key
	String type
	Integer prev_length

	new_model = instantiate_model(metamodel)
	elem_map = dict_create()

	Element retyping_keys
	retyping_keys = set_create()

	// Do the iteration
	while (set_len(models) > 0):
		tagged_model = set_pop(models)
		retyping_key = string_join(list_read(tagged_model, 0), "/")
		model = list_read(tagged_model, 1)
		set_add(retyping_keys, retyping_key)

		// Add all elements from 'model'
		keys = dict_keys(model["model"])
		second_keys = set_create()
		prev_length = -1

		while (set_len(keys) > 0):
			// Copy the new element
			key = set_pop(keys)
			type = read_type(model, key)

			new_name = retyping_key + key

			if (is_edge(model["model"][key])):
				src = cast_id(read_edge_src(model["model"][key]))
				dst = cast_id(read_edge_dst(model["model"][key]))
				if (bool_and(dict_in(elem_map, src), dict_in(elem_map, dst))):
					new_name = instantiate_link(new_model, retyping_key + type, new_name, elem_map[src], elem_map[dst])
				else:
					set_add(second_keys, key)
					new_name = ""
			elif (has_value(model["model"][key])):
				new_name = instantiate_value(new_model, retyping_key + type, new_name, model["model"][key])
			else:
				new_name = instantiate_node(new_model, retyping_key + type, new_name)

			if (new_name != ""):
				// Add the new name to a map which registers the mapping to the new name
				dict_add_fast(elem_map, cast_id(model["model"][key]), new_name)

			if (set_len(keys) == 0):
				if (set_len(second_keys) == prev_length):
					log("Cannot terminate model_join due to infinite recursion!")
					return read_root()!

				keys = second_keys
				second_keys = set_create()
				prev_length = set_len(keys)

	// Now link in the tracability model
	// Go over all TracabilityLink elements and connect them in the merged model as well

	if (element_neq(tracability_model, read_root())):
		Element tracability_links
		String tracability_link
		String new_name_src
		String new_name_dst
		String src_name
		String dst_name
		Element src_models
		String src_model
		Element dst_models
		String dst_model

		tracability_links = allInstances(tracability_model, "TracabilityLink")

		while (set_len(tracability_links) > 0):
			tracability_link = set_pop(tracability_links)

			src_name = read_attribute(tracability_model, readAssociationSource(tracability_model, tracability_link), "name")
			dst_name = read_attribute(tracability_model, readAssociationDestination(tracability_model, tracability_link), "name")

			type = read_attribute(tracability_model, tracability_link, "type")

			// Now try to find all possible combinations
			// that is, find all models for which src_name exists, and all models for which dst_name exists, and connect them
			src_models = set_copy(retyping_keys)
			while (set_len(src_models) > 0):
				src_model = set_pop(src_models)

				if (dict_in(new_model["model"], src_model + src_name)):
					// This element exists for this source model
					dst_models = set_copy(retyping_keys)

					while (set_len(dst_models) > 0):
						dst_model = set_pop(dst_models)

						if (dict_in(new_model["model"], dst_model + dst_name)):
							// Found a match for source and target, so create a link
							new_name = instantiate_link(new_model, type, "", src_model + src_name, dst_model + dst_name)

							if (new_name == ""):
								log("ERROR: could not create a tracability link; ignoring")
	return new_model!
	
Element function model_split(merged_model : Element, models : Element, tracability : Boolean):
	if (set_len(models) == 0):
		// No results needed, so stop immediately unless tracability is True
		if (bool_not(tracability)):
			return dict_create()!

	Element result
	Element metamodel
	Element model_tuple
	Element tracability_model
	Element keys
	String key
	String src
	String dst
	Element reverse
	Element mapping
	String retyping_key
	String original_type
	Element elem
	Element second_keys
	String type
	Element splitted
	String source
	String target
	String link
	String src_name
	String dst_name
	String new_name
	Integer prev_length

	result = dict_create()
	tracability_model = instantiate_model(import_node("models/Tracability"))
	reverse = make_reverse_dictionary(merged_model["model"])
	mapping = dict_create()
	second_keys = set_create()
	prev_length = -1

	while (set_len(models) > 0):
		model_tuple = set_pop(models)
		retyping_key = model_tuple[0]
		metamodel = model_tuple[1]
		dict_add_fast(result, retyping_key, instantiate_model(metamodel))

	keys = dict_keys(merged_model["model"])
	while (set_len(keys) > 0):
		key = set_pop(keys)
		elem = merged_model["model"][key]
		type = read_type(merged_model, key)
		splitted = string_split_nr(type, "/", 1)

		if (list_len(splitted) == 1):
			// Only one, so no split possible
			// Indicates a tracability element!
			if (bool_and(tracability, is_edge(elem))):
				// Got a tracability link!
				// Is always an edge (for now?)
				// Find out source and target and hope that they are already present
				src = reverse[cast_id(read_edge_src(elem))]
				dst = reverse[cast_id(read_edge_dst(elem))]

				// All present, so create the link between them
				src_name = list_pop_final(string_split_nr(src, "/", 1))
				dst_name = list_pop_final(string_split_nr(dst, "/", 1))

				source = instantiate_node(tracability_model, "Reference", "")
				target = instantiate_node(tracability_model, "Reference", "")
				instantiate_attribute(tracability_model, source, "name", src_name)
				instantiate_attribute(tracability_model, target, "name", dst_name)

				link = instantiate_link(tracability_model, "TracabilityLink", "", source, target)
				instantiate_attribute(tracability_model, link, "type", type)
		else:
			retyping_key = splitted[0]
			if (list_len(string_split(key, "/")) > 1):
				new_name = list_read(string_split_nr(key, "/", 1), 1)
			else:
				new_name = key

			if (dict_in(result, retyping_key)):
				original_type = splitted[1]

				if (is_edge(elem)):
					// Is an edge, so potentially queue it
					src = reverse[cast_id(read_edge_src(elem))]
					dst = reverse[cast_id(read_edge_dst(elem))]

					if (bool_and(dict_in(mapping, src), dict_in(mapping, dst))):
						// All present, so create the link between them
						// Though we first check the model to which it was mapped, as this should be the same as we have now
						if (bool_and(dict_in(result[retyping_key]["model"], mapping[src]), dict_in(result[retyping_key]["model"], mapping[dst]))):
							// The matching worked fine
							dict_add_fast(mapping, key, instantiate_link(result[retyping_key], original_type, new_name, mapping[src], mapping[dst]))
						else:
							log("ERROR mapping: source/target mapped to model, but not found in expected model; ignoring")
					else:
						// Still source or destination in the queue, so we wait for that
						set_add(second_keys, key)

				elif (has_value(elem)):
					// Has a value, so copy that as well
					dict_add_fast(mapping, key, instantiate_value(result[retyping_key], original_type, new_name, elem))

				else:
					// Is a node
					dict_add_fast(mapping, key, instantiate_node(result[retyping_key], original_type, new_name))

		if (set_len(keys) == 0):
			if (set_len(second_keys) == prev_length):
				log("Cannot terminate model_split due to infinite recursion!")
				return read_root()!

			keys = second_keys
			second_keys = set_create()
			prev_length = set_len(keys)

	// Finally, we also add tracability information as a separate model
	if (tracability):
		dict_add_fast(result, "__tracability", tracability_model)
		log("Tracability model created with # links = " + cast_value(dict_len(tracability_model["model"])))

	return result!
