import {
	Costs,
} from "qrc:/js/lib/calc_costs";
import {
	Times,
} from "qrc:/js/lib/calc_times";
import {
	getFutureResult,
} from "qrc:/js/lib/future";
import {
	Feature,
	FileType,
	ProcessType,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	getArticleName,
	getArticleSignatureVertex,
	getAssociatedSheetMaterialId,
	isComponentArticle,
	isJoiningArticle,
	projectName,
} from "qrc:/js/lib/graph_utils";
import {
	cadFeatureTwoDimCentroid,
	collectBrepsFromAssembly,
	collectBrepsFromVertices,
	createPngFutures,
	problematicGeometryAsm,
	VertexAnd,
} from "qrc:/js/lib/node_utils";
import {
	getSettingOrDefault,
} from "qrc:/js/lib/settings_table";
import {
	getTable,
	lookUpProcessSetupTimeFallBack,
} from "qrc:/js/lib/table_utils";
import {
	nodeUserDatumImpl,
} from "qrc:/js/lib/userdata_config";
import {
	isCompatibleToNodeUserDataEntry,
	nodeUserDatum,
	nodeUserDatumOrDefault,
	UserData,
} from "qrc:/js/lib/userdata_utils";
import {
	assert,
	assertDebug,
	bbDimensionX,
	bbDimensionY,
	boxIsEmpty,
	defaultPngResolution,
	isBoolean,
	isEqual,
} from "qrc:/js/lib/utils";
import {
	isExportReadyImpl,
} from "qrc:/js/lib/manufacturing_state";
import {
	createSheetTappingSceneNameItems,
	tappingLabel,
	ThroughHole,
} from "qrc:/js/lib/sheet_tapping_utils";
import {
	renderScene,
} from "qrc:/js/lib/geometry_utils";
import {
	computeActualManufacturingStateCached,
	computeVirtualManufacturingState,
	createManufacturingStateCache,
	ManufacturingStateCache,
} from "qrc:/js/lib/manufacturing_state_util";
import {
	back,
	front,
} from "qrc:/js/lib/array_util";
import {
	CalcSettings,
} from "qrc:/js/lib/calc_settings";
import {
	boxFromResolution,
	defaultSvgResolution,
	sceneForVertex,
} from "qrc:/js/lib/scene_utils";
import {
	articleUserDatumImpl,
} from "qrc:/js/lib/article_userdata_config";
import {
	ArticleObject,
	AttachmentMap,
	CanForceSheetPartEntry,
	NodeObject,
	ProjectObject,
	ResourceEntry,
	ResourcesObject,
	ScalePrice,
	ShopUserData,
	TappingNodeData,
	TubeCuttingDataEntry,
	getAdditionalProcess,
} from "./cli_shop_interface";
import {
	shopConstraintLevelMap,
	shopReplyStateLevelMap,
} from "./cli_shop_manufacturing_state";
import {
	computeRecursiveArticleScaleSellingPrice,
	computeNodeCosts,
	computeScaleValues,
	shopOnlyComputeSellingPriceInclGlobalSurcharges,
	processRatePerSecond,
} from "./export_calc_costs";
import {
	createCalcCache,
	CalcCache,
} from "./export_calc_cache";
import {
	computeAutomaticMechanicalDeburringTimes,
	computeManualMechanicalDeburringTimes,
	computeNodeHandlingTime,
} from "./export_calc_times";
import {
	computeMinCompletionTime,
} from "./export_calc_completion_time";

export function isExportReady(vertex: Vertex, cache: ManufacturingStateCache): boolean {
	const manufacturingState = computeActualManufacturingStateCached(
		vertex,
		shopReplyStateLevelMap,
		shopConstraintLevelMap,
		cache,
	);
	return isExportReadyImpl(manufacturingState);
}

function getAssemblyOpt(vertex: Vertex): Assembly | undefined {
	return wsi4.node.assembly(vertex) ?? wsi4.node.inputAssembly(vertex);
}

function box2D(vertex: Vertex): Box2 | undefined {
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		return undefined;
	}
	return wsi4.cam.util.boundingBox2(twoDimRep);
}

/**
 * For valid joining scale prices the joining must consist of valid source articles only.
 * A source article is considered valid if it is a manufacturable laser-part or bend-laser-part.
 */
