import {
	TableType,
} from "qrc:/js/lib/generated/enum";
import {
	isAutomaticMechanicalDeburringMaterial,
	isAutomaticMechanicalDeburringParameters,
	isBendDeduction,
	isBendLineConstraint,
	isSheetBendingMaterialMapping,
	isBendRateParameters,
	isBendTime,
	isBendTimeParameters,
	isDieGroupPriority,
	isDimensionConstraints,
	isSheetMaterial,
	isSheetMaterialScrapValue,
	isLaserSheetCuttingGas,
	isSheetCuttingMaterialMapping,
	isLaserSheetCuttingMaxThickness,
	isLaserSheetCuttingMinArea,
	isLaserSheetCuttingPierceTime,
	isLaserSheetCuttingRate,
	isLaserSheetCuttingSpeed,
	isLowerDie,
	isLowerDieGroup,
	isLowerDieUnit,
	isSheetMaterialDensity,
	isPackaging,
	isProcess,
	isProcessHandlingTime,
	isProcessIdlePeriod,
	isProcessRate,
	isProcessSetupTimeFallback,
	isProcessUnitTimeFallback,
	isScrewThread,
	isSetting,
	isSheet,
	isSheetBendingMaterial,
	isSheetCuttingMaterial,
	isSheetModulus,
	isSheetPrice,
	isSheetPriority,
	isSheetStock,
	isStringIndexedInterface,
	isSurcharge,
	isTappingTimeParameters,
	isTransportationCosts,
	isTube,
	isTubeMaterial,
	isTubeMaterialDensity,
	isTubeProfile,
	isTubeProfileGeometryCircular,
	isTubeProfileGeometryRectangular,
	isTubeProfileGeometryType,
	isTubeSpecification,
	isUpperDie,
	isUpperDieGroup,
	isUpperDieUnit,
	isTubeCuttingSpeed,
	isTubeCuttingPierceTime,
	isTubeCuttingProcess,
	isTubeCuttingProcessMapping,
	isTubePrice,
	isTubeStock,
	isTubeMaterialScrapValue,
} from "qrc:/js/lib/generated/typeguard";

import {
	tolerances,
} from "./constants";
import {
	assert,
	bbDimensionX,
	bbDimensionY,
	getRowForThreshold,
	isArray,
	parseJson,
} from "./utils";

/*
 * Functions working on tables
 */

/**
 * This interface is not intended to be instantiated.
 * This interface can be considered a map TableType -> associated interface
 */
