// Note: All functions in this module must conform to DocumentGraphHandler's script engine API.
import {
	CamCommandType,
	GeometryEntityType,
	ReplyStateIndicatorSheetCutting,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	isCamCommandSetColor,
} from "qrc:/js/lib/generated/typeguard";
import {
	back,
	front,
} from "./array_util";

import {
	tolerances,
} from "./constants";
import {
	squareNorm2,
	sub2,
} from "./geometry_utils";
import {
	isBaseProcessType,
} from "./process";
import {
	getSettingOrDefault,
} from "./settings_table";
import {
	TappingSceneCandidate,
} from "./sheet_tapping_utils";
import {
	getMutableTable,
	getTable,
	tubeProfileForGeometry,
} from "./table_utils";
import {
	collectNodeUserDataEntries,
	isCompatibleToNodeUserDataEntry,
	nodeUserDatum,
	nodeUserDatumOrDefault,
	userDataAccess,
} from "./userdata_utils";
import {
	assert,
	assertDebug,
	asyncCreateGltf,
	asyncRenderDefaultPng,
	bbDimensionX,
	bbDimensionY,
	cleanFileName,
	computeContourCount,
	computeContourLength,
	computeEntityFaceArea,
	computeSegmentLength,
} from "./utils";

export function boundingBox3d(vertex: Vertex): Box3 | undefined {
	const assembly = wsi4.node.assembly(vertex);
	if (assembly) {
		return wsi4.geo.util.boundingBox3d(assembly);
	}
	const inputAssembly = wsi4.node.inputAssembly(vertex);
	if (inputAssembly) {
		return wsi4.geo.util.boundingBox3d(inputAssembly);
	}
	return undefined;
}

export function finalVertex(): Vertex | undefined {
	const articles = wsi4.graph.articles();
	for (const article of articles) {
		for (const vertex of article) {
			if (wsi4.graph.sources(vertex).length !== 0 && wsi4.graph.targets(vertex).length === 0) {
				return vertex;
			}
		}
	}
	return undefined;
}

export function nodeLaserSheetCuttingGasId(vertex: Vertex): string | undefined {
	return nodeUserDatum("laserSheetCuttingGasId", vertex);
}

export function sheetBendingWsNumBendLines(vertex: Vertex): number | undefined {
	const workStepType = wsi4.node.workStepType(vertex);
	if (workStepType !== "sheetBending") {
		return wsi4.throwError("sheetBendingWsNumBendLines(): unexpected workStepType: " + workStepType);
	}
	const dieChoiceMap = wsi4.node.dieChoiceMap(vertex);
	if (dieChoiceMap === undefined) {
		return undefined;
	}
	return dieChoiceMap.length;
}

export function sheetCuttingWsContourCount(vertex: Vertex): number | undefined {
	const workStepType = wsi4.node.workStepType(vertex);
	if (workStepType !== "sheetCutting") {
		return wsi4.throwError("sheetCuttingWSContourCount(): unexpected workStepType:" + workStepType);
	}
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		return wsi4.throwError("TwoDimRepresentation missing");
	}
	return computeContourCount(twoDimRep);
}

export function sheetCuttingWsContourLength(vertex: Vertex): number | undefined {
	const workStepType = wsi4.node.workStepType(vertex);
	if (workStepType !== "sheetCutting") {
		return wsi4.throwError("sheetCuttingWsContourLength(): unexpected workStepType: " + workStepType);
	}
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		return wsi4.throwError("TwoDimRepresentation missing");
	}
	return computeContourLength(twoDimRep);
}

export function mappedBendMaterialId(sheetMaterialId: string): string | undefined {
	const materialMappings = getTable(TableType.sheetBendingMaterialMapping);
	const found = materialMappings.find(row => row.sheetMaterialId === sheetMaterialId);
	if (!found || typeof found.sheetBendingMaterialId !== "string") {
		wsi4.util.error("mappedBendMaterialId(): Material not mapped to bend-material.");
		return undefined;
	}
	return found.sheetBendingMaterialId;
}

/**
 * Available BendDieChoice candidates for a specific bend
 */
export interface BendDieChoiceCandidatesEntry {
	bendDescriptor: number;
	bendDieChoices: BendDieChoice[];
}

