/*
 * Defines interfaces and functionality to export a JSON representation of a project
 */

import {
	assumeGraphAxioms,
} from "qrc:/js/lib/axioms";
import {
	Times,
} from "qrc:/js/lib/calc_times";
import {
	Feature,
	FileType,
	ProcessType,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	computeNodeMass,
	computeSheetConsumption,
	getAssociatedSheetMaterialId,
	isComponentArticle,
	isJoiningArticle,
	isSemimanufacturedArticle,
	projectName,
	serializeUndirectedConnectedComponent,
	sheetIdForVertex,
} from "qrc:/js/lib/graph_utils";
import {
	cadFeatureTwoDimCentroid,
	createPngFutures,
	grossTubeConsumptionForVertex,
	isTestReportRequiredForSheet,
	tubeForVertex,
	VertexAnd,
} from "qrc:/js/lib/node_utils";
import {
	getScriptArg,
} from "qrc:/js/lib/script_args";
import {
	getSettingGraphRepResourcesConfig,
	getSettingOrDefault,
	GraphRepResourcesConfig,
} from "qrc:/js/lib/settings_table";
import {
	getTable,
} from "qrc:/js/lib/table_utils";
import {
	assert,
	assertDebug,
	computeContourCount,
	computeContourLength,
	isEqual,
} from "qrc:/js/lib/utils";
import {
	getFutureResult,
} from "qrc:/js/lib/future";
import {
	collectNodeUserDataEntries,
	hasManufacturingCostOverride,
	isCompatibleToNodeUserDataEntry,
	userDataAccess,
	nodeUserDatum,
	nodeUserDatumOrDefault,
} from "qrc:/js/lib/userdata_utils";
import {
	ArticleRepresentation,
	AttachmentMap,
	Base64ResourceMap,
	currentErpInterfaceVersion,
	GraphRepBendLine,
	GraphRepresentation,
	LegacyCosts,
	NodeRepresentation,
	ProcessRep,
	ProcessRepContent,
	ProcessRepContentAutomaticMechanicalDeburring,
	ProcessRepContentDieBending,
	ProcessRepContentLaserSheetCutting,
	ProcessRepContentManualMechanicalDeburring,
	ProcessRepContentSheet,
	ProcessRepContentSheetTapping,
	ProcessRepContentTube,
	ProcessRepContentTubeCutting,
	ProcessRepContentUserDefinedCountersinking,
	ProcessRepContentUserDefinedThreading,
	Resources,
	ScaleDataEntry,
	SubGraphResourceEntry,
	TextResourceMap,
	WorkStep,
	WorkStepContent,
} from "qrc:/js/lib/erp_interface";
import {
	tubeCuttingLayerPaths,
} from "qrc:/js/lib/tubecutting_util";

import {
	isMinExportReady,
} from "qrc:/js/lib/manufacturing_state_util";
import {
	defaultSceneForVertex,
} from "qrc:/js/lib/scene_utils";

import {
	back,
	front,
} from "qrc:/js/lib/array_util";
import {
	articleUserDatumImpl,
} from "qrc:/js/lib/article_userdata_config";
import {
	computeArticleSheetConsumption,
	computeNodeCosts,
	computeRecursiveArticleScaleSellingPrice,
	computeRecursiveArticleScaleCosts,
	computeScaleValues,
	erpExportOnlyComputeSellingPriceInclGlobalSurcharges,
	computeManufacturingPriceExclSurcharges,
} from "./export_calc_costs";
import {
	createCalcCache,
	CalcCache,
} from "./export_calc_cache";
import {
	computeAutomaticMechanicalDeburringLength,
	computeNodeTimes,
} from "./export_calc_times";
import {
	createBendDrawing,
} from "./export_bend_drawing";

function computeLegacyCosts(vertex: Vertex, calcCache: CalcCache): LegacyCosts|undefined {
	const costs = (() => {
		if (hasManufacturingCostOverride(vertex)) {
			return undefined;
		} else {
			return computeNodeCosts(vertex, undefined, calcCache);
		}
	})();
	const manufacturingPrice = computeManufacturingPriceExclSurcharges([ costs ]);
	const sellingPrice = erpExportOnlyComputeSellingPriceInclGlobalSurcharges(costs);
	if (costs === undefined || manufacturingPrice === undefined || sellingPrice === undefined) {
		return undefined;
	}
	const fixedMultiplicity = (() => {
		switch (wsi4.node.workStepType(vertex)) {
			case WorkStepType.sheet:
			case WorkStepType.tube:
			case WorkStepType.packaging: return 1;
			case WorkStepType.joining:
			case WorkStepType.sheetCutting:
			case WorkStepType.sheetBending:
			case WorkStepType.userDefined:
			case WorkStepType.userDefinedBase:
			case WorkStepType.tubeCutting:
			case WorkStepType.transform:
			case WorkStepType.undefined: return wsi4.node.multiplicity(vertex);
		}
	})();
	return {
		material: costs.material / fixedMultiplicity,
		setup: costs.setup,
		unit: costs.unit / fixedMultiplicity,
		manufacturing: manufacturingPrice,
		selling: sellingPrice,
	};
}