export interface TableTypeMap {
	sheetMaterial: SheetMaterial;
	sheetMaterialDensity: SheetMaterialDensity;
	sheetCuttingMaterialMapping: SheetCuttingMaterialMapping;
	sheetBendingMaterialMapping: SheetBendingMaterialMapping;
	bendTime: BendTime;
	bendTimeParameters: BendTimeParameters;
	bendRateParameters: BendRateParameters;
	bendLineConstraint: BendLineConstraint;
	laserSheetCuttingGas: LaserSheetCuttingGas;
	laserSheetCuttingSpeed: LaserSheetCuttingSpeed;
	laserSheetCuttingPierceTime: LaserSheetCuttingPierceTime;
	laserSheetCuttingRate: LaserSheetCuttingRate;
	laserSheetCuttingMinArea: LaserSheetCuttingMinArea;
	laserSheetCuttingMaxThickness: LaserSheetCuttingMaxThickness;
	packaging: Packaging;
	transportationCosts: TransportationCosts;
	surcharge: Surcharge;
	process: Process;
	processRate: ProcessRate;
	processSetupTimeFallback: ProcessSetupTimeFallback;
	processUnitTimeFallback: ProcessUnitTimeFallback;
	sheet: Sheet;
	sheetModulus: SheetModulus;
	sheetPrice: SheetPrice;
	upperDieGroup: UpperDieGroup;
	lowerDieGroup: LowerDieGroup;
	bendDeduction: BendDeduction;
	setting: Setting;
	automaticMechanicalDeburringMaterial: AutomaticMechanicalDeburringMaterial;
	automaticMechanicalDeburringParameters: AutomaticMechanicalDeburringParameters;
	dimensionConstraints: DimensionConstraints;
	screwThread: ScrewThread;
	tappingTimeParameters: TappingTimeParameters;
	tubeMaterial: TubeMaterial;
	tubeMaterialDensity: TubeMaterialDensity;
	tubeProfile: TubeProfile;
	tubeSpecification: TubeSpecification;
	tube: Tube;
	upperDie: UpperDie;
	lowerDie: LowerDie;
	upperDieUnit: UpperDieUnit;
	lowerDieUnit: LowerDieUnit;
	processHandlingTime: ProcessHandlingTime;
	sheetStock: SheetStock;
	processIdlePeriod: ProcessIdlePeriod;
	sheetMaterialScrapValue: SheetMaterialScrapValue;
	sheetPriority: SheetPriority;
	dieGroupPriority: DieGroupPriority;
	sheetCuttingMaterial: SheetCuttingMaterial;
	sheetBendingMaterial: SheetBendingMaterial;
	tubeCuttingProcess: TubeCuttingProcess;
	tubeCuttingProcessMapping: TubeCuttingProcessMapping;
	tubeCuttingSpeed: TubeCuttingSpeed;
	tubeCuttingPierceTime: TubeCuttingPierceTime;
	tubePrice: TubePrice;
	tubeStock: TubeStock;
	tubeMaterialScrapValue: TubeMaterialScrapValue;
}

const tableTypeGuardMap: {[index in keyof TableTypeMap]: (arg: unknown) => arg is TableTypeMap[index]} = {
	sheetMaterial: isSheetMaterial,
	sheetMaterialDensity: isSheetMaterialDensity,
	sheetCuttingMaterialMapping: isSheetCuttingMaterialMapping,
	sheetBendingMaterialMapping: isSheetBendingMaterialMapping,
	bendTime: isBendTime,
	bendTimeParameters: isBendTimeParameters,
	bendRateParameters: isBendRateParameters,
	bendLineConstraint: isBendLineConstraint,
	laserSheetCuttingGas: isLaserSheetCuttingGas,
	laserSheetCuttingSpeed: isLaserSheetCuttingSpeed,
	laserSheetCuttingPierceTime: isLaserSheetCuttingPierceTime,
	laserSheetCuttingRate: isLaserSheetCuttingRate,
	laserSheetCuttingMinArea: isLaserSheetCuttingMinArea,
	laserSheetCuttingMaxThickness: isLaserSheetCuttingMaxThickness,
	packaging: isPackaging,
	transportationCosts: isTransportationCosts,
	surcharge: isSurcharge,
	process: isProcess,
	processRate: isProcessRate,
	processSetupTimeFallback: isProcessSetupTimeFallback,
	processUnitTimeFallback: isProcessUnitTimeFallback,
	sheet: isSheet,
	sheetModulus: isSheetModulus,
	sheetPrice: isSheetPrice,
	upperDieGroup: isUpperDieGroup,
	lowerDieGroup: isLowerDieGroup,
	bendDeduction: isBendDeduction,
	setting: isSetting,
	automaticMechanicalDeburringMaterial: isAutomaticMechanicalDeburringMaterial,
	automaticMechanicalDeburringParameters: isAutomaticMechanicalDeburringParameters,
	dimensionConstraints: isDimensionConstraints,
	screwThread: isScrewThread,
	tappingTimeParameters: isTappingTimeParameters,
	tubeMaterial: isTubeMaterial,
	tubeMaterialDensity: isTubeMaterialDensity,
	tubeProfile: isTubeProfile,
	tubeSpecification: isTubeSpecification,
	tube: isTube,
	upperDie: isUpperDie,
	lowerDie: isLowerDie,
	upperDieUnit: isUpperDieUnit,
	lowerDieUnit: isLowerDieUnit,
	processHandlingTime: isProcessHandlingTime,
	sheetStock: isSheetStock,
	processIdlePeriod: isProcessIdlePeriod,
	sheetMaterialScrapValue: isSheetMaterialScrapValue,
	sheetPriority: isSheetPriority,
	dieGroupPriority: isDieGroupPriority,
	sheetCuttingMaterial: isSheetCuttingMaterial,
	sheetBendingMaterial: isSheetBendingMaterial,
	tubeCuttingProcess: isTubeCuttingProcess,
	tubeCuttingProcessMapping: isTubeCuttingProcessMapping,
	tubeCuttingSpeed: isTubeCuttingSpeed,
	tubeCuttingPierceTime: isTubeCuttingPierceTime,
	tubePrice: isTubePrice,
	tubeStock: isTubeStock,
	tubeMaterialScrapValue: isTubeMaterialScrapValue,
};

