import {
	assumeGraphAxioms,
} from "qrc:/js/lib/axioms";
import {
	addCosts,
	Costs,
} from "qrc:/js/lib/calc_costs";
import {
	readCalcSettings,
} from "qrc:/js/lib/calc_settings";
import {
	Times,
} from "qrc:/js/lib/calc_times";
import {
	DocumentAlignment,
	DocumentItemType,
	ProcessType,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	computeNodeMass,
	computeSheetConsumption,
	getArticleName,
	getArticleSignatureVertex,
	getArticleUserData,
	getAssociatedSheetMaterialId,
	isComponentArticle,
	isJoiningArticle,
	isSemimanufacturedArticle,
	netTubeConsumptionForVertex,
	sheetIdForVertex,
} from "qrc:/js/lib/graph_utils";
import {
	computeSheetConsumptionNestedParts,
	grossTubeConsumptionForVertex,
	isTestReportRequiredForSheet,
	sheetBendingWsNumBendLines,
	sheetCuttingWsContourCount,
	sheetCuttingWsContourLength,
	tubeForVertex,
	tubeProfileForVertex,
	VertexAnd,
} from "qrc:/js/lib/node_utils";
import {
	computeTubeVolume,
	getTable,
} from "qrc:/js/lib/table_utils";
import {
	getNodeUserDataEntryOrThrow,
	hasManufacturingCostOverride as hasManufacturingCostsOverrideImpl,
} from "qrc:/js/lib/userdata_utils";
import {
	assert,
	assertDebug,
	computeContourLength,
	isEqual,
} from "qrc:/js/lib/utils";
import {
	computeWcsDimensions,
} from "qrc:/js/lib/geometry_utils";
import {
	tubeCuttingLayerPaths,
} from "qrc:/js/lib/tubecutting_util";
import {
	isMinExportReady,
} from "qrc:/js/lib/manufacturing_state_util";

import {
	back,
	front,
} from "qrc:/js/lib/array_util";
import {
	computeNodeCosts,
	computeProjectSellingPrice,
	computeRecursiveArticleScaleSellingPrice,
	computeRecursiveArticleScaleCosts,
	computeScaleValues,
	computeManufacturingPriceExclSurcharges,
	computeManufacturingPriceInclSurcharges,
	computeSheetConsumptionMasses,
	articleSellingPriceWithSemimanufacturedShare,
	computeArticleManufacturingCostsExclSurcharges,
	computeTubeConsumptionMasses,
} from "./export_calc_costs";
import {
	createCalcCache,
	CalcCache,
} from "./export_calc_cache";
import {
	computeAutomaticMechanicalDeburringLength,
	computeNodeTimes,
	getFixedMultiplicity,
} from "./export_calc_times";
import {
	createHeading,
	createSeparator,
	createSpacerItem,
	createTitle,
	currencyString,
	getArticleSheetMaterialName,
	getArticleTubeMaterialName,
	getLaserSheetCuttingGasName,
	kgToString,
	mmToString,
	pngItemFromFuture,
	secondsToString,
} from "./export_utils";

function hasManufacturingCostOverride(article: readonly Readonly<Vertex>[]): boolean {
	return article.some(vertex => hasManufacturingCostsOverrideImpl(vertex));
}

function nodeTimesIfApplicable(vertex: Vertex, calcCache: CalcCache): Times|undefined {
	if (hasManufacturingCostsOverrideImpl(vertex)) {
		return undefined;
	} else {
		return computeNodeTimes(vertex, undefined, calcCache);
	}
}

function nodeCostsIfApplicable(vertex: Vertex, multiplicity: number, calcCache: CalcCache): Costs|undefined {
	if (hasManufacturingCostsOverrideImpl(vertex)) {
		return undefined;
	} else {
		return computeNodeCosts(vertex, multiplicity, calcCache);
	}
}

function articleCostsIfApplicable(article: readonly Readonly<Vertex>[], calcCache: CalcCache): Costs|undefined {
	if (hasManufacturingCostOverride(article)) {
		return undefined;
	} else {
		const multiplicity = getFixedMultiplicity(back(article));
		return computeArticleManufacturingCostsExclSurcharges(article, multiplicity, calcCache);
	}
}

function recursiveArticleCostsIfApplicable(article: readonly Readonly<Vertex>[], scaleValue: number, calcCache: CalcCache): Costs|undefined {
	if (wsi4.graph.reaching(back(article))
		.some(vertex => hasManufacturingCostsOverrideImpl(vertex))) {
		return undefined;
	} else {
		return computeRecursiveArticleScaleCosts(article, scaleValue, calcCache);
	}
}

type LegendEntry = "mfgCosts" | "sellingPrice" | "exclSurcharges" | "inclSurcharges" | "exclScrapCredit";

const legendSymbols: {[index in LegendEntry]: string} = {
	mfgCosts: "HK",
	sellingPrice: "VK",
	exclSurcharges: "¹",
	inclSurcharges: "²",
	exclScrapCredit: "³",
};

function createLegend(entries: readonly LegendEntry[]): DocumentItem[] {
	const translationKeys: {[index in LegendEntry]: string} = {
		mfgCosts: "manufacturing_costs",
		sellingPrice: "selling_price",
		exclSurcharges: "excl_surcharges",
		inclSurcharges: "incl_surcharges",
		exclScrapCredit: "excl_scrap_credit",
	};
	const text = "<small>"
		+ entries.map(key => legendSymbols[key] + ": " + wsi4.util.translate(translationKeys[key]))
			.join("; ")
		+ "</small>";
	return [
		{
			type: DocumentItemType.paragraph,
			content: {
				width: 12,
				alignment: DocumentAlignment.center,
				text: text,
			},
		},
	];
}

function createFirstPageHeader(): Array<Array<DocumentItem>> {
	return createTitle(wsi4.util.translate("Calculation"));
}

function createMaterialCostsSubSection(calcCache: CalcCache): Array<Array<DocumentItem>> {
	const rows = [
		// table header
		[
			{
				text: wsi4.util.translate("Article"),
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.right,
			},
			{
				text: "",
				alignment: DocumentAlignment.right,
			},
			{
				text: wsi4.util.translate("OverallPrice"),
				alignment: DocumentAlignment.right,
			},
		],
		// Data is added in for-loop
	];

	let sum: number|undefined = 0;
	const articles = wsi4.graph.articles();
	for (const article of articles) {
		const multiplicity = getFixedMultiplicity(front(article));
		const name = getArticleName(front(article));

		const price = article.reduce((acc: number|undefined, vertex) => {
			if (acc === undefined) {
				return undefined;
			}
			if (hasManufacturingCostsOverrideImpl(vertex)) {
				return undefined;
			}
			const costs = nodeCostsIfApplicable(vertex, multiplicity, calcCache);
			if (costs === undefined) {
				return undefined;
			}
			return acc + costs.material;
		}, 0);
		rows.push([
			{
				text: name,
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.right,
			},
			{
				text: "",
				alignment: DocumentAlignment.right,
			},
			{
				text: price === undefined ? "N/A" : currencyString(price),
				alignment: DocumentAlignment.right,
			},
		]);
		if (price === undefined) {
			sum = undefined;
		} else if (sum !== undefined) {
			sum += price;
		}
	}
	rows.push([
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "Σ " + (sum === undefined ? "N/A" : currencyString(sum)),
			alignment: DocumentAlignment.right,
		},
	]);
	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnHeaders: [],
			columnWidths: [
				40,
				20,
				20,
				20,
			],
			rows: rows,
		},
	};
	return [
		createHeading(wsi4.util.translate("MaterialCosts"), 3),
		[ tableItem ],
	];
}

