import {
	readCalcSettings,
} from "qrc:/js/lib/calc_settings";
import {
	getFutureResult,
} from "qrc:/js/lib/future";
import {
	Color,
	PrivateGuiDataType,
	ProcessType,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	isProcess,
	isTubeProfileGeometryCircular,
	isTubeProfileGeometryRectangular,
} from "qrc:/js/lib/generated/typeguard";
import {
	collectSubGraphVertices,
	computeNodeMass,
	computeSheetConsumption,
	getArticleSignatureVertex,
	getAssociatedSheetMaterialId,
	hasSheetSourceArticle,
	hasTubeSourceArticle,
	isComponentArticle,
	isJoiningArticle,
	isSemimanufacturedArticle,
	netTubeConsumptionForVertex,
	projectName,
	sheetCuttingChargeWeightForVertex,
	sheetIdForVertex,
} from "qrc:/js/lib/graph_utils";
import {
	isExportReadyImpl,
} from "qrc:/js/lib/manufacturing_state";
import {
	computeCommandsFaceArea,
	computeLowerDieAffectDistanceMap,
	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,
	computeInnerOuterContourLengths,
	isArray,
	isBoolean,
	isEqual,
	isNumber,
	readSetting,
} from "qrc:/js/lib/utils";
import {
	collectNodeUserDataEntries,
	hasManufacturingCostOverride,
	isCompatibleToNodeUserDataEntry,
	nodeUserDatumOrDefault,
	userDataAccess,
} from "qrc:/js/lib/userdata_utils";
import {
	findActiveProcess,
} from "qrc:/js/lib/process";
import {
	tubeCuttingLayerDescriptor,
	tubeCuttingLayerPaths,
} from "qrc:/js/lib/tubecutting_util";
import {
	computeActualManufacturingStateCached,
	computeVirtualManufacturingState,
	createManufacturingStateCache,
	ManufacturingStateCache,
} from "qrc:/js/lib/manufacturing_state_util";

import {
	back,
	front,
} from "qrc:/js/lib/array_util";
import {
	articleUserDatumImpl,
} from "qrc:/js/lib/article_userdata_config";
import {
	sceneForVertex,
} from "qrc:/js/lib/scene_utils";
import { nodeUserDatumImpl } from "../lib/userdata_config";
import {
	articleSellingPriceWithSemimanufacturedShare,
	computeArticleManufacturingPriceExclSurcharges,
	computeArticleManufacturingPriceInclSurcharges,
	computeManufacturingPriceExclSurcharges,
	computeNodeCosts,
	computeNodeManufacturingPriceInclSurcharges,
	computeRecursiveArticleManufacturingPriceExclSurcharges,
	computeRecursiveArticleManufacturingPriceInclSurcharges,
	computeRecursiveArticleScaleCosts,
	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;
}

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