export function isTable<Key extends keyof TableTypeMap>(type: Key, arg: unknown): arg is TableTypeMap[Key][] {
	return isArray(arg, (arg: unknown): arg is TableTypeMap[Key] => tableTypeGuardMap[type](arg));
}

export function getTable<Key extends keyof TableTypeMap>(type: Key): readonly Readonly<TableTypeMap[Key]>[] {
	return getMutableTable(type);
}

export function getMutableTable<Key extends keyof TableTypeMap>(type: Key): TableTypeMap[Key][] {
	const table = wsi4.tables.get(type).content;
	return isTable(type, table) ? table : wsi4.throwError("Table invalid for type \"" + type + "\"");
}

export function getDefaultTable<Key extends keyof TableTypeMap>(type: Key): TableTypeMap[Key][] {
	const table = wsi4.tables.getDefault(type).content;
	return isTable(type, table) ? table : wsi4.throwError("Table invalid");
}

export function setInternalTable<Key extends keyof TableTypeMap>(type: Key, table: readonly Readonly<TableTypeMap[Key]>[]): void {
	// Note: cast should be save
	wsi4.tables.setInternalTable({
		type: type,
		content: table as unknown,
	} as AnyTable);
}

export function setExternalTable<Key extends keyof TableTypeMap>(type: Key, table: readonly Readonly<TableTypeMap[Key]>[]): void {
	// Note: cast should be save
	wsi4.tables.setExternalTable({
		type: type,
		content: table as unknown,
	} as AnyTable);
}

/**
 * If there is a default table then set this table
 * If there is no default table then remove table from internal tables
 * If an internal table should be removed and there is no corresponding default table then set this table as external table
 */
export function resetInternalTables(types: TableType[]): void {
	const defaultAndRemoveTypes = types.reduce((acc: {defaultTables: AnyTable[], tablesToRemove: TableType[]}, type) => {
		const defaultTable = wsi4.tables.getDefault(type);
		// Table is empty if there is no default
		if (defaultTable.content.length === 0) {
			acc.tablesToRemove.push(type);
		} else {
			acc.defaultTables.push(defaultTable);
		}
		return acc;
	}, {
		defaultTables: [],
		tablesToRemove: [],
	});
	defaultAndRemoveTypes.tablesToRemove.forEach(wsi4.tables.removeInternalTable);
	defaultAndRemoveTypes.defaultTables.forEach(wsi4.tables.setInternalTable);
}

export function setInternalTables(anyTables: AnyTable[]): void {
	anyTables.forEach(wsi4.tables.setInternalTable);
}

/**
 * Lookup sheet in sheets table
 *
 * @returns identifier of sheet in `sheets` table or undefined, if table missing
 */
export function lookUpSheetId(targetBox: Box2, thickness: number, sheetMaterialId: string, sheetFilter: SheetFilter, table?: readonly Readonly<Sheet>[]): string | undefined {
	table = table === undefined ? getTable(TableType.sheet) : table;
	const sheetDimX = bbDimensionX(targetBox);
	const sheetDimY = bbDimensionY(targetBox);
	return table.find(row => row.dimX === sheetDimX
		&& row.dimY === sheetDimY
		&& Math.abs(row.thickness - thickness) < tolerances.thickness
		&& row.sheetMaterialId === sheetMaterialId
		&& (sheetFilter.ids.length === 0 || sheetFilter.ids.some(id => id === row.identifier)))
		?.identifier;
}