function createUnitCostsSubSection(calcCache: CalcCache): Array<Array<DocumentItem>> {
	const rows = [
		// table header
		[
			{
				text: wsi4.util.translate("Article"),
				alignment: DocumentAlignment.left,
			},
			{
				text: wsi4.util.translate("SinglePrice"),
				alignment: DocumentAlignment.right,
			},
			{
				text: wsi4.util.translate("Multiplicity"),
				alignment: DocumentAlignment.right,
			},
			{
				text: wsi4.util.translate("OverallPrice"),
				alignment: DocumentAlignment.right,
			},
		],
		// Data is added in for-loop
	];

	let sum: number|undefined = 0;
	const articles = wsi4.graph.articles();
	for (const article of articles) {
		const firstVertex = front(article);
		const articleCosts = articleCostsIfApplicable(article, calcCache);
		const name = getArticleName(firstVertex);
		const multiplicity = getFixedMultiplicity(firstVertex);
		const unitCosts = articleCosts === undefined ? undefined : articleCosts.unit;
		rows.push([
			{
				text: name,
				alignment: DocumentAlignment.left,
			},
			{
				text: unitCosts === undefined ? "N/A" : currencyString(unitCosts / multiplicity),
				alignment: DocumentAlignment.right,
			},
			{
				text: String(multiplicity),
				alignment: DocumentAlignment.right,
			},
			{
				text: unitCosts === undefined ? "N/A" : currencyString(unitCosts),
				alignment: DocumentAlignment.right,
			},
		]);
		if (unitCosts === undefined) {
			sum = undefined;
		} else if (sum !== undefined) {
			sum += unitCosts;
		}
	}
	rows.push([
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "Σ " + (sum === undefined ? "N/A" : currencyString(sum)),
			alignment: DocumentAlignment.right,
		},
	]);
	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnHeaders: [],
			columnWidths: [
				40,
				20,
				20,
				20,
			],
			rows: rows,
		},
	};
	return [
		createHeading(wsi4.util.translate("UnitCosts"), 3),
		[ tableItem ],
	];
}

function collectProcessIdsInProject(): string[] {
	const processes = new Set<string>();
	wsi4.graph.vertices()
		.forEach(vertex => processes.add(wsi4.node.processId(vertex)));
	return Array.from(processes)
		.sort(undefined);
}

function createSurchargeSummarySubSection(): DocumentItem[][] {
	const header: DocumentTableCell[] = [
		{
			text: wsi4.util.translate("Name"),
			alignment: DocumentAlignment.left,
		},
		{
			text: wsi4.util.translate("description"),
			alignment: DocumentAlignment.left,
		},
		{
			text: wsi4.util.translate("value"),
			alignment: DocumentAlignment.right,
		},
	];

	const body: DocumentTableCell[][] = getTable(TableType.surcharge)
		.map(row => ([
			{
				text: row.name,
				alignment: DocumentAlignment.left,
			},
			{
				text: row.description,
				alignment: DocumentAlignment.left,
			},
			{
				text: row.value.toFixed(4),
				alignment: DocumentAlignment.right,
			},
		]));

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				40,
				40,
				20,
			],
			columnHeaders: header,
			rows: body,
		},
	};

	return [
		createHeading(wsi4.util.translate("surcharges"), 3),
		[ tableItem ],
	];
}

function createProcessTimesSummarySubSection(calcCache: CalcCache): DocumentItem[][] {
	const header: DocumentTableCell[] = [
		{
			text: wsi4.util.translate("process"),
			alignment: DocumentAlignment.left,
		},
		{
			// Placeholder so both the process time summary and process costs summary are aligned
			text: "",
			alignment: DocumentAlignment.right,
		},
		{
			text: "Σ " + wsi4.util.translate("SetupTimeAbbr"),
			alignment: DocumentAlignment.right,
		},
		{
			text: "Σ " + wsi4.util.translate("UnitTimeAbbr"),
			alignment: DocumentAlignment.right,
		},
		{
			// Placeholder so both the process time summary and process costs summary are aligned
			text: "",
			alignment: DocumentAlignment.right,
		},
		{
			text: `Σ ${wsi4.util.translate("SetupTimeAbbr")} + Σ ${wsi4.util.translate("UnitTimeAbbr")}`,
			alignment: DocumentAlignment.right,
		},
	];
	const processTable = getTable(TableType.process);
	const body: DocumentTableCell[][] = collectProcessIdsInProject()
		.map((processId): DocumentTableCell[] => {
			const process = processTable.find(row => row.identifier === processId);
			assert(process !== undefined, "Expecting valid process");
			const times = wsi4.graph.vertices()
				.filter(vertex => wsi4.node.processId(vertex) === processId)
				.reduce((acc: Times|undefined, vertex) => {
					if (acc === undefined) {
						return undefined;
					}
					const times = nodeTimesIfApplicable(vertex, calcCache);
					if (times === undefined) {
						return undefined;
					} else {
						return new Times(
							acc.setup + times.setup,
							acc.unit + times.unit,
						);
					}
				}, new Times(0, 0));
			return [
				{
					text: process.name,
					alignment: DocumentAlignment.left,
				},
				{
					// Placeholder so both the process time summary and process costs summary are aligned
					text: "",
					alignment: DocumentAlignment.right,
				},
				{
					text: times === undefined ? "N/A" : secondsToString(times.setup, 2, "min"),
					alignment: DocumentAlignment.right,
				},
				{
					text: times === undefined ? "N/A" : secondsToString(times.unit, 2, "min"),
					alignment: DocumentAlignment.right,
				},
				{
					// Placeholder so both the process time summary and process costs summary are aligned
					text: "",
					alignment: DocumentAlignment.right,
				},
				{
					text: times === undefined ? "N/A" : secondsToString(times.setup + times.unit, 2, "min"),
					alignment: DocumentAlignment.right,
				},
			];
		});

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				25,
				15,
				15,
				15,
				15,
				15,
			],
			columnHeaders: header,
			rows: body,
		},
	};
	return [
		createHeading(wsi4.util.translate("manufacturing_time"), 4),
		[ tableItem ],
	];
}

function createProcessCostsSummarySubSection(calcCache: CalcCache): DocumentItem[][] {
	const header: DocumentTableCell[] = [
		{
			text: wsi4.util.translate("process"),
			alignment: DocumentAlignment.left,
		},
		{
			text: "Σ " + wsi4.util.translate("MaterialCosts") + legendSymbols["exclSurcharges"],
			alignment: DocumentAlignment.right,
		},
		{
			text: "Σ " + wsi4.util.translate("SetupCosts") + legendSymbols["exclSurcharges"],
			alignment: DocumentAlignment.right,
		},
		{
			text: "Σ " + wsi4.util.translate("UnitCosts") + legendSymbols["exclSurcharges"],
			alignment: DocumentAlignment.right,
		},
		{
			text: "Σ " + legendSymbols["mfgCosts"] + legendSymbols["exclSurcharges"],
			alignment: DocumentAlignment.right,
		},
		{
			text: "Σ " + legendSymbols["mfgCosts"] + legendSymbols["inclSurcharges"],
			alignment: DocumentAlignment.right,
		},
	];

	const processTable = getTable(TableType.process);
	const body: DocumentTableCell[][] = collectProcessIdsInProject()
		.map((processId): DocumentTableCell[] => {
			const process = processTable.find(row => row.identifier === processId);
			assert(process !== undefined, "Expecting valid process");
			const costs = wsi4.graph.vertices()
				.filter(vertex => wsi4.node.processId(vertex) === processId)
				.reduce((acc: Costs|undefined, vertex) => {
					const multiplicity = getFixedMultiplicity(vertex);
					return addCosts([
						acc,
						nodeCostsIfApplicable(vertex, multiplicity, calcCache),
					]);
				}, new Costs(0, 0, 0));
			const manufacturingPriceExclSurcharges = computeManufacturingPriceExclSurcharges(costs);
			const manufacturingPriceInclSurcharges = computeManufacturingPriceInclSurcharges(costs);
			return [
				{
					text: process.name,
					alignment: DocumentAlignment.left,
				},
				{
					text: costs === undefined ? "N/A" : currencyString(costs.material),
					alignment: DocumentAlignment.right,
				},
				{
					text: costs === undefined ? "N/A" : currencyString(costs.setup),
					alignment: DocumentAlignment.right,
				},
				{
					text: costs === undefined ? "N/A" : currencyString(costs.unit),
					alignment: DocumentAlignment.right,
				},
				{
					text: manufacturingPriceExclSurcharges === undefined ? "N/A" : currencyString(manufacturingPriceExclSurcharges),
					alignment: DocumentAlignment.right,
				},
				{
					text: manufacturingPriceInclSurcharges === undefined ? "N/A" : currencyString(manufacturingPriceInclSurcharges),
					alignment: DocumentAlignment.right,
				},
			];
		});

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				25,
				15,
				15,
				15,
				15,
				15,
			],
			columnHeaders: header,
			rows: body,
		},
	};
	return [
		createHeading(wsi4.util.translate("costs"), 4),
		[ tableItem ],
		createLegend([
			"mfgCosts",
			"exclSurcharges",
			"inclSurcharges",
		]),
	];
}

