import {
	back,
	front,
	itemAt,
} from "qrc:/js/lib/array_util";
import {
	tolerances,
} from "qrc:/js/lib/constants";
import {
	getSettingOrDefault,
} from "qrc:/js/lib/settings_table";
import {
	computeValidLaserCuttingGasIds,
	getSheetsInStock,
	getTable,
	getTubesInStock,
	lookUpProcessSetupTimeFallBack,
	tubeProfileForGeometry,
} from "qrc:/js/lib/table_utils";
import {
	assert,
	assertDebug,
	bbDimensionX,
	bbDimensionY,
	cleanFileName,
	computeUniqueNames,
	isEqual,
} from "qrc:/js/lib/utils";
import {
	computeOrderedProcesses,
	findActiveProcess,
	isAvailableProcess,
	workStepTypeMap,
} from "qrc:/js/lib/process";
import {
	BendLineEngravingMode,
} from "qrc:/js/lib/bend_line_engraving_mode";
import {
	getSharedDataEntry,
} from "qrc:/js/lib/shared_data";
import {
	createUpdatedNodeUserDataImpl,
	nodeUserDatumImpl,
} from "qrc:/js/lib/userdata_config";
import {
	computePotentialSheetNestingPartitions,
	SheetNestingPartition,
	SheetNestingPartitionTarget,
} from "qrc:/js/lib/sheet_nesting_partitions";
import {
	DieChoiceParams,
	DieChoiceTables,
	makeDieChoices,
} from "qrc:/js/lib/die_choice";
import {
	mappedSheetCuttingMaterialId,
} from "qrc:/js/lib/node_utils";
import {
	createUpdatedArticleUserData,
} from "./article_userdata_config";
import {
	computeLaserSheetCuttingUnitTimePerPiece,
	LaserSheetCuttingCalcParams,
	laserSheetCuttingRatePerSecond,
} from "./calc_laser_sheet_cutting";

interface SheetCuttingProcessCandidate {
	processId: string | undefined;
	cuttingGasId: string | undefined;
	sheetMaterialId: string | undefined;
}

interface SheetMaterialCuttingGas{
	cuttingGasId: string;
	sheetMaterialId: string;
}

function gatherValidMaterialGasCombinations(thickness: number, sheetMaterialId: string): SheetMaterialCuttingGas[] {
	const sheetCuttingMaterialId = mappedSheetCuttingMaterialId(sheetMaterialId);
	if (sheetCuttingMaterialId === undefined) {
		return [];
	}

	const thicknessConstraints = getTable("laserSheetCuttingMaxThickness");
	const cuttingSpeeds = getTable("laserSheetCuttingSpeed");

	const cuttingGasIds = computeValidLaserCuttingGasIds(thickness, sheetCuttingMaterialId, thicknessConstraints, cuttingSpeeds);
	if (cuttingGasIds.length === 0) {
		return [];
	}

	return cuttingGasIds.map(cuttingGasId => ({
		cuttingGasId: cuttingGasId,
		sheetMaterialId: sheetMaterialId,
	}));
}

function pickSheetMaterialId(thickness: number, twoDimBox: Box2): string | undefined {
	const nestingDistance = getSettingOrDefault("sheetNestingDistance");
	const dim0 = bbDimensionX(twoDimBox);
	const dim1 = bbDimensionY(twoDimBox);
	const dimX = Math.max(dim0, dim1) + nestingDistance;
	const dimY = Math.min(dim0, dim1) + nestingDistance;

	const candidates = getSheetsInStock().filter(sheet => Math.abs(sheet.thickness - thickness) <= tolerances.thickness && dimX < sheet.dimX && dimY < sheet.dimY);

	const defaultSheetMaterialId = getSharedDataEntry("defaultSheetMaterialId");
	if (defaultSheetMaterialId !== undefined && candidates.some(row => row.sheetMaterialId === defaultSheetMaterialId)) {
		return defaultSheetMaterialId;
	}

	const priceMap = (() => {
		const sheetPrices = getTable("sheetPrice");
		const map = new Map<string, number>();
		candidates.forEach(sheet => map.set(sheet.identifier, sheetPrices.find(row => row.sheetId === sheet.identifier)?.pricePerSheet ?? Number.MAX_VALUE));
		return map;
	})();
	candidates.sort((lhs, rhs) => (priceMap.get(lhs.identifier) ?? Number.MAX_VALUE) - (priceMap.get(rhs.identifier) ?? Number.MAX_VALUE));

	return candidates[0]?.sheetMaterialId;
}