function canComputeScalePricesForJoining(targetArticle: readonly Vertex[], manufacturingStateCache: ManufacturingStateCache): boolean {
	return wsi4.graph.reaching(back(targetArticle))
		.every(vertex => isExportReady(vertex, manufacturingStateCache))
		&& wsi4.graph.sources(front(targetArticle))
			.every(sourceVertex => {
				const sourceArticle = wsi4.graph.article(sourceVertex);
				const signatureVertex = getArticleSignatureVertex(sourceArticle);
				const wst = wsi4.node.workStepType(signatureVertex);
				return wst === WorkStepType.sheetCutting || wst === WorkStepType.sheetBending;
			});
}

/**
 * Compute scale prices for component articles
 *
 * Pre-conditions:
 * - article is component article or article is joining consisting of valid component articles
 * - all nodes are export ready
 */
function computeArticleScalePrices(article: readonly Readonly<Vertex>[], calcCache: CalcCache, manufacturingStateCache: ManufacturingStateCache): ScalePrice[] {
	assertDebug(() => isComponentArticle(article) || isJoiningArticle(article), "Expecting component or joining article");
	assertDebug(() => !isComponentArticle(article) || article.every(vertex => isExportReady(vertex, manufacturingStateCache)), "Expecting each node to be export ready");
	assertDebug(() => !isJoiningArticle(article) || canComputeScalePricesForJoining(article, manufacturingStateCache), "Expecting scale price to be computable");

	const scaleValues = (() => {
		const calcSettings: CalcSettings = {
			scaleValueMode: "relative",
			baseScaleValues: [
				2,
				5,
				10,
				20,
				50,
				100,
			],
		};
		const articleMultiplicity = wsi4.node.multiplicity(back(article));
		return computeScaleValues(articleMultiplicity, calcSettings);
	})();

	return scaleValues.map(scaleValue => ({
		scaleValue: scaleValue,
		sellingPrice: computeRecursiveArticleScaleSellingPrice(article, scaleValue, calcCache),
	}))
		.filter((obj): obj is ScalePrice => obj.sellingPrice !== undefined);
}

/**
 * Convert user data to Shop userdata
 */
function convertUserData(userData: UserData): ShopUserData {
	const material: SheetMaterial | undefined = (() => {
		const sheetMaterialId = nodeUserDatumImpl("sheetMaterialId", userData);
		if (sheetMaterialId === undefined) {
			return undefined;
		}
		const result = getTable(TableType.sheetMaterial)
			.find(row => row.identifier === sheetMaterialId);
		if (result === undefined) {
			wsi4.util.error("Material user data entry does not match any row in the current material table.");
			return undefined;
		}
		return result;
	})();
	const doubleSidedMechanicalDeburring = ((): boolean | undefined => {
		const userDataEntry = userData["deburrDoubleSided"];
		if (userDataEntry === undefined) {
			return undefined;
		}
		if (userDataEntry === undefined) {
			return undefined;
		}
		if (!isBoolean(userDataEntry)) {
			return wsi4.throwError("Deburring user data entry invalid");
		}
		return userDataEntry;
	})();
	return {
		material: material,
		doubleSidedMechanicalDeburring: doubleSidedMechanicalDeburring,
	};
}

function computeSlideGrindingCost(vertex: Vertex): number | undefined {
	const process = getTable(TableType.process)
		.find(row => row.type === ProcessType.slideGrinding);
	if (process === undefined) {
		return undefined;
	}
	const multiplicity = wsi4.node.multiplicity(vertex);
	const setupTime = lookUpProcessSetupTimeFallBack(process.identifier) ?? 0;
	const unitTimeRow = getTable(TableType.processUnitTimeFallback)
		.find(row => row.processId === process.identifier);
	const unitTime = unitTimeRow === undefined ? 0 : unitTimeRow.time * multiplicity;

	const handlingTime = computeNodeHandlingTime(vertex, undefined, process.identifier);
	if (handlingTime === undefined) {
		return undefined;
	}

	const ratePerSecond = processRatePerSecond(process.identifier);
	if (ratePerSecond === undefined) {
		return undefined;
	}
	const costs = new Costs(0., (handlingTime.setup + setupTime) * ratePerSecond, (handlingTime.unit + unitTime) * ratePerSecond);
	return shopOnlyComputeSellingPriceInclGlobalSurcharges(costs);
}

