import {
	assumeGraphAxioms,
} from "qrc:/js/lib/axioms";
import {
	Times,
} from "qrc:/js/lib/calc_times";
import {
	computeTappingUnitTimePerPiece,
	TappingTimeCalcParams,
} from "qrc:/js/lib/calc_times_tapping";
import {
	ProcessType,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	computeCompleteSingleSheetMass,
	computeDieSharingPartitions,
	computeNodeMass,
	getAssociatedSheetMaterialId,
	lookUpBendTimeParams,
} from "qrc:/js/lib/graph_utils";
import {
	grossTubeConsumptionForVertex,
	sheetBendingWsNumBendLines,
} from "qrc:/js/lib/node_utils";
import {
	getSettingOrDefault,
} from "qrc:/js/lib/settings_table";
import {
	getTable,
	lookUpAutomaticDeburringParams,
	lookUpProcessSetupTimeFallBack,
	lookUpProcessUnitTimeFallBack,
} from "qrc:/js/lib/table_utils";
import {
	assert,
	assertDebug,
	bbDimensionX,
	bbDimensionY,
	getRowForThreshold,
	isEqual,
	isNonNegativeNumber,
} from "qrc:/js/lib/utils";
import {
	hasManufacturingCostOverride,
	nodeUserDatum,
	nodeUserDatumOrDefault,
} from "qrc:/js/lib/userdata_utils";

import {
	CalcCache,
} from "./export_calc_cache";

/**
 * Compute net setup time for a die bending node partition
 *
 * Pre-condition:  BendingTimeParameters are the same for all members of the partition
 *
 * For now the representative setup time for a die bending partition is definedby the
 * highest setup time for all members of the partition.
 */
function computeDieBendingPartitionNetSetupTime(vertices: readonly Readonly<Vertex>[]): number | undefined {
	assertDebug(() => vertices.length > 0, "Pre-condition violated");
	assertDebug(() => vertices.every(vertex => wsi4.node.workStepType(vertex) === WorkStepType.sheetBending, "Pre-condition violated"));

	// Assumption:  All vertices of the partition share the same `BendTimeParameter`s
	const bendTimeParams = lookUpBendTimeParams(vertices[0]!);
	if (bendTimeParams === undefined) {
		return undefined;
	}

	const sortedBendTimesTable = (() => {
		const table = Array.from(getTable(TableType.bendTime));
		table.sort((lhs, rhs) => (lhs.mass < rhs.mass ? -1 : lhs.mass > rhs.mass ? 1 : 0));
		return table;
	})();

	const allSetupCosts = vertices.map(vertex => {
		// [kg]
		const mass = computeNodeMass(vertex);
		assert(mass !== undefined && isNonNegativeNumber(mass), "Expecting valid mass for die bending node");

		const numBends = sheetBendingWsNumBendLines(vertex);
		assert(numBends !== undefined, "Expecting valid number of bends for die bending node");

		const bendTimes = getRowForThreshold(sortedBendTimesTable, element => element.mass >= mass);
		assert(bendTimes !== undefined, "Expecting valid bend times");

		// [s]
		const setupTimeBase = bendTimeParams.setupTimeDelta * 60 + bendTimeParams.setupTimeFactor * bendTimes.setupTime * 60;

		// [s]
		const setupTimePerBend = bendTimeParams.setupTimePerBendDelta * 60 + bendTimeParams.setupTimePerBendFactor * bendTimes.setupTimePerBend * 60;

		// [s]
		return setupTimeBase + numBends * setupTimePerBend;
	});

	return Math.max(...allSetupCosts);
}