/*
 * @param bendLineData BendLineData for the bend
 * @flangeLength BendLineFlangeLength for the bend
 * @extraFlangeLength "safety distance" that is added to the geometrically computed minimum
 *
 * @return Approx. max. opening width a lower die must have to ensure that none of the bend flanges slides into the tool.
 *
 * As of now the elastic rebound of the part is not considered (a slightly larger bend angle is required to achieve the actual bend angle).
 */
export function maxLowerDieOpeningWidth(
	bendLineData: Readonly<BendLineData>,
	flangeLength: Readonly<BendLineFlangeLength>,
	extraFlangeLength: number,
): number {
	// Max. Angle that is considered manufacturable without splitting up the bend processes into two bends
	//
	// Example for two-step-bend:
	// Seamed edge with resulting angle of 180°; first bend's resulting angle: 130°; second bend's resulting angle: 180°
	//
	// Considered max angle:  153.00°
	const maxSingleBendAngle = 0.85 * Math.PI;

	//    o/2   o/2
	//  |<--->|<--->|
	//
	//  ......|
	//  \ξ    |     /
	//   \    |    /
	//    \   |   /
	// fl0 \  |  / fl1
	//      \β|β/
	//       \|/
	//        \ α
	//         \
	//          \
	// fl0, fl1:  bend legs
	// o = opening width of the sheet
	// α = bend-angle
	// β = 0.5 * (π - α)
	// ξ = 0.5 * π - β = 0.5 * α

	const bendAngle = Math.min(maxSingleBendAngle, Math.abs(bendLineData.bendAngle));
	assert(bendAngle >= 0, "bendAngle must be non-negative");
	const factor = Math.cos(.5 * bendAngle);
	const fl0 = Math.max(0, (flangeLength.flangeLengthLhs - extraFlangeLength));
	const fl1 = Math.max(0, (flangeLength.flangeLengthRhs - extraFlangeLength));
	const a = factor * fl0;
	const b = factor * fl1;
	return 2 * Math.min(a, b);
}

function dieChoiceMatchesDieUnitConstraintsImpl<Unit>(
	bendLineData: Readonly<BendLineData>,
	dies: readonly Readonly<UpperDie | LowerDie>[],
	units: readonly Readonly<Unit>[],
	extractDieId: (unit: Unit) => string,
	extractDimXSum: (unit: Unit) => number,
) {
	const firstSegment = front(bendLineData.segments);
	const lastSegment = back(bendLineData.segments);
	const bendLineLength = Math.sqrt(
		squareNorm2(sub2(
			firstSegment.content.from,
			lastSegment.content.to,
		)),
	);
	return dies.length === 0 || dies.some(die => bendLineLength <= units.filter(row => extractDieId(row) === die.identifier)
		.reduce((acc, unit) => acc + extractDimXSum(unit), 0));
}

function dieChoiceMatchesUpperDieUnitConstraints(
	bendDieChoice: Readonly<BendDieChoice>,
	bendLineData: Readonly<BendLineData>,
	upperDies: readonly Readonly<UpperDie>[],
	upperDieUnits: readonly Readonly<UpperDieUnit>[],
): boolean {
	const dies = upperDies.filter(row => row.upperDieGroupId === bendDieChoice.upperDieGroupId);
	return dieChoiceMatchesDieUnitConstraintsImpl(
		bendLineData,
		dies,
		upperDieUnits,
		unit => unit.upperDieId,
		unit => unit.multiplicity * unit.dimX,
	);
}

function dieChoiceMatchesLowerDieUnitConstraints(
	bendDieChoice: Readonly<BendDieChoice>,
	bendLineData: Readonly<BendLineData>,
	lowerDies: readonly Readonly<LowerDie>[],
	lowerDieUnits: readonly Readonly<LowerDieUnit>[],
): boolean {
	const dies = lowerDies.filter(row => row.lowerDieGroupId === bendDieChoice.lowerDieGroupId);
	return dieChoiceMatchesDieUnitConstraintsImpl(
		bendLineData,
		dies,
		lowerDieUnits,
		unit => unit.lowerDieId,
		unit => unit.multiplicity * unit.dimX,
	);
}