function computeProcessRepContentSheet(vertex: Vertex): ProcessRepContentSheet {
	assertDebug(() => wsi4.node.workStepType(vertex) === WorkStepType.sheet, "Pre-condition violated: Expecting WST sheet");

	if (wsi4.node.twoDimRep(vertex) === undefined) {
		// Valid case - nesting failed
		const targetWithThickness = wsi4.graph.reachable(vertex)
			.find(v => wsi4.node.sheetThickness(v) !== undefined);
		assert(targetWithThickness !== undefined, "Expecting target with valid sheet thickness");

		const thickness = wsi4.node.sheetThickness(targetWithThickness);
		assert(thickness !== undefined, "Expecting valid sheet thickness");

		return {
			sheetIds: [],
			consumptions: [],
			sheetThickness: thickness,
			dimX: undefined,
			dimY: undefined,
			testReportRequired: undefined,
		};
	} else {
		// If there is a nesting then there also should be a valid sheet at this point.
		// Not 100% sure if this this shouldn't be a constraint for the entire graph export.
		// Currently, this the graph export is part of various tests in contexts where the
		// graph is *not* export ready so keeping this here for now.
		assertDebug(() => isMinExportReady(vertex), "Pre-condition violated; valid sheet / nesting / sheet consumption is required");

		const testReportRequired: boolean | undefined = (() => {
			if (getSettingOrDefault("sheetTestReportEnabled")) {
				return isTestReportRequiredForSheet(vertex);
			} else {
				return undefined;
			}
		})();

		const sheetTable = getTable(TableType.sheet);

		const sheetId = sheetIdForVertex(vertex, sheetTable);
		assert(sheetId !== undefined, "Expecting valid sheet id");

		const sheet = sheetTable.find(row => row.identifier === sheetId);
		assert(sheet !== undefined, "Expecting valid sheet");

		const thickness = wsi4.node.sheetThickness(vertex);
		assert(thickness !== undefined, "Expecting valid sheet thickness");

		const sheetConsumption = computeSheetConsumption(vertex);
		assert(sheetConsumption !== undefined, "Expecting valid sheet consumption");

		return {
			sheetIds: [ sheetId ],
			consumptions: [ sheetConsumption ],
			sheetThickness: thickness,
			dimX: sheet.dimX,
			dimY: sheet.dimY,
			testReportRequired: testReportRequired,
		};
	}
}

function computeProcessRepContentLaserSheetCutting(lscVertex: Vertex): ProcessRepContentLaserSheetCutting {
	const twoDimRep = wsi4.node.twoDimRep(lscVertex);
	if (twoDimRep === undefined) {
		return wsi4.throwError("Expecting twoDimRep for vertex of WorkStepType sheetCutting");
	}
	const sheetThickness = wsi4.node.sheetThickness(lscVertex);
	if (sheetThickness === undefined) {
		return wsi4.throwError("Expecting sheetThickness for vertex of WorkStepType sheetCutting");
	}
	const boundingBox = wsi4.cam.util.boundingBox2(twoDimRep);
	const sheetVertex = wsi4.graph.reaching(lscVertex)
		.find(vertex => wsi4.node.workStepType(vertex) === WorkStepType.sheet);
	return {
		contourLength: computeContourLength(twoDimRep),
		contourCount: computeContourCount(twoDimRep),
		boundingBox: boundingBox,
		sheetThickness: sheetThickness,
		fixedRotations: isCompatibleToNodeUserDataEntry("fixedRotations", lscVertex) ? nodeUserDatumOrDefault("fixedRotations", lscVertex) : undefined,
		cuttingGasId: nodeUserDatum("laserSheetCuttingGasId", lscVertex) ?? "",
		sheetId: sheetVertex === undefined ? undefined : sheetIdForVertex(sheetVertex),
		sheetConsumption: computeArticleSheetConsumption(wsi4.graph.article(lscVertex)),
	};
}