/**
 * Compute laserSheetCuttingSpeed [mm/s] based on thickness, material and cutting gas
 */
export function laserSheetCuttingSpeed(thickness: number, sheetCuttingMaterialId: string, laserSheetCuttingGasId: string, printError = true): number | undefined {
	const table = getTable(TableType.laserSheetCuttingSpeed);
	const filteredCuttingSpeeds = table.filter(element => element.sheetCuttingMaterialId === sheetCuttingMaterialId && element.laserSheetCuttingGasId === laserSheetCuttingGasId);
	filteredCuttingSpeeds.sort((lhs, rhs) => lhs.thickness < rhs.thickness ? -1 : lhs.thickness > rhs.thickness ? 1 : 0);

	const row = getRowForThreshold(filteredCuttingSpeeds, element => element.thickness >= thickness - tolerances.thickness);
	if (row === undefined) {
		if (printError) {
			wsi4.util.error("laserSheetCuttingSpeed(): There is no laser sheet cutting speed for sheetCuttingMaterialId \"" + sheetCuttingMaterialId
				+ "\", laserSheetCuttingGasId \"" + laserSheetCuttingGasId + "\" and thickness " + thickness);
		}
		return undefined;
	}
	if (typeof row.speed !== "number") {
		return wsi4.throwError("laserSheetCuttingSpeed(): Missing property \"speed\"");
	}

	// [m/min]
	const tableValue = row.speed;

	// [mm/s]
	const result = tableValue * 1000 / 60;

	return result;
}

/**
 * Note: There is no table constraint forcing the user to map sheet materials to automatic deburring materials.
 * If there is no matching automatic deburring material this function falls back to the default table.
 *
 * If a material mapping *is* present, then expecting matching parameters though (this is enforced by a constraint).
 */
export function lookUpAutomaticDeburringParams(sheetMaterialId: string): AutomaticMechanicalDeburringParameters {
	const deburringMaterial = getTable(TableType.automaticMechanicalDeburringMaterial)
		.find(row => row.sheetMaterialId === sheetMaterialId);
	const result = (() => {
		if (deburringMaterial === undefined) {
			return getTable(TableType.automaticMechanicalDeburringParameters)[0];
		} else {
			return getTable(TableType.automaticMechanicalDeburringParameters)
				.find(row => row.automaticMechanicalDeburringMaterialId === deburringMaterial.automaticMechanicalDeburringMaterialId);
		}
	})();
	assert(result !== undefined, "Expecting valid automatic mechanical deburring parameters");
	return result;
}

export function lookUpProcessSetupTimeFallBack(processId: string): number | undefined {
	const entry = getTable(TableType.processSetupTimeFallback)
		.find(row => row.processId === processId);
	return entry === undefined ? 0 : entry.time * 60;
}

export function lookUpProcessUnitTimeFallBack(processId: string): number | undefined {
	const entry = getTable(TableType.processUnitTimeFallback)
		.find(row => row.processId === processId);
	return entry === undefined ? 0 : entry.time * 60;
}

/**
 * Compute possible cutting gases for thickness and sheetCuttingMaterial
 *
 * Note: If required data is missing the result is empty
 */