function pickSheetCuttingProcessCandidate(vertex: SlVertex, preGraph: PreDocumentGraph): SheetCuttingProcessCandidate {
	const part = wsi4.sl.node.part(vertex, preGraph);
	const thickness = wsi4.cad.thickness(part);

	const twoDimRep = wsi4.sl.node.twoDimRep(vertex, preGraph);
	const twoDimBox = twoDimRep === undefined ? undefined : wsi4.cam.util.boundingBox2(twoDimRep);

	const engravings = twoDimRep === undefined ? [] : wsi4.geo.util.sceneSegments(
		wsi4.cam.util.extractEngravings(twoDimRep),
	);

	const multiplicity = wsi4.sl.node.multiplicity(vertex, preGraph);

	const processId = ((): string | undefined => {
		// For now only one process can be handled (since laser sheet cutting technology tables are processes independent)
		const processTable = getTable("process");
		const processes = computeOrderedProcesses(row => row.active && row.type === "laserSheetCutting" && isAvailableProcess(row, processTable));
		if (processes.length === 0) {
			// Not expected - there should be no sheet metal part article in this case
			assertDebug(() => false, "No laser sheet cutting process found");
			return undefined;
		} else if (processes.length > 1) {
			wsi4.util.warn("Sheet cutting process ambiguous.  Selecting first candidate");
		}
		return front(processes).identifier;
	})();

	const sheetMaterialId = twoDimBox === undefined ? undefined : pickSheetMaterialId(thickness, twoDimBox);
	const sheetCuttingProcessCandidates = sheetMaterialId === undefined ? [] : gatherValidMaterialGasCombinations(thickness, sheetMaterialId)
		.map(candidate => {
			const price = (() => {
				if (twoDimRep === undefined) {
					return undefined;
				}

				const sheetCuttingMaterialId = mappedSheetCuttingMaterialId(candidate.sheetMaterialId);
				if (sheetCuttingMaterialId === undefined || twoDimRep === undefined) {
					return undefined;
				}

				const calcParams: LaserSheetCuttingCalcParams = {
					twoDimRep: twoDimRep,
					thickness: thickness,
					sheetCuttingMaterialId: sheetCuttingMaterialId,
					laserSheetCuttingGasId: candidate.cuttingGasId,
					engravings: engravings,
				};

				const ratePerSecond = laserSheetCuttingRatePerSecond(calcParams);
				if (ratePerSecond === undefined) {
					return undefined;
				}

				const setupTime = processId === undefined ? 0 : (lookUpProcessSetupTimeFallBack(processId) ?? 0);
				const unitTimePerPiece = computeLaserSheetCuttingUnitTimePerPiece(calcParams);
				const time = setupTime + multiplicity * unitTimePerPiece;
				return time * ratePerSecond;
			})();
			return {
				candidate: candidate,
				price: price,
			};
		})
		.sort((lhs, rhs) => (lhs.price ?? Number.MAX_VALUE) - (rhs.price ?? Number.MAX_VALUE))
		.map(obj => obj.candidate);

	// There should be at least one entry - otherwise there should be no sheet part component
	const candidate = sheetCuttingProcessCandidates[0];

	return {
		processId: processId,
		sheetMaterialId: candidate?.sheetMaterialId,
		cuttingGasId: candidate?.cuttingGasId,
	};
}

interface InitialFlatSmpArticleParams {
	type: "flatSmp";

	article: SlVertex[];

	// There must be a matching process, otherwise the article is not meaningful
	sheetCuttingProcessId: string;

	// There must be a matching process, otherwise the article is not meaningful
	sheetCuttingProcessType: ProcessType;

	// Value is unset if there is no matching material
	sheetMaterialId?: string;

	// Value is unset if there is no matching cutting gas
	cuttingGasId?: string;

	bendLineEngravingMode?: BendLineEngravingMode;

}

interface InitialBendSmpArticleParams {
	type: "bendSmp";

	article: SlVertex[];

	// There must be a matching process, otherwise the article is not meaningful
	sheetCuttingProcessId: string;

	// There must be a matching process, otherwise the article is not meaningful
	sheetCuttingProcessType: ProcessType;