function computeProcessRepContentAutomaticMechanicalDeburring(vertex: Vertex): ProcessRepContentAutomaticMechanicalDeburring {
	assumeGraphAxioms([ "deburringSubProcessNodeHasTwoDimRep" ]);
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	assert(twoDimRep !== undefined, "Expecting twoDimRep");
	const material = getAssociatedSheetMaterialId(vertex);
	return {
		netLength: material === undefined ? 0 : computeAutomaticMechanicalDeburringLength(twoDimRep, wsi4.node.multiplicity(vertex), material),
		doubleSided: nodeUserDatumOrDefault("deburrDoubleSided", vertex) ?? false,
	};
}

function computeProcessRepContentManualMechanicalDeburring(vertex: Vertex): ProcessRepContentManualMechanicalDeburring {
	return {
		doubleSided: nodeUserDatumOrDefault("deburrDoubleSided", vertex) ?? false,
	};
}

function computeProcessRepContentDieBending(vertex: Vertex): ProcessRepContentDieBending {
	const sheetThickness = wsi4.node.sheetThickness(vertex);
	if (sheetThickness === undefined) {
		return wsi4.throwError("Sheet thickness not available");
	}
	const bendLineData = (wsi4.node.computeBendLineData(vertex) ?? [])
		.map((bld): GraphRepBendLine => ({
			bendDescriptor: bld.bendDescriptor,
			constructedInnerRadius: bld.constructedInnerRadius,
			resultingInnerRadius: bld.resultingInnerRadius,
			innerRadius: bld.constructedInnerRadius,
			bendAngle: bld.bendAngle,
			segments: bld.segments,
		}));
	if (bendLineData === undefined) {
		return wsi4.throwError("Cannot compute bend line data");
	}
	return {
		bendLineData: bendLineData,
		sheetThickness: sheetThickness,
	};
}

function computeProcessRepSheetTapping(vertex: Vertex): ProcessRepContentSheetTapping {
	assertDebug(() => wsi4.node.processType(vertex) === ProcessType.sheetTapping, "Pre-condition violated");

	const twoDimRepTransformation = wsi4.node.twoDimRepTransformation(vertex);
	assert(twoDimRepTransformation !== undefined, "Expecting valid transformation");

	return {
		entries: nodeUserDatumOrDefault("sheetTappingData", vertex)
			.map(entry => ({
				center2: cadFeatureTwoDimCentroid(vertex, entry.cadFeature),
				screwThread: entry.screwThread,
			})),
	};
}

function computeProcessRepTubeCutting(vertex: Vertex): ProcessRepContentTubeCutting {
	const layered = wsi4.node.layered(vertex);
	assert(layered !== undefined, "Expecting valid layered for node of type tubeCutting");

	const cuttingPaths = [
		...tubeCuttingLayerPaths("cuttingOuterContour", layered),
		...tubeCuttingLayerPaths("cuttingInnerContour", layered),
	];

	const contourCount = cuttingPaths?.length;
	const contourLength = cuttingPaths?.reduce((acc, path) => acc + wsi4.geo.util.pathLength(path), 0.);
	const extrusionLength = wsi4.node.profileExtrusionLength(vertex);

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

	const tube = tubeForVertex(vertex);
	const tubeConsumption = tube === undefined ? undefined : grossTubeConsumptionForVertex(vertex, tube);

	return {
		contourLength: contourLength,
		contourCount: contourCount,
		profileGeometry: profileGeometry,
		profileExtrusionLength: extrusionLength,
		tubeId: tube === undefined ? undefined : tube.identifier,
		tubeConsumption: tubeConsumption,
	};
}

function computeProcessRepTube(tubeVertex: Vertex): ProcessRepContentTube {
	const tubeCuttingVertex = wsi4.graph.reachable(tubeVertex)
		.find(vertex => wsi4.node.workStepType(vertex) === WorkStepType.tubeCutting);
	assert(tubeCuttingVertex !== undefined, "Expecting reachable tubeCutting node");

	const tube = tubeForVertex(tubeCuttingVertex);
	const consumption = tube === undefined ? undefined : grossTubeConsumptionForVertex(tubeCuttingVertex, tube);

	return {
		tubeId: tube?.identifier,
		consumption: consumption,
	};
}