export function computeBendDieChoiceCandidates(vertex: Vertex): BendDieChoiceCandidatesEntry[] {
	const materialIds = collectNodeUserDataEntries("sheetMaterialId", vertex, userDataAccess.article);
	if (materialIds.length > 1) {
		wsi4.util.error("computeBendDieChoiceCandidates(): expecting at most one material");
		return [];
	}

	// It it is a valid use case that there is no material (e.g. in case there is no matching sheet).
	// This should result in 'neutral fiber' pseudo tool only.
	const materialId = materialIds[0];
	const sheetBendingMaterialId = materialId === undefined ? undefined : mappedBendMaterialId(materialId);

	const thickness = wsi4.node.sheetThickness(vertex);
	if (typeof thickness !== "number") {
		wsi4.util.error("computeBendDieChoiceCandidates(): Thickness not available");
		return [];
	}
	const bendLineDataArray = wsi4.node.computeBendLineData(vertex);
	if (!bendLineDataArray) {
		wsi4.util.error("computeBendDieChoiceCandidates(): Bend info not available");
		return [];
	}
	const upperDieGroups = getTable(TableType.upperDieGroup);
	const lowerDieGroups = getTable(TableType.lowerDieGroup);

	const upperDies = getTable(TableType.upperDie);
	const lowerDies = getTable(TableType.lowerDie);

	const upperDieUnits = getTable(TableType.upperDieUnit);
	const lowerDieUnits = getTable(TableType.lowerDieUnit);

	const unfilteredBendDeductions = getTable(TableType.bendDeduction);
	const bendDeductions = unfilteredBendDeductions.filter(bendDeduction => bendDeduction.sheetBendingMaterialId === sheetBendingMaterialId
		// Allows filtering bend deductions by removing entries from the upper die group table
		&& upperDieGroups.find(upperDieGroup => upperDieGroup.identifier === bendDeduction.upperDieGroupId) !== undefined
		// Allows filtering bend deductions by removing entries from the lower die group table
		&& lowerDieGroups.find(lowerDieGroup => lowerDieGroup.identifier === bendDeduction.lowerDieGroupId) !== undefined);

	const extraFlangeLength = getSettingOrDefault("bendFlangeSafetyDistance");
	const bendLineFlangeLengths = wsi4.node.computeBendLineFlangeLengths(vertex);
	if (bendLineFlangeLengths === undefined) {
		wsi4.util.error("computeBendDieChoiceCandidates(): Flange lengths not available");
		return [];
	}

	const dieGroupPriorities = getMutableTable(TableType.dieGroupPriority)
		.filter(row => row.sheetBendingMaterialId === sheetBendingMaterialId
			&& row.sheetThickness <= (thickness + tolerances.thickness))
		.sort((lhs, rhs) => rhs.sheetThickness - lhs.sheetThickness)
		.filter((lhs, index, self) => index === self.findIndex(rhs => lhs.upperDieGroupId === rhs.upperDieGroupId && lhs.lowerDieGroupId === rhs.lowerDieGroupId));

	return bendLineDataArray
		.map((bendLineData: BendLineData) => {
			const flangeLength = bendLineFlangeLengths.find(entry => entry.bendDescriptor === bendLineData.bendDescriptor);
			assert(flangeLength !== undefined, "Bend data inconsistent");
			const maxOpeningWidth = maxLowerDieOpeningWidth(bendLineData, flangeLength, extraFlangeLength);
			const query = {
				thickness: thickness,
				bendAngle: Math.abs(bendLineData.bendAngle),
				maxOpeningWidth: maxOpeningWidth,
			};
			const constructedRadius = bendLineData.constructedInnerRadius;
			return {
				bendDescriptor: bendLineData.bendDescriptor,
				bendDieChoices: wsi4.cam.bend.selectDieGroups(bendDeductions, upperDieGroups, lowerDieGroups, dieGroupPriorities, query, constructedRadius)
					.filter(bendDieChoice => bendDieChoice.type === "neutralAxis" || dieChoiceMatchesUpperDieUnitConstraints(
						bendDieChoice,
						bendLineData,
						upperDies,
						upperDieUnits,
					))
					.filter(bendDieChoice => bendDieChoice.type === "neutralAxis" || dieChoiceMatchesLowerDieUnitConstraints(
						bendDieChoice,
						bendLineData,
						lowerDies,
						lowerDieUnits,
					)),
			};
		});
}