function computeDeburringCosts(vertex: Vertex): Array<number> | undefined {
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		wsi4.util.error("computeDeburringCosts(): Missing twoDimRep");
		return undefined;
	}
	const multiplicity = wsi4.node.multiplicity(vertex);
	const material = getAssociatedSheetMaterialId(vertex);
	if (material === undefined) {
		return undefined;
	}

	let oneSidedTime: Times | undefined = new Times(0., 0.);
	let doubleSidedTime: Times | undefined = new Times(0., 0.);
	let processId: string | undefined = undefined;
	(() => {
		const ap = getAdditionalProcess("mechanicalDeburring");
		if (ap !== undefined) {
			switch (ap.type) {
				case "automaticMechanicalDeburring": {
					oneSidedTime = computeAutomaticMechanicalDeburringTimes(twoDimRep, false, multiplicity, material, ap.identifier);
					doubleSidedTime = computeAutomaticMechanicalDeburringTimes(twoDimRep, true, multiplicity, material, ap.identifier);
					processId = ap.identifier;
					return;
				}
				case "manualMechanicalDeburring": {
					oneSidedTime = computeManualMechanicalDeburringTimes(twoDimRep, false, multiplicity, ap.identifier);
					doubleSidedTime = computeManualMechanicalDeburringTimes(twoDimRep, true, multiplicity, ap.identifier);
					processId = ap.identifier;
					return;
				}
				default: return undefined;
			}
		}

	})();

	if (processId === undefined || oneSidedTime === undefined || doubleSidedTime === undefined) {
		return undefined;
	}

	const handlingTime = computeNodeHandlingTime(vertex, undefined, processId);
	if (handlingTime === undefined) {
		return undefined;
	}

	const ratePerSecond = processRatePerSecond(processId);
	if (ratePerSecond === undefined) {
		return undefined;
	}
	if (oneSidedTime.setup === 0. || doubleSidedTime.setup === 0) {
		const entry = getTable(TableType.processSetupTimeFallback)
			.find(row => row.processId === processId);
		if (entry !== undefined) {
			oneSidedTime.setup = entry.time * 60;
			doubleSidedTime.setup = entry.time * 60;
		}
	}

	const oneSidedCosts = new Costs(0., (handlingTime.setup + oneSidedTime.setup) * ratePerSecond, (handlingTime.unit + oneSidedTime.unit) * ratePerSecond);
	const doubleSidedCosts = new Costs(0., (handlingTime.setup + doubleSidedTime.setup) * ratePerSecond, (handlingTime.unit + doubleSidedTime.unit) * ratePerSecond);
	const oneSidedSellingPrice = shopOnlyComputeSellingPriceInclGlobalSurcharges(oneSidedCosts);
	const doubleSidedSellingPrice = shopOnlyComputeSellingPriceInclGlobalSurcharges(doubleSidedCosts);
	return oneSidedSellingPrice === undefined || doubleSidedSellingPrice === undefined ? undefined : [
		oneSidedSellingPrice,
		doubleSidedSellingPrice,
	];
}