function computeWorkStepContent(vertex: Vertex): WorkStepContent {
	const workStepType = wsi4.node.workStepType(vertex);
	switch (workStepType) {
		case WorkStepType.sheet: return computeProcessRepContentSheet(vertex);
		case WorkStepType.sheetCutting: return computeProcessRepContentLaserSheetCutting(vertex);
		case WorkStepType.sheetBending: return computeProcessRepContentDieBending(vertex);
		case WorkStepType.tubeCutting:
		case WorkStepType.tube:
		case WorkStepType.joining:
		case WorkStepType.packaging:
		case WorkStepType.transform:
		case WorkStepType.userDefined:
		case WorkStepType.userDefinedBase: return {};
		case WorkStepType.undefined: return wsi4.throwError("Unexpected WorkStepType: " + workStepType);
	}
}

function computeWorkStep(vertex: Vertex): WorkStep {
	const workStepType = wsi4.node.workStepType(vertex);
	return {
		type: workStepType,
		content: computeWorkStepContent(vertex),
	};
}

function computeProcessRepContentUserDefinedThreading(vertex: Vertex): ProcessRepContentUserDefinedThreading {
	return {
		numThreads: nodeUserDatumOrDefault("numThreads", vertex),
	};
}

function computeProcessRepContentUserDefinedCountersinking(vertex: Vertex): ProcessRepContentUserDefinedCountersinking {
	return {
		numCountersinks: nodeUserDatumOrDefault("numCountersinks", vertex),
	};
}

function computeProcessRepContent(vertex: Vertex): ProcessRepContent {
	switch (wsi4.node.processType(vertex)) {
		case ProcessType.automaticMechanicalDeburring: return computeProcessRepContentAutomaticMechanicalDeburring(vertex);
		case ProcessType.manualMechanicalDeburring: return computeProcessRepContentManualMechanicalDeburring(vertex);
		case ProcessType.dieBending: return computeProcessRepContentDieBending(vertex);
		case ProcessType.laserSheetCutting: return computeProcessRepContentLaserSheetCutting(vertex);
		case ProcessType.sheet: return computeProcessRepContentSheet(vertex);
		case ProcessType.userDefinedCountersinking: return computeProcessRepContentUserDefinedCountersinking(vertex);
		case ProcessType.userDefinedThreading: return computeProcessRepContentUserDefinedThreading(vertex);
		case ProcessType.sheetTapping: return computeProcessRepSheetTapping(vertex);
		case ProcessType.tubeCutting: return computeProcessRepTubeCutting(vertex);
		case ProcessType.tube: return computeProcessRepTube(vertex);
		case ProcessType.arcWelding:
		case ProcessType.assembling:
		case ProcessType.autogenousWelding:
		case ProcessType.bendForming:
		case ProcessType.bendingWithoutTool:
		case ProcessType.bonding:
		case ProcessType.cleaning:
		case ProcessType.coating:
		case ProcessType.cutting:
		case ProcessType.drilling:
		case ProcessType.externalPart:
		case ProcessType.forceFitting:
		case ProcessType.forming:
		case ProcessType.gasShieldedWelding:
		case ProcessType.joining:
		case ProcessType.joiningByBrazing:
		case ProcessType.joiningByWelding:
		case ProcessType.laserWelding:
		case ProcessType.machining:
		case ProcessType.magWelding:
		case ProcessType.manufacturing:
		case ProcessType.mechanicalDeburring:
		case ProcessType.migWelding:
		case ProcessType.milling:
		case ProcessType.packaging:
		case ProcessType.plasmaSheetCutting:
		case ProcessType.plasmaWelding:
		case ProcessType.powderCoating:
		case ProcessType.removalOperation:
		case ProcessType.resistanceWelding:
		case ProcessType.semiManufactured:
		case ProcessType.sheetBending:
		case ProcessType.sheetCutting:
		case ProcessType.sheetMetalFolding:
		case ProcessType.sprayPainting:
		case ProcessType.studWelding:
		case ProcessType.threading:
		case ProcessType.tigWelding:
		case ProcessType.transport:
		case ProcessType.turning:
		case ProcessType.undefined:
		case ProcessType.userDefinedBaseType:
		case ProcessType.userDefinedMachining:
		case ProcessType.waterJetSheetCutting:
		case ProcessType.slideGrinding:
		case ProcessType.userDefinedTube:
		case ProcessType.weldingWithPressure: return undefined;
	}
}

function computeProcessRep(vertex: Vertex): ProcessRep {
	return {
		type: wsi4.node.processType(vertex),
		content: computeProcessRepContent(vertex),
	};
}

function nodeTimesIfDefined(vertex: Vertex, calcCache: CalcCache): Times|undefined {
	if (hasManufacturingCostOverride(vertex)) {
		return undefined;
	} else {
		return computeNodeTimes(vertex, undefined, calcCache);
	}
}