export function computeValidLaserCuttingGasIds(
	thickness: number,
	sheetCuttingMaterialId: string,
	thicknessConstraintTable?: readonly Readonly<LaserSheetCuttingMaxThickness>[],
	laserSheetCuttingSpeedTable?: readonly Readonly<LaserSheetCuttingSpeed>[],
): string[] {
	thicknessConstraintTable = thicknessConstraintTable === undefined ? getTable(TableType.laserSheetCuttingMaxThickness) : thicknessConstraintTable;
	laserSheetCuttingSpeedTable = laserSheetCuttingSpeedTable === undefined ? getTable(TableType.laserSheetCuttingSpeed) : laserSheetCuttingSpeedTable;

	const gasIdDenyList = thicknessConstraintTable
		.filter(row => row.sheetCuttingMaterialId === sheetCuttingMaterialId)
		.filter(row => (row.maxThickness < thickness - tolerances.thickness) || (row.minThickness > thickness + tolerances.thickness))
		.map(row => row.laserSheetCuttingGasId);
	const gasIds = laserSheetCuttingSpeedTable
		.filter(row => row.sheetCuttingMaterialId === sheetCuttingMaterialId)
		.map(row => row.laserSheetCuttingGasId)
		.filter(lhs => gasIdDenyList.every(rhs => lhs !== rhs));
	return [ ...new Set(gasIds) ];
}

/**
 * Note:  Unique members are the same for both BendTimeParameters and BendRateParameters so using one of them for a common unique member type
 */
export type BendParamsUniqueMembers = BendTimeParametersUniqueMembers;

/**
 * Pick a row from BendTimeParameters[] or BendRateParameters[]
 *
 * There are two columns where a threshold is used to pick a row:
 *
 * - thickness
 * - maxBendLineNetLength
 *
 * To follow the principle of least supprise only rows sharing the same thickness are considered when applying the bendLineNetLength threshold.
 */
export function pickBendParameterRow<T extends BendParamsUniqueMembers>(table: readonly Readonly<T>[], sheetBendingMaterialId: string, maxThickness: number, maxBendLineNetLength: number): T | undefined {
	return table.filter(row => row.sheetBendingMaterialId === sheetBendingMaterialId)
		.filter(row => row.thickness <= maxThickness)
		.sort((lhs, rhs) => rhs.thickness - lhs.thickness)
		.filter((row, _index, self) => Math.abs(row.thickness - self[0].thickness) < tolerances.thickness)
		.sort((lhs, rhs) => rhs.bendLineNetLength - lhs.bendLineNetLength)
		.find(row => row.bendLineNetLength <= maxBendLineNetLength);
}

/**
 * Parse tube profile geometry JSON string
 *
 * The geometry JSON is expected to be valid.
 */
export function parseTubeProfileGeometryType(json: string): TubeProfileGeometryType {
	const obj = parseJson(json, isStringIndexedInterface);
	assert(obj !== undefined, "Tube profile geometry JSON invalid");

	const type = obj["type"];
	assert(
		isTubeProfileGeometryType(type),
		"Expecting valid tube profile geometry type.  Got " + type,
	);

	return type;
}

/**
 * Parse tube profile geometry JSON string
 *
 * The geometry JSON is expected to be valid.
 */
export function parseTubeProfileGeometry(json: string): [TubeProfileGeometryType, TubeProfileGeometryCircular|TubeProfileGeometryRectangular] {
	const obj = parseJson(json, isStringIndexedInterface);
	assert(obj !== undefined, "Tube profile geometry JSON invalid");

	const type = obj["type"];
	const content = obj["content"];

	if (isTubeProfileGeometryType(type) && isStringIndexedInterface(content)) {
		const extractorMap: {[index in TubeProfileGeometryType]: (arg: StringIndexedInterface) => TubeProfileGeometryCircular|TubeProfileGeometryRectangular} = {
			circular: (arg): TubeProfileGeometryCircular => {
				assert(
					isTubeProfileGeometryCircular(arg),
					"Tube profile geometry content invalid for type circular",
				);
				return arg;
			},
			rectangular: (arg): TubeProfileGeometryRectangular => {
				assert(
					isTubeProfileGeometryRectangular(arg),
					"Tube profile geometry content invalid for type rectangular",
				);
				return arg;
			},
		};
		return [
			type,
			extractorMap[type](content),
		];
	} else {
		wsi4.throwError("Tube profile geometry JSON invalid");
	}
}

