import {
	back,
	front,
	itemAt,
} from "qrc:/js/lib/array_util";
import {
	tolerances,
} from "qrc:/js/lib/constants";
import {
	getSettingOrDefault,
} from "qrc:/js/lib/settings_table";
import {
	getSheetsInStock,
	getTable,
	getTubesInStock,
	tubeProfileForGeometry,
	validSheetCuttingProcessesForThickness,
} from "qrc:/js/lib/table_utils";
import {
	assert,
	assertDebug,
	bbDimensionX,
	bbDimensionY,
	cleanFileName,
	computeUniqueNames,
	isEqual,
	lexicographicObjectLess,
} from "qrc:/js/lib/utils";
import {
	computeOrderedProcesses,
	findActiveProcess,
	isAvailableProcess,
	workStepTypeMap,
} from "qrc:/js/lib/process";
import {
	getSharedDataEntry,
} from "qrc:/js/lib/shared_data";
import {
	createUpdatedNodeUserDataImpl,
	nodeUserDatumImpl,
} from "qrc:/js/lib/userdata_config";
import {
	computePotentialSheetNestingPartitions,
	SheetNestingPartition,
	SheetNestingPartitionTarget,
} from "qrc:/js/lib/sheet_nesting_partitions";
import {
	createUpdatedArticleUserData,
} from "./article_userdata_config";

interface SheetCuttingProcessCandidate {
	processId: string | undefined;
	sheetMaterialId: string | undefined;
}

function pickSheetMaterialId(thickness: number, twoDimBox: Box2): string | undefined {
	const defaultSheetMaterialId = getSharedDataEntry("defaultSheetMaterialId");
	if (defaultSheetMaterialId === undefined) {
		return undefined;
	}

	const nestingDistance = getSettingOrDefault("sheetNestingDistance");
	const dim0 = bbDimensionX(twoDimBox);
	const dim1 = bbDimensionY(twoDimBox);
	const dimX = Math.max(dim0, dim1) + nestingDistance;
	const dimY = Math.min(dim0, dim1) + nestingDistance;

	const candidates = getSheetsInStock().filter(sheet => Math.abs(sheet.thickness - thickness) <= tolerances.thickness && dimX < sheet.dimX && dimY < sheet.dimY);

	if (defaultSheetMaterialId !== undefined && candidates.some(row => row.sheetMaterialId === defaultSheetMaterialId)) {
		return defaultSheetMaterialId;
	}
	return undefined;
}

function pickSheetCuttingProcessCandidate(vertex: SlVertex, preGraph: PreDocumentGraph): SheetCuttingProcessCandidate {
	const part = wsi4.sl.node.part(vertex, preGraph);
	const thickness = wsi4.cad.thickness(part);

	const twoDimRep = wsi4.sl.node.twoDimRep(vertex, preGraph);
	if (twoDimRep === undefined) {
		return {
			processId: undefined,
			sheetMaterialId: undefined,
		};
	}

	const twoDimBox = wsi4.cam.util.boundingBox2(twoDimRep);
	const sheetMaterialId = pickSheetMaterialId(thickness, twoDimBox);
	if (sheetMaterialId === undefined) {
		return {
			processId: undefined,
			sheetMaterialId: undefined,
		};
	}

	const sheetCuttingProcesses = validSheetCuttingProcessesForThickness(thickness);
	const filteredMappings = getTable("sheetCuttingProcessMapping")
		.filter(mapping => mapping.sheetMaterialId === sheetMaterialId && sheetCuttingProcesses.some(row => row.identifier === mapping.sheetCuttingProcessId));
	const processId = findActiveProcess(p => workStepTypeMap[p.type] === "sheetCutting" && filteredMappings.some(mapping => mapping.processId === p.identifier))?.identifier;

	return {
		processId: processId,
		sheetMaterialId: sheetMaterialId,
	};
}

interface InitialFlatSmpArticleParams {
	type: "flatSmp";

	article: SlVertex[];

	// There must be a matching process, otherwise the article is not meaningful
	sheetCuttingProcessId: string;

	// There must be a matching process, otherwise the article is not meaningful
	sheetCuttingProcessType: ProcessType;