function computeDieBendingUnitTimePerPiece(vertex: Vertex): number | undefined {
	// [kg]
	const mass = computeNodeMass(vertex);
	assert(mass !== undefined && isNonNegativeNumber(mass), "Expecting valid mass for die bending node");

	const sortedBendTimesTable = (() => {
		const table = Array.from(getTable(TableType.bendTime));
		table.sort((lhs, rhs) => (lhs.mass < rhs.mass ? -1 : lhs.mass > rhs.mass ? 1 : 0));
		return table;
	})();

	const bendTimes = getRowForThreshold(sortedBendTimesTable, element => element.mass >= mass);
	assert(bendTimes !== undefined, "Expecting valid bend times");

	const numBends = sheetBendingWsNumBendLines(vertex);
	assert(numBends !== undefined, "Expecting valid number of bends");

	const bendTimeParams = lookUpBendTimeParams(vertex);
	if (bendTimeParams === undefined) {
		return undefined;
	}

	// [s]
	const unitBaseTime = bendTimeParams.unitTimeDelta * 60 + bendTimeParams.unitTimeFactor * bendTimes.unitTime * 60;

	// [s]
	const unitTimePerBend = bendTimeParams.unitTimePerBendDelta * 60 + bendTimeParams.unitTimePerBendFactor * bendTimes.unitTimePerBend * 60;

	// [s]
	return unitBaseTime + numBends * unitTimePerBend;
}

function computeDieBendingSetupTime(vertex: Vertex, calcCache?: CalcCache): number | undefined {
	const partition = (() => {
		const partitions = (() => {
			const computeParts = () => computeDieSharingPartitions(wsi4.graph.vertices()
				.filter(v => wsi4.node.processType(v) === ProcessType.dieBending));
			if (calcCache === undefined) {
				return computeParts();
			} else {
				if (calcCache.dieSharingPartitions === undefined) {
					calcCache.dieSharingPartitions = computeParts();
				}
				return calcCache.dieSharingPartitions;
			}
		})();
		if (partitions === undefined) {
			return undefined;
		}
		const result = partitions.find(p => p.some(v => isEqual(v, vertex)));
		assert(result !== undefined, "Expecting one matching partition");
		return result;
	})();
	if (partition === undefined) {
		return undefined;
	}

	// [s]
	const partitionNetSetupTime = computeDieBendingPartitionNetSetupTime(partition);
	if (partitionNetSetupTime === undefined) {
		return undefined;
	}

	return partitionNetSetupTime / partition.length;
}

export function computeSheetCuttingCalcParams(vertex: Vertex): SheetCuttingCalcParams | undefined {
	assertDebug(() => wsi4.node.workStepType(vertex) === "sheetCutting", "Pre-condition violated");

	const sheetMaterialId = getAssociatedSheetMaterialId(vertex);
	if (sheetMaterialId === undefined) {
		return undefined;
	}

	// Convert from m/s^2 to mm/s^2.
	const machineAMax = getSettingOrDefault("laserSheetCuttingAMax") * 1000;

	// Convert from m/min to mm/s.
	const machineVMax = getSettingOrDefault("laserSheetCuttingVMax") * (1000 / 60);

	const engravingMode = nodeUserDatumOrDefault("bendLineEngravingMode", vertex);

	return {
		sheetMaterialId: sheetMaterialId,
		bendLineEngravingMode: engravingMode,
		machineAMax: machineAMax,
		machineVMax: machineVMax,
	};
}

function computeTubeCuttingCalcParams(vertex: Vertex): TubeCuttingCalcParams | undefined {
	assertDebug(() => wsi4.node.workStepType(vertex) === "tubeCutting", "Pre-condition violated");

	const tubeMaterialId = nodeUserDatum("tubeMaterialId", vertex);
	if (tubeMaterialId === undefined) {
		return undefined;
	}

	return {
		tubeMaterialId: tubeMaterialId,
		// [mm / s]
		machineVMax: 50 * 1000 / 60,
		// [mm / s²]
		machineAMax: 5000,
	};
}

/**
 * Calculate the Times associated to the packaging done in the vertex
 *
 * Unexpected since there is no built-in support for the `packaging` WorkStepType any more.
 */
export function packagingWsTimes(_vertex: Vertex): Times | undefined {
	return undefined;
}

/**
 * Compute approx. automatic deburring net length
 *
 * Assumption: All parts are deburred in the same orientation (180° rotations are ok).
 * Function checks whether the parts should be deburred length- or width-wise.
 */