function userDefinedScalePricesIfDefined(vertex: Vertex): UserDefinedScalePrice[] {
	if (hasManufacturingCostOverride(vertex)) {
		const values = nodeUserDatumOrDefault("userDefinedScalePrices", vertex);
		assertDebug(() => values.length > 0, "Expecting at least one user define scale price");
		return values;
	} else {
		return [];
	}
}

function costCenterForProcess(processId: string, processTable: readonly Readonly<Process>[]): string {
	const process = processTable.find(row => row.identifier === processId);
	return process === undefined ? "" : process.costCenter;
}

function computeNodeRepresentation(vertex: Vertex, calcCache: CalcCache, processTable: readonly Readonly<Process>[]): NodeRepresentation {
	const processId = wsi4.node.processId(vertex);
	return {
		processId: processId,
		costCenter: costCenterForProcess(processId, processTable),
		times: nodeTimesIfDefined(vertex, calcCache),
		userDefinedScalePrices: userDefinedScalePricesIfDefined(vertex),
		costs: computeLegacyCosts(vertex, calcCache),
		vertexKey: wsi4.util.toKey(vertex),
		sourceVertexKeys: wsi4.graph.sources(vertex)
			.map(source => wsi4.util.toKey(source)),
		targetVertexKeys: wsi4.graph.targets(vertex)
			.map(target => wsi4.util.toKey(target)),
		sourceMultiplicities: wsi4.graph.sources(vertex)
			.map(source => ({
				vertexKey: wsi4.util.toKey(source),
				multiplicity: wsi4.graph.sourceMultiplicity(source, vertex),
			})),
		importId: isCompatibleToNodeUserDataEntry("importId", vertex) ? nodeUserDatum("importId", vertex) : undefined,
		workStep: computeWorkStep(vertex),
		processRep: computeProcessRep(vertex),
		mass: computeNodeMass(vertex),
		comment: isCompatibleToNodeUserDataEntry("comment", vertex) ? nodeUserDatumOrDefault("comment", vertex) : "",
	};
}

function computeNodeRepresentations(calcCache: CalcCache) {
	const processTable = getTable(TableType.process);
	return wsi4.graph.vertices()
		.map(vertex => computeNodeRepresentation(vertex, calcCache, processTable));
}

export interface ScaleDataConfigRelativeValues {
	type: "relativeValues";
	values: number[];
}

export interface ScaleDataConfigAbsoluteValues {
	type: "absoluteValues";
	values: number[];
}

export interface ScaleDataConfigActualMultiplicity {
	type: "actualMultiplicity";
}

export interface ScaleDataConfigNone {
	type: "none";
}

export type ScaleDataConfig = ScaleDataConfigNone
| ScaleDataConfigActualMultiplicity
| ScaleDataConfigRelativeValues
| ScaleDataConfigAbsoluteValues;

function computeScaleData(article: readonly Vertex[], scaleDataConfig: ScaleDataConfig, calcCache: CalcCache): ScaleDataEntry[] {
	assertDebug(
		() => !isSemimanufacturedArticle(article),
		"Pre-condition violated.  Expecting non-sheet-articles only",
	);

	const scaleValues = ((): number[] => {
		assert(isComponentArticle(article) || isJoiningArticle(article), "Expecting component-article or joining-article");
		switch (scaleDataConfig.type) {
			case "none": {
				return [];
			}
			case "actualMultiplicity": {
				return [ wsi4.node.multiplicity(back(article)) ];
			}
			case "relativeValues": {
				return computeScaleValues(
					wsi4.node.multiplicity(back(article)),
					{
						scaleValueMode: "relative",
						baseScaleValues: scaleDataConfig.values,
					},
				);
			}
			case "absoluteValues": {
				return computeScaleValues(
					wsi4.node.multiplicity(back(article)),
					{
						scaleValueMode: "absolute",
						baseScaleValues: scaleDataConfig.values,
					},
				);
			}
		}
	})();

	const result: ScaleDataEntry[] = [];
	scaleValues.forEach(scaleValue => {
		const articleScaleCosts = (() => {
			const subGraph = [
				...article,
				...wsi4.graph.reaching(front(article)),
			];
			if (subGraph.some(vertex => hasManufacturingCostOverride(vertex))) {
				return undefined;
			} else {
				assertDebug(() => !isSemimanufacturedArticle(article), "Expecting *no* sheet article");
				return computeRecursiveArticleScaleCosts(article, scaleValue, calcCache);
			}
		})();
		const sellingPrice = computeRecursiveArticleScaleSellingPrice(
			article,
			scaleValue,
			calcCache,
		);
		if (sellingPrice !== undefined) {
			result.push({
				scaleValue: scaleValue,
				scaleCosts: articleScaleCosts,
				manufacturingCosts: computeManufacturingPriceExclSurcharges(articleScaleCosts),
				sellingPrice: sellingPrice,
			});
		}
	});

	return result;
}

