import {
	readCalcSettings,
} from "qrc:/js/lib/calc_settings";
import {
	getFutureResult,
} from "qrc:/js/lib/future";
import {
	PrivateGuiDataType,
	ProcessType,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	isProcess,
	isTubeProfileGeometryCircular,
	isTubeProfileGeometryRectangular,
} from "qrc:/js/lib/generated/typeguard";
import {
	collectSubGraphVertices,
	computeNodeMass,
	computeSheetConsumption,
	consumableConsumptionsForVertex,
	getArticleSignatureVertex,
	getAssociatedSheetMaterialId,
	getAssociatedTubeMaterialId,
	hasSheetSourceArticle,
	hasTubeSourceArticle,
	isComponentArticle,
	isJoiningArticle,
	isSemimanufacturedArticle,
	netTubeConsumptionForVertex,
	projectName,
	sheetCuttingChargeWeightForVertex,
} from "qrc:/js/lib/graph_utils";
import {
	isExportReadyImpl,
} from "qrc:/js/lib/manufacturing_state";
import {
	computeCommandsFaceArea,
	computeSheetConsumptionNestedParts,
	createPngFutures as createPngFuturesImpl,
	grossTubeConsumptionForVertex,
	sheetBendingWsNumBendLines,
	sheetCuttingWsContourCount,
	tubeForVertex,
	tubeProfileForVertex,
} from "qrc:/js/lib/node_utils";
import {
	getTable,
} from "qrc:/js/lib/table_utils";
import {
	assert,
	assertDebug,
	defaultPngResolution,
	isArray,
	isBoolean,
	isEqual,
	isNumber,
	readSetting,
} from "qrc:/js/lib/utils";
import {
	hasManufacturingCostOverride,
	isCompatibleToNodeUserDataEntry,
	nodeUserDatum,
	nodeUserDatumOrDefault,
} from "qrc:/js/lib/userdata_utils";
import {
	findActiveProcess,
} from "qrc:/js/lib/process";
import {
	tubeCuttingLayerPaths,
} from "qrc:/js/lib/tubecutting_util";
import {
	computeActualManufacturingStateCached,
	computeVirtualManufacturingState,
	createManufacturingStateCache,
	ManufacturingStateCache,
} from "qrc:/js/lib/manufacturing_state_util";

import {
	back,
	front,
	itemAt,
} from "qrc:/js/lib/array_util";
import {
	articleUserDatumImpl,
} from "qrc:/js/lib/article_userdata_config";
import {
	asyncSceneForVertex,
	defaultSvgResolution,
	sceneForVertex,
} from "qrc:/js/lib/scene_utils";
import {
	nodeUserDatumImpl,
} from "qrc:/js/lib/userdata_config";
import {
	computeConsumableCosts,
} from "qrc:/js/lib/calc_consumables";
import {
	articleSellingPriceWithSemimanufacturedShare,
	computeArticleManufacturingPriceExclSurcharges,
	computeArticleManufacturingPriceInclSurcharges,
	computeManufacturingPriceExclSurcharges,
	computeNodeCosts,
	computeNodeManufacturingPriceInclSurcharges,
	computeRecursiveArticleManufacturingPriceExclSurcharges,
	computeRecursiveArticleManufacturingPriceInclSurcharges,
	computeRecursiveArticleScaleSellingPrice,
	computeScaleValues,
	computeSheetConsumptionMasses,
	computeTubeConsumptionMasses,
	guiOnlyApplyGlobalSurcharges,
} from "./export_calc_costs";
import {
	createCalcCache,
	CalcCache,
} from "./export_calc_cache";
import {
	computeNodeTimes,
	getFixedMultiplicity,
} from "./export_calc_times";
import {
	mmToString,
	currencyString,
	secondsToString,
	squareMmToString,
} from "./export_utils";
import {
	guiConstraintLevelMap,
	guiReplyStateLevelMap,
} from "./gui_manufacturing_state";

function isExportReady(vertex: Vertex, cache: ManufacturingStateCache): boolean {
	const manufacturingState = computeActualManufacturingStateCached(
		vertex,
		guiReplyStateLevelMap,
		guiConstraintLevelMap,
		cache,
	);
	return isExportReadyImpl(manufacturingState);
}

type HtmlAlignment = "left" | "center" | "right";

interface HtmlTableCell {
	align: HtmlAlignment;
	text: string;
	colspan?: number;
}

interface HtmlTable {
	th?: HtmlTableCell[];
	td: HtmlTableCell[][];
}

function tableCellToHtml(cell: HtmlTableCell, tag: string): string {
	return `<${tag} align="${cell.align}" colspan="${cell.colspan ?? 1}">${cell.text}</${tag}>`;
}

function tableObjToHtml(table: HtmlTable): string {
	let result = "\n<table width=\"100%\">";
	result += table.th === undefined ? "" : "\n<tr>" + table.th.map(cell => `\n${tableCellToHtml(cell, "th")}`)
		.join("") + "\n</tr>";
	result += table.td.map(cells => "\n<tr>" + cells.map(cell => `\n${tableCellToHtml(cell, "td")}`)
		.join("") + "\n</tr>")
		.join("");
	result += "\n</table>";
	return result;
}

function computeMultiplicity(vertex: Vertex): number {
	return getFixedMultiplicity(vertex);
}

function computeNodeBaseText(vertex: Vertex, manufacturingStateCache: ManufacturingStateCache, calcCache?: CalcCache) {
	const processNameHtml = (() => {
		const processId = wsi4.node.processId(vertex);
		const table = getTable(TableType.process);
		if (!isArray(table, isProcess)) {
			return wsi4.throwError("Table invalid");
		}
		const row = table.find(element => element.identifier === processId);
		if (row === undefined) {
			// Not considered an error as wsi4 file could be from another user
			return wsi4.util.translate("Unknown");
		}
		return "<strong>" + row.name + "</strong>";
	})();

	const rows: HtmlTableCell[][] = [];
	rows.push(((): HtmlTableCell[] => {
		const material = getMaterialIfAny(getArticleSignatureVertex(wsi4.graph.article(vertex)));
		// undefined row is not considered an error as wsi4 file could be from another user
		return [
			{
				align: "left",
				text: wsi4.util.translate("Material"),
			},
			{
				align: "right",
				text: material ?? "N/A",
			},
		];
	})());

	rows.push(((): HtmlTableCell[] => {
		const thickness = wsi4.node.sheetThickness(vertex);
		if (thickness === undefined) {
			return [];
		} else {
			return [
				{
					align: "left",
					text: wsi4.util.translate("SheetThickness"),
				},
				{
					align: "right",
					text: thickness.toFixed(2),
				},
			];
		}
	})());

	const multiplicity = computeMultiplicity(vertex);
	rows.push([
		{
			align: "left",
			text: wsi4.util.translate("Multiplicity"),
		},
		{
			align: "right",
			text: multiplicity.toFixed(0),
		},
	]);

	rows.push(((): HtmlTableCell[] => {
		const costs = (() => {
			if (isExportReady(vertex, manufacturingStateCache) && !hasManufacturingCostOverride(vertex)) {
				return computeNodeCosts(vertex, multiplicity, calcCache);
			} else {
				return undefined;
			}
		})();
		const manPrice = computeManufacturingPriceExclSurcharges(costs);
		return [
			{
				align: "left",
				text: wsi4.util.translate("manufacturing_costs_abbr_per_piece"),
			},
			{
				align: "right",
				text: manPrice === undefined ? "N/A" : currencyString(manPrice / multiplicity),
			},
		];
	})());

	return processNameHtml + "<br>" + tableObjToHtml({td: rows.filter(row => row.length > 0)});
}