function createProcessSummarySubSection(calcCache: CalcCache): DocumentItem[][] {
	return [
		createHeading(wsi4.util.translate("process"), 3),
		...createProcessTimesSummarySubSection(calcCache),
		...createProcessCostsSummarySubSection(calcCache),
	];
}

function createSheetConsumptionSubSection(): DocumentItem[][] {
	const sheetVertices = wsi4.graph.vertices()
		.filter(vertex => wsi4.node.workStepType(vertex) === WorkStepType.sheet && wsi4.node.twoDimRep(vertex) !== undefined);
	if (sheetVertices.length === 0) {
		// No valid sheet available so skipping this sub section
		return [];
	}

	const materialTable = getTable(TableType.sheetMaterial);
	const densityTable = getTable(TableType.sheetMaterialDensity);
	const sheetTable = getTable(TableType.sheet);
	const sheetPriceTable = getTable(TableType.sheetPrice);

	const consumptionMap = (() => {
		const map = new Map<string, [number, number]>();
		sheetVertices.forEach(vertex => {
			const sheetId = sheetIdForVertex(vertex, sheetTable);
			assert(sheetId !== undefined, "Expecting valid sheet id");
			const consumptionTotal = computeSheetConsumption(vertex, sheetTable);
			assert(consumptionTotal !== undefined, "Expecting valid sheet consumptionTotal");
			const consumptionParts = computeSheetConsumptionNestedParts(vertex);
			assert(consumptionParts !== undefined, "Expecting valid part sheet consumption");
			if (map.has(sheetId)) {
				const acc = map.get(sheetId)!;
				map.set(sheetId, [
					acc[0] + consumptionTotal,
					acc[1] + consumptionParts,
				]);
			} else {
				map.set(sheetId, [
					consumptionTotal,
					consumptionParts,
				]);
			}
		});
		return map;
	})();

	const header: readonly Readonly<DocumentTableCell>[] = [
		{
			text: wsi4.util.translate("sheet"),
			alignment: DocumentAlignment.left,
		},
		{
			text: wsi4.util.translate("price") + " [" + wsi4.util.translate("currency") + " / kg]",
			alignment: DocumentAlignment.right,
		},
		{
			text: wsi4.util.translate("Consumption"),
			alignment: DocumentAlignment.right,
		},
		{
			text: wsi4.util.translate("charge_weight_in_kg"),
			alignment: DocumentAlignment.right,
		},
		{
			text: wsi4.util.translate("scrap_in_kg"),
			alignment: DocumentAlignment.right,
		},
		{
			text: wsi4.util.translate("MaterialCosts") + legendSymbols["exclSurcharges"] + "⁺" + legendSymbols["exclScrapCredit"],
			alignment: DocumentAlignment.right,
		},
	];

	const body: readonly Readonly<DocumentTableCell>[][] = Array.from(consumptionMap.keys())
		.map(sheetId => {
			const sheet = sheetTable.find(row => row.identifier === sheetId);
			assert(sheet !== undefined, "Expecting valid sheet for sheetId " + sheetId);

			const price = sheetPriceTable.find(row => row.sheetId === sheetId);
			assert(price !== undefined, "Expecting valid sheet price for sheetId " + sheetId);

			const material = materialTable.find(row => row.identifier === sheet.sheetMaterialId);
			assert(material !== undefined, "Expecting valid material for sheetId " + sheetId);

			const density = densityTable.find(row => row.sheetMaterialId === material.identifier);
			assert(density !== undefined, "Expecting valid density for materialId " + material.identifier);

			// [m³]
			const singleSheetVolume = 0.001 * 0.001 * 0.001 * sheet.dimX * sheet.dimY * sheet.thickness;

			const [
				consumptionTotal,
				consumptionParts,
			] = consumptionMap.get(sheetId)!;

			const consumptionScrap = consumptionTotal - consumptionParts;

			return [
				{
					text: sheet.name,
					alignment: DocumentAlignment.left,
				},
				{
					text: (price.pricePerSheet / (singleSheetVolume * density.density)).toFixed(2),
					alignment: DocumentAlignment.right,
				},
				{
					text: consumptionTotal.toFixed(3),
					alignment: DocumentAlignment.right,
				},
				{
					text: (consumptionTotal * singleSheetVolume * density.density).toFixed(2),
					alignment: DocumentAlignment.right,
				},
				{
					text: (consumptionScrap * singleSheetVolume * density.density).toFixed(2),
					alignment: DocumentAlignment.right,
				},
				{
					text: (consumptionTotal * price.pricePerSheet).toFixed(2),
					alignment: DocumentAlignment.right,
				},
			];
		});

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				20,
				16,
				16,
				16,
				16,
				16,
			],
			columnHeaders: header,
			rows: body,
		},
	};
	return [
		createHeading(wsi4.util.translate("sheet") + " - " + wsi4.util.translate("Consumption"), 3),
		[ tableItem ],
		createLegend([
			"exclSurcharges",
			"exclScrapCredit",
		]),
	];
}