	sheetBendingProcessId: string;

	sheetBendingProcessType: ProcessType;

	// Value is unset if there is no matching sheet
	sheetMaterialId?: string;

	// Value is unset if there is no matching cutting gas
	cuttingGasId?: string;

	bendLineEngravingMode?: BendLineEngravingMode;
}

interface InitialTubePartArticleParams {
	type: "tubePart";

	article: SlVertex[];
	processId?: string;
	tubeMaterialId?: string;
	tubeSpecificationId?: string;
	nestorTargetLength?: number;
}

interface InitialGenericArticleParams {
	type: "generic";
	article: SlVertex[];
	processType: ProcessType;
	processId: string;
}

export type InitialArticleParams = InitialFlatSmpArticleParams
| InitialBendSmpArticleParams
| InitialTubePartArticleParams
| InitialGenericArticleParams;

function computeInitialSmpArticleParams(article: SlVertex[], preGraph: PreDocumentGraph): InitialFlatSmpArticleParams | InitialBendSmpArticleParams {
	const scpc = pickSheetCuttingProcessCandidate(front(article), preGraph);

	const result = ((): InitialBendSmpArticleParams | InitialFlatSmpArticleParams => {
		if (article.some(v => wsi4.sl.node.workStepType(v, preGraph) === "sheetBending")) {
			const bendingProcess = findActiveProcess(row => row.type === "dieBending");
			return {
				type: "bendSmp",
				article: article,
				sheetCuttingProcessId: scpc.processId ?? "",
				sheetCuttingProcessType: "laserSheetCutting",
				sheetBendingProcessId: bendingProcess?.identifier ?? "",
				sheetBendingProcessType: "dieBending",
			};
		} else {
			return {
				type: "flatSmp",
				article: article,
				sheetCuttingProcessId: scpc.processId ?? "",
				sheetCuttingProcessType: "laserSheetCutting",
			};
		}
	})();

	if (scpc.sheetMaterialId !== undefined) {
		result.sheetMaterialId = scpc.sheetMaterialId;
	}

	if (scpc.cuttingGasId !== undefined) {
		result.cuttingGasId = scpc.cuttingGasId;
	}

	const bendLineEngravingMode = getSharedDataEntry("defaultBendLineEngravingMode");
	if (bendLineEngravingMode !== "none") {
		result.bendLineEngravingMode = bendLineEngravingMode;
	}

	return result;
}

function computeInitialTubePartArticleParams(article: SlVertex[], preGraph: PreDocumentGraph): InitialTubePartArticleParams {
	const vertex = front(article);
	assertDebug(() => wsi4.sl.node.workStepType(vertex, preGraph) === "tubeCutting");

	const processTable = getTable("process");
	const tubeCuttingProcesses = computeOrderedProcesses(row => row.type === "tubeCutting" && isAvailableProcess(row, processTable));
	assert(tubeCuttingProcesses.length !== 0, "Expecting at least one available tube cutting process at this point");

	const result: InitialTubePartArticleParams = {
		type: "tubePart",
		article: article,
	};

	const profileGeometry = wsi4.sl.node.tubeProfileGeometry(front(article), preGraph);
	if (profileGeometry === undefined) {
		return result;
	}

	const profileExtrusionLength = wsi4.sl.node.profileExtrusionLength(front(article), preGraph);
	const profile = tubeProfileForGeometry(profileGeometry);
	const tube = getTubesInStock()
		.filter(row => profileExtrusionLength <= row.dimX && (profile === undefined || row.tubeProfileId === profile.identifier))
		.shift();
	if (tube === undefined) {
		return result;
	}

	result.tubeMaterialId = tube.tubeMaterialId;
	result.tubeSpecificationId = tube.tubeSpecificationId;
	result.nestorTargetLength = Math.max(0, tube.dimX - getSettingOrDefault("tubeClampingLength"));

	// Note: There might be more than one Process per TubeCuttingProcess
	// so applying sort as long as there is no logic to further refine the selection.
	const tubeCuttingProcessMappings = getTable("tubeCuttingProcessMapping")
		.filter(mapping => tubeCuttingProcesses.some(process => mapping.processId === process.identifier))
		.filter(mapping => tube.tubeMaterialId === mapping.tubeMaterialId)
		.sort((lhs, rhs) => lhs.processId < rhs.processId ? -1 : lhs.processId === rhs.processId ? 0 : 1);
	if (tubeCuttingProcessMappings.length === 0) {
		return result;
	}

	const process = tubeCuttingProcesses.find(row => row.identifier === tubeCuttingProcessMappings[0]!.processId);
	if (process !== undefined) {
		result.processId = process.identifier;
	}

	return result;
}