function computeSheetCuttingNodeText(vertex: Vertex) {
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	assert(twoDimRep !== undefined, "Expecting valid two dim rep");

	const rows: HtmlTableCell[][] = [];

	const contourCount = sheetCuttingWsContourCount(vertex);
	rows.push([
		{
			align: "left",
			text: wsi4.util.translate("NumPiercings"),
		},
		{
			align: "right",
			text: (contourCount ?? 0).toString(),
		},
	]);

	rows.push([
		{
			align: "left",
			text: wsi4.util.translate("ContourLength"),
		},
		{
			align: "right",
			text: wsi4.cam.util.cuttingContourLength(twoDimRep).toFixed(0),
		},
	]);

	const relSemimanufacturedShare = wsi4.graph.semimanufacturedSourceShare(vertex);
	rows.push([
		{
			align: "left",
			text: wsi4.util.translate("semimanufactured_source_share") + " [%]",
		},
		{
			align: "right",
			text: (100 * relSemimanufacturedShare).toFixed(2),
		},
	]);

	const overallChargeWeight = sheetCuttingChargeWeightForVertex(vertex);
	const chargeWeightPerPiece = overallChargeWeight === undefined ? undefined : overallChargeWeight / wsi4.node.multiplicity(vertex);
	rows.push([
		{
			align: "left",
			text: wsi4.util.translate("charge_weight_in_kg_per_piece"),
		},
		{
			align: "right",
			text: chargeWeightPerPiece === undefined ? "N/A" : chargeWeightPerPiece.toFixed(2),
		},
	]);

	const fixedRotations = nodeUserDatumOrDefault("fixedRotations", vertex);
	rows.push([
		{
			align: "left",
			text: wsi4.util.translate("fixed_rotations") + " [°]",
		},
		{
			align: "right",
			text: fixedRotations.length === 0 ? "--" : fixedRotations.join(", "),
		},
	]);

	return tableObjToHtml({td: rows});
}

function computeSheetBendingNodeText(vertex: Vertex) {
	const numBends = sheetBendingWsNumBendLines(vertex);
	if (numBends === undefined) {
		return wsi4.util.translate("Unknown");
	}
	return wsi4.util.translate("NumBends") + ": " + numBends.toString();
}

function sheetPricePerKg(sheet: Readonly<Sheet>): number {
	const sheetPrice = getTable(TableType.sheetPrice)
		.find(row => row.sheetId === sheet.identifier);
	assert(sheetPrice !== undefined);

	// [kg / m³]
	const density = getTable(TableType.sheetMaterialDensity)
		.find(row => row.sheetMaterialId === sheet.sheetMaterialId);
	assert(density !== undefined);

	// [mm³] -> [m³]
	const volume = 0.001 * 0.001 * 0.001 * sheet.dimX * sheet.dimY * sheet.thickness;

	// [kg]
	const mass = volume * density.density;

	// [Currency / kg]
	return sheetPrice.pricePerSheet / mass;
}

function computeSheetNodeText(vertex: Vertex) {
	// For now copy-pasting from gui_export_calculation; refactoring of nestor related API should allow to reduce the amount of code duplication; see #1478 and #1503
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		// This is not considered an error as it is possible that no sheet could be found for a CAD input
		return "N/A";
	}

	const sheet = wsi4.node.findAssociatedSheet(vertex);
	if (sheet === undefined) {
		return "N/A";
	}

	const totalSheetConsumption = computeSheetConsumption(vertex);
	const partsSheetConsumption = computeSheetConsumptionNestedParts(vertex);

	if (totalSheetConsumption === undefined || partsSheetConsumption === undefined) {
		return "N/A";
	} else {
		const consumptionMasses = computeSheetConsumptionMasses(
			sheet,
			{
				total: totalSheetConsumption,
				parts: partsSheetConsumption,
			},
		);

		const pricePerKg = sheetPricePerKg(sheet);

		return tableObjToHtml({
			td: [
				[
					{
						align: "left",
						text: wsi4.util.translate("sheet"),
					},
					{
						align: "right",
						text: sheet.name,
					},
				],
				[
					{
						align: "left",
						text: wsi4.util.translate("Consumption"),
					},
					{
						align: "right",
						text: totalSheetConsumption.toFixed(3),
					},
				],
				[
					{
						align: "left",
						text: wsi4.util.translate("charge_weight_in_kg"),
					},
					{
						align: "right",
						text: consumptionMasses.total.toFixed(3),
					},
				],
				[
					{
						align: "left",
						text: wsi4.util.translate("scrap_in_kg"),
					},
					{
						align: "right",
						text: consumptionMasses.scrap.toFixed(3),
					},
				],
				[
					{
						align: "left",
						text: wsi4.util.translate("sheet_price_in_currency_per_kg"),
					},
					{
						align: "right",
						text: pricePerKg === undefined ? "N/A" : pricePerKg.toFixed(2),
					},
				],
			],
		});
	}
}

function computeTubeCuttingNodeText(vertex: Vertex): string {
	assertDebug(() => wsi4.node.workStepType(vertex) === WorkStepType.tubeCutting, "Pre-condition violated");

	const layered = wsi4.node.layered(vertex);
	const extrusionLength = wsi4.node.profileExtrusionLength(vertex);
	const cuttingPaths = layered === undefined ? undefined : [
		...tubeCuttingLayerPaths("cuttingOuterContour", layered),
		...tubeCuttingLayerPaths("cuttingInnerContour", layered),
	];
	const contourCount = cuttingPaths?.length;
	const cuttingLength = cuttingPaths?.reduce((acc, path) => acc + wsi4.geo.util.pathLength(path), 0.);

	const tube = tubeForVertex(vertex);
	const tubeMaterialName = tube === undefined ? undefined : getTable(TableType.tubeMaterial)
		.find(row => row.identifier === tube.tubeMaterialId)
		?.name;
	const tubeSpecificationName = tube === undefined ? undefined : getTable(TableType.tubeSpecification)
		.find(row => row.identifier === tube.tubeSpecificationId)
		?.name;
	const profileName = tubeProfileForVertex(vertex)?.name ?? (() => {
		const profile = wsi4.node.tubeProfileGeometry(vertex);
		if (profile === undefined) {
			return undefined;
		}
		switch (profile.type) {
			case "rectangular": {
				assert(isTubeProfileGeometryRectangular(profile.content), "Variant inconsistent");
				const y = profile.content.dimY.toFixed(1);
				const z = profile.content.dimZ.toFixed(1);
				const t = profile.content.thickness.toFixed(1);
				return `(${y} x ${z} x ${t})`;
			}
			case "circular": {
				assert(isTubeProfileGeometryCircular(profile.content), "Variant inconsistent");
				const r = profile.content.outerRadius.toFixed(1);
				const t = profile.content.thickness.toFixed(1);
				return `(${r} x ${t})`;
			}
		}
	})();

	const profileGeometry = wsi4.node.tubeProfileGeometry(vertex);
	const grossConsumption = tube === undefined ? undefined : grossTubeConsumptionForVertex(vertex, tube);
	const numParts = wsi4.node.multiplicity(vertex);
	const tubeConsumptionPerPiece = grossConsumption === undefined ? undefined : grossConsumption / numParts;
	const netConsumption = tube === undefined ? undefined : netTubeConsumptionForVertex(vertex, tube);
	const consumptionMasses = (() => {
		if (tube === undefined
			|| profileGeometry === undefined
			|| grossConsumption === undefined
			|| netConsumption === undefined) {
			return undefined;
		} else {
			return computeTubeConsumptionMasses(tube, profileGeometry, {
				gross: grossConsumption,
				net: netConsumption,
			});
		}
	})();

	return tableObjToHtml({
		td: [
			[
				{
					align: "left",
					text: wsi4.util.translate("Material"),
				},
				{
					align: "right",
					text: tubeMaterialName ?? "N/A",
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("specification"),
				},
				{
					align: "right",
					text: tubeSpecificationName ?? "N/A",
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("profile"),
				},
				{
					align: "right",
					text: profileName ?? "N/A",
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("extrusion_depth"),
				},
				{
					align: "right",
					text: mmToString(extrusionLength),
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("NumPiercings"),
				},
				{
					align: "right",
					text: contourCount === undefined ? "N/A" : contourCount.toString(),
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("ContourLength"),
				},
				{
					align: "right",
					text: cuttingLength === undefined ? "N/A" : mmToString(cuttingLength),
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("approx_tube_consumption_per_piece"),
				},
				{
					align: "right",
					text: tubeConsumptionPerPiece === undefined ? "N/A" : tubeConsumptionPerPiece.toFixed(3),
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("charge_weight_in_kg_per_piece"),
				},
				{
					align: "right",
					text: consumptionMasses === undefined ? "N/A" : (consumptionMasses.gross / numParts).toFixed(3),
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("scrap_in_kg_per_piece"),
				},
				{
					align: "right",
					text: consumptionMasses === undefined ? "N/A" : (consumptionMasses.scrap / numParts).toFixed(3),
				},
			],
		],
	});
}