function createTubeConsumptionSubSection(): DocumentItem[][] {
	const tubeCuttingVerticesWithTubes: readonly [Vertex, Tube][] = wsi4.graph.vertices()
		.filter(vertex => wsi4.node.workStepType(vertex) === WorkStepType.tubeCutting)
		.map(vertex => ([
			vertex,
			tubeForVertex(vertex),
		]))
		.filter((tuple): tuple is [Vertex, Tube] => tuple[1] !== undefined);
	if (tubeCuttingVerticesWithTubes.length === 0) {
		// No valid tube cutting nodes available so skipping this sub section
		return [];
	}

	const materialTable = getTable(TableType.tubeMaterial);
	const densityTable = getTable(TableType.tubeMaterialDensity);
	const tubeTable = getTable(TableType.tube);
	const tubePriceTable = getTable(TableType.tubePrice);

	const consumptionMap = (() => {
		const map = new Map<string, [number, number]>();
		tubeCuttingVerticesWithTubes.forEach(([
			vertex,
			tube,
		]) => {
			const grossConsumption = grossTubeConsumptionForVertex(vertex, tube);
			assert(grossConsumption !== undefined, "Expecting valid tube gross consumption");
			const netConsumption = netTubeConsumptionForVertex(vertex);
			assert(netConsumption !== undefined, "Expecting valid tube net consumption");
			if (map.has(tube.identifier)) {
				const acc = map.get(tube.identifier)!;
				map.set(tube.identifier, [
					acc[0] + grossConsumption,
					acc[1] + netConsumption,
				]);
			} else {
				map.set(tube.identifier, [
					grossConsumption,
					netConsumption,
				]);
			}
		});
		return map;
	})();

	const header: readonly Readonly<DocumentTableCell>[] = [
		{
			text: wsi4.util.translate("tube"),
			alignment: DocumentAlignment.left,
		},
		{
			text: wsi4.util.translate("price") + " [" + wsi4.util.translate("currency") + " / kg]",
			alignment: DocumentAlignment.right,
		},
		{
			text: wsi4.util.translate("Consumption"),
			alignment: DocumentAlignment.right,
		},
		{
			text: wsi4.util.translate("charge_weight_in_kg"),
			alignment: DocumentAlignment.right,
		},
		{
			text: wsi4.util.translate("scrap_in_kg"),
			alignment: DocumentAlignment.right,
		},
		{
			text: wsi4.util.translate("MaterialCosts") + legendSymbols["exclSurcharges"] + "⁺" + legendSymbols["exclScrapCredit"],
			alignment: DocumentAlignment.right,
		},
	];

	const body: readonly Readonly<DocumentTableCell>[][] = Array.from(consumptionMap.keys())
		.map(tubeId => {
			const tube = tubeTable.find(row => row.identifier === tubeId);
			assert(tube !== undefined, "Expecting valid tube for tubeId " + tubeId);

			const price = tubePriceTable.find(row => row.tubeId === tubeId);
			assert(price !== undefined, "Expecting valid tube price for tubeId " + tubeId);

			const material = materialTable.find(row => row.identifier === tube.tubeMaterialId);
			assert(material !== undefined, "Expecting valid material for tubeId " + tubeId);

			const densityRow = densityTable.find(row => row.tubeMaterialId === material.identifier);
			assert(densityRow !== undefined, "Expecting valid density for materialId " + material.identifier);

			// [kg / m³] -> [kg / mm³]
			const density = 0.001 * 0.001 * 0.001 * densityRow.density;

			// [mm³]
			const singleTubeVolume = computeTubeVolume(tube);

			const [
				grossConsumption,
				netConsumption,
			] = consumptionMap.get(tubeId)!;

			const scap = grossConsumption - netConsumption;

			return [
				{
					text: tube.name,
					alignment: DocumentAlignment.left,
				},
				{
					text: (price.pricePerTube / (singleTubeVolume * density)).toFixed(2),
					alignment: DocumentAlignment.right,
				},
				{
					text: grossConsumption.toFixed(3),
					alignment: DocumentAlignment.right,
				},
				{
					text: (grossConsumption * singleTubeVolume * density).toFixed(2),
					alignment: DocumentAlignment.right,
				},
				{
					text: (scap * singleTubeVolume * density).toFixed(2),
					alignment: DocumentAlignment.right,
				},
				{
					text: (grossConsumption * price.pricePerTube).toFixed(2),
					alignment: DocumentAlignment.right,
				},
			];
		});

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				20,
				16,
				16,
				16,
				16,
				16,
			],
			columnHeaders: header,
			rows: body,
		},
	};
	return [
		createHeading(wsi4.util.translate("tube") + " - " + wsi4.util.translate("Consumption"), 3),
		[ tableItem ],
		createLegend([
			"exclSurcharges",
			"exclScrapCredit",
		]),
	];
}

function createSetupCostsSubSection(calcCache: CalcCache): Array<Array<DocumentItem>> {
	const rows = [
		// table header
		[
			{
				text: wsi4.util.translate("Article"),
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: wsi4.util.translate("OverallPrice"),
				alignment: DocumentAlignment.right,
			},
		],
		// Data is added in for-loop
	];

	let sum: number|undefined = 0;
	const articles = wsi4.graph.articles();
	for (const article of articles) {
		const firstVertex = front(article);
		const articleCosts = articleCostsIfApplicable(article, calcCache);
		const name = getArticleName(firstVertex);
		const price = articleCosts === undefined ? undefined : articleCosts.setup;
		rows.push([
			{
				text: name,
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: price === undefined ? "N/A" : currencyString(price),
				alignment: DocumentAlignment.right,
			},
		]);
		if (price === undefined) {
			sum = undefined;
		} else if (sum !== undefined) {
			sum += price;
		}
	}
	rows.push([
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "Σ " + (sum === undefined ? "N/A" : currencyString(sum)),
			alignment: DocumentAlignment.right,
		},
	]);
	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				40,
				20,
				20,
				20,
			],
			columnHeaders: [],
			rows: rows,
		},
	};
	return [
		createHeading(wsi4.util.translate("SetupCosts"), 3),
		[ tableItem ],
	];
}

function createOverallPriceSubSection(calcCache: CalcCache): Array<Array<DocumentItem>> {
	const manufacturingPrice = computeManufacturingPriceExclSurcharges(
		wsi4.graph.articles()
			// Costs of semimanufactured parts are distributed
			// among their respective target articles.
			.filter(article => !isSemimanufacturedArticle(article))
			.map(article => articleCostsIfApplicable(article, calcCache)),
	);
	const sellingPrice = computeProjectSellingPrice(calcCache);
	const rows = [
		[
			{
				text: wsi4.util.translate("OverallManufacturingCosts"),
				alignment: DocumentAlignment.left,
			},
			{
				text: manufacturingPrice === undefined ? "N/A" : currencyString(manufacturingPrice),
				alignment: DocumentAlignment.right,
			},
		],
		[
			{
				text: wsi4.util.translate("OverallSellingPrice"),
				alignment: DocumentAlignment.left,
			},
			{
				text: sellingPrice === undefined ? "N/A" : currencyString(sellingPrice),
				alignment: DocumentAlignment.right,
			},
		],
	];

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			columnWidths: [
				50,
				50,
			],
			columnHeaders: [],
			rows: rows,
		},
	};
	return [
		createHeading(wsi4.util.translate("OverallPrice"), 3),
		[ tableItem ],
	];
}