// Density in [kg/mm³]
export function lookUpDensity(sheetMaterialId: string, tableInput?: readonly Readonly<SheetMaterialDensity>[]): number|undefined {
	const table = tableInput ?? getTable(TableType.sheetMaterialDensity);
	const found = table.find(row => row.sheetMaterialId === sheetMaterialId);
	if (found === undefined) {
		return undefined;
	}

	// [kg/m³]
	const tableDensity = found.density;

	// [kg/mm³]
	const result = tableDensity / 1000000000;
	return result;
}

/**
 * @returns True if sheet is considered available
 */
export function isSheetInStock(sheetId: string, tableInput?: readonly Readonly<SheetStock>[]): boolean {
	const table = tableInput ?? getTable(TableType.sheetStock);
	const row = table.find(row => row.sheetId === sheetId);
	return row === undefined || row.count > 0;
}

/**
 * @returns All sheets that are in stock
 */
export function getSheetsInStock(
	sheetTableInput?: readonly Readonly<Sheet>[],
	sheetStockTableInput?: readonly Readonly<SheetStock>[],
): readonly Readonly<Sheet>[] {
	const sheetStockTable = sheetStockTableInput ?? getTable(TableType.sheetStock);
	const sheetTable = sheetTableInput ?? getTable(TableType.sheet);
	return sheetTable.filter(row => isSheetInStock(row.identifier, sheetStockTable));
}

/**
 * @returns True if tube is considered available
 */
export function isTubeInStock(tubeId: string, tableInput?: readonly Readonly<TubeStock>[]): boolean {
	const table = tableInput ?? getTable(TableType.tubeStock);
	const row = table.find(row => row.tubeId === tubeId);
	return row === undefined || row.count > 0;
}

/**
 * @returns All tubes that are in stock
 */
export function getTubesInStock(
	tubeTableInput?: readonly Readonly<Tube>[],
	tubeStockTableInput?: readonly Readonly<TubeStock>[],
): Readonly<Tube>[] {
	const tubeStockTable = tubeStockTableInput ?? getTable(TableType.tubeStock);
	const tubeTable = tubeTableInput ?? getTable(TableType.tube);
	return tubeTable.filter(row => isTubeInStock(row.identifier, tubeStockTable));
}

export interface TubeCuttingParams {
	thickness: number;
	tubeCuttingProcessId: string;
}

export function lookUpTubeCuttingSpeed(params: Readonly<TubeCuttingParams>, tableInput?: readonly Readonly<TubeCuttingSpeed>[]): number | undefined {
	const table = (tableInput ?? getTable(TableType.tubeCuttingSpeed));
	const filteredRows = table.filter(row => row.tubeCuttingProcessId === params.tubeCuttingProcessId)
		.sort((lhs, rhs) => lhs.thickness - rhs.thickness);
	const row = getRowForThreshold(filteredRows, row => params.thickness - tolerances.thickness <= row.thickness);
	if (row === undefined) {
		return undefined;
	}

	// [m/min] -> [mm/s]
	return row.speed * 1000 / 60;
}

export function lookUpTubeCuttingPierceTime(params: Readonly<TubeCuttingParams>, tableInput?: readonly Readonly<TubeCuttingPierceTime>[]): number | undefined {
	const table = (tableInput ?? getTable(TableType.tubeCuttingPierceTime));
	const filteredRows = table.filter(row => row.tubeCuttingProcessId === params.tubeCuttingProcessId)
		.sort((lhs, rhs) => lhs.thickness - rhs.thickness);

	// [s]
	return getRowForThreshold(filteredRows, row => params.thickness - tolerances.thickness <= row.thickness)?.time;
}