function computeArticleRepresentation(article: Array<Vertex>, scaleDataConfig: ScaleDataConfig, calcCache: CalcCache): ArticleRepresentation {
	const materials = collectNodeUserDataEntries("sheetMaterialId", front(article), userDataAccess.article)
		.filter((lhs, index, self) => lhs !== undefined && self.findIndex(rhs => rhs !== undefined && lhs === rhs) === index);

	const scaleData = (() => {
		if (isSemimanufacturedArticle(article)) {
			return [];
		} else {
			return computeScaleData(
				article,
				scaleDataConfig,
				calcCache,
			);
		}
	})();
	const articleUserData = wsi4.node.articleUserData(front(article));
	return {
		vertexKeys: article.map(vertex => wsi4.util.toKey(vertex)),
		name: articleUserDatumImpl("name", articleUserData) ?? "",
		externalPartNumber: articleUserDatumImpl("externalPartNumber", articleUserData) ?? "",
		externalDrawingNumber: articleUserDatumImpl("externalDrawingNumber", articleUserData) ?? "",
		externalRevisionNumber: articleUserDatumImpl("externalRevisionNumber", articleUserData) ?? "",
		comment: articleUserDatumImpl("comment", articleUserData) ?? "",
		multiplicity: wsi4.node.multiplicity(back(article)),
		sheetMaterialId: materials.length === 1 ? materials[0]! : undefined,
		globalMaterialId: materials.length === 1 ? materials[0]! : undefined,
		scaleData: scaleData,
	};
}

function computeArticleRepresentations(scaleDataConfig: ScaleDataConfig, scaleCostsCacheInput?: CalcCache) {
	const calcCache = scaleCostsCacheInput ?? createCalcCache();
	return wsi4.graph.articles()
		.map(vertices => computeArticleRepresentation(vertices, scaleDataConfig, calcCache));
}

function createBase64ResourceMapImpl(createContentOpt: (vertex: Vertex) => ArrayBuffer | undefined, compressionEnabled: boolean) {
	type VertexAndContent = { vertex: Vertex; content : ArrayBuffer; };
	return wsi4.graph.vertices()
		.map(vertex => ({vertex: vertex,
			content: createContentOpt(vertex)}))
		.filter((vac): vac is VertexAndContent => vac.content !== undefined)
		.reduce((acc: Base64ResourceMap, vac) => {
			const content = compressionEnabled ? wsi4.util.compress(vac.content) : vac.content;
			acc[wsi4.util.toKey(vac.vertex)] = wsi4.util.arrayBufferToString(wsi4.util.toBase64(content));
			return acc;
		}, {});
}

function isResourceEnabled<Key extends keyof GraphRepResourcesConfig>(key: Key, settings: readonly Readonly<Setting>[]): boolean {
	const config = getSettingGraphRepResourcesConfig(settings);
	return config[key];
}

type TextFileResources = Pick<GraphRepResourcesConfig,
"dxfs"
| "dxfsCompressed"
| "geos"
| "geosCompressed"
| "svgs"
| "svgsCompressed"
| "inputSteps"
| "inputStepsCompressed"
| "outputSteps"
| "outputStepsCompressed"
>;

const textFileResourceCompressionEnabledMap: Readonly<{[index in keyof TextFileResources]: boolean}> = {
	dxfs: false,
	dxfsCompressed: true,
	geos: false,
	geosCompressed: true,
	svgs: false,
	svgsCompressed: true,
	inputSteps: false,
	inputStepsCompressed: true,
	outputSteps: false,
	outputStepsCompressed: true,
};

function createDxf(vertex: Vertex): ArrayBuffer|undefined {
	if (wsi4.node.twoDimRep(vertex) === undefined) {
		return undefined;
	} else {
		return wsi4.geo.util.renderScene(defaultSceneForVertex(vertex), FileType.dxf, {});
	}
}

function createGeo(vertex: Vertex): ArrayBuffer|undefined {
	if (wsi4.node.twoDimRep(vertex) === undefined) {
		return undefined;
	} else {
		return wsi4.geo.util.renderScene(defaultSceneForVertex(vertex), FileType.geo, {});
	}
}