export function computeAutomaticMechanicalDeburringLength(twoDimRep: TwoDimRepresentation, multiplicity: number, sheetMaterialId: string): number {
	const autoDeburringParams = lookUpAutomaticDeburringParams(sheetMaterialId);
	const dim = (() => {
		assumeGraphAxioms([ "deburringSubProcessNodeHasTwoDimRep" ]);
		const bbox = wsi4.cam.util.boundingBox2(twoDimRep);
		return {
			x: bbDimensionX(bbox),
			y: bbDimensionY(bbox),
		};
	})();

	// If maxLenght is too small then there should be a warning due to a violated process dimension constraint.
	const maxLength = Math.max(autoDeburringParams.maxDimY, Math.min(dim.x, dim.y));
	if (maxLength !== autoDeburringParams.maxDimY) {
		wsi4.util.warn("computeAutomaticMechanicalDeburringLength(): Part exceeds machine dimensions.  Increasing machine machine's dimY.");
	}

	const countX = Math.min(multiplicity, Math.floor(maxLength / dim.x));
	const countY = Math.min(multiplicity, Math.floor(maxLength / dim.y));
	assert(countX > 0 || countY > 0);
	// If part's x-axis should be parallel to machine's y-axis
	const flipPart = countX * dim.x > countY * dim.y;
	// [mm]
	return flipPart ? Math.ceil(multiplicity / countX) * dim.y : Math.ceil(multiplicity / countY) * dim.x;
}

function computeAutomaticMechanicalDeburringUnitTime(twoDimRep: TwoDimRepresentation, deburrDoubleSided: boolean, multiplicity: number, sheetMaterialId: string): number {
	// [mm]
	const effectiveLength = computeAutomaticMechanicalDeburringLength(twoDimRep, multiplicity, sheetMaterialId);
	const autoDeburringParams = lookUpAutomaticDeburringParams(sheetMaterialId);
	const doubleSidedFactor = deburrDoubleSided ? 2 : 1;
	// [mm/s]
	const speed = autoDeburringParams.speed * 1000 / 60;
	// [s]
	const unitTimeBase = multiplicity * autoDeburringParams.unitTimeBase * 60;
	return doubleSidedFactor * (unitTimeBase + effectiveLength / speed);
}

export function computeAutomaticMechanicalDeburringTimes(twoDimRep: TwoDimRepresentation, deburrDoubleSided: boolean, multiplicity: number, sheetMaterialId: string, processId: string): Times {
	const unitTime = computeAutomaticMechanicalDeburringUnitTime(
		twoDimRep,
		deburrDoubleSided,
		multiplicity,
		sheetMaterialId,
	);
	const setupTime = lookUpProcessSetupTimeFallBack(processId);
	return new Times(
		setupTime ?? 0,
		unitTime,
	);
}

function automaticMechanicalDeburringUnitTime(vertex: Vertex, multiplicity: number): number | undefined {
	assumeGraphAxioms([ "deburringSubProcessNodeHasTwoDimRep" ]);
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		return undefined;
	}
	const material = getAssociatedSheetMaterialId(vertex);
	if (material === undefined) {
		return undefined;
	}
	return computeAutomaticMechanicalDeburringUnitTime(
		twoDimRep,
		nodeUserDatumOrDefault("deburrDoubleSided", vertex) ?? false,
		multiplicity,
		material,
	);
}

function computeManualMechanicalDeburringUnitTimePerPiece(twoDimRep: TwoDimRepresentation, deburrDoubleSided: boolean): number {
	const doubleSidedFactor = deburrDoubleSided ? 2 : 1;
	// [mm]
	const contourLength = wsi4.cam.util.cuttingContourLength(twoDimRep);
	const speed = getSettingOrDefault("manualMechanicalDeburringSpeed") * 1000 / 60;
	// [s]
	return doubleSidedFactor * contourLength / speed;
}

export function computeManualMechanicalDeburringTimes(twoDimRep: TwoDimRepresentation, deburrDoubleSided: boolean, multiplicity: number, processId: string): Times {
	const unitTime = multiplicity * computeManualMechanicalDeburringUnitTimePerPiece(twoDimRep, deburrDoubleSided);
	const setupTime = lookUpProcessSetupTimeFallBack(processId);
	return new Times(
		setupTime ?? 0,
		unitTime,
	);
}