export function computeUpperDieAffectDistances(vertex: Vertex): Array<number> {
	const upperDieGroupTable = getTable(TableType.upperDieGroup);
	const bendLineDataArray = wsi4.node.computeBendLineData(vertex);
	if (!bendLineDataArray) {
		wsi4.util.error("computeUpperDieAffectDistances(): No bend info available for vertex " + vertex.toString());
		return [];
	}
	const dieChoiceArray = wsi4.node.dieChoiceMap(vertex);
	if (dieChoiceArray === undefined) {
		wsi4.util.error("computeUpperDieAffectDistances(): die choice missing for vertex " + vertex.toString());
		return [];
	}
	return dieChoiceArray.map(entry => {
		const upperDieGroup = upperDieGroupTable.find(element => element.identifier === entry.bendDieChoice.upperDieGroupId);
		if (upperDieGroup === undefined) {
			if (entry.bendDieChoice.upperDieGroupId.length !== 0) {
				wsi4.util.error("computeUpperDieAffectDistances(): upper die group not found in table: " + entry.bendDieChoice.upperDieGroupId);
			}
			return 0;
		} else {
			const bendLineData = bendLineDataArray.find(data => data.bendDescriptor === entry.bendDescriptor);
			assert(bendLineData !== undefined, "Expecting valid bend line data entry");

			const angle = Math.min(Math.abs(bendLineData.bendAngle), 150 / 180 * Math.PI);
			return (Math.PI - angle) * upperDieGroup.radius;
		}
	});
}

export function computeLowerDieAffectDistanceMapImpl(
	dieChoiceMap: readonly Readonly<DieChoiceMapEntry>[],
	bendLineDataMap: readonly Readonly<BendLineData>[],
	thickness: number,
): BendDieAffectDistanceEntry[] {
	assertDebug(() => dieChoiceMap.length === bendLineDataMap.length, "Pre-condition violated");
	const lowerDieGroupTable = getTable(TableType.lowerDieGroup);
	return dieChoiceMap.map(entry => {
		const lowerDieGroup = lowerDieGroupTable.find(element => element.identifier === entry.bendDieChoice.lowerDieGroupId);
		if (lowerDieGroup === undefined) {
			if (entry.bendDieChoice.lowerDieGroupId.length !== 0) {
				wsi4.util.error("computeLowerDieAffectDistanceMap(): lower die group not found in table: " + entry.bendDieChoice.lowerDieGroupId);
			}
			return {
				bendDescriptor: entry.bendDescriptor,
				affectDistance: 0,
			};
		} else {
			const bendLineData = bendLineDataMap.find(data => data.bendDescriptor === entry.bendDescriptor);
			assert(bendLineData !== undefined, "Expecting valid bend line data");

			const o = .5 * lowerDieGroup.openingWidth;
			const innerRadius = entry.bendDieChoice.baseClass.innerRadius;
			const outerRadius = innerRadius + thickness;
			const alpha = Math.min(.5 * 150 / 180 * Math.PI, .5 * Math.abs(bendLineData.bendAngle));
			const straightPart = (o - Math.sin(alpha) * outerRadius) / Math.cos(alpha);
			const roundPart = outerRadius * alpha;
			const distance = roundPart + straightPart + entry.bendDieChoice.baseClass.roundDeduction;
			return {
				bendDescriptor: entry.bendDescriptor,
				affectDistance: distance,
			};
		}
	});
}

export function computeLowerDieAffectDistanceMap(vertex: Vertex): BendDieAffectDistanceEntry[] {
	assertDebug(() => wsi4.node.workStepType(vertex) === WorkStepType.sheetBending, "Pre-condition violated");

	const thickness = wsi4.node.sheetThickness(vertex);
	assert(thickness !== undefined);

	const bendLineDataArray = wsi4.node.computeBendLineData(vertex);
	assert(bendLineDataArray !== undefined);

	const dieChoiceArray = wsi4.node.dieChoiceMap(vertex);
	assert(dieChoiceArray !== undefined);

	return computeLowerDieAffectDistanceMapImpl(dieChoiceArray, bendLineDataArray, thickness);
}

export function mappedSheetCuttingMaterialId(sheetMaterialId: string): string | undefined {
	const table = getTable(TableType.sheetCuttingMaterialMapping);
	const found = table.find(row => row.sheetMaterialId === sheetMaterialId);
	if (found === undefined) {
		wsi4.util.error("mappedSheetCuttingMaterialId(): no mapped laser sheet cutting material available for sheetMaterialId \"" + sheetMaterialId + "\"");
		return undefined;
	}
	return found.sheetCuttingMaterialId;
}