function createSvg(vertex: Vertex): ArrayBuffer|undefined {
	if (wsi4.node.twoDimRep(vertex) === undefined) {
		return undefined;
	} else {
		return wsi4.geo.util.renderScene(defaultSceneForVertex(vertex), FileType.svg, {});
	}
}

function createStep(assembly: Assembly | undefined): ArrayBuffer | undefined {
	return assembly === undefined ? undefined : wsi4.geo.assembly.createStep(assembly);
}

const textFileResourceContentComputationMap: Readonly<{[index in keyof TextFileResources]: (vertex: Vertex) => ArrayBuffer | undefined }> = {
	dxfs: createDxf,
	dxfsCompressed: createDxf,
	geos: createGeo,
	geosCompressed: createGeo,
	svgs: createSvg,
	svgsCompressed: createSvg,
	inputSteps: vertex => createStep(wsi4.node.inputAssembly(vertex)),
	inputStepsCompressed: vertex => createStep(wsi4.node.inputAssembly(vertex)),
	outputSteps: vertex => createStep(wsi4.node.assembly(vertex)),
	outputStepsCompressed: vertex => createStep(wsi4.node.assembly(vertex)),
};

function computeTextFileResourceMap<Key extends keyof TextFileResources>(key: Key, settingsTable: readonly Readonly<Setting>[]): Base64ResourceMap {
	if (isResourceEnabled(key, settingsTable)) {
		return createBase64ResourceMapImpl(
			textFileResourceContentComputationMap[key],
			textFileResourceCompressionEnabledMap[key],
		);
	} else {
		return {};
	}
}

function computeAttachmentResources(): AttachmentMap {
	const settings = getTable(TableType.setting);
	if (isResourceEnabled("attachments", settings)) {
		return wsi4.graph.vertices()
			.filter(v => isCompatibleToNodeUserDataEntry("attachments", v))
			.reduce((acc: AttachmentMap, vertex) => {
				const attachments = nodeUserDatumOrDefault("attachments", vertex);
				if (attachments.length === 0) {
					return acc;
				} else {
					acc[wsi4.util.toKey(vertex)] = attachments;
					return acc;
				}
			}, {});
	} else {
		return {};
	}
}

function computePngResources(pngFutures: readonly Readonly<VertexAnd<ArrayBufferFuture>>[]): Base64ResourceMap {
	assertDebug(
		() => isResourceEnabled("pngs", getTable(TableType.setting)) || (pngFutures.length === 0),
		"Expecting empty futures in case PNGs are disabled",
	);
	return createBase64ResourceMapImpl((vertex: Vertex) => {
		const obj = pngFutures.find(vertexAndFuture => isEqual(vertexAndFuture.vertex, vertex));
		return obj === undefined ? undefined : getFutureResult<"arrayBuffer">(obj.data);
	}, false);
}

function computeUndirectedConnectedComponentsResources(): SubGraphResourceEntry[] {
	if (!isResourceEnabled("subGraphs", getTable(TableType.setting))) {
		return [];
	}

	return wsi4.graph.undirectedConnectedComponents()
		.map(vertices => {
			const buffer = serializeUndirectedConnectedComponent(front(vertices));
			const base64 = wsi4.util.toBase64(buffer);
			return {
				vertexKeys: vertices.map(v => wsi4.util.toKey(v)),
				dataBase64: wsi4.util.arrayBufferToString(base64),
			};
		});
}

function computeBendDrawingTextResourceMap(settingsTable: readonly Readonly<Setting>[]): TextResourceMap {
	if (!isResourceEnabled("bendDrawingHtmls", settingsTable) || !wsi4.isFeatureEnabled(Feature.bendMeasurementScene)) {
		return {};
	}

	const result: TextResourceMap = {};
	wsi4.graph.vertices()
		.filter(vertex => wsi4.node.workStepType(vertex) === WorkStepType.sheetBending)
		.forEach(vertex => {
			const content = createBendDrawing(vertex);
			const html = wsi4.documentCreator.renderIntoHtml(content, { orientation: "landscape" });
			result[wsi4.util.toKey(vertex)] = html;
		});

	return result;
}