function nodeToObject(vertex: Vertex, manufacturingStateCache: ManufacturingStateCache): NodeObject {
	const assemblyOpt = getAssemblyOpt(vertex);
	const result: NodeObject = {
		vertex: wsi4.util.toKey(vertex),
		nodeIdKey: wsi4.util.toKey(wsi4.node.nodeId(vertex)),
		rootIdKey: wsi4.util.toKey(wsi4.node.rootId(vertex)),
		name: getArticleName(vertex),
		assemblyId: assemblyOpt === undefined ? undefined : wsi4.util.toKey(assemblyOpt),
		processId: wsi4.node.processId(vertex),
		processType: wsi4.node.processType(vertex),
		workStepType: wsi4.node.workStepType(vertex),
		processTypeTranslation: wsi4.util.translate(wsi4.node.processType(vertex)),
		userData: convertUserData(wsi4.node.userData(vertex)),
		importMultiplicity: wsi4.node.importMultiplicity(vertex),
		sources: wsi4.graph.sources(vertex)
			.map(v => wsi4.util.toKey(v)),
		targets: wsi4.graph.targets(vertex)
			.map(v => wsi4.util.toKey(v)),
		actualManufacturingState: computeActualManufacturingStateCached(
			vertex,
			shopReplyStateLevelMap,
			shopConstraintLevelMap,
			manufacturingStateCache,
		),
		virtualManufacturingState: computeVirtualManufacturingState(
			vertex,
			shopReplyStateLevelMap,
			shopConstraintLevelMap,
		),
		forcedProcessType: wsi4.node.forcedProcessType(vertex),
		comment: undefined,
		deburringPreviewPrices: undefined,
		deburringPrice: undefined,
		fixedRotations: undefined,
		flipSide: undefined,
		height: undefined,
		numCountersinks: undefined,
		numThreads: undefined,
		testReportPrice: undefined,
		testReportRequired: undefined,
		thickness: undefined,
		width: undefined,
		slideGrindingPreviewPrice: undefined,
		editingState: wsi4.node.editingState(vertex),
		hasTwoDimInput: wsi4.node.hasTwoDimInput(vertex),
		importId: wsi4.node.importId(vertex),
	};
	if (wsi4.node.workStepType(vertex)
		.startsWith("sheet") && wsi4.node.workStepType(vertex) !== "sheet") {
		result.thickness = wsi4.node.sheetThickness(vertex);
		const box = box2D(vertex);
		if (box === undefined) {
			return wsi4.throwError("nodeToObject(): Missing Box2");
		}
		assertDebug(() => !boxIsEmpty(box), "Box is empty.");
		result.width = bbDimensionX(box);
		result.height = bbDimensionY(box);
	}
	if (isCompatibleToNodeUserDataEntry("testReportRequired", vertex) && getSettingOrDefault("sheetTestReportEnabled")) {
		const required = nodeUserDatumOrDefault("testReportRequired", vertex);
		result.testReportRequired = required;
		const price = shopOnlyComputeSellingPriceInclGlobalSurcharges(new Costs(getSettingOrDefault("sheetTestReportCosts"), 0, 0));
		result.testReportPrice = required ? price : 0;
	}
	if (isCompatibleToNodeUserDataEntry("fixedRotations", vertex)) {
		result.fixedRotations = nodeUserDatumOrDefault("fixedRotations", vertex);
	}
	if (wsi4.node.workStepType(vertex) === WorkStepType.sheetCutting && getAdditionalProcess("mechanicalDeburring") !== undefined) {
		result.deburringPreviewPrices = computeDeburringCosts(vertex);
	}
	if (wsi4.node.workStepType(vertex) === WorkStepType.sheetCutting && getAdditionalProcess("slideGrinding") !== undefined) {
		result.slideGrindingPreviewPrice = computeSlideGrindingCost(vertex);
	}
	{
		const processType = wsi4.node.processType(vertex);
		if (processType === ProcessType.automaticMechanicalDeburring || processType === ProcessType.manualMechanicalDeburring) {
			const costs = computeNodeCosts(vertex);
			result.deburringPrice = shopOnlyComputeSellingPriceInclGlobalSurcharges(costs);
		}
	}
	if (wsi4.node.flipSide(vertex) !== undefined) {
		result.flipSide = wsi4.node.flipSide(vertex);
	}

	if (wsi4.node.isInitial(vertex) && wsi4.node.workStepType(vertex) !== WorkStepType.userDefinedBase) {
		result.comment = nodeUserDatumOrDefault("comment", vertex);
	}

	if (isCompatibleToNodeUserDataEntry("numThreads", vertex)) {
		result.numThreads = nodeUserDatumOrDefault("numThreads", vertex);
	}
	if (isCompatibleToNodeUserDataEntry("numCountersinks", vertex)) {
		result.numCountersinks = nodeUserDatumOrDefault("numCountersinks", vertex);
	}

	return result;
}

function nodesToObject(vertices: Array<Vertex>, manufacturingStateCache: ManufacturingStateCache): Array<NodeObject> {
	return vertices.map(vertex => nodeToObject(vertex, manufacturingStateCache));
}

function articleToObject(article: Array<Vertex>, calcCache: CalcCache, manufacturingStateCache: ManufacturingStateCache): ArticleObject {
	const firstVertex = front(article);
	const multiplicity = wsi4.node.multiplicity(firstVertex);
	const articleIsExportReady = article.every(vertex => isExportReady(vertex, manufacturingStateCache));
	const articleIsComponent = isComponentArticle(article);

	const approxPrice = (() => {
		if (articleIsComponent && articleIsExportReady) {
			return computeRecursiveArticleScaleSellingPrice(article, multiplicity, calcCache);
		} else {
			return undefined;
		}
	})();

	const scalePrices = (() => {
		if ((articleIsComponent && articleIsExportReady) || (isJoiningArticle(article) && canComputeScalePricesForJoining(article, manufacturingStateCache))) {
			return computeArticleScalePrices(article, calcCache, manufacturingStateCache);
		} else {
			return [];
		}
	})();

	const nodes = nodesToObject(article, manufacturingStateCache);
	const articleUserData = wsi4.node.articleUserData(firstVertex);

	return {
		name: articleUserDatumImpl("name", articleUserData) ?? "",
		externalPartNumber: articleUserDatumImpl("externalPartNumber", articleUserData) ?? "",
		externalDrawingNumber: articleUserDatumImpl("externalDrawingNumber", articleUserData) ?? "",
		externalRevisionNumber: articleUserDatumImpl("externalRevisionNumber", articleUserData) ?? "",
		multiplicity: multiplicity,
		nodes: nodes,
		approxPrice: approxPrice,
		scalePrices: scalePrices,
	};
}

