import {
	TableType,
} from "qrc:/js/lib/generated/enum";
import {
	bestSheetModuliNestingDescriptor,
	computeNestingFractions,
	getArticleName,
} from "./graph_utils";
import {
	cadFeatureTwoDimCentroid,
	computeTappingCandidates,
	nestingTargetBoxForVertex,
} from "./node_utils";
import {
	getSharedDataEntry,
} from "./shared_data";
import {
	createSheetTappingSceneNameItems,
	createSheetTappingSceneIndexItems,
	TappingSceneSelectionEntry,
} from "./sheet_tapping_utils";
import {
	getTable,
} from "./table_utils";
import {
	nodeUserDatumOrDefault,
} from "./userdata_utils";
import {
	assert,
	assertDebug,
	bbDimensionX,
	bbDimensionY,
} from "./utils";
import {
	getFutureResult,
} from "./future";

interface ScriptSceneConfig {
	cuttingContours?: boolean;
	engravings?: boolean;
	bendLineShowLabels?: boolean;
	bendLineShowAffectedSegments?: boolean;
	sheetTappingLabelMode?: "names" | "indices";
	tubeCuttingShowTubeContours?: boolean;
	tubeCuttingShowVirtualCuts?: boolean;
	fontSize?: number;
	sheetBendingBendZones?: boolean;
	sheetBendingLowerDieAffectZones?: boolean;

	/**
	 * sheetTapping use-case:
	 *
	 * When creating a sheetTapping specific scene for an article that has *no* sheetTapping node yet,
	 * then the respective labels / indices for *candidates* core holes can be added to the Scene via this interface.
	 *
	 * However, in case of an actual sheetTapping node the labels for the actually configured threads are added implicitly,
	 * so there is no need to add the lables here.
	 */
	sceneTextItems?: SceneTextItem[];
}

export function generateUniqueSceneIdentifier(vertex: Vertex): string {
	const id = (() => {
		const a = getArticleName(vertex);
		if (a.length > 0) {
			return a;
		} else {
			const r = wsi4.util.toKey(wsi4.node.rootId(vertex));
			return (new Date())
				.toISOString() + "_" + r + "_";
		}
	})();
	// Trumpf laser machines can only read parts with name length <= 50
	// Because we use this sceneId in the geo name we split the id to <= 50 characters
	// We remove the front characters because different part names normally differ in the end, not the beginnning (e.g. Baugruppe1^Teil3, Baugruppe1^Teil1)
	return id.slice(-50);
}

interface ExtraSceneData {
	// Corresponds to SceneSceneData's sceneId
	sceneId: string | undefined;
}

function computeExtraSceneData(vertex: Vertex): ExtraSceneData {
	return {
		sceneId: generateUniqueSceneIdentifier(vertex),
	};
}

function sheetTappingTextItems(vertex: Vertex, config: ScriptSceneConfig): SceneTextItem[] | undefined {
	assertDebug(() => wsi4.node.processType(vertex) === "sheetTapping", "Pre-condition violated");

	// 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 (wsi4.node.part(vertex) === undefined) {
		wsi4.util.warn("Sheet tapping not supported for loaded node.");
		return undefined;
	}

	assertDebug(() => wsi4.node.part(vertex) !== undefined, "Pre-condition violated");

	const sheetTappingData = nodeUserDatumOrDefault("sheetTappingData", vertex);
	{
		const threadsTable = getTable("screwThread");
		if (sheetTappingData.some(data => threadsTable.every(row => row.identifier !== data.screwThreadId))) {
			wsi4.util.error("At least one sheet tapping data entry invalid; cannot compute sheetTapping Scene.");
			return undefined;
		}
	}

	switch (config.sheetTappingLabelMode ?? "names") {
		case "names": {
			const screwThreads = getTable("screwThread");
			const entries: TappingSceneSelectionEntry[] = sheetTappingData
				.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(entries);
		}
		case "indices": {
			const candidates = computeTappingCandidates(vertex);
			return createSheetTappingSceneIndexItems(candidates);
		}
	}
}

function computeExtraTextItems(vertex: Vertex, config: ScriptSceneConfig): SceneTextItem[] | undefined {
	const specificItems = (() => {
		if (wsi4.node.processType(vertex) === "sheetTapping") {
			return sheetTappingTextItems(vertex, config);
		}
		return undefined;
	})();
	if (specificItems === undefined && config.sceneTextItems === undefined) {
		return undefined;
	}
	return [
		...(specificItems ?? []),
		...(config.sceneTextItems ?? []),
	];
}

interface SheetExtraData {
	nestingDissections: NestingDissection[];
	extraText: string;
}