function createScalePriceSubSection(calcCache: CalcCache): DocumentItem[][] {
	const createNameRow = (vertex: Vertex) => [
		{
			text: getArticleName(vertex),
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
	];
	const createScalePriceRow = (
		mfgCostsExclSurcharges: number|undefined,
		mfgCostsInclSurcharges: number|undefined,
		sellingPrice: number|undefined,
		scaleValue: number,
	) => [
		{
			text: "",
			alignment: DocumentAlignment.left,
		},
		{
			text: scaleValue,
			alignment: DocumentAlignment.right,
		},
		{
			text: mfgCostsExclSurcharges === undefined ? "N/A" : currencyString(mfgCostsExclSurcharges / scaleValue),
			alignment: DocumentAlignment.right,
		},
		{
			text: mfgCostsExclSurcharges === undefined ? "N/A" : currencyString(mfgCostsExclSurcharges),
			alignment: DocumentAlignment.right,
		},
		{
			text: mfgCostsInclSurcharges === undefined ? "N/A" : currencyString(mfgCostsInclSurcharges),
			alignment: DocumentAlignment.right,
		},
		{
			text: sellingPrice === undefined ? "N/A" : currencyString(sellingPrice / scaleValue),
			alignment: DocumentAlignment.right,
		},
		{
			text: sellingPrice === undefined ? "N/A" : currencyString(sellingPrice),
			alignment: DocumentAlignment.right,
		},
	];
	const createScalePriceRows = (article: Vertex[]) => {
		const multiplicity = wsi4.node.multiplicity(back(article));
		const calcConfig = readCalcSettings();
		return computeScaleValues(multiplicity, calcConfig)
			.map(scaleValue => {
				const costs = recursiveArticleCostsIfApplicable(article, scaleValue, calcCache);
				const mfgPriceExclSurcharges = computeManufacturingPriceExclSurcharges(costs);
				const mfgPriceInclSurcharges = computeManufacturingPriceInclSurcharges(costs);
				const sellingPrice = computeRecursiveArticleScaleSellingPrice(article, scaleValue, calcCache);
				return costs === undefined && sellingPrice === undefined ? [] : createScalePriceRow(
					mfgPriceExclSurcharges,
					mfgPriceInclSurcharges,
					sellingPrice,
					scaleValue,
				);
			});
	};

	const rows = wsi4.graph.vertices()
		.filter(vertex => wsi4.graph.targets(vertex).length === 0)
		.reduce(
			(acc: unknown[], vertex) => [
				...acc,
				createNameRow(vertex),
				...createScalePriceRows(wsi4.graph.article(vertex)),
			],
			[],
		);

	const table = [
		{
			type: DocumentItemType.table,
			content: {
				width: 12,
				columnWidths: [
					25,
					12.5,
					12.5,
					12.5,
					12.5,
					12.5,
					12.5,
				],
				columnHeaders: [
					{
						text: wsi4.util.translate("Article"),
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("Multiplicity"),
						alignment: DocumentAlignment.right,
					},
					{
						text: legendSymbols["mfgCosts"] + legendSymbols["exclSurcharges"] + " / " + wsi4.util.translate("piece"),
						alignment: DocumentAlignment.right,
					},
					{
						text: legendSymbols["mfgCosts"] + legendSymbols["exclSurcharges"],
						alignment: DocumentAlignment.right,
					},
					{
						text: legendSymbols["mfgCosts"] + legendSymbols["inclSurcharges"],
						alignment: DocumentAlignment.right,
					},
					{
						text: legendSymbols["sellingPrice"] + " / " + wsi4.util.translate("piece"),
						alignment: DocumentAlignment.right,
					},
					{
						text: legendSymbols["sellingPrice"],
						alignment: DocumentAlignment.right,
					},
				],
				rows: rows,
			},
		},
	];
	return [
		createHeading(wsi4.util.translate("scale_prices"), 3),
		table,
		createLegend([
			"mfgCosts",
			"sellingPrice",
			"exclSurcharges",
			"inclSurcharges",
		]),
	];
}

/**
 * Section lists costs for component articles and joining articles (if any)
 *
 * Sheet articles are not listed.
 * Material costs are incorporated in the costs liststed for the respective articles in this section.
 */
function createArticleSellingPriceSubSection(calcCache: CalcCache): DocumentItem[][] {
	const rows: DocumentTableRow[] =
wsi4.graph
	.articles()
	.filter(article => isComponentArticle(article) || isJoiningArticle(article))
	.map(article => {
		const name = (() => {
			const n = getArticleName(front(article));
			return n === undefined ? "N/A" : n;
		})();
		const multiplicity = getFixedMultiplicity(front(article));
		const articleCosts = articleCostsIfApplicable(article, calcCache);
		const mfgPriceExclSurcharges = computeManufacturingPriceExclSurcharges(articleCosts);
		const mfgPriceInclSurcharges = computeManufacturingPriceInclSurcharges(articleCosts);
		const sellingPrice = articleSellingPriceWithSemimanufacturedShare(article, multiplicity, calcCache);
		return [
			{
				text: name,
				alignment: DocumentAlignment.left,
			},
			{
				text: multiplicity.toFixed(0),
				alignment: DocumentAlignment.right,
			},
			{
				text: mfgPriceExclSurcharges === undefined ? "N/A" : currencyString(mfgPriceExclSurcharges / multiplicity),
				alignment: DocumentAlignment.right,
			},
			{
				text: mfgPriceExclSurcharges === undefined ? "N/A" : currencyString(mfgPriceExclSurcharges),
				alignment: DocumentAlignment.right,
			},
			{
				text: mfgPriceInclSurcharges === undefined ? "N/A" : currencyString(mfgPriceInclSurcharges),
				alignment: DocumentAlignment.right,
			},
			{
				text: sellingPrice === undefined ? "N/A" : currencyString(sellingPrice / multiplicity),
				alignment: DocumentAlignment.right,
			},
			{
				text: sellingPrice === undefined ? "N/A" : currencyString(sellingPrice),
				alignment: DocumentAlignment.right,
			},
		];
	});
	const table = [
		{
			type: DocumentItemType.table,
			content: {
				width: 12,
				columnWidths: [
					25,
					12.5,
					12.5,
					12.5,
					12.5,
					12.5,
					12.5,
				],
				columnHeaders: [
					{
						text: wsi4.util.translate("Article"),
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("Multiplicity"),
						alignment: DocumentAlignment.right,
					},
					{
						text: legendSymbols["mfgCosts"] + legendSymbols["exclSurcharges"] + " / " + wsi4.util.translate("piece"),
						alignment: DocumentAlignment.right,
					},
					{
						text: legendSymbols["mfgCosts"] + legendSymbols["exclSurcharges"],
						alignment: DocumentAlignment.right,
					},
					{
						text: legendSymbols["mfgCosts"] + legendSymbols["inclSurcharges"],
						alignment: DocumentAlignment.right,
					},
					{
						text: legendSymbols["sellingPrice"] + " / " + wsi4.util.translate("piece"),
						alignment: DocumentAlignment.right,
					},
					{
						text: legendSymbols["sellingPrice"],
						alignment: DocumentAlignment.right,
					},
				],
				rows: rows,
			},
		},
	];
	return [
		createHeading(wsi4.util.translate("Articles"), 3),
		table,
		createLegend([
			"mfgCosts",
			"sellingPrice",
			"exclSurcharges",
			"inclSurcharges",
		]),
	];
}

function createSellingPriceSection(calcCache: CalcCache): DocumentItem[][] {
	return [
		createHeading(wsi4.util.translate("Summary"), 2),
		[ createSpacerItem(12) ],
		...createArticleSellingPriceSubSection(calcCache),
		[ createSpacerItem(12) ],
		...createOverallPriceSubSection(calcCache),
		[ createSpacerItem(12) ],
		...createScalePriceSubSection(calcCache),
	];
}

function createSummarySection(calcCache: CalcCache): Array<Array<DocumentItem>> {
	return [
		createHeading(wsi4.util.translate("cost_analysis"), 2),
		[ createSpacerItem(12) ],
		...createSurchargeSummarySubSection(),
		[ createSpacerItem(12) ],
		...createProcessSummarySubSection(calcCache),
		[ createSpacerItem(12) ],
		...createSheetConsumptionSubSection(),
		[ createSpacerItem(12) ],
		...createTubeConsumptionSubSection(),
		[ createSpacerItem(12) ],
		...createSetupCostsSubSection(calcCache),
		[ createSpacerItem(12) ],
		...createUnitCostsSubSection(calcCache),
		[ createSpacerItem(12) ],
		...createMaterialCostsSubSection(calcCache),
	];
}

function createTimeAndCostTable(times: Times|undefined, costs: Costs|undefined, multiplicity: number): Array<DocumentItem> {
	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: [
				// Row 0
				[
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("MaterialCosts"),
						alignment: DocumentAlignment.left,
					},
					{
						text: costs === undefined ? "N/A" : currencyString(costs.material),
						alignment: DocumentAlignment.right,
					},
				],

				// Row 0
				[
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("MaterialCosts") + " / " + wsi4.util.translate("piece"),
						alignment: DocumentAlignment.left,
					},
					{
						text: costs === undefined ? "N/A" : currencyString(costs.material / multiplicity),
						alignment: DocumentAlignment.right,
					},
				],

				// Row 1
				[
					{
						text: wsi4.util.translate("SetupTimeAbbr"),
						alignment: DocumentAlignment.left,
					},
					{
						text: times === undefined ? "N/A" : secondsToString(times.setup),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("SetupCosts"),
						alignment: DocumentAlignment.left,
					},
					{
						text: costs === undefined ? "N/A" : currencyString(costs.setup),
						alignment: DocumentAlignment.right,
					},
				],

				// Row 2
				[
					{
						text: wsi4.util.translate("UnitTimeAbbr") + " / " + wsi4.util.translate("piece"),
						alignment: DocumentAlignment.left,
					},
					{
						text: times === undefined ? "N/A" : secondsToString(times.unit / multiplicity),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("UnitCosts") + " / " + wsi4.util.translate("piece"),
						alignment: DocumentAlignment.left,
					},
					{
						text: costs === undefined ? "N/A" : currencyString(costs.unit / multiplicity),
						alignment: DocumentAlignment.right,
					},
				],

				// Row 3
				[
					{
						text: wsi4.util.translate("UnitTimeAbbr"),
						alignment: DocumentAlignment.left,
					},
					{
						text: times === undefined ? "N/A" : secondsToString(times.unit),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("UnitCosts"),
						alignment: DocumentAlignment.left,
					},
					{
						text: costs === undefined ? "N/A" : currencyString(costs.unit),
						alignment: DocumentAlignment.right,
					},
				],
			],
		},
	};
	return [ tableItem ];
}