function articlesToObject(calcCache: CalcCache, manufacturingStateCache: ManufacturingStateCache): Array<ArticleObject> {
	return wsi4.graph.articles()
		.map(article => {
			const result = articleToObject(article, calcCache, manufacturingStateCache);
			return result;
		});
}

function computeTappingSvg(
	vertex: Vertex,
	throughHoles: readonly Readonly<ThroughHole>[],
): string {
	const scene = sceneForVertex(vertex, {
		cuttingContours: true,
		engravings: true,
		sceneTextItems: throughHoles.map((candidate, index): SceneTextItem => tappingLabel(candidate.diameter, candidate.center2, index.toFixed(0))),
		fontSize: 32,
	});
	return wsi4.util.arrayBufferToString(renderScene(scene, FileType.svg, {resolution: defaultSvgResolution()}));
}

function computeThroughHoles(vertex: Vertex, screwThreads: readonly Readonly<ScrewThread>[]): ThroughHole[] {
	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 [];
	}

	return wsi4.cad.features(part)
		.filter(feature => feature.type === "throughHole")
		.map((feature): ThroughHole => {
			// This is currently limited to ThroughHoles.
			// However, note that this is not enforced at type-level as of now.
			const diameter = wsi4.cad.coreHoleDiameter(feature.content, part);
			return {
				cadThroughHole: feature.content,
				center2: cadFeatureTwoDimCentroid(vertex, feature),
				diameter: diameter,
			};
		})
		.filter(throughHole => screwThreads.some(screwThread => Math.abs(screwThread.coreHoleDiameter - throughHole.diameter) < screwThread.symmetricTolerance));
}

function computeTappingNodeData(unknownVertices: readonly Vertex[]): TappingNodeData[] {
	const screwThreads = getTable(TableType.screwThread);
	return wsi4.graph.articles()
		.filter(article => {
			const relevantVertex = article.find(vertex => wsi4.node.processType(vertex) === ProcessType.sheetTapping)
				?? article.find(vertex => wsi4.node.workStepType(vertex) === "sheetCutting");
			return relevantVertex !== undefined && unknownVertices.some(other => isEqual(relevantVertex, other));
		})
		.map((article): [Vertex, ThroughHole[]] => {
			const partVertex = article.find(vertex => wsi4.node.workStepType(vertex) === "sheetBending") ?? article.find(vertex => wsi4.node.workStepType(vertex) === "sheetCutting");
			assert(partVertex !== undefined);
			return [
				partVertex,
				computeThroughHoles(partVertex, screwThreads),
			];
		})
		.filter(tuple => tuple[1].length > 0)
		.map(([
			partVertex,
			throughHoles,
		]) => {
			const svg = computeTappingSvg(partVertex, throughHoles);
			const selection = ((): SheetTappingDataEntry[] => {
				const vertex = wsi4.graph.article(partVertex).find(vertex => wsi4.node.processType(vertex) === ProcessType.sheetTapping);
				if (vertex === undefined) {
					return [];
				} else {
					return nodeUserDatumOrDefault("sheetTappingData", vertex);
				}
			})();

			return {
				partVertexKey: wsi4.util.toKey(partVertex),
				throughHoles: throughHoles,
				selection: selection,
				svg: svg,
			};
		});
}

function computeTubeCuttingData(unknownVertices: readonly Vertex[]): TubeCuttingDataEntry[] {
	return wsi4.graph.vertices()
		.filter(vertex => wsi4.node.processType(vertex) === ProcessType.tubeCutting)
		.filter(lhs => unknownVertices.some(rhs => isEqual(lhs, rhs)))
		.map(vertex => {
			const dimX = wsi4.node.profileExtrusionLength(vertex);
			assert(dimX !== undefined, "Expecting valid profile extrusion length");

			const profileGeometry = wsi4.node.tubeProfileGeometry(vertex);
			assert(profileGeometry !== undefined, "Expecting valid profile geometry");

			return {
				vertexKey: wsi4.util.toKey(vertex),
				processId: wsi4.node.processId(vertex),
				dimX: dimX,
				tubeProfileGeometry: profileGeometry,
				tubeMaterialId: nodeUserDatum("tubeMaterialId", vertex),
				tubeSpecificationId: nodeUserDatum("tubeSpecificationId", vertex),
				comment: undefined,
				deburringPreviewPrices: undefined,
				deburringPrice: undefined,
				fixedRotations: undefined,
				flipSide: undefined,
				height: undefined,
				numCountersinks: undefined,
				numThreads: undefined,
				testReportPrice: undefined,
				testReportRequired: undefined,
				thickness: undefined,
				width: undefined,
			};
		});
}

