import {
	AddResultStatus,
	InputType,
	ProcessType,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	isStringIndexedInterface,
} from "qrc:/js/lib/generated/typeguard";
import {
	front,
} from "./array_util";

import {
	checkGraphAxioms,
} from "./axioms";
import {
	getArticleName,
	getArticleSignatureVertex,
	getArticleUserData,
	makeArticleNameUnique,
} from "./graph_utils";
import {
	computeNameProposal,
} from "./node_utils";
import {
	computeOrderedProcesses,
	findActiveProcess,
} from "./process";
import {
	getSharedDataEntry,
} from "./shared_data";
import {
	getTable,
} from "./table_utils";
import {
	ArticleUserData,
	createUpdatedGraphUserData,
} from "./userdata_config";
import {
	insertInitialValuesIfMissing,
} from "./userdata_initial_values";
import {
	createUpdatedNodeUserData,
	getNodeUserDataEntry,
	getNodeUserDataEntryOrThrow,
	isCompatibleToNodeUserDataEntry,
} from "./userdata_utils";
import {
	assert,
	rootIdToVertex,
} from "./utils";

function requiresArticleUserDataInitialization(article: readonly Readonly<Vertex>[]): boolean {
	const signatureVertex = getArticleSignatureVertex(article);
	const articleUserData = getNodeUserDataEntryOrThrow("articleUserData", signatureVertex);
	return articleUserData.name.length === 0;
}

function checkArticleUserData() {
	assert(wsi4.graph.articles()
		.every(article => !requiresArticleUserDataInitialization(article)), "Expecting all article user data to be initialized");
}

function checkGraphValidity() {
	checkGraphAxioms();
	checkArticleUserData();
}

// When deleting a sheetBending node then the new signature node is the respective sheet cutting node.
// In this case the respective user data entry needs to be added.
function addUninitializedArticleUserDataIfMissing() {
	wsi4.graph.articles()
		.map(article => getArticleSignatureVertex(article))
		.filter(vertex => getNodeUserDataEntry("articleUserData", vertex) === undefined)
		.map(vertex => wsi4.node.rootId(vertex))
		.forEach(rootId => {
			const vertex = wsi4.node.vertexFromRootId(rootId);
			assert(vertex !== undefined, "Expecting valid vertex");
			const newUserData = insertInitialValuesIfMissing(vertex, wsi4.node.userData(vertex));
			wsi4.graphManipulator.changeNodeUserData(vertex, newUserData);
		});
}

function initializeArticleUserData(rootIdNameProposalMap = new Map<string, string>()): void {
	// Note:  *not* readonly as the container is extended in the mapping-function below
	const existingNames: string[] = wsi4.graph.articles()
		.filter(article => !requiresArticleUserDataInitialization(article))
		.map(article => getArticleName(front(article)));

	const verticesWithUserData = wsi4.graph.articles()
		.filter(article => requiresArticleUserDataInitialization(article))
		.map(article => getArticleSignatureVertex(article))
		.map(signatureVertex => {
			const articleUserData = (() => {
				const nameProposal = (() => {
					const mapValue = rootIdNameProposalMap.get(
						wsi4.util.toKey(wsi4.node.rootId(signatureVertex)),
					);
					return mapValue === undefined ? computeNameProposal(signatureVertex) : mapValue;
				})();
				const articleName = makeArticleNameUnique(nameProposal, existingNames);
				existingNames.push(articleName);

				const articleUserData = getArticleUserData(signatureVertex);
				assert(articleUserData.name.length === 0, "Expecting empty article name");
				articleUserData.name = articleName;

				return articleUserData;
			})();

			return {
				vertex: signatureVertex,
				userData: createUpdatedNodeUserData("articleUserData", signatureVertex, articleUserData),
			};
		});
	if (verticesWithUserData.length > 0) {
		const success = wsi4.graphManipulator.changeNodesUserData(verticesWithUserData);
		assert(success, "Expecting success");
	}
}

export function addArc(sourceVertex: Vertex, targetVertex: Vertex): boolean {
	wsi4.util.warn("addArc(): This function is deprecated and will be removed in a future release.");
	const r = wsi4.graphManipulator.addArc(sourceVertex, targetVertex);
	initializeArticleUserData();
	// Axioms are violated (probalby only temporarily though).
	// As this function is deprecated deactivating the check for now should be ok.
	// checkGraphValidity();
	return r;
}

