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,
	computeTappingCandidates,
	createPngFutures,
	nodeLaserSheetCuttingGasId,
	problematicGeometryAsm,
	VertexAnd,
} from "qrc:/js/lib/node_utils";
import {
	getProcessTypes,
} from "qrc:/js/lib/process_utils";
import {
	getSettingOrDefault,
} from "qrc:/js/lib/settings_table";
import {
	getTable,
	lookUpProcessSetupTimeFallBack,
} from "qrc:/js/lib/table_utils";
import {
	nodeUserDatumImpl,
	SheetTappingDataEntry,
} from "qrc:/js/lib/userdata_config";
import {
	isCompatibleToNodeUserDataEntry,
	nodeUserDatum,
	nodeUserDatumOrDefault,
	UserData,
} from "qrc:/js/lib/userdata_utils";
import {
	assert,
	assertDebug,
	bbDimensionX,
	bbDimensionY,
	isBoolean,
	isEqual,
} from "qrc:/js/lib/utils";
import {
	isExportReadyImpl,
} from "qrc:/js/lib/manufacturing_state";
import {
	addSheetTappingNumbersToScene,
	addSheetTappingTextToScene,
	TappingSceneCandidate,
} from "qrc:/js/lib/sheet_tapping_utils";
import {
	addIopsToScene,
	createEmptyScene,
	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 {
	sceneForVertex,
} from "qrc:/js/lib/scene_utils";
import {
	articleUserDatumImpl,
} from "qrc:/js/lib/article_userdata_config";
import {
	getAllowedAdditionalProcesses,
} from "./cli_shop_configuration";
import {
	ArticleObject,
	AttachmentMap,
	CanForceSheetPartEntry,
	NodeObject,
	ProjectObject,
	ResourceEntry,
	ResourcesObject,
	ScalePrice,
	ShopUserData,
	TappingNodeData,
	TubeCuttingDataEntry,
} 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,
} 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) {
			return wsi4.throwError("Material user data entry does not match any row in the current material table.");
		}
		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 ratePerSecond = processRatePerSecond(process.identifier);
	if (ratePerSecond === undefined) {
		return undefined;
	}
	const costs = new Costs(0., setupTime * ratePerSecond, 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 = "";
	if (getProcessTypes()
		.some(t => t === ProcessType.automaticMechanicalDeburring)) {
		const process = getTable(TableType.process)
			.find(row => row.type === ProcessType.automaticMechanicalDeburring);
		assert(process !== undefined, "Expecting valid process");
		oneSidedTime = computeAutomaticMechanicalDeburringTimes(twoDimRep, false, multiplicity, material, process.identifier);
		doubleSidedTime = computeAutomaticMechanicalDeburringTimes(twoDimRep, true, multiplicity, material, process.identifier);
		processId = "automaticMechanicalDeburringId";

	} else {
		if (getProcessTypes()
			.every(t => t !== ProcessType.manualMechanicalDeburring)) {
			wsi4.util.error("computeDeburringCosts(): Missing process type");
			return undefined;
		}
		const process = getTable(TableType.process)
			.find(row => row.type === ProcessType.manualMechanicalDeburring);
		assert(process !== undefined, "Expecting valid process");
		oneSidedTime = computeManualMechanicalDeburringTimes(twoDimRep, false, multiplicity, process.identifier);
		doubleSidedTime = computeManualMechanicalDeburringTimes(twoDimRep, true, multiplicity, process.identifier);
		processId = "manualMechanicalDeburringId";
	}

	if (oneSidedTime === undefined || doubleSidedTime === 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., oneSidedTime.setup * ratePerSecond, oneSidedTime.unit * ratePerSecond);
	const doubleSidedCosts = new Costs(0., doubleSidedTime.setup * ratePerSecond, 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,
		cuttingGasId: 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,
	};
	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");
		}
		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 && getAllowedAdditionalProcesses()
		.some(a => a === "mechanicalDeburring")) {
		result.deburringPreviewPrices = computeDeburringCosts(vertex);
	}
	if (wsi4.node.workStepType(vertex) === WorkStepType.sheetCutting && getAllowedAdditionalProcesses()
		.some(a => a === "slideGrinding")) {
		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.processType(vertex) === ProcessType.laserSheetCutting) {
		result.cuttingGasId = nodeLaserSheetCuttingGasId(vertex);
	}
	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,
	candidates: readonly Readonly<TappingSceneCandidate>[],
	screwThreads: readonly Readonly<ScrewThread>[],
): string {
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		return "";
	}

	const iops = wsi4.cam.util.extractInnerOuterPolygons(twoDimRep);
	if (iops.length !== 1) {
		return "";
	}

	let scene = createEmptyScene();
	scene = addIopsToScene(scene, iops);
	scene = addSheetTappingNumbersToScene(scene, candidates, screwThreads);
	return wsi4.util.arrayBufferToString(renderScene(scene, FileType.svg));
}

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.processType(vertex) === ProcessType.laserSheetCutting);
			return relevantVertex !== undefined && unknownVertices.some(other => isEqual(relevantVertex, other));
		})
		.map((article): [Vertex, TappingSceneCandidate[]] => {
			const partVertex = article.find(vertex => wsi4.node.workStepType(vertex) === "sheetBending") ?? article.find(vertex => wsi4.node.workStepType(vertex) === "sheetCutting");
			assert(partVertex !== undefined);
			const candidates = computeTappingCandidates(partVertex);
			return [
				partVertex,
				candidates,
			];
		})
		.filter(tuple => tuple[1].length > 0)
		.map(([
			partVertex,
			candidates,
		]) => {
			const svg = computeTappingSvg(partVertex, candidates, screwThreads);
			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),
				candidates: candidates,
				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,
				cuttingGasId: 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) {
	const scene = sceneForVertex(
		vertex,
		{
			bendLineShowLabels: true,
			bendLineShowAffectedSegments: true,
			tubeCuttingShowVirtualCuts: true,
			tubeCuttingShowTubeContours: true,
		},
	);
	const tappingVertex = wsi4.graph.article(vertex)
		.find(v => wsi4.node.processType(v) === ProcessType.sheetTapping);
	if (tappingVertex === undefined) {
		return scene;
	}
	const screwThreads = getTable("screwThread");
	const selection = nodeUserDatumOrDefault("sheetTappingData", tappingVertex)
		.map(entry => {
			const screwThread = screwThreads.find(row => row.identifier === entry.screwThread.identifier);
			assert(screwThread !== undefined, "Expecting valid screw thread");
			return {
				cadFeature: entry.cadFeature,
				screwThread: screwThread,
				center2: cadFeatureTwoDimCentroid(vertex, entry.cadFeature),
			};
		});
	return addSheetTappingTextToScene(scene, selection);
}

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 = {};
		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 scene = createSceneForArticle(vertex);
				result[wsi4.util.toKey(vertex)] = wsi4.util.arrayBufferToString(wsi4.geo.util.renderScene(scene, FileType.svg, {}));
			});
		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);
		acc[wsi4.util.toKey(vertexAndFuture.vertex)] = measurementScenes.map(measurementScene => wsi4.util.arrayBufferToString(wsi4.geo.util.renderScene(measurementScene.scene, FileType.svg, {})));
		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)));
		} 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),
				}));
		} 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,
	};
}