function manualMechanicalDeburringUnitTimePerPiece(vertex: Vertex): number {
	assumeGraphAxioms([ "deburringSubProcessNodeHasTwoDimRep" ]);
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		return wsi4.throwError("Expecting twoDimRep");
	}
	return computeManualMechanicalDeburringUnitTimePerPiece(twoDimRep, nodeUserDatumOrDefault("deburrDoubleSided", vertex) ?? false);
}

function userDefinedMachiningSubProcessUnitTimePerPiece(vertex: Vertex): number {
	const type = wsi4.node.processType(vertex);
	const process = getTable(TableType.process)
		.find(row => row.type === type);
	if (process === undefined) {
		return 0;
	}
	const unitTimeRow = getTable(TableType.processUnitTimeFallback)
		.find(row => row.processId === process.identifier);
	const numOperations = (() => {
		if (type === ProcessType.userDefinedThreading) {
			return nodeUserDatumOrDefault("numThreads", vertex);
		} else if (type === ProcessType.userDefinedCountersinking) {
			return nodeUserDatumOrDefault("numCountersinks", vertex);
		} else {
			return wsi4.throwError("Unexpected process type: " + type);
		}
	})();
	// [s]
	return unitTimeRow === undefined ? 0 : numOperations * unitTimeRow.time * 60;
}

function userDefinedThreadingUnitTimePerPiece(vertex: Vertex): number {
	return userDefinedMachiningSubProcessUnitTimePerPiece(vertex);
}

function userDefinedCountersinkingUnitTimePerPiece(vertex: Vertex): number {
	return userDefinedMachiningSubProcessUnitTimePerPiece(vertex);
}

function slideGrindingUnitTimePerPiece(vertex: Vertex): number {
	const processId = wsi4.node.processId(vertex);
	const unitTimeRow = getTable(TableType.processUnitTimeFallback)
		.find(row => row.processId === processId);
	// [s]
	return unitTimeRow === undefined ? 0 : unitTimeRow.time * 60;
}

function sheetTappingUnitTimePerPiece(vertex: Vertex): number {
	const thickness = wsi4.node.sheetThickness(vertex);
	assert(thickness !== undefined, "Expecting valid thickness");

	const tappingDataEntries = nodeUserDatumOrDefault("sheetTappingData", vertex)
		.map(entry => ({
			screwThreadId: entry.screwThreadId,
			depth: thickness,
		}));

	const params: TappingTimeCalcParams = {
		tappingData: tappingDataEntries,
		processId: wsi4.node.processId(vertex),
	};
	return computeTappingUnitTimePerPiece(params);
}

function computeSheetUnitTime(vertex: Vertex): number {
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		return 0;
	} else {
		const unitTimePerSheet = lookUpUnitTime(vertex, 1);
		const numSheets = wsi4.cam.nest2.nestingDescriptors(twoDimRep)
			.reduce((acc, nestingDescriptor) => acc + wsi4.cam.nest2.nestingMultiplicity(twoDimRep, nestingDescriptor), 0);
		return unitTimePerSheet === undefined ? 0 : numSheets * unitTimePerSheet;
	}
}

/**
 * Compute unit time for a tube vertex
 *
 * Due to the special multiplicity semantics for semimanufactured WSTs this function always
 * considers the actual multiplicity, i.e. no scaled multiplicity.  Scale values based on
 * the result of this function need to apply scaling on the result instead of tweaking the
 * input multiplicity.
 */
function computeTubeUnitTime(tubeVertex: Vertex): number | undefined {
	assertDebug(
		() => wsi4.node.workStepType(tubeVertex) === WorkStepType.tube,
		"Pre-condition violated",
	);

	// [s]
	const unitTimePerTube = lookUpUnitTime(tubeVertex, 1);
	if (unitTimePerTube === undefined || unitTimePerTube === 0) {
		return 0;
	}

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

	const tubeConsumption = grossTubeConsumptionForVertex(tubeCuttingVertex);
	if (tubeConsumption === undefined) {
		return undefined;
	}

	const numTubes = Math.ceil(tubeConsumption);
	assert(numTubes >= 1, "Expecting at least one tube to be required");

	return numTubes * unitTimePerTube;
}