function createSheetWsDetails(vertex: Vertex, calcCache: CalcCache): Array<Array<DocumentItem>> {
	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 [];
	}

	assertDebug(
		() => isMinExportReady(vertex),
		"Pre-condition violated; expecting valid sheet (consumption) if there is a valid nesting",
	);

	const sheetId = sheetIdForVertex(vertex);
	assert(sheetId !== undefined, "Expecting valid sheet id");

	const sheetTable = getTable(TableType.sheet);
	const sheet = sheetTable.find(row => row.identifier === sheetId);
	assert(sheet !== undefined, "Expecting valid sheet id");

	const consumptionTotal = computeSheetConsumption(vertex);
	assert(consumptionTotal !== undefined, "Expecting valid sheet consumption for export ready graph");

	const consumptionParts = computeSheetConsumptionNestedParts(vertex);
	assert(consumptionParts !== undefined, "Expecting valid part sheet consumption");

	const consumptionMasses = computeSheetConsumptionMasses(sheet, {
		total: consumptionTotal,
		parts: consumptionParts,
	});

	const tableRows = [
		[
			{
				text: wsi4.util.translate("Identifier"),
				alignment: DocumentAlignment.left,
			},
			{
				text: sheetId,
				alignment: DocumentAlignment.right,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
		],
		[
			{
				text: wsi4.util.translate("Consumption"),
				alignment: DocumentAlignment.left,
			},
			{
				text: consumptionTotal.toFixed(3),
				alignment: DocumentAlignment.right,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
		],
		[
			{
				text: wsi4.util.translate("charge_weight_in_kg"),
				alignment: DocumentAlignment.left,
			},
			{
				text: consumptionMasses.total.toFixed(3),
				alignment: DocumentAlignment.right,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
		],
		[
			{
				text: wsi4.util.translate("scrap_in_kg"),
				alignment: DocumentAlignment.left,
			},
			{
				text: consumptionMasses.scrap.toFixed(3),
				alignment: DocumentAlignment.right,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
		],
		[
			{
				text: wsi4.util.translate("test_report"),
				alignment: DocumentAlignment.left,
			},
			{
				alignment: DocumentAlignment.right,
				text: isTestReportRequiredForSheet(vertex) ? "✓" : "✗",
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
		],
	];

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: tableRows,
		},
	};

	const multiplicity = getFixedMultiplicity(vertex);
	const times = nodeTimesIfApplicable(vertex, calcCache);
	const costs = nodeCostsIfApplicable(vertex, multiplicity, calcCache);
	return [
		createHeading(wsi4.util.translate("sheet"), 4),
		[ tableItem ],
		createTimeAndCostTable(times, costs, 1),
	];
}

function createLaserSheetCuttingDetails(vertex: Vertex, calcCache: CalcCache): Array<Array<DocumentItem>> {
	const cuttingLength = sheetCuttingWsContourLength(vertex);
	const numPiercings = sheetCuttingWsContourCount(vertex);
	const cuttingGasName = getLaserSheetCuttingGasName(vertex);

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: [
				// Row 0
				[
					{
						text: wsi4.util.translate("ContourLength"),
						alignment: DocumentAlignment.left,
					},
					{
						text: cuttingLength === undefined ? "N/A" : mmToString(cuttingLength),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
				],

				// Row 1
				[
					{
						text: wsi4.util.translate("NumPiercings"),
						alignment: DocumentAlignment.left,
					},
					{
						text: numPiercings,
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
				],

				// Row 2
				[
					{
						text: wsi4.util.translate("CuttingGas"),
						alignment: DocumentAlignment.left,
					},
					{
						text: cuttingGasName,
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
				],
			],
		},
	};

	const multiplicity = getFixedMultiplicity(vertex);
	const times = nodeTimesIfApplicable(vertex, calcCache);
	const costs = nodeCostsIfApplicable(vertex, multiplicity, calcCache);
	return [
		createHeading(wsi4.util.translate(wsi4.node.processType(vertex)), 4),
		[ tableItem ],
		createTimeAndCostTable(times, costs, wsi4.node.multiplicity(vertex)),
	];
}

function createSheetCuttingWsDetails(vertex: Vertex, calcCache: CalcCache): Array<Array<DocumentItem>> {
	const processType = wsi4.node.processType(vertex);
	if (processType === "laserSheetCutting") {
		return createLaserSheetCuttingDetails(vertex, calcCache);
	} else {
		console.error("Calculation for process \"" + processType + "\" is not implemented.");
		return [];
	}
}

function createDieBendingProcessDetails(vertex: Vertex, calcCache: CalcCache): Array<Array<DocumentItem>> {
	const numBends = sheetBendingWsNumBendLines(vertex);

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: [
				// Row 0
				[
					{
						text: wsi4.util.translate("NumBends"),
						alignment: DocumentAlignment.left,
					},
					{
						text: numBends,
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
				],
			],
		},
	};

	const multiplicity = getFixedMultiplicity(vertex);
	const times = nodeTimesIfApplicable(vertex, calcCache);
	const costs = nodeCostsIfApplicable(vertex, multiplicity, calcCache);
	return [
		createHeading(wsi4.util.translate(wsi4.node.processType(vertex)), 4),
		[ tableItem ],
		createTimeAndCostTable(times, costs, wsi4.node.multiplicity(vertex)),
	];
}

function createSheetBendingWsDetails(vertex: Vertex, calcCache: CalcCache): Array<Array<DocumentItem>> {
	const processType = wsi4.node.processType(vertex);
	if (processType === "dieBending") {
		return createDieBendingProcessDetails(vertex, calcCache);
	} else {
		console.error("Calculation for process \"" + processType + "\" is not implemented.");
		return [];
	}
}

function createJoiningWsDetails(vertex: Vertex, calcCache: CalcCache): Array<Array<DocumentItem>> {
	const sourceVertices = wsi4.graph.sources(vertex);
	const rows = sourceVertices.map(sourceVertex => {
		const multiplicity = wsi4.node.multiplicity(sourceVertex);
		const name = getArticleName(sourceVertex);
		return [
			{
				text: multiplicity.toString() + " x",
				alignment: DocumentAlignment.left,
			},
			{
				text: "\"" + name + "\"",
				alignment: DocumentAlignment.left,
			},
			{
				text: /* spacer */ "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
			{
				text: "",
				alignment: DocumentAlignment.left,
			},
		];
	});

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			alignment: DocumentAlignment.left,
			width: 12,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: rows,
		},
	};

	const multiplicity = getFixedMultiplicity(vertex);
	const times = nodeTimesIfApplicable(vertex, calcCache);
	const costs = nodeCostsIfApplicable(vertex, multiplicity, calcCache);
	return [
		createHeading(wsi4.util.translate(wsi4.node.processType(vertex)), 4),
		[ tableItem ],
		createTimeAndCostTable(times, costs, wsi4.node.multiplicity(vertex)),
	];
}

function addSheetProcessSection(vertex: Vertex): Array<Array<DocumentItem>> {
	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: [
				[
					{
						text: wsi4.util.translate("test_report"),
						alignment: DocumentAlignment.left,
					},
					{
						text: JSON.stringify(isTestReportRequiredForSheet(vertex)),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.right,
					},
				],
			],
		},
	};
	return [ [ tableItem ] ];
}