export function cadFeatureTwoDimCentroid(vertex: Vertex, cadFeature: Readonly<CadFeature>): Point2 {
	const projection = wsi4.node.twoDimProjection(vertex, cadFeature);
	const iop = wsi4.geo.util.createIop(projection);
	assert(iop !== undefined, "Expecting valid IOP for the projection of a " + cadFeature.type);
	assertDebug(() => wsi4.geo.util.innerPolygons(iop).length === 0, "Expecting no inner polygons");
	return wsi4.geo.util.centroid(wsi4.geo.util.outerPolygon(iop));
}

/**
 * Note: The associated node is not neccessarily of type `sheetTapping`.
 * E.g. candidates need to be computed in advance for wsi4webui.
 */
export function computeTappingCandidates(vertex: Vertex): TappingSceneCandidate[] {
	const part = wsi4.node.part(vertex);

	// For now legacy graphs are supported where an 2D input sheet parts have no underlying part.
	// In these cases sheet tapping is not supported.
	if (part === undefined) {
		return [];
	}

	const thickness = wsi4.node.sheetThickness(vertex);
	assert(thickness !== undefined, "Pre-condition violated");

	const screwThreads = getTable(TableType.screwThread);
	return wsi4.cad.features(part)
		.map((feature): TappingSceneCandidate | undefined => {
			const centroid = cadFeatureTwoDimCentroid(vertex, feature);
			const diameter = wsi4.cad.coreHoleDiameter(feature, part);
			const matchingScrewThreads = screwThreads
				.filter(screwThread => (thickness + tolerances.thickness) > screwThread.minDepth)
				.filter(screwThread => Math.abs(screwThread.coreHoleDiameter - diameter) < screwThread.symmetricTolerance);
			return {
				cadFeature: feature,
				center2: centroid,
				matchingScrewThreads: matchingScrewThreads,
			};
		})
		.filter((arg): arg is TappingSceneCandidate => arg !== undefined)
		.filter(candidate => candidate.matchingScrewThreads.length > 0);
}

export function computeMaxBendLineNetLength(vertex: Vertex): number {
	const bendLineData = wsi4.node.computeBendLineData(vertex);
	if (bendLineData === undefined || bendLineData.length === 0) {
		return wsi4.throwError("Expecting valid bend line data");
	}
	return bendLineData.map(bld => bld.segments.reduce((acc, segment) => acc + computeSegmentLength(segment), 0))
		.sort((lhs, rhs) => rhs - lhs)[0]!;
}

/**
 * Compute area of all selected entities for the vertex
 *
 * Pre-condition: associated node provides assembly
 * Pre-condition: associated node is of WorkStepType transform
 * Pre-condition: all underlying AssemblyPath:s are valid for assembly
 * Pre-condition: all resolved Assembly:s provide as associated brep
 */
export function computeCommandsFaceArea(vertex: Vertex): number {
	const rootAssembly = wsi4.node.assembly(vertex);
	assert(rootAssembly !== undefined, "Expecting assembly");
	assert(wsi4.node.workStepType(vertex) === WorkStepType.transform, "Expecting WorkStepType transform");
	return wsi4.node.commands(vertex)
		.reduce((mainAcc, command) => { // eslint-disable-line array-callback-return
			switch (command.type) {
				case CamCommandType.setColor: {
					assert(isCamCommandSetColor(command.content), "Expecting CamCommandSetColor");
					return mainAcc + command.content.entities.reduce((acc, entity) => acc + computeEntityFaceArea(rootAssembly, entity), 0);
				}
			}
		}, 0);
}

export function computeNameProposal(vertex: Vertex): string {
	const assemblyName = (() => {
		const getAssemblyName = ((assembly: Assembly | undefined) => {
			if (assembly === undefined) {
				return "";
			} else {
				return wsi4.geo.assembly.name(assembly);
			}
		});

		const inputAssemblyName = getAssemblyName(wsi4.node.inputAssembly(vertex));
		if (inputAssemblyName === "") {
			return cleanFileName(getAssemblyName(wsi4.node.assembly(vertex)));
		} else {
			return cleanFileName(inputAssemblyName);
		}
	})();
	if (assemblyName === "") {
		// Appending _0 by default to ensure all generic names have the same format
		// Creating unique names is *not* the job of this function
		const processType = wsi4.node.processType(vertex);
		if (isBaseProcessType(processType)) {
			return wsi4.util.translate(processType) + "_0";
		} else {
			return wsi4.util.translate("Article") + "_0";
		}
	} else {
		return assemblyName;
	}
}