function computeTubeNodeText(tubeVertex: Vertex): string {
	assertDebug(() => wsi4.node.workStepType(tubeVertex) === WorkStepType.tube, "Pre-condition violated");

	const tubeCuttingVertex = (() => {
		const vertices = wsi4.graph.reachable(tubeVertex)
			.filter(vertex => wsi4.node.workStepType(vertex) === WorkStepType.tubeCutting);
		assert(vertices.length === 1, "Expecting exactly one reachable tubeCutting node");
		return vertices[0]!;
	})();

	const profileGeometry = wsi4.node.tubeProfileGeometry(tubeCuttingVertex);
	const tube = tubeForVertex(tubeCuttingVertex);
	const grossConsumption = tube === undefined ? undefined : grossTubeConsumptionForVertex(tubeCuttingVertex, tube);
	const netConsumption = tube === undefined ? undefined : netTubeConsumptionForVertex(tubeCuttingVertex, tube);
	const consumptionMasses = (() => {
		if (tube === undefined
			|| profileGeometry === undefined
			|| grossConsumption === undefined
			|| netConsumption === undefined) {
			return undefined;
		} else {
			return computeTubeConsumptionMasses(tube, profileGeometry, {
				gross: grossConsumption,
				net: netConsumption,
			});
		}
	})();

	return tableObjToHtml({
		td: [
			[
				{
					align: "left",
					text: wsi4.util.translate("tube"),
				},
				{
					align: "right",
					text: tube === undefined ? "N/A" : tube.name,
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("Consumption"),
				},
				{
					align: "right",
					text: grossConsumption === undefined ? "N/A" : grossConsumption.toFixed(3),
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("charge_weight_in_kg"),
				},
				{
					align: "right",
					text: consumptionMasses === undefined ? "N/A" : consumptionMasses.gross.toFixed(3),
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("scrap_in_kg"),
				},
				{
					align: "right",
					text: consumptionMasses === undefined ? "N/A" : consumptionMasses.scrap.toFixed(3),
				},
			],
		],
	});
}

function computeUserDefinedThreadingText(vertex: Vertex): string {
	return wsi4.util.translate("num_threads") + ": " + nodeUserDatumOrDefault("numThreads", vertex)
		.toFixed(0);
}

function computeUserDefinedCountersinkingText(vertex: Vertex): string {
	return wsi4.util.translate("num_countersinks") + ": " + nodeUserDatumOrDefault("numCountersinks", vertex)
		.toFixed(0);
}

function computeCoatingNodeText(vertex: Vertex): string {
	const area = computeCommandsFaceArea(vertex);
	return tableObjToHtml({
		td: [
			[
				{
					align: "left",
					text: wsi4.util.translate("area"),
				},
				{
					align: "right",
					text: squareMmToString(area * wsi4.node.multiplicity(vertex)),
				},
			],
			[
				{
					align: "left",
					text: wsi4.util.translate("area") + " / " + wsi4.util.translate("piece"),
				},
				{
					align: "right",
					text: squareMmToString(area),
				},
			],
		],
	});
}

function computeAutomaticMechanicalDeburringText(vertex: Vertex): string {
	const doubleSided = nodeUserDatumOrDefault("deburrDoubleSided", vertex);
	return tableObjToHtml({
		td: [
			[
				{
					align: "left",
					text: wsi4.util.translate("double_sided"),
				},
				{
					align: "right",
					text: doubleSided ? "✓" : "✗",
				},
			],
		],
	});
}

function computeProcessSpecificText(vertex: Vertex): string {
	const type = wsi4.node.processType(vertex);
	if (type === ProcessType.userDefinedThreading) {
		return computeUserDefinedThreadingText(vertex);
	} else if (type === ProcessType.userDefinedCountersinking) {
		return computeUserDefinedCountersinkingText(vertex);
	} else if (type === ProcessType.powderCoating) {
		return computeCoatingNodeText(vertex);
	} else if (type === ProcessType.automaticMechanicalDeburring) {
		return computeAutomaticMechanicalDeburringText(vertex);
	} else {
		return "";
	}
}

function computeCommonDetails(vertex: Vertex, manufacturingStateCache: ManufacturingStateCache, calcCache: CalcCache): string {
	const multiplicity = computeMultiplicity(vertex);
	const nodeTimes = (() => {
		if (isExportReady(vertex, manufacturingStateCache) && !hasManufacturingCostOverride(vertex)) {
			return computeNodeTimes(vertex, multiplicity, calcCache);
		} else {
			return undefined;
		}
	})();
	const nodeCosts = (() => {
		if (isExportReady(vertex, manufacturingStateCache)) {
			return computeNodeCosts(vertex, multiplicity, calcCache);
		} else {
			return undefined;
		}
	})();

	const rowsIf = (cond: boolean, cells: HtmlTableCell[][]): HtmlTableCell[][] => cond ? cells : [];
	const tr = (arg: string) => wsi4.util.translate(arg);

	const timesAndCostsHtml = nodeTimes === undefined ? "" : tableObjToHtml({
		td: [
			[
				{
					align: "left",
					text: "Tr",
				},
				{
					align: "right",
					text: secondsToString(nodeTimes.setup),
				},
			],
			[
				{
					align: "left",
					text: "Te",
				},
				{
					align: "right",
					text: secondsToString(nodeTimes.unit),
				},
			],
			...rowsIf(nodeTimes.unit > 0 && multiplicity > 1, [
				[
					{
						align: "left",
						text: `Te (${tr("piece")})`,
					},
					{
						align: "right",
						text: secondsToString(nodeTimes.unit / multiplicity),
					},
				],
			]),
			[
				{
					align: "left",
					text: "",
				},
				{
					align: "right",
					text: "",
				},
			],
			[
				{
					align: "left",
					text: tr("MaterialCosts"),
				},
				{
					align: "right",
					text: nodeCosts === undefined ? "N/A" : currencyString(nodeCosts.material),
				},
			],
			...rowsIf(nodeCosts !== undefined && nodeCosts.material > 0 && multiplicity > 1, [
				[
					{
						align: "left",
						text: `${tr("MaterialCosts")} (${tr("piece")})`,
					},
					{
						align: "right",
						text: nodeCosts === undefined ? "N/A" : currencyString(nodeCosts.material / multiplicity),
					},
				],
			]),
			[
				{
					align: "left",
					text: tr("SetupCosts"),
				},
				{
					align: "right",
					text: nodeCosts === undefined ? "N/A" : currencyString(nodeCosts.setup),
				},
			],
			[
				{
					align: "left",
					text: tr("UnitCosts"),
				},
				{
					align: "right",
					text: nodeCosts === undefined ? "N/A" : currencyString(nodeCosts.unit),
				},
			],
			...rowsIf(nodeCosts !== undefined && nodeCosts.unit > 0 && multiplicity > 1, [
				[
					{
						align: "left",
						text: `${tr("UnitCosts")} (${tr("piece")})`,
					},
					{
						align: "right",
						text: nodeCosts === undefined ? "N/A" : currencyString(nodeCosts.unit / multiplicity),
					},
				],
			]),
		],
	});

	const consumablesHtml = ((): string => {
		if (nodeTimes === undefined) {
			return "";
		}

		const processId = wsi4.node.processId(vertex);
		if (processId.length === 0) {
			return "";
		}

		// unit time is assumed to entail the underlying multiplicity
		const overallUnitTimeInSeconds = nodeTimes.unit;

		const consumableConsumptions = consumableConsumptionsForVertex(vertex, overallUnitTimeInSeconds);
		if (consumableConsumptions.length === 0) {
			return "";
		}

		const consumableCosts = computeConsumableCosts(consumableConsumptions);
		return tableObjToHtml({
			td: [
				[
					{
						align: "left",
						text: wsi4.tables.name("consumable"),
						colspan: 3,
					},
				],
				...consumableCosts.map((obj, index): HtmlTableCell[] => ([
					{
						align: "left",
						text: obj.consumable.name,
					},
					{
						align: "right",
						text: itemAt(index, consumableConsumptions).consumedUnits.toFixed(3) + ` [${obj.consumable.unit}]`,
					},
					{
						align: "right",
						text: wsi4.util.toCurrencyString(obj.costs, 2),
					},
				])),
			],
		});

	})();

	if (consumablesHtml.length > 0) {
		return timesAndCostsHtml + "<br><br>" + consumablesHtml;
	} else {
		return timesAndCostsHtml;
	}
}