/**
 * Change `vertex`' name to `name`
 */
export function changeArticleUserData(inputVertex: Vertex, articleUserData: Readonly<ArticleUserData>): boolean {
	const signatureVertex = getArticleSignatureVertex(wsi4.graph.article(inputVertex));
	const nodeUserData = createUpdatedNodeUserData("articleUserData", signatureVertex, articleUserData);
	return changeNodeUserData(signatureVertex, nodeUserData);
}

export function changeArticleNames(verticesWithNames: readonly Readonly<[ Vertex, string ]>[]): boolean {
	const verticesWithUserData = verticesWithNames.map(verticesWithNames => {
		const [
			inputVertex,
			name,
		] = verticesWithNames;

		const signatureVertex = getArticleSignatureVertex(wsi4.graph.article(inputVertex));
		const nodeUserData = wsi4.node.userData(signatureVertex);
		const articleUserData = getNodeUserDataEntryOrThrow("articleUserData", signatureVertex, nodeUserData);

		articleUserData.name = name;

		return {
			vertex: signatureVertex,
			userData: createUpdatedNodeUserData("articleUserData", signatureVertex, articleUserData, nodeUserData),
		};
	});
	if (verticesWithNames.length > 0) {
		return changeNodesUserData(verticesWithUserData);
	} else {
		return true;
	}
}

/**
 * Change `vertex`' name to `name`
 */
export function changeArticleName(inputVertex: Vertex, name: string): boolean {
	const signatureVertex = getArticleSignatureVertex(wsi4.graph.article(inputVertex));
	const nodeUserData = wsi4.node.userData(signatureVertex);
	const articleUserData = getNodeUserDataEntryOrThrow("articleUserData", signatureVertex, nodeUserData);

	if (articleUserData.name === name) {
		return true;
	} else {
		articleUserData.name = name;
		return changeNodeUserData(signatureVertex, createUpdatedNodeUserData("articleUserData", signatureVertex, articleUserData, nodeUserData));
	}
}

/**
 * Change the multiplicity of the associated node.
 *
 * There also is setMultiplicity() which sets the multiplicity of all nodes allowing this.
 */
export function changeMultiplicity(vertex: Vertex, multiplicity: number): boolean {
	assert(wsi4.node.isImport(vertex), "Expecting import node");
	const r = wsi4.graphManipulator.changeImportMultiplicity(vertex, multiplicity);
	initializeArticleUserData();
	checkGraphValidity();
	return r;
}

/**
 * Change the multiplicity of the multiple nodes.
 *
 * There also is setMultiplicity() which sets the multiplicity of all nodes allowing this.
 *
 * Pre-condition:  Each vertex is an import node
 */
export function changeMultipleMultiplicities(verticesWithMultiplicities: readonly Readonly<VertexWithMultiplicity>[]): boolean {
	assert(wsi4.util.isDebug() || verticesWithMultiplicities.every(vwm => wsi4.node.isImport(vwm.vertex)));
	const r = wsi4.graphManipulator.changeImportMultiplicities(verticesWithMultiplicities);
	initializeArticleUserData();
	checkGraphValidity();
	return r;
}

export function changeNodeProcessId(vertex: Vertex, processId: string, forced: boolean): boolean {
	const r = wsi4.graphManipulator.changeProcess(vertex, processId, forced);
	addUninitializedArticleUserDataIfMissing();
	initializeArticleUserData();
	checkGraphValidity();
	return r;
}

export function changeNodesProcessId(data: readonly VertexWithProcessTypeData[]): boolean {
	const r = wsi4.graphManipulator.changeNodesProcess(data);
	addUninitializedArticleUserDataIfMissing();
	initializeArticleUserData();
	checkGraphValidity();
	return r;
}

function parentProcess(processId: string): string|undefined {
	const table = getTable(TableType.process);
	const processEntry = table.find(process => process.identifier === processId);
	return processEntry === undefined ? undefined : typeof processEntry.parentIdentifier !== "string" ? undefined : processEntry.parentIdentifier;
}
function hasParentProcess(processId: string, parentCandidate: string): boolean {
	if (processId === parentCandidate) {
		return true;
	}
	const process = parentProcess(processId);
	return process !== undefined && process !== "" && hasParentProcess(process, parentCandidate);
}