function computeInitialGenericArticleParams(article: SlVertex[], preGraph: PreDocumentGraph): InitialGenericArticleParams {
	assert(article.length === 1, "Expecting generic article of length 1; actual length: " + article.length.toString());
	const wst = wsi4.sl.node.workStepType(front(article), preGraph);
	const process = findActiveProcess(row => workStepTypeMap[row.type] === wst);
	return {
		type: "generic",
		article: article,
		processId: process?.identifier ?? "",
		processType: process?.type ?? "externalPart",
	};
}

export function computeInitialArticleParams(article: SlVertex[], preGraph: PreDocumentGraph): InitialArticleParams {
	const wsts = article.map(v => wsi4.sl.node.workStepType(v, preGraph));
	if (wsts.some(wst => wst === "sheetCutting" || wst === "sheetBending")) {
		return computeInitialSmpArticleParams(article, preGraph);
	} else if (wsts.some(wst => wst === "tubeCutting")) {
		return computeInitialTubePartArticleParams(article, preGraph);
	} else {
		return computeInitialGenericArticleParams(article, preGraph);
	}
}

export function gatherInitialArticleParams(preGraph: PreDocumentGraph, articles?: SlVertex[][]): InitialArticleParams[] {
	return (articles ?? wsi4.sl.graph.articles(preGraph)).map(article => computeInitialArticleParams(article, preGraph));
}

function initialSheetCuttingNodeUserData(params: InitialFlatSmpArticleParams | InitialBendSmpArticleParams): StringIndexedInterface {
	let result: StringIndexedInterface = {};
	if (params.type === "flatSmp" && params.sheetMaterialId !== undefined) {
		// In case of a bendSmp article the material should end up in the sheetBending node
		result = createUpdatedNodeUserDataImpl("sheetMaterialId", params.sheetMaterialId, result);
	}
	if (params.cuttingGasId !== undefined) {
		result = createUpdatedNodeUserDataImpl("laserSheetCuttingGasId", params.cuttingGasId, result);
	}
	if (params.bendLineEngravingMode !== undefined) {
		result = createUpdatedNodeUserDataImpl("bendLineEngravingMode", params.bendLineEngravingMode, result);
	}
	return result;
}

function initialSheetBendingNodeUserData(params: InitialBendSmpArticleParams): StringIndexedInterface {
	let result: StringIndexedInterface = {};
	if (params.sheetMaterialId !== undefined) {
		result = createUpdatedNodeUserDataImpl("sheetMaterialId", params.sheetMaterialId, result);
	}
	if (params.bendLineEngravingMode !== undefined) {
		result = createUpdatedNodeUserDataImpl("bendLineEngravingMode", params.bendLineEngravingMode, result);
	}
	return result;
}

function gatherDieChoiceParams(vertex: SlVertex, graph: PreDocumentGraph, sheetMaterialId?: string): DieChoiceParams {
	const part = wsi4.sl.node.part(vertex, graph);
	return {
		sheetMaterialId: sheetMaterialId,
		thickness: wsi4.cad.thickness(part),
		bendLineData: wsi4.sl.node.bendLineData(vertex, graph) ?? [],
		flangeLengths: wsi4.sl.node.bendLineFlangeLengths(vertex, graph) ?? [],

	};
}