function geometryEntities(replyState: ReplyStateIndicatorMap, key: "sheetBending" | "sheetCutting" | "tubeCutting"): GeometryEntity[] {
	// Assumption:  geometry entites are computed for the assembly that holds the brep
	const assemblyPath: Readonly<AssemblyPath> = {
		indices: [],
	};

	const map = replyState[key];
	if (map !== undefined) {
		const udf = ReplyStateIndicatorSheetCutting.undetectedFeatures;
		const content = map.replyStateIndicators[udf];
		if (content !== undefined) {
			return content.unassignedFaceDescriptors.map(fd => ({
				assemblyPath: assemblyPath,
				descriptor: {
					type: GeometryEntityType.face,
					content: {
						value: fd,
					},
				},
			}));
		}
	}
	return [];
}

export function extractGeometryErrorEntities(vertex: Vertex): GeometryEntity[] {
	const replyState = wsi4.node.camReplyStateIndicators(vertex);
	const undetectedFeaturesTypes = [
		WorkStepType.sheetCutting,
		WorkStepType.sheetBending,
		WorkStepType.tubeCutting,
	];
	return undetectedFeaturesTypes.reduce((acc: GeometryEntity[], key) => [
		...acc,
		...geometryEntities(replyState, key),
	], []);
}

export interface VertexAnd<Data> {
	vertex: Vertex;
	data: Data;
}

/**
 * Spawn an async function for the assemblies of the supplied vertices, returning an array of futures.
 * If a vertex doesn't have an assembly, its inputAssembly is used instead.
 * If it has neither, the vertex is ignored.
 */
function createVertexFutures<Data>(vertices: readonly Vertex[], f: (assembly: Assembly) => Data): VertexAnd<Data>[] {
	// Sorting so futures of large assemblies start as early as possible
	return vertices.reduce(
		(acc: [Vertex, Assembly, number][], vertex) => {
			const assembly = wsi4.node.assembly(vertex) ?? wsi4.node.inputAssembly(vertex);
			if (assembly !== undefined) {
				acc.push([
					vertex,
					assembly,
					wsi4.geo.assembly.recursiveSubAssemblies(assembly).length,
				]);
			}
			return acc;
		},
		[])
		.sort(([
			_rhsVertex,
			_rhsAssembly,
			rhsNumSubAsms,
		], [
			_lhsVertex,
			_lhsAssembly,
			lhsNumSubAsms,
		]) => rhsNumSubAsms - lhsNumSubAsms)
		.map(([
			vertex,
			assembly,
		]) => ({
			vertex: vertex,
			data: f(assembly),
		}));
}

/**
 * Create png futures for vertices providing an Assembly
 *
 * If there is no Assembly for a vertex it will be ignored.
 */
export function createPngFutures(vertices: readonly Vertex[]): VertexAnd<ArrayBufferFuture>[] {
	return createVertexFutures(vertices, asyncRenderDefaultPng);
}

/**
 * Create gltf futures for vertices providing an Assembly
 */
export function createGltfFutures(vertices: readonly Vertex[]): VertexAnd<ArrayBufferFuture>[] {
	return createVertexFutures(vertices, asyncCreateGltf);
}

export function isTestReportRequiredForSheet(sheetVertex: Vertex): boolean {
	assert(wsi4.node.workStepType(sheetVertex) === WorkStepType.sheet, "Expecting sheet WST");
	return wsi4.graph.reachable(sheetVertex)
		.some(vertex => isCompatibleToNodeUserDataEntry("testReportRequired", vertex) && nodeUserDatumOrDefault("testReportRequired", vertex));
}

export function nestingTargetBoxForVertex(sheetVertex: Vertex): Box2 | undefined {
	assertDebug(
		() => wsi4.node.workStepType(sheetVertex) === WorkStepType.sheet,
		"Pre-condition violated: Expecting WST sheet",
	);

	const twoDimRep = wsi4.node.twoDimRep(sheetVertex);
	if (twoDimRep === undefined) {
		// Valid case; no nesting available
		return undefined;
	}

	// Note: All nestings share the same boundary
	const nestingDescriptor = 0;
	const targetBoundary = wsi4.cam.nest2.nestingTargetBoundary(twoDimRep, nestingDescriptor);
	return wsi4.geo.util.boundingBox2d(targetBoundary);
}