function sheetExtraData(vertex: Vertex): SheetExtraData | undefined {
	if (wsi4.node.workStepType(vertex) !== "sheet") {
		return undefined;
	}

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

	const sheet = wsi4.node.findAssociatedSheet(vertex);
	if (sheet === undefined) {
		// Handling gracefully here to make scene computation more robust for incomplete graphs.
		return undefined;
	}

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

	// Sheet modulus table entry is not mandatory.
	// Using neutral default value as fallback.
	const sheetModulusRow: Readonly<{
		xModulus: number,
		yModulus: number,
		applyToAll: boolean,
	}> = getTable(TableType.sheetModulus)
		.find(row => row.sheetId === sheet.identifier) ?? {
		xModulus: 0,
		yModulus: 0,
		applyToAll: true,
	};
	const nestingFractions = computeNestingFractions(twoDimRep, targetBox, sheetModulusRow);

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

	const bestNestingDescriptor = (() => {
		if (sheetModulusRow.applyToAll) {
			return undefined;
		} else {
			return bestSheetModuliNestingDescriptor(twoDimRep, nestingFractions);
		}
	})();

	const nestingDissections = nestingFractions
		.filter(item => bestNestingDescriptor === undefined || item.nestingDescriptor === bestNestingDescriptor)
		.map((fraction): NestingDissection => {
			const d: NestingDissection = {
				nestingDescriptor: fraction.nestingDescriptor,
			};
			if (fraction.effectiveDimX < targetBoxDimX) {
				d.xDissection = fraction.effectiveDimX;
			}
			if (fraction.effectiveDimY < targetBoxDimY) {
				d.yDissection = fraction.effectiveDimY;
			}
			return d;
		});

	const extraText = sheet.name + (sheet.description.length === 0 ? "" : " (" + sheet.description + ")");

	return {
		nestingDissections: nestingDissections,
		extraText: extraText,
	};
}

/**
 * Create a [[SceneFuture]] representing a given vertex's 2d view.
 * @param vertex The Vertex to generate the Scene for
 * @param partialConfig Configuration for the scene
 * @returns The 2d representation of vertex in a Scene
 */
export function asyncSceneForVertex(vertex: Vertex, partialConfig: ScriptSceneConfig): SceneFuture {
	const wst = wsi4.node.workStepType(vertex);

	const config: SceneConfig = {
		elements: {
			cuttingContours: partialConfig.cuttingContours ?? false,
			engravings: partialConfig.engravings ?? false,
			sheetBendingBendZones: partialConfig.sheetBendingBendZones ?? false,
			sheetBendingLowerDieAffectZones: partialConfig.sheetBendingLowerDieAffectZones ?? false,
			sheetBendingDieAffectedSegments: partialConfig.bendLineShowAffectedSegments ?? false,
			sheetBendingBendLineLabels: partialConfig.bendLineShowLabels ?? false,
			sheetBendingOverlappingAreas: partialConfig.bendLineShowAffectedSegments ?? false,
			tubeCuttingTubeContours: partialConfig.tubeCuttingShowTubeContours ?? false,
			tubeCuttingVirtualCuts: partialConfig.tubeCuttingShowVirtualCuts ?? false,
		},
	};

	if (partialConfig.fontSize !== undefined) {
		config.fontSize = partialConfig.fontSize;
	}

	const extraData = computeExtraSceneData(vertex);
	if (extraData.sceneId !== undefined) {
		config.sceneId = extraData.sceneId;
	}

	const textItems = computeExtraTextItems(vertex, partialConfig);
	if (textItems !== undefined) {
		config.textItems = textItems;
	}

	switch (wst) {
		case "sheet": {
			const extraData = sheetExtraData(vertex);
			if (extraData !== undefined) {
				config.sheetNestingDissections = extraData.nestingDissections;
				config.sheetExtraText = extraData.extraText;
			}
			break;
		}
		case "sheetCutting":
		case "sheetBending": {
			const l = getSharedDataEntry("bendLineEngravingLength");
			if (l > 0) {
				config.sheetBendingBendLineMaxEngravingLength = l;
			}
			break;
		}
		default: break;
	}

	return wsi4.node.asyncScene(vertex, config);
}

export function sceneForVertex(vertex: Vertex, partialConfig: ScriptSceneConfig): Scene {
	return getFutureResult<"scene">(asyncSceneForVertex(vertex, partialConfig));
}

/**
 * Create a [[Scene]] representing a given vertex's 2d view.
 * @param vertex The Vertex to generate the Scene for
 * @returns The 2d representation of vertex in a Scene
 *
 * Convenience function utilizing the default scene config.
 */
export function asyncDefaultSceneForVertex(vertex: Vertex): SceneFuture {
	return asyncSceneForVertex(vertex, {
		cuttingContours: true,
		engravings: true,
	});
}

/**
 * Create a [[Scene]] representing a given vertex's 2d view.
 * @param vertex The Vertex to generate the Scene for
 * @returns The 2d representation of vertex in a Scene
 *
 * Convenience function utilizing the default scene config.
 */
export function defaultSceneForVertex(vertex: Vertex): Scene {
	return getFutureResult<"scene">(asyncDefaultSceneForVertex(vertex));
}

/**
 * Create a [[Resolution]] that is not used (for DXF and GEO rendering for example)
 */
export function unusedResolution(): Resolution {
	return {
		width: -1,
		height: -1,
	};
}

/**
 * Create default [[Resolution]] for SVGs (1600x1200)
 */
export function defaultSvgResolution(): Resolution {
	return {
		width: 1600,
		height: 1200,
	};
}

export function boxFromResolution(resolution: Resolution): Box2 {
	return {
		lower: {
			entries: [
				0,
				0,
			],
		},
		upper: {
			entries: [
				resolution.width,
				resolution.height,
			],
		},
	};
}