/**
 * Look for setup time in fallback value table
 * @returns Setup time [s] from table (if any)
 */
function lookUpSetupTime(vertex: Vertex): number|undefined {
	const processId = wsi4.node.processId(vertex);
	return lookUpProcessSetupTimeFallBack(processId);
}

/**
 * Look for unit time in fallback value table
 * @returns Unit time [s] from table (if any)
 */
function lookUpUnitTime(vertex: Vertex, multiplicity: number): number|undefined {
	const processId = wsi4.node.processId(vertex);
	const unitTimePerPiece = lookUpProcessUnitTimeFallBack(processId);
	return unitTimePerPiece === undefined ? undefined : multiplicity * unitTimePerPiece;
}

export function getFixedMultiplicity(vertex: Vertex): number {
	switch (wsi4.node.workStepType(vertex)) {
		case WorkStepType.packaging:
		case WorkStepType.tube:
		case WorkStepType.sheet: return 1;
		case WorkStepType.sheetCutting:
		case WorkStepType.sheetBending:
		case WorkStepType.joining:
		case WorkStepType.userDefined:
		case WorkStepType.userDefinedBase:
		case WorkStepType.tubeCutting:
		case WorkStepType.transform:
		case WorkStepType.undefined: return wsi4.node.multiplicity(vertex);
	}
}

function computeNodeSetupTime(
	vertex: Vertex,
	userData: StringIndexedInterface,
	computeHandlingTime: () => Times | undefined,
	calcCache?: CalcCache,
): number | undefined {
	const computeSetupTime = (specificSetupTime?: () => number | undefined): number | undefined => {
		const userDataValue = nodeUserDatum("userDefinedSetupTime", vertex, userData);
		if (userDataValue !== undefined) {
			return userDataValue;
		} else if (specificSetupTime !== undefined) {
			const handlingTime = computeHandlingTime();
			if (handlingTime === undefined) {
				return undefined;
			}
			const setupTime = specificSetupTime();
			if (setupTime === undefined) {
				return undefined;
			}
			return handlingTime.setup + setupTime;
		} else {
			const handlingTime = computeHandlingTime();
			if (handlingTime === undefined) {
				return undefined;
			}
			return handlingTime.setup + (lookUpSetupTime(vertex) ?? 0);
		}
	};

	switch (wsi4.node.workStepType(vertex)) {
		case WorkStepType.sheetBending: {
			const processType = wsi4.node.processType(vertex);
			if (processType === ProcessType.dieBending) {
				return computeSetupTime(() => computeDieBendingSetupTime(vertex, calcCache));
			} else {
				return computeSetupTime();
			}
		}
		case WorkStepType.packaging: {
			const times = packagingWsTimes(vertex);
			return computeSetupTime(() => times?.setup);
		}
		case WorkStepType.sheetCutting:
		case WorkStepType.joining:
		case WorkStepType.userDefined:
		case WorkStepType.userDefinedBase:
		case WorkStepType.sheet:
		case WorkStepType.tubeCutting:
		case WorkStepType.transform:
		case WorkStepType.tube:
		case WorkStepType.undefined: {
			return computeSetupTime();
		}
	}
}

/**
 * Compute times for a node
 * @returns setup- and unit-time for the node
 *
 * Note: Tables are expected to be consistent
 */