export function changeNodeThickness(vertex: Vertex, thickness: number): boolean {
	if (!wsi4.node.isInitial(vertex)) {
		return false;
	}

	if (!hasParentProcess(wsi4.node.processId(vertex), "sheetCuttingId")) {
		return false;
	}
	const r = wsi4.graphManipulator.changeNodeThickness(vertex, thickness);
	initializeArticleUserData();
	checkGraphValidity();
	return r;
}

export function changeGraphUserData(graphUserData: StringIndexedInterface): boolean {
	const result = wsi4.graphManipulator.changeGraphUserData(graphUserData);
	checkGraphValidity();
	return result;
}

export function changeProjectName(name: string): boolean {
	const r = wsi4.graphManipulator.changeGraphUserData(createUpdatedGraphUserData("name", name));
	checkGraphValidity();
	return r;
}

export function clearGraph(): void {
	wsi4.graphManipulator.clear();
	checkGraphValidity();
}

export function createExternalNode(): Vertex|undefined {
	const processes = computeOrderedProcesses(WorkStepType.userDefinedBase)
		.filter(p => p.active);
	if (processes.length === 0) {
		return undefined;
	}

	const vertex = wsi4.graphManipulator.createImportNode(
		front(processes).type,
		front(processes).identifier,
	);
	if (vertex === undefined) {
		return wsi4.throwError("Couldn't create node");
	}
	const rootId = wsi4.node.rootId(vertex);
	if (!wsi4.graphManipulator.changeProcess(vertex, "externalPartId", true)) {
		return undefined;
	}
	const newVertex = rootIdToVertex(rootId);
	if (!wsi4.graphManipulator.changeProcess(newVertex, "externalPartId", false)) {
		return undefined;
	}
	initializeArticleUserData();
	checkGraphValidity();

	return rootIdToVertex(rootId);
}

export function createNode(processType: ProcessType = ProcessType.undefined, processId = ""): Vertex {
	const rootId = wsi4.node.rootId(wsi4.graphManipulator.createNode(processType, processId));
	initializeArticleUserData();
	checkGraphValidity();

	const result = wsi4.node.vertexFromRootId(rootId);
	assert(result !== undefined, "Expecting valid vertex");
	return result;
}

export function deleteArc(sourceVertex: Vertex, targetVertex: Vertex): boolean {
	wsi4.util.warn("deleteArc(): This function is deprecated and will be removed in a future release.");
	const r = wsi4.graphManipulator.deleteArc(sourceVertex, targetVertex);
	initializeArticleUserData();
	checkGraphValidity();
	return r;
}

export function deleteNodes(vertices: Vertex[]): boolean {
	if (vertices.length === 0) {
		return true;
	} else {
		const r = wsi4.graphManipulator.deleteNodes(vertices);
		addUninitializedArticleUserDataIfMissing();
		initializeArticleUserData();
		// FIXME three are cases (node_deletion.ts test) where axioms are violated right now - see #2048
		// checkGraphValidity();
		return r;
	}
}

export function deleteNode(vertex: Vertex): boolean {
	return deleteNodes([ vertex ]);
}

export function injectNode(sourceVertex: Vertex, targetVertex: Vertex, vertexToInject: Vertex): boolean {
	const r = wsi4.graphManipulator.injectNode(sourceVertex, targetVertex, vertexToInject);
	initializeArticleUserData();
	// FIXME three are cases (node_deletion.ts test) where axioms are violated right now - see #2048
	// checkGraphValidity();
	return r;
}