	// Value is unset if there is no matching material
	sheetMaterialId?: string;

	bendLineEngravingMode?: BendLineEngravingMode;

}

interface InitialBendSmpArticleParams {
	type: "bendSmp";

	article: SlVertex[];

	// There must be a matching process, otherwise the article is not meaningful
	sheetCuttingProcessId: string;

	// There must be a matching process, otherwise the article is not meaningful
	sheetCuttingProcessType: ProcessType;

	sheetBendingProcessId: string;

	sheetBendingProcessType: ProcessType;

	// Value is unset if there is no matching sheet
	sheetMaterialId?: string;

	bendLineEngravingMode?: BendLineEngravingMode;
}

interface InitialTubePartArticleParams {
	type: "tubePart";

	article: SlVertex[];
	processId?: string;
	tubeMaterialId?: string;
	tubeSpecificationId?: string;
	nestorTargetLength?: number;
}

interface InitialGenericArticleParams {
	type: "generic";
	article: SlVertex[];
	processType: ProcessType;
	processId: string;
}

export type InitialArticleParams = InitialFlatSmpArticleParams
| InitialBendSmpArticleParams
| InitialTubePartArticleParams
| InitialGenericArticleParams;

function computeInitialSmpArticleParams(article: SlVertex[], preGraph: PreDocumentGraph): InitialFlatSmpArticleParams | InitialBendSmpArticleParams {
	const scpc = pickSheetCuttingProcessCandidate(front(article), preGraph);

	const result = ((): InitialBendSmpArticleParams | InitialFlatSmpArticleParams => {
		const sheetCuttingProcessType = getTable("process").find(row => row.identifier === scpc.processId)?.type ?? "sheetCutting";
		if (article.some(v => wsi4.sl.node.workStepType(v, preGraph) === "sheetBending")) {
			const bendingProcess = findActiveProcess(row => row.type === "dieBending");
			return {
				type: "bendSmp",
				article: article,
				sheetCuttingProcessId: scpc.processId ?? "",
				sheetCuttingProcessType: sheetCuttingProcessType,
				sheetBendingProcessId: bendingProcess?.identifier ?? "",
				sheetBendingProcessType: "dieBending",
			};
		} else {
			return {
				type: "flatSmp",
				article: article,
				sheetCuttingProcessId: scpc.processId ?? "",
				sheetCuttingProcessType: sheetCuttingProcessType,
			};
		}
	})();

	if (scpc.sheetMaterialId !== undefined) {
		result.sheetMaterialId = scpc.sheetMaterialId;
	}

	const bendLineEngravingMode = getSharedDataEntry("defaultBendLineEngravingMode");
	if (bendLineEngravingMode !== "none") {
		result.bendLineEngravingMode = bendLineEngravingMode;
	}

	return result;
}