function computeResources(pngFutures: readonly Readonly<VertexAnd<ArrayBufferFuture>>[]): Resources {
	const settingsTable = getTable(TableType.setting);
	return {
		bendDrawingHtmls: computeBendDrawingTextResourceMap(settingsTable),
		dxfs: computeTextFileResourceMap("dxfs", settingsTable),
		dxfsCompressed: computeTextFileResourceMap("dxfsCompressed", settingsTable),
		geos: computeTextFileResourceMap("geos", settingsTable),
		geosCompressed: computeTextFileResourceMap("geosCompressed", settingsTable),
		inputSteps: computeTextFileResourceMap("inputSteps", settingsTable),
		inputStepsCompressed: computeTextFileResourceMap("inputStepsCompressed", settingsTable),
		outputSteps: computeTextFileResourceMap("outputSteps", settingsTable),
		outputStepsCompressed: computeTextFileResourceMap("outputStepsCompressed", settingsTable),
		attachments: computeAttachmentResources(),
		pngs: computePngResources(pngFutures),
		svgs: computeTextFileResourceMap("svgs", settingsTable),
		svgsCompressed: computeTextFileResourceMap("svgsCompressed", settingsTable),
		subGraphs: computeUndirectedConnectedComponentsResources(),
	};
}

function projectId(): string|undefined {
	const config = getScriptArg("projectConfig");
	return config === undefined ? undefined : config.id;
}

const defaultScaleDataConfig: ScaleDataConfigNone = {
	type: "none",
};

/**
 * Create graph representation for current graph
 *
 * Assumption:  Underlying nodes are export ready
 */
function createGraphRepLatestVersion(scaleDataConfig: ScaleDataConfig = defaultScaleDataConfig, pngFutures?: readonly Readonly<VertexAnd<ArrayBufferFuture>>[], calcCache?: CalcCache): GraphRepresentation {
	pngFutures = (() => {
		if (isResourceEnabled("pngs", getTable(TableType.setting))) {
			return pngFutures ?? createPngFutures(wsi4.graph.vertices());
		} else {
			return [];
		}
	})();
	calcCache = calcCache ?? createCalcCache();
	return {
		creator: wsi4.util.programInfo(),
		erpInterfaceVersion: currentErpInterfaceVersion,
		projectName: projectName(),
		projectId: projectId(),
		nodes: computeNodeRepresentations(calcCache),
		articles: computeArticleRepresentations(scaleDataConfig, calcCache),
		resources: computeResources(pngFutures),
	};
}

function applyDeprecatedSheetArticleSemantics(graphRep: GraphRepresentation): GraphRepresentation {
	assert(
		graphRep.erpInterfaceVersion === "v1",
		"Pre-condition violated: Expecting graph rep with version v1",
	);

	const sheetArticlesToReplace: ArticleRepresentation[] = [];
	const articlesToKeep: ArticleRepresentation[] = [];
	graphRep.articles.forEach(articleRep => {
		if (articleRep.vertexKeys.length !== 1) {
			articlesToKeep.push(articleRep);
			return;
		}

		const nodeRep = graphRep.nodes.find(nodeRep => nodeRep.vertexKey === articleRep.vertexKeys[0]);
		assert(nodeRep !== undefined, "Expecting valid node");
		if (nodeRep.processRep.type === ProcessType.sheet && nodeRep.targetVertexKeys.length === 1) {
			sheetArticlesToReplace.push(articleRep);
		} else {
			articlesToKeep.push(articleRep);
		}
	});
	assert(
		sheetArticlesToReplace.length + articlesToKeep.length === graphRep.articles.length,
		"Article rep container lengths inconsistent",
	);

	sheetArticlesToReplace.forEach(sheetArticleRep => {
		const sheetNodeRep = graphRep.nodes.find(nodeRep => nodeRep.vertexKey === sheetArticleRep.vertexKeys[0]);
		assert(sheetNodeRep !== undefined, "Expecting valid sheet node");

		const targetArticleRep = articlesToKeep.find(articleRep => articleRep.vertexKeys[0] === sheetNodeRep.targetVertexKeys[0]);
		assert(targetArticleRep !== undefined, "Expecting valid target node of sheet node");

		targetArticleRep.vertexKeys = [
			sheetNodeRep.vertexKey,
			...targetArticleRep.vertexKeys,
		];
	});

	graphRep.articles = articlesToKeep;
	graphRep.erpInterfaceVersion = "v0";
	return graphRep;
}

export function createGraphRepresentation(scaleDataConfig: ScaleDataConfig = defaultScaleDataConfig, pngFutures?: readonly Readonly<VertexAnd<ArrayBufferFuture>>[], calcCache?: CalcCache): GraphRepresentation {
	const graphRep = createGraphRepLatestVersion(scaleDataConfig, pngFutures, calcCache);
	switch (getSettingOrDefault("erpInterfaceVersion") ?? currentErpInterfaceVersion) {
		case "v0": return applyDeprecatedSheetArticleSemantics(graphRep);
		case "v1": return graphRep;
	}
}