function initialSheetComponentNodeUpdates(
	articleParams: InitialFlatSmpArticleParams | InitialBendSmpArticleParams,
	preGraph: PreDocumentGraph,
	bendDieChoiceTables: () => DieChoiceTables,
): SlNodeUpdate[] {
	return articleParams.article.map((v): SlNodeUpdate => {
		const wst = wsi4.sl.node.workStepType(v, preGraph);
		const content = (() => {
			switch (wst) {
				case "sheetCutting": {
					const result: SlNodeUpdateSheetCutting = {
						vertex: v,
						processId: articleParams.sheetCuttingProcessId,
						processType: articleParams.sheetCuttingProcessType,
						nodeUserData: initialSheetCuttingNodeUserData(articleParams),
					};
					return result;
				}
				case "sheetBending": {
					const process = findActiveProcess(row => row.type === "dieBending");
					assert(process !== undefined, "There should be no sheet bending node without an active sheet bending process");
					assert(articleParams.type === "bendSmp", "Initial article articleParams inconsistent");
					const dieChoiceParams = gatherDieChoiceParams(front(articleParams.article), preGraph, articleParams.sheetMaterialId);
					const bendTables = articleParams.sheetMaterialId === undefined ? undefined : bendDieChoiceTables();
					const dieChoiceMap = makeDieChoices(dieChoiceParams, bendTables);
					const result: SlNodeUpdateSheetBending = {
						vertex: v,
						processId: process.identifier,
						processType: process.type,
						dieChoiceMap: dieChoiceMap,
						nodeUserData: initialSheetBendingNodeUserData(articleParams),
					};
					return result;
				}
				case "joining":
				case "packaging":
				case "sheet":
				case "transform":
				case "tube":
				case "tubeCutting":
				case "undefined":
				case "userDefined":
				case "userDefinedBase": return wsi4.throwError("Unexpected WST: " + wst);
			}
		})();
		return {
			type: wst,
			content: content,
		};
	});
}

function initialTubeCuttingUserData(params: InitialTubePartArticleParams): StringIndexedInterface {
	let result = {};
	if (params.tubeMaterialId !== undefined) {
		result = createUpdatedNodeUserDataImpl("tubeMaterialId", params.tubeMaterialId, result);
	}
	if (params.tubeSpecificationId !== undefined) {
		result = createUpdatedNodeUserDataImpl("tubeSpecificationId", params.tubeSpecificationId, result);
	}
	return result;
}

function initialTubeComponentNodeUpdates(params: InitialTubePartArticleParams): SlNodeUpdate[] {
	const tubeCuttingContent: SlNodeUpdateTubeCutting = {
		vertex: front(params.article),
		processType: "tubeCutting",
		processId: params.processId ?? "",
		nodeUserData: initialTubeCuttingUserData(params),
	};
	return [
		{
			type: "tubeCutting",
			content: tubeCuttingContent,
		},
	];
}

function initialGenericArticleNodeUpdates(params: InitialGenericArticleParams, preGraph: PreDocumentGraph): SlNodeUpdate[] {
	assert(params.article.length === 1, "Expecting only one vertex for generic articles");
	const vertex = front(params.article);
	const wst = wsi4.sl.node.workStepType(vertex, preGraph);
	const content = (() => {
		switch (wst) {
			case "joining":
			case "packaging":
			case "transform":
			case "tubeCutting":
			case "undefined":
			case "userDefined":
			case "userDefinedBase": {
				return {
					vertex: vertex,
					processId: params.processId,
					processType: params.processType,
					userData: {},
				};
			}
			case "sheet":
			case "tube":
			case "sheetCutting":
			case "sheetBending": return wsi4.throwError("Unexpected WST: " + wst);
		}
	})();
	return [
		{
			type: wst,
			content: content,
		},
	];
}

export function computeInitialNodeUpdatesForArticle(
	article: SlVertex[],
	preGraph: PreDocumentGraph,
	params: InitialArticleParams,
	bendDieChoiceTables: () => DieChoiceTables,
): SlNodeUpdate[] {
	const wsts = article.map(v => wsi4.sl.node.workStepType(v, preGraph));
	if (wsts.some(wst => wst === "sheetCutting" || wst === "sheetBending")) {
		assert(params.type === "flatSmp" || params.type === "bendSmp", "Unexpected initial article params type: " + params.type);
		return initialSheetComponentNodeUpdates(params, preGraph, bendDieChoiceTables);
	} else if (wsts.some(wst => wst === "tubeCutting")) {
		assert(params.type === "tubePart", "Unexpected initial article params type: " + params.type);
		return initialTubeComponentNodeUpdates(params);
	} else if (wsts.some(wst => wst === "sheet" || wst === "tube")) {
		// Semimanufactureds are updated via the associated target's source update
		return [];
	} else {
		assert(params.type === "generic");
		return initialGenericArticleNodeUpdates(params, preGraph);
	}
}