function computeInitialTubePartArticleParams(article: SlVertex[], preGraph: PreDocumentGraph): InitialTubePartArticleParams {
	const vertex = front(article);
	assertDebug(() => wsi4.sl.node.workStepType(vertex, preGraph) === "tubeCutting");

	const processTable = getTable("process");
	const tubeCuttingProcesses = computeOrderedProcesses(row => row.type === "tubeCutting" && isAvailableProcess(row, processTable));
	assert(tubeCuttingProcesses.length !== 0, "Expecting at least one available tube cutting process at this point");

	const result: InitialTubePartArticleParams = {
		type: "tubePart",
		article: article,
	};

	// Not returning early if any of these is undefined since a process needs to be picked in any case (as of now).
	// (An unset processId is not handled well a.t.m. - if this should be implemented
	// then processId should be turned into an *optional* property of GraphNode so there
	// is a clear distinction between a populated and an unpopulated processId.)
	const defaultTubeMaterialId = getSharedDataEntry("defaultTubeMaterialId");
	const profileGeometry = wsi4.sl.node.tubeProfileGeometry(front(article), preGraph);

	const profileExtrusionLength = wsi4.sl.node.profileExtrusionLength(front(article), preGraph);
	const profile = profileGeometry === undefined ? undefined : tubeProfileForGeometry(profileGeometry);
	const tube = defaultTubeMaterialId === undefined ? undefined : getTubesInStock()
		.filter(row => profileExtrusionLength <= row.dimX
			&& (profile !== undefined && row.tubeProfileId === profile.identifier)
			&& row.tubeMaterialId === defaultTubeMaterialId)
		.sort((lhs, rhs) => lexicographicObjectLess(lhs, rhs))
		.shift();

	// Not returning at this point since a process needs to be picked in any case.
	if (tube !== undefined) {
		result.tubeMaterialId = tube.tubeMaterialId;
		result.tubeSpecificationId = tube.tubeSpecificationId;
		result.nestorTargetLength = Math.max(0, tube.dimX - getSettingOrDefault("tubeClampingLength"));
	}

	// Note: There might be more than one Process per TubeCuttingProcess
	// so applying sort as long as there is no logic to further refine the selection.
	const tubeCuttingProcessMappings = getTable("tubeCuttingProcessMapping")
		.filter(mapping => tubeCuttingProcesses.some(process => mapping.processId === process.identifier))
		.filter(mapping => tube === undefined || tube.tubeMaterialId === mapping.tubeMaterialId)
		.sort((lhs, rhs) => lhs.processId < rhs.processId ? -1 : lhs.processId === rhs.processId ? 0 : 1);
	if (tubeCuttingProcessMappings.length === 0) {
		return result;
	}

	const process = tubeCuttingProcesses.find(row => row.identifier === tubeCuttingProcessMappings[0]!.processId);
	if (process !== undefined) {
		result.processId = process.identifier;
	}

	return result;
}

function computeInitialGenericArticleParams(article: SlVertex[], preGraph: PreDocumentGraph): InitialGenericArticleParams {
	assert(article.length === 1, "Expecting generic article of length 1; actual length: " + article.length.toString());
	const wst = wsi4.sl.node.workStepType(front(article), preGraph);
	const process = findActiveProcess(row => workStepTypeMap[row.type] === wst);
	return {
		type: "generic",
		article: article,
		processId: process?.identifier ?? "",
		processType: process?.type ?? "externalPart",
	};
}

export function computeInitialArticleParams(article: SlVertex[], preGraph: PreDocumentGraph): InitialArticleParams {
	const wsts = article.map(v => wsi4.sl.node.workStepType(v, preGraph));
	if (wsts.some(wst => wst === "sheetCutting" || wst === "sheetBending")) {
		return computeInitialSmpArticleParams(article, preGraph);
	} else if (wsts.some(wst => wst === "tubeCutting")) {
		return computeInitialTubePartArticleParams(article, preGraph);
	} else {
		return computeInitialGenericArticleParams(article, preGraph);
	}
}

export function gatherInitialArticleParams(preGraph: PreDocumentGraph, articles?: SlVertex[][]): InitialArticleParams[] {
	return (articles ?? wsi4.sl.graph.articles(preGraph)).map(article => computeInitialArticleParams(article, preGraph));
}

function initialSheetCuttingNodeUserData(params: InitialFlatSmpArticleParams | InitialBendSmpArticleParams): StringIndexedInterface {
	let result: StringIndexedInterface = {};
	if (params.type === "flatSmp" && params.sheetMaterialId !== undefined) {
		// In case of a bendSmp article the material should end up in the sheetBending node
		result = createUpdatedNodeUserDataImpl("sheetMaterialId", params.sheetMaterialId, result);
	}
	if (params.bendLineEngravingMode !== undefined) {
		result = createUpdatedNodeUserDataImpl("bendLineEngravingMode", params.bendLineEngravingMode, result);
	}
	return result;
}

function initialSheetBendingNodeUserData(params: InitialBendSmpArticleParams): StringIndexedInterface {
	let result: StringIndexedInterface = {};
	if (params.sheetMaterialId !== undefined) {
		result = createUpdatedNodeUserDataImpl("sheetMaterialId", params.sheetMaterialId, result);
	}
	if (params.bendLineEngravingMode !== undefined) {
		result = createUpdatedNodeUserDataImpl("bendLineEngravingMode", params.bendLineEngravingMode, result);
	}
	return result;
}