function computeNodeUnitTime(
	vertex: Vertex,
	multiplicityInput: number|undefined,
	userData: StringIndexedInterface,
	computeHandlingTime: () => Times | undefined,
): number | undefined {
	const multiplicity = multiplicityInput ?? getFixedMultiplicity(vertex);

	const computeUnitTime = (specificOverallUnitTime?: () => number | undefined): number | undefined => {
		const userDataValue = nodeUserDatum("userDefinedUnitTimePerPiece", vertex, userData);
		if (userDataValue !== undefined) {
			return multiplicity * userDataValue;
		} else if (specificOverallUnitTime !== undefined) {
			const specificTime = specificOverallUnitTime();
			if (specificTime === undefined) {
				return undefined;
			}
			const handlingTime = computeHandlingTime();
			if (handlingTime === undefined) {
				return undefined;
			}
			return handlingTime.unit + specificTime;
		} else {
			const handlingTime = computeHandlingTime();
			if (handlingTime === undefined) {
				return undefined;
			}
			return handlingTime.unit + (lookUpUnitTime(vertex, multiplicity) ?? 0);
		}
	};

	switch (wsi4.node.workStepType(vertex)) {
		case WorkStepType.sheetCutting: {
			return computeUnitTime(() => {
				const params = computeSheetCuttingCalcParams(vertex);
				if (params === undefined) {
					return undefined;
				}
				const unitTimePerPiece = wsi4.calc.sheetCuttingUnitTimePerPiece(vertex, params);
				if (unitTimePerPiece === undefined) {
					return undefined;
				}
				return multiplicity * unitTimePerPiece;
			});
		}
		case WorkStepType.sheetBending: {
			const processType = wsi4.node.processType(vertex);
			if (processType === ProcessType.dieBending) {
				const unitTimePerPiece = computeDieBendingUnitTimePerPiece(vertex);
				if (unitTimePerPiece === undefined) {
					return undefined;
				}
				return computeUnitTime(() => multiplicity * unitTimePerPiece);
			} else {
				return computeUnitTime();
			}
		}
		case WorkStepType.tubeCutting: {
			const params = computeTubeCuttingCalcParams(vertex);
			if (params === undefined) {
				return undefined;
			}
			const unitTimePerPiece = wsi4.calc.tubeCuttingUnitTimePerPiece(vertex, params);
			if (unitTimePerPiece === undefined) {
				return undefined;
			}
			return computeUnitTime(() => multiplicity * unitTimePerPiece);
		}
		case WorkStepType.userDefined: {
			const processType = wsi4.node.processType(vertex);
			if (processType === ProcessType.automaticMechanicalDeburring) {
				return computeUnitTime(() => automaticMechanicalDeburringUnitTime(vertex, multiplicity));
			} else if (processType === ProcessType.manualMechanicalDeburring) {
				return computeUnitTime(() => multiplicity * manualMechanicalDeburringUnitTimePerPiece(vertex));
			} else if (processType === ProcessType.userDefinedThreading) {
				return computeUnitTime(() => multiplicity * userDefinedThreadingUnitTimePerPiece(vertex));
			} else if (processType === ProcessType.userDefinedCountersinking) {
				return computeUnitTime(() => multiplicity * userDefinedCountersinkingUnitTimePerPiece(vertex));
			} else if (processType === ProcessType.slideGrinding) {
				return computeUnitTime(() => multiplicity * slideGrindingUnitTimePerPiece(vertex));
			} else if (processType === ProcessType.sheetTapping) {
				return computeUnitTime(() => multiplicity * sheetTappingUnitTimePerPiece(vertex));
			} else {
				return computeUnitTime();
			}
		}
		case WorkStepType.packaging: {
			assert(
				!wsi4.util.isDebug() || multiplicity === undefined || multiplicity === 1,
				"Expecting multiplicity = 1 for node of type packaging;  actual value = " + multiplicity.toString(),
			);
			const times = packagingWsTimes(vertex);
			return computeUnitTime(() => times?.unit);
		}
		case WorkStepType.sheet: {
			return computeUnitTime(() => computeSheetUnitTime(vertex));
		}
		case WorkStepType.tube: {
			assertDebug(
				() => multiplicity === undefined || multiplicity === 1,
				"Expecting multiplicity = 1 for node of type tube;  actual value = " + multiplicity.toString(),
			);
			return computeUnitTime(() => computeTubeUnitTime(vertex));
		}
		case WorkStepType.joining:
		case WorkStepType.userDefinedBase:
		case WorkStepType.transform:
		case WorkStepType.undefined: {
			return computeUnitTime();
		}
	}
}