function computeNodeWstSpecificText(vertex: Vertex, manufacturingStateCache: ManufacturingStateCache, calcCache: CalcCache) {
	const commonDetails = computeCommonDetails(vertex, manufacturingStateCache, calcCache);
	const specificDetails = (() => {
		switch (wsi4.node.workStepType(vertex)) {
			case WorkStepType.sheetCutting: return computeSheetCuttingNodeText(vertex);
			case WorkStepType.sheetBending: return computeSheetBendingNodeText(vertex);
			case WorkStepType.sheet: return computeSheetNodeText(vertex);
			case WorkStepType.joining:
			case WorkStepType.packaging:
			case WorkStepType.transform: return computeProcessSpecificText(vertex);
			case WorkStepType.userDefined: return computeProcessSpecificText(vertex);
			case WorkStepType.userDefinedBase: return computeProcessSpecificText(vertex);
			case WorkStepType.tubeCutting: return computeTubeCuttingNodeText(vertex);
			case WorkStepType.tube: return computeTubeNodeText(vertex);
			case WorkStepType.undefined: return wsi4.throwError("Unexpected WorkStepType: " + wsi4.node.workStepType(vertex));
		}
	})();
	return commonDetails + (specificDetails === "" ? "" : "<br><br>" + specificDetails);
}

function computeNodeToolTipText(vertex: Vertex) {
	if (!wsi4.util.isDebug()) {
		return "";
	}
	return "Vertex: " + wsi4.util.toNumber(vertex).toString() + "\n" + wsi4.node.dump(vertex);
}

function computeNodeRepresentation(vertex: Vertex, processTable: readonly Readonly<Process>[]): PrivateNodeRepresentation {
	const sources = wsi4.graph.sources(vertex);
	const targets = wsi4.graph.targets(vertex);

	const wst = wsi4.node.workStepType(vertex);
	const processType = wsi4.node.processType(vertex);
	const processId = wsi4.node.processId(vertex);
	const process = processTable.find(row => row.identifier === processId);
	const nodeUserData = wsi4.node.userData(vertex);

	return {
		nodeId: wsi4.node.nodeId(vertex),
		rootId: wsi4.node.rootId(vertex),
		sourceNodeIds: sources.map(vertex => wsi4.node.nodeId(vertex)),
		targetNodeIds: targets.map(vertex => wsi4.node.nodeId(vertex)),
		workStepType: wst,
		processType: processType,
		processId: processId,
		processName: process !== undefined
			? process.name
			: processType !== "undefined"
				? wsi4.util.translate(processType)
				: "N/A",
		comment: nodeUserDatumImpl("comment", nodeUserData) ?? "",
		hasTwoDimInput: wsi4.node.hasTwoDimInput(vertex),
		hasCalcOverride: hasManufacturingCostOverride(vertex, nodeUserData)
			|| nodeUserDatumImpl("userDefinedMaterialCostsPerPiece", nodeUserData) !== undefined
			|| nodeUserDatumImpl("userDefinedSetupTime", nodeUserData) !== undefined
			|| nodeUserDatumImpl("userDefinedUnitTimePerPiece", nodeUserData) !== undefined,
	};
}

function computeNodeRepresentations() {
	const vertices = wsi4.graph.vertices();
	const processTable = getTable(TableType.process);
	return vertices.map(vertex => computeNodeRepresentation(vertex, processTable));
}

function computeGuiArticle(vertices: Array<Vertex>): PrivateArticleRepresentation {
	const articleUserData = wsi4.node.articleUserData(front(vertices));
	const signatureVertex = getArticleSignatureVertex(vertices);
	return {
		name: articleUserDatumImpl("name", articleUserData) ?? `<${wsi4.util.translate(wsi4.node.processType(back(vertices)))}>`,
		externalPartNumber: articleUserDatumImpl("externalPartNumber", articleUserData) ?? "",
		externalDrawingNumber: articleUserDatumImpl("externalDrawingNumber", articleUserData) ?? "",
		externalRevisionNumber: articleUserDatumImpl("externalRevisionNumber", articleUserData) ?? "",
		comment: articleUserDatumImpl("comment", articleUserData) ?? "",
		multiplicity: getFixedMultiplicity(back(vertices)),
		nodeId: wsi4.node.nodeId(signatureVertex),
		rootId: wsi4.node.rootId(signatureVertex),
		nodeIds: vertices.map(vertex => wsi4.node.nodeId(vertex)),
	};
}

function computeGuiArticles() {
	return wsi4.graph.articles()
		.map(vertices => computeGuiArticle(vertices));
}

function computeAssemblyResources(vertices: readonly Vertex[], getAssembly: (vertex: Vertex) => Assembly | undefined): PrivateAssemblyResourceEntry[] {
	return vertices.filter(vertex => getAssembly(vertex) !== undefined)
		.map(vertex => {
			const assembly = getAssembly(vertex);
			assert(assembly !== undefined, "Expecting valid assembly");
			return {
				nodeId: wsi4.node.nodeId(vertex),
				assembly: assembly,
			};
		});
}

function computeInputAssemblyResources(outdatedVertices: readonly Vertex[]): PrivateAssemblyResourceEntry[] {
	return computeAssemblyResources(outdatedVertices, vertex => wsi4.node.inputAssembly(vertex));
}

function computeOutputAssemblyResources(outdatedVertices: readonly Vertex[]): PrivateAssemblyResourceEntry[] {
	return computeAssemblyResources(outdatedVertices, vertex => wsi4.node.assembly(vertex));
}

interface NodeIdAnd<Future> {
	nodeId: GraphNodeId;
	future: Future;
}

function computeRelevantVertices(preds: readonly ((vertex: Vertex, outdatedVertices: readonly Vertex[]) => boolean)[], outdatedVertices: readonly Vertex[]): Vertex[] {
	return wsi4.graph.vertices()
		.filter(vertex => preds.some(pred => pred(vertex, outdatedVertices)));
}