export function injectSourceNode(initiatingVertex: Vertex, processType = ProcessType.undefined, processId = ""): Vertex|undefined {
	// The vertex that initiated the action
	const initiatingRootId = wsi4.node.rootId(initiatingVertex);

	const currentSourceVertices = wsi4.graph.sources(initiatingVertex);
	if (currentSourceVertices.length !== 1) {
		wsi4.util.error("injectSourceNode(): Prepending a node at this position not (yet) supported");
		return undefined;
	}
	const sourceRootId = wsi4.node.rootId(front(currentSourceVertices));

	if (wsi4.graph.targets(rootIdToVertex(sourceRootId)).length > 1) {
		wsi4.util.error("injectSourceNode(): Prepending a node before node that has source with more than one target not supported");
		return undefined;
	}

	// Everythink ok so adding new vertex
	const newVertex = wsi4.graphManipulator.createNode(processType, processId);
	const sourceVertex = wsi4.node.vertexFromRootId(sourceRootId);
	if (sourceVertex === undefined) {
		return wsi4.throwError("Expecting valid vertex");
	}
	const targetVertex = wsi4.node.vertexFromRootId(initiatingRootId);
	if (targetVertex === undefined) {
		return wsi4.throwError("Expecting valid vertex");
	}

	const newRootId = wsi4.node.rootId(newVertex);
	const injectSuccess = injectNode(sourceVertex, targetVertex, newVertex);
	if (!injectSuccess) {
		return undefined;
	}
	return wsi4.node.vertexFromRootId(newRootId);
}

// Utility function to allow for correct type deduction when used as default argument
function undefinedProcessType(): ProcessType {
	return ProcessType.undefined;
}

export function injectTargetNode(initiatingVertex: Vertex, processType = undefinedProcessType(), processId = ""): Vertex|undefined {
	// The vertex that initiated the action
	const initiatingRootId = wsi4.node.rootId(initiatingVertex);

	const currentTargetVertices = wsi4.graph.targets(initiatingVertex);
	if (currentTargetVertices.length !== 1) {
		wsi4.util.error("injectTargetNode(): Appending a node at this position not (yet) supported");
		return undefined;
	}
	const targetRootId = wsi4.node.rootId(front(currentTargetVertices));

	// Everythink ok so adding new vertex
	const newVertex = wsi4.graphManipulator.createNode(processType, processId);
	const sourceVertex = wsi4.node.vertexFromRootId(initiatingRootId);
	if (sourceVertex === undefined) {
		return wsi4.throwError("Expecting valid vertex");
	}
	const targetVertex = wsi4.node.vertexFromRootId(targetRootId);
	if (targetVertex === undefined) {
		return wsi4.throwError("Expecting valid vertex");
	}

	const newRootId = wsi4.node.rootId(newVertex);
	const injectSuccess = injectNode(sourceVertex, targetVertex, newVertex);
	if (!injectSuccess) {
		return undefined;
	}

	return wsi4.node.vertexFromRootId(newRootId);
}

function appendTerminatingNode(sourceVertices: Vertex[], processType = undefinedProcessType(), processId = ""): Vertex|undefined {
	const sourceRootIds = sourceVertices.map(vertex => wsi4.node.rootId(vertex));
	const targetRootId = wsi4.node.rootId(wsi4.graphManipulator.createNode(processType, processId));
	const success = sourceRootIds.reduce((acc: boolean, sourceRootId) => {
		const sourceVertex = wsi4.node.vertexFromRootId(sourceRootId);
		if (sourceVertex === undefined) {
			return wsi4.throwError("Vetex invalid");
		}
		const targetVertex = wsi4.node.vertexFromRootId(targetRootId);
		if (targetVertex === undefined) {
			return wsi4.throwError("Vetex invalid");
		}
		return acc && wsi4.graphManipulator.addArc(sourceVertex, targetVertex);
	}, true);
	assert(success, "Could not at least one arc");

	initializeArticleUserData();
	checkGraphValidity();

	return wsi4.node.vertexFromRootId(targetRootId);
}

export function appendNode(vertex: Vertex, processType = undefinedProcessType(), processId = ""): Vertex|undefined {
	if (wsi4.graph.targets(vertex).length > 0) {
		return injectTargetNode(vertex, processType, processId);
	} else {
		const r = appendTerminatingNode([ vertex ], processType, processId);
		initializeArticleUserData();
		checkGraphValidity();
		return r;
	}
}

export function appendNodeWithProcessId(processId: string, inputVertex: Vertex): Vertex|undefined {
	const process = getTable(TableType.process)
		.find(row => row.identifier === processId);
	assert(process !== undefined, "Process ID invalid: " + processId);
	return appendNode(inputVertex, process.type, process.identifier);
}