export function tubeProfileForGeometry(profileGeometry: Readonly<TubeProfileGeometry>, tubeProfilesInput?: readonly Readonly<TubeProfile>[]): TubeProfile | undefined {
	const tubeProfiles = tubeProfilesInput ?? getTable(TableType.tubeProfile);
	return tubeProfiles.map(row => {
		const [
			type,
			content,
		] = parseTubeProfileGeometry(row.geometryJson);
		return {
			identifier: row.identifier,
			type: type,
			content: content,
			row: row,
		};
	})
		.filter(obj => obj.type === profileGeometry.type)
		.find(obj => { // eslint-disable-line array-callback-return
			if (obj.type !== profileGeometry.type || Math.abs(obj.content.thickness - profileGeometry.content.thickness) > tolerances.thickness) {
				return false;
			}
			switch (profileGeometry.type) {
				case "circular": {
					assert(isTubeProfileGeometryCircular(obj.content));
					assert(isTubeProfileGeometryCircular(profileGeometry.content));
					return Math.abs(obj.content.outerRadius - profileGeometry.content.outerRadius) < tolerances.thickness;
				}
				case "rectangular": {
					assert(isTubeProfileGeometryRectangular(obj.content));
					assert(isTubeProfileGeometryRectangular(profileGeometry.content));
					return (Math.abs(obj.content.dimY - profileGeometry.content.dimY) < tolerances.thickness
						&& Math.abs(obj.content.dimZ - profileGeometry.content.dimZ) < tolerances.thickness)
					|| (Math.abs(obj.content.dimY - profileGeometry.content.dimZ) < tolerances.thickness
						&& Math.abs(obj.content.dimZ - profileGeometry.content.dimY) < tolerances.thickness);
				}
			}
		})?.row;
}

interface RoundedRectangleAreaArgs {
	width: number;
	height: number;
	radius: number;
}

function roundedRectangleArea(args: RoundedRectangleAreaArgs): number {
	return args.width * args.height - (4 - Math.PI) * args.radius * args.radius;
}

function computeProfileArea(geometry: Readonly<TubeProfileGeometry>): number {
	switch (geometry.type) {
		case "rectangular": {
			// The exact dimensions w.r.t. the radii are undefined.
			// Using these assumptions:
			// 	- outer radius = 2 * `thickness`
			// 	- inner radius = `thickness`
			const content = geometry.content;
			assert(isTubeProfileGeometryRectangular(content));
			const t = content.thickness;
			const a0 = roundedRectangleArea({
				width: content.dimY,
				height: content.dimZ,
				radius: 2 * t,
			});
			const a1 = roundedRectangleArea({
				width: Math.max(0, content.dimY - 2 * t),
				height: Math.max(0, content.dimZ - 2 * t),
				radius: t,
			});
			const result = a0 - a1;
			assert(result > 0, "Rectangular profile area invalid");
			return result;
		}
		case "circular": {
			const content = geometry.content;
			assert(isTubeProfileGeometryCircular(content));
			const outerRadius = content.outerRadius;
			const innerRadius = Math.max(0, content.outerRadius - content.thickness);
			return Math.PI * outerRadius * outerRadius - Math.PI * innerRadius * innerRadius;
		}
	}
}

export function tubeProfileGeometryForTube(tube: Readonly<Tube>, profileTableInput?: readonly Readonly<TubeProfile>[]): TubeProfileGeometry {
	const profileTable = profileTableInput ?? getTable(TableType.tubeProfile);
	const profile = profileTable.find(row => row.identifier === tube.tubeProfileId);
	assert(profile !== undefined, "Expecting associated profile for tube " + tube.identifier);
	const [
		type,
		content,
	] = parseTubeProfileGeometry(profile.geometryJson);
	return {
		type: type,
		content: content,
	};
}

/**
 * Compute volume of one tube
 *
 * For rectangular tubes the radii are assumed to be `thickness` and `2 * thickness` respectively.
 */
export function computeTubeVolume(tube: Readonly<Tube>, geometryInput?: Readonly<TubeProfileGeometry>): number {
	const geometry = geometryInput ?? tubeProfileGeometryForTube(tube);

	// [mm²]
	const profileArea = computeProfileArea(geometry);

	// [mm³]
	return tube.dimX * profileArea;
}