function targetMultPotentiallyOutdated(inputVertex: Vertex, outdatedVertices: readonly Vertex[]): boolean {
	return wsi4.graph.reachable(inputVertex)
		.filter(vertex => wsi4.node.isImport(vertex))
		.some(vertex => outdatedVertices.some(other => isEqual(vertex, other)));
}

function selfIsOutdated(lhs: Vertex, outdatedVertices: readonly Vertex[]): boolean {
	return outdatedVertices.some(rhs => isEqual(lhs, rhs));
}

function createPngFutures(outdatedVertices: readonly Vertex[]): NodeIdAnd<ArrayBufferFuture>[] {
	return createPngFuturesImpl(outdatedVertices, defaultPngResolution())
		.map((obj): NodeIdAnd<ArrayBufferFuture> => ({
			nodeId: wsi4.node.nodeId(obj.vertex),
			future: obj.data,
		}));
}

function computePngResources(nodeIdAndFutures: readonly Readonly<NodeIdAnd<ArrayBufferFuture>>[]): PrivateBinaryResourceEntry[] {
	return nodeIdAndFutures.map(obj => ({
		nodeId: obj.nodeId,
		data: getFutureResult<"arrayBuffer">(obj.future),
	}));
}

function createTechnicalDrawingsFutures(outdatedVertices: readonly Vertex[]): NodeIdAnd<MeasurementScenesFuture>[] {
	return outdatedVertices.filter(vertex => wsi4.node.workStepType(vertex) === WorkStepType.sheetBending)
		.map(vertex => ({
			nodeId: wsi4.node.nodeId(vertex),
			future: wsi4.node.asyncBendMeasurementScenes(vertex, 16, defaultSvgResolution()),
		}));
}

function createDefaultSceneFutures(outdatedVertices: readonly Vertex[]): NodeIdAnd<SceneFuture>[] {
	return outdatedVertices.filter(vertex => (wsi4.node.twoDimRep(vertex) !== undefined) !== (wsi4.node.layered(vertex) !== undefined))
		.map(vertex => ({
			nodeId: wsi4.node.nodeId(vertex),
			future: asyncSceneForVertex(vertex, {
				cuttingContours: true,
				engravings: true,
				bendLineShowLabels: true,
				bendLineShowAffectedSegments: true,
				tubeCuttingShowVirtualCuts: true,
			}),
		}));
}

function computeTechnicalDrawingScenes(nodeIdAndFutures: readonly Readonly<NodeIdAnd<MeasurementScenesFuture>>[]): PrivateMeasurementScenesResourceEntry[] {
	return nodeIdAndFutures.map(obj => ({
		nodeId: obj.nodeId,
		data: getFutureResult<"measurementScenes">(obj.future),
	}));
}

function computeDefaultScenes(nodeIdAndFutures: readonly Readonly<NodeIdAnd<SceneFuture>>[]): PrivateSceneResourceEntry[] {
	return nodeIdAndFutures.map(obj => ({
		nodeId: obj.nodeId,
		data: getFutureResult<"scene">(obj.future),
	}));
}

function computeBendZoneScenes(outdatedVertices: readonly Vertex[], manufacturingStateCache: ManufacturingStateCache): PrivateSceneResourceEntry[] {
	return outdatedVertices.filter(vertex => wsi4.node.twoDimRep(vertex) !== undefined)
		.filter(vertex => isExportReady(vertex, manufacturingStateCache))
		.map(vertex => ({
			nodeId: wsi4.node.nodeId(vertex),
			data: sceneForVertex(vertex, {
				sheetBendingBendZones: true,
			}),
		}));
}

function computeTubeOutlineScenes(outdatedVertices: readonly Vertex[]): PrivateSceneResourceEntry[] {
	return outdatedVertices
		.filter(vertex => wsi4.node.workStepType(vertex) === WorkStepType.tubeCutting)
		.map(vertex => ({
			nodeId: wsi4.node.nodeId(vertex),
			data: sceneForVertex(vertex, {
				tubeCuttingShowTubeContours: true,
			}),
		}));
}

function computeDieAffectZoneScenes(outdatedVertices: readonly Vertex[]): PrivateSceneResourceEntry[] {
	return outdatedVertices.filter(vertex => wsi4.node.twoDimRep(vertex) !== undefined)
		.filter(vertex => wsi4.node.processType(vertex) === ProcessType.dieBending)
		.map(vertex => ({
			nodeId: wsi4.node.nodeId(vertex),
			data: sceneForVertex(vertex, {
				sheetBendingLowerDieAffectZones: true,
			}),
		}));
}

function computeEditingNotesResources(outdatedVertices: readonly Vertex[]): PrivateEditingNotesResourceEntry[] {
	return outdatedVertices.map(v => ({
		nodeId: wsi4.node.nodeId(v),
		notes: wsi4.node.editingState(v).map(entry => ({
			type: entry.type,
			message: (() => {
				const fieldName = ((): string => {
					switch (entry.content.nodeDatumType) {
						case "sheetMaterialId": return wsi4.tables.name("sheetMaterial");
						case "sheetTappingData": return wsi4.tables.name("screwThread");
						case "tubeMaterialId": return wsi4.tables.name("tubeMaterial");
						case "tubeSpecificationId": return wsi4.tables.name("tubeSpecification");
						case "processId": return wsi4.util.translate("process");
						case "dieChoiceMap": return wsi4.util.translate("die_choice");
					}
				})();
				switch (entry.type) {
					case "missingDatum": return fieldName;
					case "invalidDatum": return `${fieldName}: ${entry.content.value}`;
				}
			})(),
		})),
	}));
}

function computeManufacturingStateResources(outdatedVertices: readonly Vertex[], manufacturingStateCache: ManufacturingStateCache): PrivateManufacturingStateResourceEntry[] {
	return outdatedVertices.map(vertex => {
		const actualState = (() => {
			const state = computeActualManufacturingStateCached(
				vertex,
				guiReplyStateLevelMap,
				guiConstraintLevelMap,
				manufacturingStateCache,
			);
			assert(state !== undefined, "Expecting valid map entry");
			return state;
		})();

		const virtualState = computeVirtualManufacturingState(
			vertex,
			guiReplyStateLevelMap,
			guiConstraintLevelMap,
		);

		return {
			nodeId: wsi4.node.nodeId(vertex),
			actualState: actualState,
			virtualState: virtualState,
		};
	});
}