export function redo(): boolean {
	const r = wsi4.graphManipulator.redo();
	// FIXME see #2047
	// Cannot graph validity at this point as an (from the script env point of view) invalid graph might be stored as part of the undo history
	// (e.g. partially initialized article attributes)
	// checkGraphValidity();
	return r;
}

export function undo(): boolean {
	const u = wsi4.graphManipulator.undo();
	// FIXME see #2047
	// Cannot graph validity at this point as an (from the script env point of view) invalid graph might be stored as part of the undo history
	// (e.g. partially initialized article attributes)
	// checkGraphValidity();
	return u;
}

// Set multiplicity of all nodes that provide ImportGraphNodeAttributes
export function setMultiplicity(multiplicity: number): boolean {
	if (multiplicity < 1) {
		return wsi4.throwError("Multiplicity invalid. Expecting value >= 1. Got " + multiplicity.toString());
	}
	const verticesWithMultiplicities = wsi4.graph.vertices()
		.filter(v => (wsi4.node.isImport(v)))
		.map(v => ({
			vertex: v,
			multiplicity: multiplicity,
		}));
	return changeMultipleMultiplicities(verticesWithMultiplicities);
}

export function deleteArticle(vertex: Vertex): boolean {
	wsi4.util.warn("deleteArticle(): This function is deprecated and will be removed in a future release.");
	const r = deleteReducibleComponent(vertex);
	initializeArticleUserData();
	checkGraphValidity();
	return r;
}

export function changeJoining(vertex: Vertex, joining: Joining): boolean {
	const result = wsi4.graphManipulator.changeJoining(vertex, joining);
	checkGraphValidity();
	return result;
}

export function changeSheetFilter(vertex: Vertex, sheetFilter: SheetFilter): boolean {
	wsi4.util.warn("changeSheetFilter(): This function is deprecated and will be removed in a future release.  Use changeSheetFilterSheetIdsOfSheet instead.");
	const result = wsi4.graphManipulator.changeSheetFilter(vertex, sheetFilter);
	checkGraphValidity();
	return result;
}

export function changeNodesUserData(verticesWithUserData: VertexWithUserData[]): boolean {
	const result = wsi4.graphManipulator.changeNodesUserData(verticesWithUserData);
	initializeArticleUserData();
	checkGraphValidity();
	return result;
}

export function changeNodeUserData(vertex: Vertex, userData: StringIndexedInterface): boolean {
	return changeNodesUserData([
		{
			vertex: vertex,
			userData: userData,
		},
	]);
}

export function changeSheetFilterSheetIdsOfSheet(vertex: Vertex, sheetFilterSheetIds: string[]): boolean {
	assert(wsi4.node.workStepType(vertex) === WorkStepType.sheet, "Wrong workStepType.");
	const vertices : VertexWithUserData[] = [];
	for (const r of wsi4.graph.reachable(vertex)) {
		if (isCompatibleToNodeUserDataEntry("sheetFilterSheetIds", r)) {
			vertices.push({
				vertex: r,
				userData: createUpdatedNodeUserData("sheetFilterSheetIds", r, sheetFilterSheetIds),
			});
		}
	}
	return changeNodesUserData(vertices);
}

export function changeFlipSideUsage(inputVertex: Vertex, flipSide: boolean): boolean {
	const result = wsi4.graphManipulator.changeFlipSideUsage(inputVertex, flipSide);
	initializeArticleUserData();
	checkGraphValidity();
	return result;
}

export function changeBendCorrectionFlag(vertex: Vertex, correctBends: boolean): boolean {
	const result = wsi4.graphManipulator.changeBendCorrectionFlag(vertex, correctBends);
	initializeArticleUserData();
	checkGraphValidity();
	return result;
}

export function deleteReducibleComponent(vertex: Vertex): boolean {
	const result = wsi4.graphManipulator.deleteReducibleComponent(vertex);
	initializeArticleUserData();
	checkGraphValidity();
	return result;
}

export function changeDieChoiceMap(vertex: Vertex, dieChoiceMap: readonly Readonly<DieChoiceMapEntry>[]): boolean {
	const result = wsi4.graphManipulator.changeDieChoiceMap(vertex, dieChoiceMap);
	initializeArticleUserData();
	checkGraphValidity();
	return result;
}