function makeDieChoices(alternatives: readonly Readonly<DieChoiceAlternativesEntry>[]): DieChoiceMapEntry[] {
	return alternatives.map(entry => {
		assert(entry.bendDieChoices.length > 0, "There must be at least the neutral axis pseudo tool");
		return {
			bendDescriptor: entry.bendDescriptor,
			bendDieChoice: entry.bendDieChoices[0]!,
		};
	});
}

function initialSheetComponentNodeUpdates(
	articleParams: InitialFlatSmpArticleParams | InitialBendSmpArticleParams,
	preGraph: PreDocumentGraph,
): SlNodeUpdate[] {
	return articleParams.article.map((v): SlNodeUpdate => {
		const wst = wsi4.sl.node.workStepType(v, preGraph);
		const content = (() => {
			switch (wst) {
				case "sheetCutting": {
					const result: SlNodeUpdateSheetCutting = {
						vertex: v,
						processId: articleParams.sheetCuttingProcessId,
						processType: articleParams.sheetCuttingProcessType,
						nodeUserData: initialSheetCuttingNodeUserData(articleParams),
					};
					return result;
				}
				case "sheetBending": {
					const process = findActiveProcess(row => row.type === "dieBending");
					assert(process !== undefined, "There should be no sheet bending node without an active sheet bending process");
					assert(articleParams.type === "bendSmp", "Initial article articleParams inconsistent");
					const alternatives = wsi4.sl.node.dieChoiceAlternatives(v, articleParams.sheetMaterialId, preGraph);
					const dieChoiceMap = makeDieChoices(alternatives);
					const result: SlNodeUpdateSheetBending = {
						vertex: v,
						processId: process.identifier,
						processType: process.type,
						dieChoiceMap: dieChoiceMap,
						nodeUserData: initialSheetBendingNodeUserData(articleParams),
					};
					return result;
				}
				case "joining":
				case "packaging":
				case "sheet":
				case "transform":
				case "tube":
				case "tubeCutting":
				case "undefined":
				case "userDefined":
				case "userDefinedBase": return wsi4.throwError("Unexpected WST: " + wst);
			}
		})();
		return {
			type: wst,
			content: content,
		};
	});
}

function initialTubeCuttingUserData(params: InitialTubePartArticleParams): StringIndexedInterface {
	let result = {};
	if (params.tubeMaterialId !== undefined) {
		result = createUpdatedNodeUserDataImpl("tubeMaterialId", params.tubeMaterialId, result);
	}
	if (params.tubeSpecificationId !== undefined) {
		result = createUpdatedNodeUserDataImpl("tubeSpecificationId", params.tubeSpecificationId, result);
	}
	return result;
}

function initialTubeComponentNodeUpdates(params: InitialTubePartArticleParams): SlNodeUpdate[] {
	const tubeCuttingContent: SlNodeUpdateTubeCutting = {
		vertex: front(params.article),
		processType: "tubeCutting",
		processId: params.processId ?? "",
		nodeUserData: initialTubeCuttingUserData(params),
	};
	return [
		{
			type: "tubeCutting",
			content: tubeCuttingContent,
		},
	];
}

function initialGenericArticleNodeUpdates(params: InitialGenericArticleParams, preGraph: PreDocumentGraph): SlNodeUpdate[] {
	assert(params.article.length === 1, "Expecting only one vertex for generic articles");
	const vertex = front(params.article);
	const wst = wsi4.sl.node.workStepType(vertex, preGraph);
	const content = (() => {
		switch (wst) {
			case "joining":
			case "packaging":
			case "transform":
			case "tubeCutting":
			case "undefined":
			case "userDefined":
			case "userDefinedBase": {
				return {
					vertex: vertex,
					processId: params.processId,
					processType: params.processType,
					userData: {},
				};
			}
			case "sheet":
			case "tube":
			case "sheetCutting":
			case "sheetBending": return wsi4.throwError("Unexpected WST: " + wst);
		}
	})();
	return [
		{
			type: wst,
			content: content,
		},
	];
}