function createSceneForArticle(vertex: Vertex, fontSize: number) {
	assertDebug(() => wsi4.node.processType(vertex) !== "sheetTapping", "Unexpected ProcessType");
	const extraTexts = (() => {
		const tappingVertex = wsi4.graph.article(vertex)
			.find(v => wsi4.node.processType(v) === ProcessType.sheetTapping);
		if (tappingVertex === undefined) {
			return [];
		}
		const screwThreads = getTable("screwThread");
		const selection = nodeUserDatumOrDefault("sheetTappingData", tappingVertex)
			.map(entry => {
				const screwThread = screwThreads.find(row => row.identifier === entry.screwThreadId);
				assert(screwThread !== undefined, "Expecting valid screw thread");
				return {
					cadFeature: entry.cadFeature,
					screwThread: screwThread,
					center2: cadFeatureTwoDimCentroid(vertex, entry.cadFeature),
				};
			});
		return createSheetTappingSceneNameItems(selection);
	})();
	return sceneForVertex(vertex, {
		cuttingContours: true,
		engravings: true,
		bendLineShowLabels: true,
		bendLineShowAffectedSegments: true,
		tubeCuttingShowVirtualCuts: true,
		tubeCuttingShowTubeContours: true,
		fontSize: fontSize,
		sceneTextItems: extraTexts,
	});
}

function futuresToCanForceData(vertexAndFutures: readonly Readonly<VertexAnd<BooleanFuture>>[]): CanForceSheetPartEntry[] {
	return vertexAndFutures.map(vertexAndFuture => ({
		vertexKey: wsi4.util.toKey(vertexAndFuture.vertex),
		canForce: getFutureResult<"boolean">(vertexAndFuture.data),
	}));
}

function computeProblematicGeometryGltfs(unknownVertices: readonly Vertex[]): ResourceEntry {
	const result: ResourceEntry = {};
	wsi4.graph.vertices()
		.filter(lhs => unknownVertices.some(rhs => isEqual(lhs, rhs)))
		.filter(vertex => wsi4.node.hasProblematicGeometries(vertex))
		.forEach(vertex => {
			const asm = problematicGeometryAsm(vertex);
			const ba = wsi4.geo.assembly.gltf(asm, wsi4.geo.brep.dracoRepresentation(collectBrepsFromAssembly(asm)), {}, []);
			const base64 = wsi4.util.toBase64(ba);
			result[wsi4.util.toKey(vertex)] = wsi4.util.arrayBufferToString(base64);
		});
	return result;
}