export function setCommands(vertex: Vertex, commands: CamCommand[]): boolean {
	const result = wsi4.graphManipulator.setCommands(vertex, commands);
	initializeArticleUserData();
	checkGraphValidity();
	return result;
}

function addConstAssemblyImpl(addAssemblyFunc: () => AddResult): Vertex|undefined {
	const rootIdOpt = (() => {
		const addResult = addAssemblyFunc();
		if (addResult.status === AddResultStatus.success) {
			assert(addResult.vertices.length === 1, "Expecting exactly one vertex");
			return wsi4.node.rootId(front(addResult.vertices));
		} else {
			return undefined;
		}
	})();

	if (rootIdOpt !== undefined) {
		initializeArticleUserData();
		checkGraphValidity();

		const vertex = wsi4.node.vertexFromRootId(rootIdOpt);
		assert(vertex !== undefined, "Expecting valid vertex");
		return vertex;
	} else {
		return undefined;
	}
}

/**
 * Add Assembly to graph
 *
 * @returns Resulting [[Vertex]] if successful;  undefined else
 */
export function addConstAssembly(assembly: Assembly): Vertex|undefined {
	return addConstAssemblyImpl(() => wsi4.graphManipulator.addConstAssembly(assembly));
}

/**
 * Add Assembly to graph and assign the graph node
 *
 * @returns Resulting [[Vertex]] if successful;  undefined else
 */
export function addConstAssemblyWithProcess(assembly: Assembly, processType: ProcessType, processId: string): Vertex|undefined {
	return addConstAssemblyImpl(() => wsi4.graphManipulator.addConstAssemblyWithProcess(
		assembly,
		processType,
		processId,
	));
}

function addImportIdToUserData(importIdMap: Readonly<Map<string, string>>) {
	const vertices = wsi4.graph.vertices();
	const vertexWithUserDatas = Array.from(importIdMap.keys())
		.map(rootIdKey => {
			const vertex = vertices.find(vertex => wsi4.util.toKey(wsi4.node.rootId(vertex)) === rootIdKey);
			assert(vertex !== undefined, "Expecting valid vertex for rootId");
			const importId = importIdMap.get(rootIdKey);
			assert(importId !== undefined, "Expecting valid import id");
			return {
				vertex: vertex,
				userData: createUpdatedNodeUserData(
					"importId",
					vertex,
					importId,
				),
			};
		});
	if (vertexWithUserDatas.length === 0) {
		return;
	}
	changeNodesUserData(vertexWithUserDatas);
}

function articleHasDeburringNode(article: readonly Vertex[]): boolean {
	return article.some(vertex => {
		const processType = wsi4.node.processType(vertex);
		return processType === ProcessType.automaticMechanicalDeburring
			|| processType === ProcessType.manualMechanicalDeburring;
	});
}

function insertDeburringNodes(processType: ProcessType, terminatingRootIds: readonly GraphNodeRootId[]) {
	const process = findActiveProcess(processType);
	if (process === undefined) {
		wsi4.util.warn("Cannot insert deburring node(s).  No active process found for process type " + processType);
	} else {
		terminatingRootIds.map(rootId => {
			const vertex = wsi4.node.vertexFromRootId(rootId);
			assert(vertex !== undefined, "Expecting valid vertex for rootId " + wsi4.util.toKey(rootId));
			return vertex;
		})
			.reduce((acc: Vertex[], vertex) => ([
				...acc,
				...wsi4.graph.reaching(vertex),
				vertex,
			]), [])
			.filter(vertex => wsi4.node.processType(vertex) === ProcessType.laserSheetCutting)
			.filter(vertex => !articleHasDeburringNode(wsi4.graph.article(vertex)))
			.map(vertex => wsi4.node.rootId(vertex))
			.forEach(rootId => {
				const sourceVertex = wsi4.node.vertexFromRootId(rootId);
				assert(sourceVertex !== undefined, "Expecting valid vertex for RootId " + wsi4.util.toKey(rootId));
				const newVertex = appendNodeWithProcessId(process.identifier, sourceVertex);
				if (newVertex === undefined) {
					wsi4.util.warn("Automatic node insertion failed for process type " + processType);
				}
			});
	}
}