export function computeInitialNodeUpdatesForArticle(
	article: SlVertex[],
	preGraph: PreDocumentGraph,
	params: InitialArticleParams,
): SlNodeUpdate[] {
	const wsts = article.map(v => wsi4.sl.node.workStepType(v, preGraph));
	if (wsts.some(wst => wst === "sheetCutting" || wst === "sheetBending")) {
		assert(params.type === "flatSmp" || params.type === "bendSmp", "Unexpected initial article params type: " + params.type);
		return initialSheetComponentNodeUpdates(params, preGraph);
	} else if (wsts.some(wst => wst === "tubeCutting")) {
		assert(params.type === "tubePart", "Unexpected initial article params type: " + params.type);
		return initialTubeComponentNodeUpdates(params);
	} else if (wsts.some(wst => wst === "sheet" || wst === "tube")) {
		// Semimanufactureds are updated via the associated target's source update
		return [];
	} else {
		assert(params.type === "generic");
		return initialGenericArticleNodeUpdates(params, preGraph);
	}
}

export function computeInitialNodeUpdates(initialArticleParams: InitialArticleParams[], preGraph: PreDocumentGraph): SlNodeUpdate[] {
	const result: SlNodeUpdate[] = [];
	initialArticleParams.forEach(params => {
		const article = params.article;
		result.push(...computeInitialNodeUpdatesForArticle(article, preGraph, params));
	});

	return result;
}

export function computeSheetNestingPrePartitions(sheetNestingPartitions: readonly Readonly<SheetNestingPartition<SlVertex>>[]): SlSheetNestingPrePartition[] {
	const sheetProcess = findActiveProcess(row => row.active && row.type === "sheet");
	return sheetNestingPartitions.map(partition => {
		const obj: SlSheetNestingPrePartition = {
			compatibleTargets: partition.ids.map(v => ({
				vertex: v,
				fixedRotations: [],
				sheetFilterSheetIds: [],
			})),
			nestingDistance: getSettingOrDefault("sheetNestingDistance"),
			nestorConfig: {
				timeout: getSharedDataEntry("nestorTimeLimit"),
				numRelevantNestings: 0,
			},
		};
		if (partition.sheetMaterialId !== undefined) {
			obj.sheetMaterialId = partition.sheetMaterialId;
		}
		if (sheetProcess !== undefined) {
			obj.sheetProcessId = sheetProcess.identifier;
		}
		return obj;
	});
}

export function computeTubeNestingPrePartitions(initialArticleParams: readonly Readonly<InitialArticleParams>[]): SlTubeNestingPrePartition[] {
	const tubeProcess = findActiveProcess(row => row.type === "tube");
	return initialArticleParams.filter(params => params.type === "tubePart")
		.map(params => {
			assert(params.type === "tubePart");
			const obj: SlTubeNestingPrePartition = {
				targetVertex: front(params.article),
				nestingDistance: getSettingOrDefault("tubeNestingDistance"),
				targetLength: params.nestorTargetLength ?? 0,
			};
			if (tubeProcess !== undefined) {
				obj.tubeProcessId = tubeProcess.identifier;
			}
			return obj;
		});
}

/**
 * Compute sheet nesting partitions for a PreDocumentGraph
 *
 * A PreDocumentGraph could be
 * 	- An initially created graph
 * 	- An incomplete graph
 * 	- A graph where only sub-sections have been reset (e.g. by toggling a node's WST).
 *
 * The effective data for the nesting pre-partitions are looked up in three locations:
 * 	1.  Submitted initialArticleParams
 * 	2. (if not yet found) The PreDocumentGraph
 * 	3. (if not yet found) Computed InitialArticleParams
 */