function addAutomaticDeburringProcessSection(vertex: Vertex): Array<Array<DocumentItem>> {
	assumeGraphAxioms([ "deburringSubProcessNodeHasTwoDimRep" ]);
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		return wsi4.throwError("Missing twoDimRep");
	}
	const deburringLength = computeAutomaticMechanicalDeburringLength(twoDimRep, wsi4.node.multiplicity(vertex), getAssociatedSheetMaterialId(vertex));
	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: [
				[
					{
						text: wsi4.util.translate("net_length"),
						alignment: DocumentAlignment.left,
					},
					{
						text: deburringLength === undefined ? "N/A" : mmToString(deburringLength),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.right,
					},
				],
				[
					{
						text: wsi4.util.translate("double_sided"),
						alignment: DocumentAlignment.left,
					},
					{
						text: getNodeUserDataEntryOrThrow("deburrDoubleSided", vertex) ? "✓" : "✗",
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.right,
					},
				],
			],
		},
	};
	return [ [ tableItem ] ];
}

function addManualDeburringProcessSection(vertex: Vertex): Array<Array<DocumentItem>> {
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		return [];
	}
	const contourLength = computeContourLength(twoDimRep);
	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: [
				[
					{
						text: wsi4.util.translate("ContourLength"),
						alignment: DocumentAlignment.left,
					},
					{
						text: mmToString(contourLength),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.right,
					},
				],
				[
					{
						text: wsi4.util.translate("double_sided"),
						alignment: DocumentAlignment.left,
					},
					{
						text: getNodeUserDataEntryOrThrow("deburrDoubleSided", vertex) ? "✓" : "✗",
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.right,
					},
				],
			],
		},
	};
	return [ [ tableItem ] ];
}

function addUserDefinedThreadingProcessSection(vertex: Vertex) {
	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: [
				[
					{
						text: wsi4.util.translate("num_threads"),
						alignment: DocumentAlignment.left,
					},
					{
						text: getNodeUserDataEntryOrThrow("numThreads", vertex)
							.toFixed(0),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.right,
					},
				],
			],
		},
	};
	return [ [ tableItem ] ];
}

function addUserDefinedCountersinkingProcessSection(vertex: Vertex) {
	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: [
				[
					{
						text: wsi4.util.translate("num_countersinks"),
						alignment: DocumentAlignment.left,
					},
					{
						text: getNodeUserDataEntryOrThrow("numCountersinks", vertex)
							.toFixed(0),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.right,
					},
				],
			],
		},
	};
	return [ [ tableItem ] ];
}

function addProcessSpecificSection(vertex: Vertex): Array<Array<DocumentItem>> {
	const processType = wsi4.node.processType(vertex);
	if (processType === ProcessType.sheet) {
		return addSheetProcessSection(vertex);
	} else if (processType === ProcessType.automaticMechanicalDeburring) {
		return addAutomaticDeburringProcessSection(vertex);
	} else if (processType === ProcessType.manualMechanicalDeburring) {
		return addManualDeburringProcessSection(vertex);
	} else if (processType === ProcessType.userDefinedThreading) {
		return addUserDefinedThreadingProcessSection(vertex);
	} else if (processType === ProcessType.userDefinedCountersinking) {
		return addUserDefinedCountersinkingProcessSection(vertex);
	} else {
		return [];
	}
}

function createUserDefinedWsDetails(vertex: Vertex, calcCache: CalcCache): Array<Array<DocumentItem>> {
	const processTable = getTable(TableType.process);
	const processId = wsi4.node.processId(vertex);
	const row = processTable.find(element => element.identifier === processId);
	if (row === undefined) {
		return wsi4.throwError("Table invalid");
	}
	if (typeof row.name !== "string") {
		return wsi4.throwError("Table invalid");
	}

	const multiplicity = getFixedMultiplicity(vertex);
	const times = nodeTimesIfApplicable(vertex, calcCache);
	const costs = nodeCostsIfApplicable(vertex, multiplicity, calcCache);
	return [
		createHeading(row.name, 4),
		...addProcessSpecificSection(vertex),
		createTimeAndCostTable(times, costs, wsi4.node.multiplicity(vertex)),
	];
}

function createPackagingWsDetails(vertex: Vertex, calcCache: CalcCache): Array<Array<DocumentItem>> {
	const processTable = getTable(TableType.process);
	const processId = wsi4.node.processId(vertex);
	const row = processTable.find(element => element.identifier === processId);
	assert(row !== undefined, "Tables invalid");
	const multiplicity = getFixedMultiplicity(vertex);
	const times = nodeTimesIfApplicable(vertex, calcCache);
	const costs = nodeCostsIfApplicable(vertex, multiplicity, calcCache);
	return [
		createHeading(row.name, 4),
		...addProcessSpecificSection(vertex),
		createTimeAndCostTable(times, costs, 1),
	];
}

function createTubeCuttingWsDetails(vertex: Vertex, _calcCache: CalcCache): Array<Array<DocumentItem>> {
	assertDebug(() => wsi4.node.workStepType(vertex) === WorkStepType.tubeCutting, "Pre-condition violated");

	const tube = tubeForVertex(vertex);
	const tubeSpecificationName = tube === undefined ? undefined : getTable(TableType.tubeSpecification)
		.find(row => row.identifier === tube.tubeSpecificationId)
		?.name;
	const profileName = tubeProfileForVertex(vertex)?.name;

	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 overallTubeConsumption = grossTubeConsumptionForVertex(vertex);
	const tubeConsumptionPerPiece = overallTubeConsumption === undefined ? undefined : overallTubeConsumption / wsi4.node.multiplicity(vertex);

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: [
				[
					{
						text: wsi4.util.translate("profile"),
						alignment: DocumentAlignment.left,
					},
					{
						text: profileName,
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("specification"),
						alignment: DocumentAlignment.left,
					},
					{
						text: tubeSpecificationName ?? "N/A",
						alignment: DocumentAlignment.right,
					},
				],
				[
					{
						text: wsi4.util.translate("NumPiercings"),
						alignment: DocumentAlignment.left,
					},
					{
						text: contourCount === undefined ? "N/A" : contourCount.toFixed(0),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("extrusion_depth"),
						alignment: DocumentAlignment.left,
					},
					{
						text: mmToString(extrusionLength),
						alignment: DocumentAlignment.right,
					},
				],
				[
					{
						text: wsi4.util.translate("ContourLength"),
						alignment: DocumentAlignment.left,
					},
					{
						text: cuttingLength === undefined ? "N/A" : mmToString(cuttingLength),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("approx_tube_consumption_per_piece"),
						alignment: DocumentAlignment.left,
					},
					{
						text: tubeConsumptionPerPiece === undefined ? "N/A" : tubeConsumptionPerPiece.toFixed(3),
						alignment: DocumentAlignment.right,
					},
				],
			],
		},
	};
	return [ [ tableItem ] ];
}

function createTubeWsDetails(tubeVertex: Vertex, _calcCache: CalcCache): Array<Array<DocumentItem>> {
	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,
			});
		}
	})();

	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 12,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: [
				[
					{
						alignment: DocumentAlignment.left,
						text: wsi4.util.translate("tube"),
					},
					{
						alignment: DocumentAlignment.right,
						text: tube === undefined ? "N/A" : tube.name,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.right,
					},
				],
				[
					{
						alignment: DocumentAlignment.left,
						text: wsi4.util.translate("Consumption"),
					},
					{
						alignment: DocumentAlignment.right,
						text: grossConsumption === undefined ? "N/A" : grossConsumption.toFixed(3),
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.right,
					},
				],
				[
					{
						text: wsi4.util.translate("charge_weight_in_kg"),
						alignment: DocumentAlignment.left,
					},
					{
						text: consumptionMasses === undefined ? "N/A" : consumptionMasses.gross.toFixed(3),
						alignment: DocumentAlignment.right,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
				],
				[
					{
						text: wsi4.util.translate("scrap_in_kg"),
						alignment: DocumentAlignment.left,
					},
					{
						text: consumptionMasses === undefined ? "N/A" : consumptionMasses.scrap.toFixed(3),
						alignment: DocumentAlignment.right,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
					{
						text: "",
						alignment: DocumentAlignment.left,
					},
				],
			],
		},
	};
	return [ [ tableItem ] ];
}