function deburrIfRequired(terminatingRootIds: readonly GraphNodeRootId[]) {
	const automaticProcessConfig = getSharedDataEntry("automaticProcessConfig");
	if (automaticProcessConfig.automaticDeburringEnabled) {
		insertDeburringNodes(ProcessType.automaticMechanicalDeburring, terminatingRootIds);
	} else if (automaticProcessConfig.manualDeburringEnabled) {
		insertDeburringNodes(ProcessType.manualMechanicalDeburring, terminatingRootIds);
	}
}

/**
 * Post-process a sub-graph
 *
 * @param terminatingRootIds Root IDs defining the sub-graph
 */
function postProcessSubGraph(terminatingRootIds: readonly GraphNodeRootId[]): void {
	deburrIfRequired(terminatingRootIds);
}
/**
 * Add data to graph
 *
 * @param inputs Data that should be added to graph
 * @param importIdNameProposalMap Optional mapping input id to a name proposal for corresponding vertices
 * @returns [[AddResult]]s for the respective [[Input]]s
 */
export function addToGraph(inputs: Input[], importIdNameProposalMap = new Map<string, string>()): AddResult[] {
	const originalAddResults = wsi4.graphManipulator.add(inputs);

	const vertexRootIdMap = originalAddResults.reduce((acc, addResult) => {
		addResult.vertices.forEach(vertex => acc.set(wsi4.util.toKey(vertex), wsi4.node.rootId(vertex)));
		return acc;
	}, new Map<string, GraphNodeRootId>());

	const rootIdImportIdMap = (() => {
		const map = new Map<string, string>();
		originalAddResults.forEach(addResult => addResult.vertices.forEach(vertex => map.set(wsi4.util.toKey(wsi4.node.rootId(vertex)), addResult.id)));
		return map;
	})();

	const rootIdNameProposalMap = originalAddResults.reduce((acc, addResult) => {
		const proposal = importIdNameProposalMap.get(addResult.id);
		if (proposal !== undefined) {
			addResult.vertices.forEach(vertex => acc.set(wsi4.util.toKey(wsi4.node.rootId(vertex)), proposal));
		}
		return acc;
	}, new Map<string, string>());

	// Note:  This changes the graph and hence invalidates originalAddResults underlying vertices
	// Note:  The order of function calls is important here; since other graph manipulating functions might in turn
	//        lead to calls of initializeArticleUserData() this function call should be the first one to ensure
	//        rootIdNameProposalMap has any effect.
	initializeArticleUserData(rootIdNameProposalMap);

	// Note:  This changes the graph and hence invalidates originalAddResults underlying vertices
	addImportIdToUserData(rootIdImportIdMap);

	// Post processing should only be applied to vertices that are
	// - newley added to the graph
	// - *not* part of a loaded DocumentGraph
	const postProcessingSubGraph = (() => {
		const relevantInputs = inputs.filter(input => input.type !== InputType.undefined && input.type !== InputType.documentGraph);
		const result: GraphNodeRootId[] = [];
		originalAddResults.filter(addResult => relevantInputs.some(input => isStringIndexedInterface(input.content) && input.content["id"] === addResult.id))
			.forEach(addResult => result.push(
				...addResult.vertices.map(oldVertex => {
					const rootId = vertexRootIdMap.get(wsi4.util.toKey(oldVertex));
					assert(rootId !== undefined);
					return rootId;
				}),
			));
		return result;
	})();

	// Note:  This (potentially) changes the graph and hence invalidates originalAddResults's underlying vertices
	postProcessSubGraph(postProcessingSubGraph);

	return originalAddResults.map((addResult): AddResult => ({
		id: addResult.id,
		status: addResult.status,
		vertices: addResult.vertices.map(oldVertex => {
			const rootId = vertexRootIdMap.get(wsi4.util.toKey(oldVertex));
			assert(rootId !== undefined, "Expecting valid root id");
			const newVertex = wsi4.node.vertexFromRootId(rootId);
			assert(newVertex !== undefined, "Expecting valid vertex");
			return newVertex;
		}),
	}));
}