export function computeInitialNodeUpdates(initialArticleParams: InitialArticleParams[], preGraph: PreDocumentGraph): SlNodeUpdate[] {
	// Pre-fetching the tables (notably the bendDeduction table) reduces the computation time by ~170ms per bend part.
	// There sill remains a varying time (likely ~40ms per bend - not verified) so it might be even better to do the whole computation in C++.
	let cachedBendDieChoiceTables: DieChoiceTables | null = null;
	const bendDieChoiceTables = () => {
		if (cachedBendDieChoiceTables === null) {
			cachedBendDieChoiceTables = {
				upperDieGroups: getTable("upperDieGroup"),
				lowerDieGroups: getTable("lowerDieGroup"),
				upperDies: getTable("upperDie"),
				lowerDies: getTable("lowerDie"),
				upperDieUnits: getTable("upperDieUnit"),
				lowerDieUnits: getTable("lowerDieUnit"),
				bendDeductions: getTable("bendDeduction"),
			};
		}
		assert(cachedBendDieChoiceTables !== null);
		return cachedBendDieChoiceTables;
	};

	const result: SlNodeUpdate[] = [];
	initialArticleParams.forEach(params => {
		const article = params.article;
		result.push(...computeInitialNodeUpdatesForArticle(article, preGraph, params, bendDieChoiceTables));
	});

	return result;
}

export function computeSheetNestingPrePartitions(sheetNestingPartitions: readonly Readonly<SheetNestingPartition<SlVertex>>[]): SlSheetNestingPrePartition[] {
	const sheetProcess = findActiveProcess(row => row.active && row.type === "sheet");
	return sheetNestingPartitions.map(partition => {
		const obj: SlSheetNestingPrePartition = {
			compatibleTargets: partition.ids.map(v => ({
				vertex: v,
				fixedRotations: [],
				sheetFilterSheetIds: [],
			})),
			nestingDistance: getSettingOrDefault("sheetNestingDistance"),
			nestorConfig: {
				timeout: getSharedDataEntry("nestorTimeLimit"),
				numRelevantNestings: 0,
			},
		};
		if (partition.sheetMaterialId !== undefined) {
			obj.sheetMaterialId = partition.sheetMaterialId;
		}
		if (sheetProcess !== undefined) {
			obj.sheetProcessId = sheetProcess.identifier;
		}
		return obj;
	});
}

export function computeTubeNestingPrePartitions(initialArticleParams: readonly Readonly<InitialArticleParams>[]): SlTubeNestingPrePartition[] {
	const tubeProcess = findActiveProcess(row => row.type === "tube");
	return initialArticleParams.filter(params => params.type === "tubePart")
		.map(params => {
			assert(params.type === "tubePart");
			const obj: SlTubeNestingPrePartition = {
				targetVertex: front(params.article),
				nestingDistance: getSettingOrDefault("tubeNestingDistance"),
				targetLength: params.nestorTargetLength ?? 0,
			};
			if (tubeProcess !== undefined) {
				obj.tubeProcessId = tubeProcess.identifier;
			}
			return obj;
		});
}

/**
 * Compute sheet nesting partitions for a PreDocumentGraph
 *
 * A PreDocumentGraph could be
 * 	- An initially created graph
 * 	- An incomplete graph
 * 	- A graph where only sub-sections have been reset (e.g. by toggling a node's WST).
 *
 * The effective data for the nesting pre-partitions are looked up in three locations:
 * 	1.  Submitted initialArticleParams
 * 	2. (if not yet found) The PreDocumentGraph
 * 	3. (if not yet found) Computed InitialArticleParams
 */