function createUserDefinedBaseWsDetails(vertex: Vertex, calcCache: CalcCache): Array<Array<DocumentItem>> {
	const multiplicity = getFixedMultiplicity(vertex);
	const times = nodeTimesIfApplicable(vertex, calcCache);
	const costs = nodeCostsIfApplicable(vertex, multiplicity, calcCache);
	return [
		createHeading(wsi4.util.translate(wsi4.node.processType(vertex)), 4),
		...addProcessSpecificSection(vertex),
		createTimeAndCostTable(times, costs, wsi4.node.multiplicity(vertex)),
	];
}

function createNodeDetails(vertex: Vertex, calcCache: CalcCache): Array<Array<DocumentItem>> {
	switch (wsi4.node.workStepType(vertex)) {
		case WorkStepType.sheet: return createSheetWsDetails(vertex, calcCache);
		case WorkStepType.sheetCutting: return createSheetCuttingWsDetails(vertex, calcCache);
		case WorkStepType.sheetBending: return createSheetBendingWsDetails(vertex, calcCache);
		case WorkStepType.joining: return createJoiningWsDetails(vertex, calcCache);
		case WorkStepType.userDefined: return createUserDefinedWsDetails(vertex, calcCache);
		case WorkStepType.userDefinedBase: return createUserDefinedBaseWsDetails(vertex, calcCache);
		case WorkStepType.packaging: return createPackagingWsDetails(vertex, calcCache);
		case WorkStepType.tubeCutting: return createTubeCuttingWsDetails(vertex, calcCache);
		case WorkStepType.tube: return createTubeWsDetails(vertex, calcCache);
		case WorkStepType.transform: return [];
		case WorkStepType.undefined: return [];
	}
}

// Document creation - internal calculation
function createArticleDetails(article: Vertex[], pngFutures: readonly Readonly<VertexAnd<ArrayBufferFuture>>[]): Array<Array<DocumentItem>> {
	const vertex = getArticleSignatureVertex(article);
	const dimensions = (() => {
		const assembly = wsi4.node.assembly(vertex) ?? wsi4.node.inputAssembly(vertex);
		if (assembly === undefined) {
			return undefined;
		} else {
			return computeWcsDimensions(assembly);
		}
	})();

	const multiplicity = getFixedMultiplicity(vertex);

	// [kg]
	const mass = computeNodeMass(vertex);
	const material = getArticleSheetMaterialName(vertex) ?? getArticleTubeMaterialName(vertex);
	const articleUserData = getArticleUserData(vertex);
	const tableItem = {
		type: DocumentItemType.table,
		content: {
			width: 10,
			alignment: DocumentAlignment.left,
			columnWidths: [
				22.5,
				22.5,
				10.0,
				22.5,
				22.5,
			],
			columnHeaders: [],
			rows: [
				// Row 0
				[
					{
						text: wsi4.util.translate("part_number"),
						alignment: DocumentAlignment.left,
					},
					{
						text: articleUserData.externalPartNumber,
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("drawing_number"),
						alignment: DocumentAlignment.left,
					},
					{
						text: articleUserData.externalDrawingNumber,
						alignment: DocumentAlignment.right,
					},
				],
				// Row 1
				[
					{
						text: wsi4.util.translate("revision_number"),
						alignment: DocumentAlignment.left,
					},
					{
						text: articleUserData.externalRevisionNumber,
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: /* unused */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: /* unused */ "",
						alignment: DocumentAlignment.left,
					},
				],
				// Row 2
				[
					{
						text: wsi4.util.translate("Material"),
						alignment: DocumentAlignment.left,
					},
					{
						text: material === undefined ? wsi4.util.translate("Unknown") : material,
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("Length"),
						alignment: DocumentAlignment.left,
					},
					{
						text: dimensions === undefined ? "N/A" : mmToString(dimensions.x, 2),
						alignment: DocumentAlignment.right,
					},
				],

				// Row 3
				[
					{
						text: wsi4.util.translate("Mass"),
						alignment: DocumentAlignment.left,
					},
					{
						text: mass === undefined ? "N/A" : kgToString(mass),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("Width"),
						alignment: DocumentAlignment.left,
					},
					{
						text: dimensions === undefined ? "N/A" : mmToString(dimensions.y, 2),
						alignment: DocumentAlignment.right,
					},
				],

				// Row 4
				[
					{
						text: wsi4.util.translate("Multiplicity"),
						alignment: DocumentAlignment.left,
					},
					{
						text: Number(multiplicity)
							.toString(),
						alignment: DocumentAlignment.right,
					},
					{
						text: /* spacer */ "",
						alignment: DocumentAlignment.left,
					},
					{
						text: wsi4.util.translate("Height"),
						alignment: DocumentAlignment.left,
					},
					{
						text: dimensions === undefined ? "N/A" : mmToString(dimensions.z, 2),
						alignment: DocumentAlignment.right,
					},
				],
			],
		},
	};

	return [
		createHeading(wsi4.util.translate("PartData"), 4),
		[
			tableItem,
			pngItemFromFuture(vertex, 2, DocumentAlignment.right, pngFutures),
		],
	];
}

function createDetailsSection(pngFutures: readonly Readonly<VertexAnd<ArrayBufferFuture>>[], calcCache: CalcCache): Array<Array<DocumentItem>> {
	const result: Array<Array<DocumentItem>> = [];

	createHeading(wsi4.util.translate("Details"), 2);

	const articles = wsi4.graph.articles();
	let count = 0;
	for (const article of articles) {
		const firstVertex = front(article);
		const vertices = wsi4.graph.article(firstVertex);

		if (vertices.length === 0) {
			continue;
		}

		result.push(createHeading(getArticleName(firstVertex), 3), ...createArticleDetails(vertices, pngFutures));

		for (const vertex of vertices) {
			result.push(...createNodeDetails(vertex, calcCache));
			result.push([ createSpacerItem(12) ]);
		}

		if (count !== articles.length - 1) {
			// Skip separator after last article
			result.push(createSeparator());
		}
		++count;
	}
	return result;
}

function complementFutures(pngFuturesInput: readonly Readonly<VertexAnd<ArrayBufferFuture>>[]): Readonly<VertexAnd<ArrayBufferFuture>>[] {
	const result = Array.from(pngFuturesInput);
	wsi4.graph.articles()
		.map(article => getArticleSignatureVertex(article))
		.filter(vertex => result.every(vab => !isEqual(vab.vertex, vertex)))
		.forEach(vertex => {
			const assembly = wsi4.node.assembly(vertex) ?? wsi4.node.inputAssembly(vertex);
			if (assembly !== undefined) {
				result.push({
					vertex: vertex,
					data: wsi4.geo.assembly.asyncRenderIntoPng(
						assembly,
						wsi4.geo.assembly.computeDefaultCamera(assembly),
					),
				});
			}
		});
	return result;
}

export function createInternalCalc(pngFuturesInput: readonly Readonly<VertexAnd<ArrayBufferFuture>>[] = [], calcCache?: CalcCache): Array<Array<DocumentItem>> {
	const pngFutures = complementFutures(pngFuturesInput);
	calcCache = calcCache ?? createCalcCache();
	return [
		...createFirstPageHeader(),
		[ createSpacerItem(12) ],
		...createSellingPriceSection(calcCache),
		[ createSpacerItem(12) ],
		...createSummarySection(calcCache),
		[ createSpacerItem(12) ],
		...createDetailsSection(pngFutures, calcCache),
	];
}