function createResourcesObject(
	knownVertices: readonly Vertex[],
	pngFutures: readonly VertexAnd<ArrayBufferFuture>[],
	technicalDrawingsFutures: readonly Readonly<VertexAnd<MeasurementScenesFuture>>[],
	dracoRepresentation: DracoRepresentation,
	canForceSheetPartFutures: readonly Readonly<VertexAnd<BooleanFuture>>[],
): ResourcesObject {
	const unknownVertices: readonly Vertex[] = wsi4.graph.vertices()
		.filter(lhs => knownVertices.every(rhs => !isEqual(lhs, rhs)));

	const svgs = ((): ResourceEntry => {
		const result: ResourceEntry = {};
		const vertices = unknownVertices
			.filter(vertex => {
				switch (wsi4.node.workStepType(vertex)) {
					// In case nesting fails there is no TwoDimRep
					case "sheet": return wsi4.node.twoDimRep(vertex) !== undefined;
					case "sheetCutting":
					case "sheetBending":
					case "tubeCutting": return true;
					default: return false;
				}
			});
		let maxSheetWidth = Math.max(...vertices.filter(v => wsi4.node.workStepType(v) === WorkStepType.sheet).map(v => {
			const twoDimRep = wsi4.node.twoDimRep(v);
			assert(twoDimRep !== undefined);
			return Math.max(...wsi4.cam.nest2.nestingDescriptors(twoDimRep).map(n => {
				const bbox = wsi4.geo.util.boundingBox2d(wsi4.cam.nest2.nestingTargetBoundary(twoDimRep, n));
				return boxIsEmpty(bbox) ? .0 : bbDimensionX(bbox);
			}));
		}));
		if (maxSheetWidth <= .0) {
			maxSheetWidth = 3000; // width of large format sheet
		}
		unknownVertices
			.filter(vertex => {
				switch (wsi4.node.workStepType(vertex)) {
					// In case nesting fails there is no TwoDimRep
					case "sheet": return wsi4.node.twoDimRep(vertex) !== undefined;
					case "sheetCutting":
					case "sheetBending":
					case "tubeCutting": return true;
					default: return false;
				}
			})
			.forEach(vertex => {
				const fontSizeFactor = 0.014; // determined such that the font size for a sheet view of 3000 is 42 (readable in shop)
				const scene = createSceneForArticle(vertex, maxSheetWidth * fontSizeFactor);
				const res = (() => {
					if (wsi4.node.workStepType(vertex) === WorkStepType.sheet) {
						const bb = wsi4.geo.util.sceneBoundingBox(scene);
						if (boxIsEmpty(bb)) {
							return defaultSvgResolution();
						}
						assert(maxSheetWidth >= bbDimensionX(bb));
						return {
							width: maxSheetWidth,
							height: bbDimensionY(bb),
						};
					} else {
						return defaultSvgResolution();
					}
				})();
				result[wsi4.util.toKey(vertex)] = wsi4.util.arrayBufferToString(wsi4.geo.util.renderScene(scene, FileType.svg, {resolution: res}));
			});
		return result;
	})();

	const attachments = ((): AttachmentMap => unknownVertices
		.filter(v => isCompatibleToNodeUserDataEntry("attachments", v))
		.reduce((res: AttachmentMap, v: Vertex) => {
			res[wsi4.util.toKey(v)] = nodeUserDatumOrDefault("attachments", v);
			return res;
		}, {}))();

	const pngs = (() => pngFutures.reduce((acc: ResourceEntry, obj) => {
		const buffer = getFutureResult<"arrayBuffer">(obj.data);
		const base64 = wsi4.util.toBase64(buffer);
		acc[wsi4.util.toKey(obj.vertex)] = wsi4.util.arrayBufferToString(base64);
		return acc;
	}, {}))();

	const technicalDrawings = (() => technicalDrawingsFutures.reduce((acc: Record<string, string[]>, vertexAndFuture) => {
		const measurementScenes = getFutureResult<"measurementScenes">(vertexAndFuture.data);
		const resolution = defaultSvgResolution();
		const viewPort = boxFromResolution(resolution);
		acc[wsi4.util.toKey(vertexAndFuture.vertex)] = measurementScenes.map(measurementScene => wsi4.util.arrayBufferToString(
			wsi4.geo.util.renderScene(measurementScene.scene, FileType.svg, {
				resolution: resolution,
				viewPort: viewPort,
			}),
		));
		return acc;
	}, {}))();

	const gltfs = ((): ResourceEntry => {
		const result: ResourceEntry = {};
		unknownVertices.filter(v => isRelevantGltfVertex(v, knownVertices)).forEach(v => {
			const a = getAssemblyOpt(v);
			if (a !== undefined) {
				result[wsi4.util.toKey(v)] = wsi4.util.arrayBufferToString(wsi4.util.toBase64(wsi4.geo.assembly.gltf(a, dracoRepresentation, {}, [])));
			}
		});
		return result;
	})();

	const sheetTappingNodeData = computeTappingNodeData(unknownVertices);
	const tubeCuttingData = computeTubeCuttingData(unknownVertices);

	return {
		attachments: attachments,
		gltfs: gltfs,
		pngs: pngs,
		svgs: svgs,
		technicalDrawings: technicalDrawings,
		sheetTappingNodeData: sheetTappingNodeData,
		tubeCuttingData: tubeCuttingData,
		canForceSheetPartData: futuresToCanForceData(canForceSheetPartFutures),
		problematicGeometryGlfts: computeProblematicGeometryGltfs(unknownVertices),
	};
}

function computeNetSellingPrice(articles: ArticleObject[]): number {
	// We add undefined approx price as .0 because we only compute net selling price if possible to export
	// This means the only vertices that have approxPrice undefined are joining and semi manufactures nodes which we don't compute into the overall price (tested in cli/tests/ts/shop_approx_article_price.ts)
	return articles.reduce((val: number, article) => val + (article.approxPrice ?? .0), .0);
}