export function computePreGraphPotentialSheetNestingPartitions(preGraph: PreDocumentGraph, initialArticleParams: InitialArticleParams[]): SheetNestingPartition<SlVertex>[] {
	const paramsFromGraph = (article: readonly SlVertex[]): InitialFlatSmpArticleParams | undefined => {
		const sheetMaterialId = (() => {
			const vertex = article.find(v => wsi4.sl.node.workStepType(v, preGraph) === "sheetBending") ?? article.find(v => wsi4.sl.node.workStepType(v, preGraph) === "sheetCutting");
			assert(vertex !== undefined);
			return nodeUserDatumImpl("sheetMaterialId", wsi4.sl.node.nodeUserData(vertex, preGraph));
		})();

		const vertex = article.find(v => wsi4.sl.node.workStepType(v, preGraph) === "sheetCutting");
		assert(vertex !== undefined);

		const processType = wsi4.sl.node.processType(vertex, preGraph);
		const processId = wsi4.sl.node.processId(vertex, preGraph);

		if (sheetMaterialId === undefined && processType === "undefined" && processId.length === 0) {
			return undefined;
		}

		const result: InitialFlatSmpArticleParams = {
			type: "flatSmp",
			article: Array.from(article),
			sheetCuttingProcessType: processType,
			sheetCuttingProcessId: processId,
		};

		if (sheetMaterialId !== undefined) {
			result.sheetMaterialId = sheetMaterialId;
		}

		return result;
	};

	const targets: SheetNestingPartitionTarget<SlVertex>[] = [];
	wsi4.sl.graph.articles(preGraph)
		.forEach(article => {
			const vertex = article.find(v => wsi4.sl.node.workStepType(v, preGraph) === "sheetCutting");
			if (vertex === undefined) {
				return;
			}

			const articleParams = initialArticleParams.find(params => (params.type === "flatSmp" || params.type === "bendSmp") && (isEqual(front(article), front(params.article))))
				?? paramsFromGraph(article)
				?? computeInitialArticleParams(article, preGraph);

			if (articleParams === undefined) {
				wsi4.throwError("Expecting initial article params;\n" + JSON.stringify(initialArticleParams, null, "\t"));
			}

			assert(articleParams.type === "flatSmp" || articleParams.type === "bendSmp");

			const part = wsi4.sl.node.part(vertex, preGraph);
			const thickness = wsi4.cad.thickness(part);

			const target: SheetNestingPartitionTarget<SlVertex> = {
				id: vertex,
				processIdSheetCutting: articleParams.sheetCuttingProcessId,
				thickness: thickness,
			};

			if (articleParams.sheetMaterialId !== undefined) {
				target.sheetMaterialId = articleParams.sheetMaterialId;
			}

			targets.push(target);
		});

	return computePotentialSheetNestingPartitions<SlVertex>(targets);
}

function generateName(vertex: SlVertex, preGraph: PreDocumentGraph): string {
	const assemblyNameIfAny = (asm: Assembly | undefined): string | undefined => {
		if (asm === undefined) {
			return undefined;
		}
		const assemblyName = cleanFileName(wsi4.geo.assembly.name(asm));
		if (assemblyName.length === 0) {
			return undefined;
		}
		return assemblyName;
	};

	const assemblyName = assemblyNameIfAny(wsi4.sl.node.inputAssembly(vertex, preGraph));
	if (assemblyName !== undefined) {
		assertDebug(() => assemblyName.length > 0);
		return assemblyName;
	}

	// Using a suffix for all generated names for consistency.
	// (Names are made unique later on.)
	const defaultSuffix = "_0";
	const wst = wsi4.sl.node.workStepType(vertex, preGraph);
	if (wst === "sheet" || wst === "tube") {
		return wsi4.util.translate(wst) + defaultSuffix;
	} else {
		return wsi4.util.translate("Article") + defaultSuffix;
	}
}

export function computeInitialArticleUpdates(
	articles: SlVertex[][],
	preGraph: PreDocumentGraph,
	importIdNameProposalMap: Map<string, string>,
	existingNames: readonly Readonly<string>[],
): SlArticleUpdate[] {
	const proposals = articles.map(article => {
		const vertex = back(article);
		const importId = wsi4.sl.node.importId(vertex, preGraph);
		if (importId === undefined) {
			return generateName(vertex, preGraph);
		}
		return importIdNameProposalMap.get(importId) ?? generateName(vertex, preGraph);
	});
	const uniqueNames = computeUniqueNames(proposals, existingNames);
	return articles.map((article, index): SlArticleUpdate => ({
		vertex: back(article),
		articleUserData: createUpdatedArticleUserData("name", itemAt(index, uniqueNames), {}),
	}));

}