function tableCellToHtml(cell: HtmlTableCell, tag: string): string {
	return `<${tag} align="${cell.align}">${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 table = getTable(TableType.sheetMaterial);
		const materials = collectNodeUserDataEntries("sheetMaterialId", vertex, userDataAccess.self | userDataAccess.reachable)
			.filter((lhs, index, self) => lhs !== undefined && index === self.findIndex(rhs => rhs !== undefined && lhs === rhs))
			.map(material => table.find(row => material !== undefined && row.identifier === material))
			.filter((material): material is SheetMaterial => material !== undefined)
			.map(material => material.name);
		if (materials.length === 0) {
			return [];
		}
		// undefined row is not considered an error as wsi4 file could be from another user
		return [
			{
				align: "left",
				text: wsi4.util.translate("materials"),
			},
			{
				align: "right",
				text: materials.join(", "),
			},
		];
	})());

	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(1),
				},
			];
		}
	})());

	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(),
		},
	]);

	const [
		innerContourLength,
		outerContourLength,
	] = computeInnerOuterContourLengths(twoDimRep);
	rows.push([
		{
			align: "left",
			text: wsi4.util.translate("ContourLength"),
		},
		{
			align: "right",
			text: (innerContourLength + outerContourLength).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 sheetTable = getTable(TableType.sheet);
	const sheetId = sheetIdForVertex(vertex, sheetTable);
	const totalSheetConsumption = computeSheetConsumption(vertex, sheetTable);
	const partsSheetConsumption = computeSheetConsumptionNestedParts(vertex);

	if (sheetId === undefined || totalSheetConsumption === undefined || partsSheetConsumption === undefined) {
		return "N/A";
	} else {
		const sheet = sheetTable.find(row => row.identifier === sheetId);
		assert(sheet !== undefined, "Expecting matching sheet for id " + sheetId);

		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),
					},
				],
			]),
		],
	});
	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) ?? "",
	};
}

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

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

function computeDefaultScenes(outdatedVertices: readonly Vertex[]): PrivateSceneResourceEntry[] {
	return outdatedVertices.filter(vertex => (wsi4.node.twoDimRep(vertex) !== undefined) !== (wsi4.node.layered(vertex) !== undefined))
		.map(vertex => {
			const scene = sceneForVertex(
				vertex,
				{
					bendLineShowLabels: true,
					bendLineShowAffectedSegments: true,
					tubeCuttingShowVirtualCuts: true,
				},
			);

			return {
				nodeId: wsi4.node.nodeId(vertex),
				data: scene,
			};
		});
}

function createDefaultScene(): Scene {
	return wsi4.geo.util.createScene({
		material: "",
		thickness: 0,
		identifier: "",
		comment: "",
		globalMaterial: "",

	});
}

function computeBendZoneScenes(outdatedVertices: readonly Vertex[], manufacturingStateCache: ManufacturingStateCache): PrivateSceneResourceEntry[] {
	return outdatedVertices.filter(vertex => wsi4.node.twoDimRep(vertex) !== undefined)
		.filter(vertex => isExportReady(vertex, manufacturingStateCache))
		.map(vertex => {
			const data = (() => {
				const bendZones = (() => {
					const twoDimRep = wsi4.node.twoDimRep(vertex);
					assert(twoDimRep !== undefined, "Expecting valid twoDimRep");
					return wsi4.cam.bend.extractBendZones(twoDimRep);
				})();

				return wsi4.geo.util.addInnerOuterPolygonsToScene(
					createDefaultScene(),
					bendZones,
					{
						strokeWidth: 1,
						strokeColor: Color.green,
					},
				);
			})();

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

function computeTubeOutlineScenes(outdatedVertices: readonly Vertex[]): PrivateSceneResourceEntry[] {
	return outdatedVertices
		.filter(vertex => wsi4.node.workStepType(vertex) === WorkStepType.tubeCutting)
		.map(vertex => ({
			vertex: vertex,
			layered: wsi4.node.layered(vertex),
		}))
		.filter(obj => obj.layered !== undefined)
		.map(obj => {
			const layered = obj.layered;
			assert(layered !== undefined);
			assertDebug(() => wsi4.geo.util.layers(layered)
				.some(layer => layer.descriptor === 2));
			const style: SceneStyle = {
				strokeWidth: 1,
				strokeColor: Color.blue,
			};
			const data = wsi4.geo.util.addLayersToScene(
				createDefaultScene(),
				layered,
				[ tubeCuttingLayerDescriptor("tubeOutlines", layered) ],
				style,
			);
			return {
				nodeId: wsi4.node.nodeId(obj.vertex),
				data: data,
			};
		});
}

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 => {
			const data = (() => {
				const dieAffectZones = (() => {
					const twoDimRep = wsi4.node.twoDimRep(vertex);
					assert(twoDimRep !== undefined, "Expecting valid twoDimRep");
					const affectDistanceMap = computeLowerDieAffectDistanceMap(vertex);
					return wsi4.cam.bend.computeLowerDieAffectZones(twoDimRep, affectDistanceMap);
				})();

				return wsi4.geo.util.addPolygonsToScene(
					createDefaultScene(),
					dieAffectZones,
					{
						strokeWidth: 0,
						strokeColor: Color.lightgrey,
						fillColor: {
							entries: [
								0.5,
								0.5,
								0.5,
								1,
							],
						},
					},
				);
			})();

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

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 = (() => {
					const subGraph = [
						...article,
						...wsi4.graph.reaching(front(article)),
					];
					if (subGraph.some(vertex => hasManufacturingCostOverride(vertex))) {
						return undefined;
					} else {
						const scaleCosts = computeRecursiveArticleScaleCosts(article, scaleValue, calcCache);
						return scaleCosts === undefined ? undefined : computeManufacturingPriceExclSurcharges([ scaleCosts ]);
					}
				})();
				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,
		],
		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 {
	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 ?? "";
	} else {
		return "";
	}
}

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>>[],
	technicalDrawingFutures: readonly Readonly<NodeIdAnd<MeasurementScenesFuture>>[],
	calcCache: CalcCache,
	manufacturingStateCache: ManufacturingStateCache,
	reducedGuiEnabled = false,
): PrivateResources {
	const inputAssemblies = computeInputAssemblyResources(outdatedVertices);
	const outputAssemblies = computeOutputAssemblyResources(outdatedVertices);
	const defaultScenes = computeDefaultScenes(outdatedVertices);
	const bendZoneScenes = computeBendZoneScenes(outdatedVertices, manufacturingStateCache);
	const tubeOutlineScenes = computeTubeOutlineScenes(outdatedVertices);
	const lowerDieAffectZoneScenes = computeDieAffectZoneScenes(outdatedVertices);
	const pngs = computePngResources(pngFutures);
	const technicalDrawingScenes = reducedGuiEnabled ? [] : computeTechnicalDrawingScenes(technicalDrawingFutures);
	const manufacturingStates = computeManufacturingStateResources(outdatedVertices, 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);
	return {
		pngs: pngs,
		inputAssemblies: inputAssemblies,
		outputAssemblies: outputAssemblies,
		defaultScenes: defaultScenes,
		bendZoneScenes: bendZoneScenes,
		lowerDieAffectZoneScenes: lowerDieAffectZoneScenes,
		technicalDrawingScenes: technicalDrawingScenes,
		tubeOutlineScenes: tubeOutlineScenes,
		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: [],
			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 technicalDrawingFutures = reducedGuiEnabled ? [] : createTechnicalDrawingsFutures(outdatedVertices);
	const calcCache = createCalcCache();
	const manufacturingStateCache = createManufacturingStateCache();
	const sourceMults = computeSourceMults();
	return {
		nodes: computeNodeRepresentations(),
		articles: computeGuiArticles(),
		sourceMults: sourceMults,
		resources: computeGuiResources(
			outdatedVertices,
			pngFutures,
			technicalDrawingFutures,
			calcCache,
			manufacturingStateCache,
			reducedGuiEnabled,
		),
		data: {
			projectName: projectName(),
		},
	};
}

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