function isRelevantGltfVertex(vertex: Vertex, knownVertices: readonly Vertex[]): boolean {
	let result = true;
	result = result && knownVertices.every(other => !isEqual(vertex, other));
	result = result && (() => {
		const relevantWsts: readonly WorkStepType[] = [
			WorkStepType.joining,
			WorkStepType.sheetBending,
			WorkStepType.sheetCutting,
			WorkStepType.tubeCutting,
			WorkStepType.userDefinedBase,
		];
		const lhs = wsi4.node.workStepType(vertex);
		return relevantWsts.some(rhs => lhs === rhs);
	})();
	result = result && (wsi4.node.assembly(vertex) !== undefined || wsi4.node.inputAssembly(vertex) !== undefined);
	return result;
}

/**
 * @param knownVertices Vertices will be skipped when computing resources
 */
export function createProjectObj(
	knownVertices: readonly Vertex[],
	pngFutures?: readonly Readonly<VertexAnd<ArrayBufferFuture>>[],
	technicalDrawingsFutures?: readonly Readonly<VertexAnd<MeasurementScenesFuture>>[],
	calcCacheInput?: CalcCache,
	manufacturingStateCacheInput?: ManufacturingStateCache,
): ProjectObject {
	const calcCache = calcCacheInput ?? createCalcCache();
	const manufacturingStateCache = manufacturingStateCacheInput ?? createManufacturingStateCache();

	pngFutures = (() => {
		const relevantWorkSteps: readonly WorkStepType[] = [
			WorkStepType.joining,
			WorkStepType.sheetBending,
			WorkStepType.tubeCutting,
			WorkStepType.userDefinedBase,
		];

		const isRelevantVertex = (vertex: Vertex) => {
			let result = true;
			result = result && knownVertices.every(other => !isEqual(vertex, other));
			result = result && (() => {
				const lhs = wsi4.node.workStepType(vertex);
				return relevantWorkSteps.some(rhs => lhs === rhs);
			})();
			return result;
		};

		if (pngFutures === undefined) {
			return createPngFutures(wsi4.graph.vertices()
				.filter(vertex => isRelevantVertex(vertex)), defaultPngResolution());
		} else {
			return pngFutures.filter(obj => isRelevantVertex(obj.vertex));
		}
	})();

	technicalDrawingsFutures = ((): VertexAnd<MeasurementScenesFuture>[] => {
		const isRelevantVertex = (vertex: Vertex) => {
			let result = true;
			result = result && knownVertices.every(other => !isEqual(vertex, other));
			result = result && wsi4.node.workStepType(vertex) === WorkStepType.sheetBending;
			return result;
		};

		if (!wsi4.isFeatureEnabled(Feature.bendMeasurementScene)) {
			return [];
		} else if (technicalDrawingsFutures === undefined) {
			const bendDrawingFontSize = 30;
			return wsi4.graph.vertices()
				.filter(vertex => isRelevantVertex(vertex))
				.map(vertex => ({
					vertex: vertex,
					data: wsi4.node.asyncBendMeasurementScenes(vertex, bendDrawingFontSize, defaultSvgResolution()),
				}));
		} else {
			return technicalDrawingsFutures.filter(entry => isRelevantVertex(entry.vertex));
		}
	})();

	const dracoRepresentation = wsi4.geo.brep.dracoRepresentation(collectBrepsFromVertices(wsi4.graph.vertices().filter(vertex => isRelevantGltfVertex(vertex, knownVertices))));

	const canForceSheetPartFutures = wsi4.graph.vertices()
		.filter(vertex => wsi4.node.processType(vertex) === ProcessType.externalPart)
		.filter(lhs => knownVertices.every(rhs => !isEqual(lhs, rhs)))
		.map(vertex => ({
			vertex: vertex,
			data: wsi4.node.asyncCanForceSheetMetalPart(vertex),
		}));

	const articles = articlesToObject(
		calcCache,
		manufacturingStateCache,
	);

	const allNodesExportReady = wsi4.graph.vertices()
		.every(vertex => isExportReady(vertex, manufacturingStateCache));
	const minCompletionTime = allNodesExportReady ? computeMinCompletionTime(calcCache) : undefined;
	const netSellingPrice = allNodesExportReady ? computeNetSellingPrice(articles) : undefined;

	const resources = createResourcesObject(
		knownVertices,
		pngFutures,
		technicalDrawingsFutures,
		dracoRepresentation,
		canForceSheetPartFutures,
	);

	return {
		creator: wsi4.util.programInfo(),
		projectName: projectName(),
		articles: articles,
		resources: resources,
		minCompletionTime: minCompletionTime,
		netSellingPrice: netSellingPrice,
	};
}