function computeNodeHandlingTime(vertex: Vertex, multiplicityInput?: number): Times | undefined {
	const multiplicity = (() => {
		if (wsi4.node.workStepType(vertex) === WorkStepType.sheet) {
			assert(
				multiplicityInput === undefined || multiplicityInput === 1,
				"Expecting multiplicity 1 for sheet node",
			);
			const twoDimRep = wsi4.node.twoDimRep(vertex);
			if (twoDimRep === undefined) {
				return 0;
			} else {
				return wsi4.cam.nest2.nestingDescriptors(twoDimRep)
					.reduce((acc, nestingDescriptor) => acc + wsi4.cam.nest2.nestingMultiplicity(twoDimRep, nestingDescriptor), 0);
			}
		} else {
			return multiplicityInput ?? getFixedMultiplicity(vertex);
		}
	})();

	const mass = (() => {
		if (wsi4.node.workStepType(vertex) === WorkStepType.sheet) {
			return computeCompleteSingleSheetMass(vertex);
		} else {
			return computeNodeMass(vertex) ?? 0;
		}
	})();
	if (mass === undefined) {
		return undefined;
	}

	const processId = wsi4.node.processId(vertex);
	const processHandlingTimes = getTable(TableType.processHandlingTime)
		.filter(row => row.processId === processId)
		.filter(row => row.mass < mass)
		.sort((lhs, rhs) => rhs.mass - lhs.mass);

	if (processHandlingTimes.length === 0) {
		return new Times(0, 0);
	} else {
		return new Times(
			// [s]
			60 * processHandlingTimes[0]!.setupTimeDelta,
			// [s]
			60 * processHandlingTimes[0]!.unitTimeDelta * multiplicity,
		);
	}
}

function computeNodeTimesImpl(vertex: Vertex, multiplicityInput?: number, calcCache?: CalcCache): Times | undefined {
	if (hasManufacturingCostOverride(vertex)) {
		return undefined;
	}

	let cachedHandlingTime: Times | undefined | null = null;
	const computeHandlingTime = (): Times | undefined => {
		if (cachedHandlingTime === null) {
			cachedHandlingTime = computeNodeHandlingTime(vertex, multiplicityInput);
		}
		return cachedHandlingTime;
	};

	const userData = wsi4.node.userData(vertex);

	const setupTime = computeNodeSetupTime(
		vertex,
		userData,
		computeHandlingTime,
		calcCache,
	);
	if (setupTime === undefined) {
		return undefined;
	}

	const unitTime = computeNodeUnitTime(
		vertex,
		multiplicityInput,
		userData,
		computeHandlingTime,
	);
	if (unitTime === undefined) {
		return undefined;
	}

	return new Times(setupTime, unitTime);
}

/**
 * Compute times for a node
 * @returns setup- and unit-time for the node
 *
 * Note: Tables are expected to be consistent
 */
export function computeNodeTimes(vertex: Vertex, multiplicityInput?: number, cache?: CalcCache): Times | undefined {
	if (cache === undefined) {
		return computeNodeTimesImpl(vertex, multiplicityInput);
	} else {
		const multOneTimes = ((): Times | undefined => {
			const entry = cache.nodeTimesEntries.find(entry => isEqual(entry.vertex, vertex));
			if (entry === undefined) {
				// It is important to use the actual multiplicity for the time computation.
				// E.g. in case of automatic deburring a simple nesting is computed for the time computation.
				// The computation would become meaningless if done for a different multiplicity.
				const actualMult = getFixedMultiplicity(vertex);
				const actualMultTimes = computeNodeTimesImpl(vertex, actualMult, cache);
				const multOneTimes = actualMultTimes === undefined
					? undefined
					: new Times(actualMultTimes.setup, actualMultTimes.unit / actualMult);
				cache.nodeTimesEntries.push({
					vertex: vertex,
					multOneTimes: multOneTimes,
				});
				return multOneTimes;
			} else {
				return entry.multOneTimes;
			}
		})();
		if (multOneTimes === undefined) {
			return undefined;
		}

		const multiplicity = multiplicityInput ?? getFixedMultiplicity(vertex);
		return new Times(
			multOneTimes.setup,
			multiplicity * multOneTimes.unit,
		);
	}
}