function computeSubGraphScaleValueText(article: readonly Vertex[], calcCache: CalcCache, manufacturingStateCache: ManufacturingStateCache): string {
	if (collectSubGraphVertices(article)
		.some(v => !isExportReady(v, manufacturingStateCache))) {
		return "";
	}

	const calcConfig = readCalcSettings();
	if (calcConfig.baseScaleValues.length === 0) {
		return "";
	}

	const scaleValues = (() => {
		const multiplicity = wsi4.node.multiplicity(back(article));
		return computeScaleValues(multiplicity, calcConfig);
	})();

	const tableHtml = tableObjToHtml({
		td: [
			[
				{
					align: "left",
					text: wsi4.util.translate("scale_size"),
				},
				{
					align: "right",
					text: wsi4.util.translate("manufacturing_costs_abbr") + "¹ / " + wsi4.util.translate("piece"),
				},
				{
					align: "right",
					text: wsi4.util.translate("manufacturing_costs_abbr") + "¹",
				},
				{
					align: "right",
					text: wsi4.util.translate("manufacturing_costs_abbr") + "² / " + wsi4.util.translate("piece"),
				},
				{
					align: "right",
					text: wsi4.util.translate("manufacturing_costs_abbr") + "²",
				},
				{
					align: "right",
					text: wsi4.util.translate("net_selling_price_abbr") + " / " + wsi4.util.translate("piece"),
				},
				{
					align: "right",
					text: wsi4.util.translate("net_selling_price_abbr"),
				},
			],
			...scaleValues.map((scaleValue): HtmlTableCell[] => {
				const manPriceExclSurcharges = computeRecursiveArticleManufacturingPriceExclSurcharges(article, scaleValue, calcCache);
				const manPriceInclSurcharges = computeRecursiveArticleManufacturingPriceInclSurcharges(article, scaleValue, calcCache);
				const sellingPrice = computeRecursiveArticleScaleSellingPrice(article, scaleValue, calcCache);
				return [
					{
						align: "right",
						text: scaleValue.toFixed(0),
					},
					{
						align: "right",
						text: manPriceExclSurcharges === undefined ? "N/A" : currencyString(manPriceExclSurcharges / scaleValue),
					},
					{
						align: "right",
						text: manPriceExclSurcharges === undefined ? "N/A" : currencyString(manPriceExclSurcharges),
					},
					{
						align: "right",
						text: manPriceInclSurcharges === undefined ? "N/A" : currencyString(manPriceInclSurcharges / scaleValue),
					},
					{
						align: "right",
						text: manPriceInclSurcharges === undefined ? "N/A" : currencyString(manPriceInclSurcharges),
					},
					{
						align: "right",
						text: sellingPrice === undefined ? "N/A" : currencyString(sellingPrice / scaleValue),
					},
					{
						align: "right",
						text: sellingPrice === undefined ? "N/A" : currencyString(sellingPrice),
					},
				];
			}),
		],
	});
	const legendHtml = "<small>"
		+ "¹: " + wsi4.util.translate("excl_surcharges") + "<br>"
		+ "²: " + wsi4.util.translate("incl_surcharges") + "<br>"
		+ "</small>";
	return tableHtml + "<br>" + legendHtml;
}

function sheetTestReportTargetOutdated(inputVertex: Vertex, outdatedVertices: readonly Vertex[]): boolean {
	return wsi4.node.workStepType(inputVertex) === WorkStepType.sheet && wsi4.graph.reachable(inputVertex)
		.filter(vertex => isCompatibleToNodeUserDataEntry("testReportRequired", vertex))
		.some(vertex => selfIsOutdated(vertex, outdatedVertices));
}

function computeNodeTextResources(outdatedVertices: readonly Vertex[], calcCache: CalcCache, manufacturingStateCache: ManufacturingStateCache): PrivateNodeTextResourceEntry[] {
	const relevantVertices = computeRelevantVertices(
		[
			selfIsOutdated,
			targetMultPotentiallyOutdated,
			sheetTestReportTargetOutdated,
			associatedSheetIsOutdated,
		],
		outdatedVertices,
	);
	return relevantVertices.map(vertex => ({
		nodeId: wsi4.node.nodeId(vertex),
		brief: computeNodeBaseText(vertex, manufacturingStateCache, calcCache),
		details: computeNodeWstSpecificText(vertex, manufacturingStateCache, calcCache),
		toolTip: computeNodeToolTipText(vertex),
	}));
}

function computeReducedNodeTextResources(outdatedVertices: readonly Vertex[]): PrivateNodeTextResourceEntry[] {
	const relevantVertices = computeRelevantVertices(
		[
			selfIsOutdated,
			targetMultPotentiallyOutdated,
			sheetTestReportTargetOutdated,
		],
		outdatedVertices,
	);
	return relevantVertices.map(vertex => ({
		nodeId: wsi4.node.nodeId(vertex),
		brief: `<strong>${wsi4.util.translate(wsi4.node.processType(vertex))}</strong>`,
		details: "",
		toolTip: "",
	}));
}

function associatedSheetIsOutdated(inputVertex: Vertex, outdatedVertices: readonly Vertex[]): boolean {
	return wsi4.graph.reaching(inputVertex)
		.filter(vertex => wsi4.node.workStepType(vertex) === WorkStepType.sheet)
		.some(vertex => selfIsOutdated(vertex, outdatedVertices));
}

function articleCalcOutdated(article: readonly Vertex[], outdatedVertices: readonly Vertex[]): boolean {
	return article.some(vertex => selfIsOutdated(vertex, outdatedVertices))
		|| wsi4.graph.reachable(back(article))
			.filter(vertex => wsi4.node.isImport(vertex))
			.some(vertex => selfIsOutdated(vertex, outdatedVertices))
		|| (() => {
			const signatureVertex = getArticleSignatureVertex(article);
			const wst = wsi4.node.workStepType(signatureVertex);
			return ((wst === WorkStepType.sheetCutting || wst === WorkStepType.sheetBending) && associatedSheetIsOutdated(signatureVertex, outdatedVertices))
				|| (wst === WorkStepType.joining && wsi4.graph.reaching(signatureVertex)
					.some(vertex => selfIsOutdated(vertex, outdatedVertices)));
		})();
}

function computeArticleTextResources(outdatedVertices: readonly Vertex[], calcCache: CalcCache, manufacturingStateCache: ManufacturingStateCache): PrivateArticleTextResourceEntry[] {
	return wsi4.graph.articles()
		.filter(article => articleCalcOutdated(article, outdatedVertices))
		.map(article => {
			const signatureVertex = getArticleSignatureVertex(article);
			const detailsText = (() => {
				if (isSemimanufacturedArticle(article)) {
					return "";
				} else {
					assert(isComponentArticle(article) || isJoiningArticle(article), "Unexpected article structure");
					return computeSubGraphScaleValueText(article, calcCache, manufacturingStateCache);
				}
			})();

			return {
				nodeId: wsi4.node.nodeId(signatureVertex),
				details: detailsText,
			};
		});
}

function computeNodeCalcData(
	outdatedVertices: readonly Vertex[],
	calcCache: CalcCache,
	manufacturingStateCache: ManufacturingStateCache,
): PrivateNodeCalcDataResourceEntry[] {
	const relevantVertices = computeRelevantVertices(
		[
			selfIsOutdated,
			targetMultPotentiallyOutdated,
			sheetTestReportTargetOutdated,
			associatedSheetIsOutdated,
		],
		outdatedVertices,
	);
	const surchargeTable = getTable(TableType.surcharge);
	return relevantVertices.map(vertex => {
		const exportReady = isExportReady(vertex, manufacturingStateCache);
		const hasManSellingPrice = hasManufacturingCostOverride(vertex);
		const multiplicity = getFixedMultiplicity(vertex);
		const times = (() => {
			if (!exportReady || hasManSellingPrice) {
				return undefined;
			} else {
				return computeNodeTimes(vertex, multiplicity, calcCache);
			}
		})();
		const costs = (() => {
			if (!exportReady || hasManufacturingCostOverride(vertex)) {
				return undefined;
			} else {
				return computeNodeCosts(vertex, multiplicity, calcCache);
			}
		})();
		const manufacturingPriceExclSurcharges = computeManufacturingPriceExclSurcharges(costs);
		const manufacturingPriceInclSurcharges = (() => {
			if (exportReady || hasManufacturingCostOverride(vertex)) {
				return computeNodeManufacturingPriceInclSurcharges(
					vertex,
					multiplicity,
					calcCache,
				);
			} else {
				return undefined;
			}
		})();
		const sellingPrice = guiOnlyApplyGlobalSurcharges(
			manufacturingPriceInclSurcharges,
			surchargeTable,
		);
		const o: PrivateNodeCalcDataResourceEntry = {
			nodeId: wsi4.node.nodeId(vertex),
		};
		if (times !== undefined) {
			o.setupTime = times.setup;
			o.unitTime = times.unit;
		}
		if (costs !== undefined) {
			o.materialCosts = costs.material;
		}
		if (manufacturingPriceExclSurcharges !== undefined) {
			o.manufacturingPriceExclSurcharges = manufacturingPriceExclSurcharges;
		}
		if (manufacturingPriceInclSurcharges !== undefined) {
			o.manufacturingPriceInclSurcharges = manufacturingPriceInclSurcharges;
		}
		if (sellingPrice !== undefined) {
			o.sellingPrice = sellingPrice;
		}
		return o;
	});
}