export function computePreGraphPotentialSheetNestingPartitions(preGraph: PreDocumentGraph, initialArticleParams: InitialArticleParams[]): SheetNestingPartition<SlVertex>[] {
	const paramsFromGraph = (article: readonly SlVertex[]): InitialFlatSmpArticleParams | undefined => {
		const sheetMaterialId = (() => {
			const vertex = article.find(v => wsi4.sl.node.workStepType(v, preGraph) === "sheetBending") ?? article.find(v => wsi4.sl.node.workStepType(v, preGraph) === "sheetCutting");
			assert(vertex !== undefined);
			return nodeUserDatumImpl("sheetMaterialId", wsi4.sl.node.nodeUserData(vertex, preGraph));
		})();

		const vertex = article.find(v => wsi4.sl.node.workStepType(v, preGraph) === "sheetCutting");
		assert(vertex !== undefined);

		const cuttingGasId = nodeUserDatumImpl("laserSheetCuttingGasId", wsi4.sl.node.nodeUserData(vertex, preGraph));
		const processType = wsi4.sl.node.processType(vertex, preGraph);
		const processId = wsi4.sl.node.processId(vertex, preGraph);

		if (sheetMaterialId === undefined && cuttingGasId === undefined && processType === "undefined" && processId.length === 0) {
			return undefined;
		}

		const result: InitialFlatSmpArticleParams = {
			type: "flatSmp",
			article: Array.from(article),
			sheetCuttingProcessType: processType,
			sheetCuttingProcessId: processId,
		};

		if (sheetMaterialId !== undefined) {
			result.sheetMaterialId = sheetMaterialId;
		}

		if (cuttingGasId !== undefined) {
			result.cuttingGasId = cuttingGasId;
		}

		return result;
	};

	const targets: SheetNestingPartitionTarget<SlVertex>[] = [];
	wsi4.sl.graph.articles(preGraph)
		.forEach(article => {
			const vertex = article.find(v => wsi4.sl.node.workStepType(v, preGraph) === "sheetCutting");
			if (vertex === undefined) {
				return;
			}

			const articleParams = initialArticleParams.find(params => (params.type === "flatSmp" || params.type === "bendSmp") && (isEqual(front(article), front(params.article))))
				?? paramsFromGraph(article)
				?? computeInitialArticleParams(article, preGraph);

			if (articleParams === undefined) {
				wsi4.throwError("Expecting initial article params;\n" + JSON.stringify(initialArticleParams, null, "\t"));
			}

			assert(articleParams.type === "flatSmp" || articleParams.type === "bendSmp");

			const part = wsi4.sl.node.part(vertex, preGraph);
			const thickness = wsi4.cad.thickness(part);

			const target: SheetNestingPartitionTarget<SlVertex> = {
				id: vertex,
				sheetCuttingProcessId: articleParams.sheetCuttingProcessId,
				thickness: thickness,
			};

			if (articleParams.sheetMaterialId !== undefined) {
				target.sheetMaterialId = articleParams.sheetMaterialId;
			}
			if (articleParams.cuttingGasId !== undefined) {
				target.laserSheetCuttingGasId = articleParams.cuttingGasId;
			}

			targets.push(target);
		});

	return computePotentialSheetNestingPartitions<SlVertex>(targets);
}

function generateName(vertex: SlVertex, preGraph: PreDocumentGraph): string {
	const assemblyNameIfAny = (asm: Assembly | undefined): string | undefined => {
		if (asm === undefined) {
			return undefined;
		}
		const assemblyName = cleanFileName(wsi4.geo.assembly.name(asm));
		if (assemblyName.length === 0) {
			return undefined;
		}
		return assemblyName;
	};

	const assemblyName = assemblyNameIfAny(wsi4.sl.node.inputAssembly(vertex, preGraph));
	if (assemblyName !== undefined) {
		assertDebug(() => assemblyName.length > 0);
		return assemblyName;
	}

	// Using a suffix for all generated names for consistency.
	// (Names are made unique later on.)
	const defaultSuffix = "_0";
	const wst = wsi4.sl.node.workStepType(vertex, preGraph);
	if (wst === "sheet" || wst === "tube") {
		return wsi4.util.translate(wst) + defaultSuffix;
	} else {
		return wsi4.util.translate("Article") + defaultSuffix;
	}
}

export function computeInitialArticleUpdates(
	articles: SlVertex[][],
	preGraph: PreDocumentGraph,
	importIdNameProposalMap: Map<string, string>,
	existingNames: readonly Readonly<string>[],
): SlArticleUpdate[] {
	const proposals = articles.map(article => {
		const vertex = back(article);
		const importId = wsi4.sl.node.importId(vertex, preGraph);
		if (importId === undefined) {
			return generateName(vertex, preGraph);
		}
		return importIdNameProposalMap.get(importId) ?? generateName(vertex, preGraph);
	});
	const uniqueNames = computeUniqueNames(proposals, existingNames);
	return articles.map((article, index): SlArticleUpdate => ({
		vertex: back(article),
		articleUserData: createUpdatedArticleUserData("name", itemAt(index, uniqueNames), {}),
	}));

}