/**
 * Net volume of all nested parts without scrap
 *
 * Inner contours are *not* considered, if their area is below the associated
 * threshold setting (default = 0.)  A threshold > 0 might lead to slightly
 * inaccurate results in case smaller parts are nested within inner contours
 * of other parts.  The worst case sheet consumption is equal to the combined
 * volume of the bounding boxes of all nestings.
 *
 * In combination with the associated total sheet consumption this value is
 * complementary w.r.t the scrap.
 *
 * If there is no valid nesting for `vertex` then the consumption is undefined.
 * If there is no valid sheet for `vertex` then the consumption is undefined.
 */
export function computeSheetConsumptionNestedParts(vertex: Vertex): number | undefined {
	assertDebug(
		() => wsi4.node.workStepType(vertex) === WorkStepType.sheet,
		"Pre-condition violated: Expecting WST sheet",
	);

	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		return undefined;
	}

	const targetBox = nestingTargetBoxForVertex(vertex);
	assert(targetBox !== undefined, "Execting valid target box");

	const targetBoxDimX = bbDimensionX(targetBox);
	const targetBoxDimY = bbDimensionY(targetBox);
	const sheetVolume = targetBoxDimX * targetBoxDimY;

	const areaThreshold = getSettingOrDefault("sheetScrapAreaThreshold");

	const nestingDescriptors = wsi4.cam.nest2.nestingDescriptors(twoDimRep);
	const partNetVolume = nestingDescriptors.reduce((acc, nd) => {
		const mult = wsi4.cam.nest2.nestingMultiplicity(twoDimRep, nd);
		return acc + mult * wsi4.cam.nest2.netVolumeNestedParts(twoDimRep, nd, areaThreshold);
	}, 0.);
	return partNetVolume / sheetVolume;
}

/**
 * Assumption:  Seaching Brep of a *component* assembly, so the first assembly providing a valid brep is picked.
 */
function findAssemblyWithBrep(assembly: Assembly): Assembly | undefined {
	const asms = [
		assembly,
		...wsi4.geo.assembly.recursiveSubAssemblies(assembly),
	];
	return asms.find(asm => wsi4.geo.assembly.brep(asm) !== undefined);
}

// Color red as rgb
const red: Vector3 = {
	entries: [
		1,
		0,
		0,
	],
};

/**
 * Creates new assembly where colored faces indicate problems
 *
 * Note:  Geometry is considered problematic if it does not fit the sheet heuristics for a part.
 */
export function problematicGeometryAsm(vertex: Vertex): Assembly {
	assertDebug(
		() => wsi4.node.hasProblematicGeometries(vertex),
		"Pre-condition violated:  Node has no problematic geometries",
	);

	const entities = extractGeometryErrorEntities(vertex);
	assert(entities.length > 0, "Expected array with at least one geometric entity");

	const topLevelAsm = wsi4.node.inputAssembly(vertex);
	assert(topLevelAsm !== undefined, "Vertex must possess an assembly");

	const asmWithBrep = findAssemblyWithBrep(topLevelAsm);
	assert(asmWithBrep !== undefined, "Expecting assembly with brep");

	return wsi4.geo.assembly.setEntityColors(asmWithBrep, entities, red);
}

export function tubeProfileForVertex(vertex: Vertex, tubeProfilesInput?: readonly Readonly<TubeProfile>[]): TubeProfile | undefined {
	assertDebug(() => wsi4.node.workStepType(vertex) === WorkStepType.tubeCutting, "Pre-condition violated");

	const profileGeometry = wsi4.node.tubeProfileGeometry(vertex);
	if (profileGeometry === undefined) {
		return undefined;
	}

	return tubeProfileForGeometry(profileGeometry, tubeProfilesInput);
}

export function getTubeMaterialId(vertex: Vertex): string | undefined {
	const entries = collectNodeUserDataEntries("tubeMaterialId", vertex, userDataAccess.article | userDataAccess.reachable);
	assert(entries.length <= 1, "Expecting at most one matching entry");
	return entries.shift();
}

export function getTubeMaterialIdOrThrow(vertex: Vertex): string {
	const result = getTubeMaterialId(vertex);
	assert(result !== undefined, "Expecting valid tube material id");
	return result;
}