function computeArticleCalcData(
	outdatedVertices: readonly Vertex[],
	calcCache: CalcCache,
	manufacturingStateCache: ManufacturingStateCache,
): PrivateArticleCalcDataResourceEntry[] {
	return wsi4.graph.articles()
		.filter(article => articleCalcOutdated(article, outdatedVertices))
		.map((article): PrivateArticleCalcDataResourceEntry => {
			const signatureVertex = getArticleSignatureVertex(article);
			const articleIsExportReady = article.every(vertex => isExportReady(vertex, manufacturingStateCache));

			const subGraphIsExportReady = articleIsExportReady && wsi4.graph.reaching(article[0]!)
				.every(vertex => isExportReady(vertex, manufacturingStateCache));

			const multiplicity = getFixedMultiplicity(article[0]!);
			const approxSemimanufacturedShare = (() => {
				if (hasSheetSourceArticle(article) || hasTubeSourceArticle(article)) {
					return wsi4.graph.semimanufacturedSourceShare(front(article));
				} else {
					return undefined;
				}
			})();
			const manufacturingPriceExclSurcharges = (() => {
				if (articleIsExportReady) {
					return computeArticleManufacturingPriceExclSurcharges(article, multiplicity, calcCache);
				} else {
					return undefined;
				}
			})();
			const manufacturingPriceInclSurcharges = (() => {
				if (articleIsExportReady) {
					return computeArticleManufacturingPriceInclSurcharges(article, multiplicity, calcCache);
				} else {
					return undefined;
				}
			})();
			const recursiveManufacturingPriceExclSurcharges = (() => {
				if (subGraphIsExportReady) {
					return computeRecursiveArticleManufacturingPriceExclSurcharges(article, multiplicity, calcCache);
				} else {
					return undefined;
				}
			})();
			const recursiveManufacturingPriceInclSurcharges = (() => {
				if (subGraphIsExportReady) {
					return computeRecursiveArticleManufacturingPriceInclSurcharges(article, multiplicity, calcCache);
				} else {
					return undefined;
				}
			})();
			const sellingPrice = (() => {
				if (articleIsExportReady) {
					return articleSellingPriceWithSemimanufacturedShare(
						article,
						multiplicity,
						calcCache,
					);
				} else {
					return undefined;
				}
			})();
			const recursiveSellingPrice = (() => {
				if (subGraphIsExportReady) {
					return computeRecursiveArticleScaleSellingPrice(
						article,
						multiplicity,
						calcCache,
					);
				} else {
					return undefined;
				}
			})();
			const o: PrivateArticleCalcDataResourceEntry = {
				nodeId: wsi4.node.nodeId(signatureVertex),
			};
			if (approxSemimanufacturedShare !== undefined) {
				o.approxSemimanufacturedShare = approxSemimanufacturedShare;
			}
			if (manufacturingPriceExclSurcharges !== undefined) {
				o.manufacturingPriceExclSurcharges = manufacturingPriceExclSurcharges;
			}
			if (manufacturingPriceInclSurcharges !== undefined) {
				o.manufacturingPriceInclSurcharges = manufacturingPriceInclSurcharges;
			}
			if (recursiveManufacturingPriceExclSurcharges !== undefined) {
				o.recursiveManufacturingPriceExclSurcharges = recursiveManufacturingPriceExclSurcharges;
			}
			if (recursiveManufacturingPriceInclSurcharges !== undefined) {
				o.recursiveManufacturingPriceInclSurcharges = recursiveManufacturingPriceInclSurcharges;
			}
			if (sellingPrice !== undefined) {
				o.sellingPrice = sellingPrice;
			}
			if (recursiveSellingPrice !== undefined) {
				o.recursiveSellingPrice = recursiveSellingPrice;
			}
			return o;
		});
}

function getMaterialIfAny(signatureVertex: Vertex): string | undefined {
	const wst = wsi4.node.workStepType(signatureVertex);
	if (wst === WorkStepType.sheet || wst === WorkStepType.sheetCutting || wst === WorkStepType.sheetBending) {
		const materials = getTable(TableType.sheetMaterial);
		const materialId = getAssociatedSheetMaterialId(signatureVertex);
		return materials.find(row => row.identifier === materialId)?.name ?? "N/A";
	} else if (wst === WorkStepType.tube || wst === WorkStepType.tubeCutting) {
		const materials = getTable(TableType.tubeMaterial);
		const materialId = getAssociatedTubeMaterialId(signatureVertex);
		return materials.find(row => row.identifier === materialId)?.name ?? "N/A";
	} else if (wst === WorkStepType.userDefinedBase) {
		const materialId = nodeUserDatum("purchasePartMaterialId", signatureVertex);
		return getTable("purchasePartMaterial").find(row => row.identifier === materialId)?.name ?? "N/A";
	} else {
		return undefined;
	}
}

function computeArticleSignatureNodeData(outdatedVertices: readonly Vertex[]): PrivateArticleSignatureNodeResourceEntry[] {
	return wsi4.graph.articles()
		.map(article => getArticleSignatureVertex(article))
		.filter(lhs => outdatedVertices.some(rhs => isEqual(lhs, rhs)))
		.map(signatureVertex => {
			const o: PrivateArticleSignatureNodeResourceEntry = {
				nodeId: wsi4.node.nodeId(signatureVertex),
				material: getMaterialIfAny(signatureVertex) ?? "",
			};
			{
				const mass = computeNodeMass(signatureVertex);
				if (mass !== undefined) {
					o.mass = mass;
				}
			}
			{
				const sheetThickness = wsi4.node.sheetThickness(signatureVertex);
				if (sheetThickness !== undefined) {
					o.sheetThickness = sheetThickness;
				}
			}
			return o;
		});
}

function computeNodeProblematicGeometryData(outdatedVertices: readonly Vertex[]): PrivateProblematicGeometryResourceEntry[] {
	return outdatedVertices.map(vertex => ({
		nodeId: wsi4.node.nodeId(vertex),
		hasProblematicGeometries: wsi4.node.hasProblematicGeometries(vertex),
	}));
}

function isSheetRelatedWstAvailable() {
	const processTable = getTable(TableType.process);
	const process = findActiveProcess(p => p.type === "laserSheetCutting" || p.type === "dieBending", processTable);
	return process !== undefined;
}

function computeNodeForceSheetMetalPartData(outdatedVertices: readonly Vertex[]): PrivateForceSheetMetalPartResourceEntry[] {
	if (isSheetRelatedWstAvailable()) {
		return outdatedVertices.map(vertex => ({
			nodeId: wsi4.node.nodeId(vertex),
			canForceFuture: wsi4.node.asyncCanForceSheetMetalPart(vertex),
		}));
	} else {
		return [];
	}
}