export function getTubeSpecificationId(vertex: Vertex): string | undefined {
	const entries = collectNodeUserDataEntries("tubeSpecificationId", vertex, userDataAccess.article | userDataAccess.reachable);
	assert(entries.length <= 1, "Expecting at most one matching entry");
	return entries.shift();
}

export function getTubeSpecificationIdOrThrow(vertex: Vertex): string {
	const result = getTubeSpecificationId(vertex);
	assert(result !== undefined, "Expecting valid tube specification id");
	return result;
}

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

	const profile = tubeProfileForVertex(vertex);
	if (profile === undefined) {
		return undefined;
	}

	const materialId = getTubeMaterialId(vertex);
	if (materialId === undefined) {
		return undefined;
	}

	const specificationId = getTubeSpecificationId(vertex);
	if (specificationId === undefined) {
		return undefined;
	}

	const tubeStockTable = getTable(TableType.tubeStock);
	const isInStock = (tube: Tube) => {
		const stockRow = tubeStockTable.find(row => row.tubeId === tube.identifier);
		return stockRow === undefined || stockRow.count > 0;
	};

	const clampingLength = getSettingOrDefault("tubeClampingLength");
	const minLength = wsi4.node.profileExtrusionLength(vertex) + clampingLength;
	return getTable(TableType.tube)
		.filter(row => row.tubeProfileId === profile.identifier
			&& row.tubeMaterialId === materialId
			&& row.tubeSpecificationId === specificationId
			&& row.dimX >= minLength
			&& isInStock(row))
		.sort((lhs, rhs) => rhs.dimX - lhs.dimX)
		.shift();
}

/**
 * Compute the number of parts that can be trivially nested on a given tube.
 */
function computeNumPartsPerTube(tube: Readonly<Tube>, extrusionLength: number): number | undefined {
	const nestingDistance = getSettingOrDefault("tubeNestingDistance");
	const clampingLength = getSettingOrDefault("tubeClampingLength");
	const tubeNetLength = Math.max(0, tube.dimX - clampingLength);

	const lengthPerPart = extrusionLength + nestingDistance;
	if (tubeNetLength - extrusionLength <= 0) {
		return undefined;
	}

	return 1 + Math.floor((tubeNetLength - extrusionLength) / lengthPerPart);
}

/**
 * For a tubeCutting node compute the actual tube consumption.
 *
 * The result entails the scrap.
 *
 * In combination with netTubeConsumptionForVertex() the scrap can be computed.
 */
export function grossTubeConsumptionForVertex(vertex: Vertex, tubeInput?: Readonly<Tube>): number | undefined {
	assertDebug(() => wsi4.node.workStepType(vertex) === WorkStepType.tubeCutting, "Pre-condition violated");

	const tube = tubeInput ?? tubeForVertex(vertex);
	if (tube === undefined) {
		return undefined;
	}

	const partsPerTube = computeNumPartsPerTube(tube, wsi4.node.profileExtrusionLength(vertex));
	if (partsPerTube === undefined) {
		return undefined;
	}

	const numParts = wsi4.node.multiplicity(vertex);
	return numParts / partsPerTube;
}

export function engravingSegmentsForVertex(vertex: Vertex): Segment[] {
	assertDebug(() => wsi4.node.workStepType(vertex) === WorkStepType.sheetCutting, "Pre-condition violated");

	const twoDimRep = wsi4.node.twoDimRep(vertex);
	assert(twoDimRep !== undefined, "TwoDimRep invalid");

	const result = wsi4.geo.util.sceneSegments(
		wsi4.cam.util.extractEngravings(twoDimRep),
	);

	const engravingMode = nodeUserDatumOrDefault("bendLineEngravingMode", vertex);
	if (engravingMode === "none") {
		return result;
	}

	const bendLineData = wsi4.node.computeBendLineData(vertex);
	assert(bendLineData !== undefined, "Expecting valid bend line data");

	const addBendLineSegmentsIf = (pred: (bld: BendLineData) => boolean) => {
		bendLineData.forEach(bld => {
			if (pred(bld)) {
				result.push(...bld.segments);
			}
		});
	};

	switch (engravingMode) {
		case "upwardOnly": addBendLineSegmentsIf(bld => bld.bendAngle >= 0); break;
		case "downwardOnly": addBendLineSegmentsIf(bld => bld.bendAngle < 0); break;
		case "all": addBendLineSegmentsIf(() => true); break;
	}

	return result;
}