function computeGuiResources(
	outdatedVertices: readonly Vertex[],
	pngFutures: readonly Readonly<NodeIdAnd<ArrayBufferFuture>>[],
	defaultSceneFutures: readonly Readonly<NodeIdAnd<SceneFuture>>[],
	technicalDrawingFutures: readonly Readonly<NodeIdAnd<MeasurementScenesFuture>>[],
	calcCache: CalcCache,
	manufacturingStateCache: ManufacturingStateCache,
	reducedGuiEnabled = false,
): PrivateResources {
	const inputAssemblies = computeInputAssemblyResources(outdatedVertices);
	const outputAssemblies = computeOutputAssemblyResources(outdatedVertices);
	const bendZoneScenes = computeBendZoneScenes(outdatedVertices, manufacturingStateCache);
	const tubeOutlineScenes = computeTubeOutlineScenes(outdatedVertices);
	const lowerDieAffectZoneScenes = computeDieAffectZoneScenes(outdatedVertices);

	// As long as there are editing issues the manufacturing state is omitted since subsequent issues are to be expected.
	const editingNotes = computeEditingNotesResources(outdatedVertices);
	const outdatedMfgStateVertices = outdatedVertices.filter(v => {
		const nodeId = wsi4.node.nodeId(v);
		return editingNotes.every(entry => !isEqual(nodeId, entry.nodeId) || entry.notes.length === 0);
	});
	const manufacturingStates = computeManufacturingStateResources(outdatedMfgStateVertices, manufacturingStateCache);

	const nodeTexts = reducedGuiEnabled ? computeReducedNodeTextResources(outdatedVertices) : computeNodeTextResources(outdatedVertices, calcCache, manufacturingStateCache);
	const articleTexts = reducedGuiEnabled ? [] : computeArticleTextResources(outdatedVertices, calcCache, manufacturingStateCache);
	const nodeCalcData = reducedGuiEnabled ? [] : computeNodeCalcData(outdatedVertices, calcCache, manufacturingStateCache);
	const articleCalcData = reducedGuiEnabled ? [] : computeArticleCalcData(outdatedVertices, calcCache, manufacturingStateCache);
	const articleSignatureNodeData = computeArticleSignatureNodeData(outdatedVertices);
	const nodeProblematicGeometryData = computeNodeProblematicGeometryData(outdatedVertices);
	const nodeForceSheetMetalPartData = computeNodeForceSheetMetalPartData(outdatedVertices);

	const defaultScenes = computeDefaultScenes(defaultSceneFutures);
	const pngs = computePngResources(pngFutures);
	const technicalDrawingScenes = reducedGuiEnabled ? [] : computeTechnicalDrawingScenes(technicalDrawingFutures);

	return {
		pngs: pngs,
		inputAssemblies: inputAssemblies,
		outputAssemblies: outputAssemblies,
		defaultScenes: defaultScenes,
		bendZoneScenes: bendZoneScenes,
		lowerDieAffectZoneScenes: lowerDieAffectZoneScenes,
		technicalDrawingScenes: technicalDrawingScenes,
		tubeOutlineScenes: tubeOutlineScenes,
		editingNotes: editingNotes,
		manufacturingStates: manufacturingStates,
		nodeTexts: nodeTexts,
		articleTexts: articleTexts,
		articleCalcData: articleCalcData,
		articleSignatureNodeData: articleSignatureNodeData,
		nodeCalcData: nodeCalcData,
		nodeProblematicGeometryData: nodeProblematicGeometryData,
		nodeForceSheetMetalPartData: nodeForceSheetMetalPartData,
	};
}

function computeSourceMults(): PrivateSourceMultEntry[] {
	const result: PrivateSourceMultEntry[] = [];
	wsi4.graph.articles()
		.forEach(sourceArticle => {
			const sourceArticleSigVertex = getArticleSignatureVertex(sourceArticle);
			const sourceArticleLastVertex = sourceArticle[sourceArticle.length - 1]!;
			const targets = wsi4.graph.targets(sourceArticleLastVertex);
			targets.forEach(targetArticleFirstVertex => {
				const targetArticle = wsi4.graph.article(targetArticleFirstVertex);
				const targetArticleSigVertex = getArticleSignatureVertex(targetArticle);
				const mult = wsi4.graph.sourceMultiplicity(
					sourceArticleLastVertex,
					targetArticleFirstVertex,
				);
				result.push({
					sourceArticleNodeId: wsi4.node.nodeId(sourceArticleSigVertex),
					targetArticleNodeId: wsi4.node.nodeId(targetArticleSigVertex),
					multiplicity: mult,
				});
			});
		});
	return result;
}

function nodeIdsToVertices(nodeIds: Number[]): Vertex[] {
	// Invalid node ids are ignored.
	// If an id is invalid this is not considered a bug because the previous graph representation
	// might be outdated and the new representation will be computed after calling this function.
	const idVertexMap = wsi4.graph.vertices()
		.reduce((acc, vertex) => {
			const key = wsi4.util.toKey(wsi4.node.nodeId(vertex));
			acc.set(key, vertex);
			return acc;
		}, new Map<string, Vertex>());
	return nodeIds.map(nodeId => idVertexMap.get(nodeId.toString()))
		.filter((vertex): vertex is Vertex => vertex !== undefined);
}

function computeGuiDataContentEmptyGraph(): PrivateGuiDataGraphRep {
	return {
		nodes: [],
		articles: [],
		sourceMults: [],
		resources: {
			pngs: [],
			inputAssemblies: [],
			outputAssemblies: [],
			defaultScenes: [],
			bendZoneScenes: [],
			lowerDieAffectZoneScenes: [],
			technicalDrawingScenes: [],
			tubeOutlineScenes: [],
			editingNotes: [],
			manufacturingStates: [],
			nodeTexts: [],
			articleTexts: [],
			articleCalcData: [],
			articleSignatureNodeData: [],
			nodeCalcData: [],
			nodeProblematicGeometryData: [],
			nodeForceSheetMetalPartData: [],
		},
		data: {
			projectName: projectName(),
		},
	};
}

function computeGuiDataContentNonEmptyGraph(outdatedVertices: readonly Vertex[]): PrivateGuiDataGraphRep {
	// For now this is an internal development feature so *not* utilizing gui_local_settings.ts for now
	const reducedGuiEnabled = readSetting("reduced_gui_enabled", false, isBoolean);

	const pngFutures = createPngFutures(outdatedVertices);
	const defaultSceneFutures = createDefaultSceneFutures(outdatedVertices);
	const technicalDrawingFutures = reducedGuiEnabled ? [] : createTechnicalDrawingsFutures(outdatedVertices);
	const calcCache = createCalcCache();
	const manufacturingStateCache = createManufacturingStateCache();

	const sourceMults = computeSourceMults();
	const result: PrivateGuiDataGraphRep = {
		nodes: computeNodeRepresentations(),
		articles: computeGuiArticles(),
		sourceMults: sourceMults,
		resources: computeGuiResources(
			outdatedVertices,
			pngFutures,
			defaultSceneFutures,
			technicalDrawingFutures,
			calcCache,
			manufacturingStateCache,
			reducedGuiEnabled,
		),
		data: {
			projectName: projectName(),
		},
	};
	return result;
}

function isOutdatedVertex(inputVertex: Vertex, knownVertices: readonly Vertex[]): boolean {
	return knownVertices.every(knownVertex => !isEqual(inputVertex, knownVertex));
}

export function computeGraphRepresentation(knownNodeIds: unknown[]): void {
	assert(isArray(knownNodeIds, isNumber), "Expecting array of numbers");
	const outdatedVertices = (() => {
		const knownVertices = nodeIdsToVertices(knownNodeIds);
		return wsi4.graph.vertices()
			.filter(vertex => isOutdatedVertex(vertex, knownVertices));
	})();

	const vertices = wsi4.graph.vertices();
	const graphRep: PrivateGuiData = (() => {
		if (vertices.length === 0) {
			// Special case is required to ensure the widget is cleared when the last vertices are deleted
			return {
				type: PrivateGuiDataType.graphRep,
				content: computeGuiDataContentEmptyGraph(),
			};
		} else {
			return {
				type: PrivateGuiDataType.graphRep,
				content: computeGuiDataContentNonEmptyGraph(outdatedVertices),
			};
		}
	})();
	wsi4.internal.emitGuiData(graphRep);
}
