// Note: All functions in this module must conform to DocumentGraphHandler's script engine API.
import {
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	nodeLaserSheetCuttingGasId,
} from "qrc:/js/lib/node_utils";
import {
	bbDimensionX,
	bbDimensionY,
	isEqual,
	thicknessEquivalence,
} from "qrc:/js/lib/utils";
import {
	getSharedDataEntry,
} from "qrc:/js/lib/shared_data";

import {
	getSettingOrDefault,
} from "qrc:/js/lib/settings_table";
import {
	computeAllowedSheets,
} from "qrc:/js/lib/graph_utils";
import {
	accessSelfAndReachable,
	collectNodeUserDataEntries,
} from "qrc:/js/lib/userdata_utils";

/**
 * Compute material of node or targets of sheet node
 *
 * @param vertex  Either a sheet or node or the target of the new sheet node to probably merge
 */
function material(vertex: Vertex): string {
	// Get userData of sheet node and the reaching ones
	const materials = collectNodeUserDataEntries("sheetMaterial", vertex, accessSelfAndReachable)
		.filter((material, index, self) => self.findIndex(mat => mat.identifier === material.identifier) === index);
	if (materials.length !== 1) {
		return "";
	}
	return materials[0].identifier;
}

/**
 * Check if the thickness of the vertices are equal
 *
 * @param target A sheetCutting [[Vertex]]
 * @param source A sheet or sheetCutting [[Vertex]]
 */
function sameThickness(target: Vertex, source: Vertex): boolean {
	if (wsi4.node.workStepType(target) !== "sheetCutting") {
		return wsi4.throwError("sameThickness(): Target has wrong workStepType \"" + wsi4.node.workStepType(target) + "\"");
	}
	const targetThickness = wsi4.node.sheetThickness(target);
	if (targetThickness === undefined) {
		return wsi4.throwError("sameThickness(): Missing thickness");
	}
	if (wsi4.node.workStepType(source) !== "sheet" && wsi4.node.workStepType(source) !== "sheetCutting") {
		return wsi4.throwError("sameThickness(): Source has wrong workStepType \"" + wsi4.node.workStepType(source) + "\"");
	}
	const sourceThickness = wsi4.node.sheetThickness(source);
	if (sourceThickness === undefined) {
		return wsi4.throwError("sameThickness(): Missing thickness");
	}
	return thicknessEquivalence(targetThickness, sourceThickness);
}

/**
 * Compute laserSheetCutting gas of sheet node
 *
 * @param vertex  A sheet node
 */
function sheetNodeCuttingGas(vertex: Vertex): string {
	if (wsi4.node.workStepType(vertex) !== "sheet") {
		return wsi4.throwError("sheetNodeCuttingGas(): Vertex has wrong workStepType \"" + wsi4.node.workStepType(vertex) + "\"");
	}
	if (wsi4.graph.targets(vertex).length < 1) {
		return wsi4.throwError("sheetNodeCuttingGas(): Missing targets");
	}
	const processType = wsi4.node.processType(wsi4.graph.targets(vertex)[0]);
	if (processType !== "laserSheetCutting") {
		return wsi4.throwError("sheetNodeCuttingGas(): Target has wrong ProcessType \"" + processType + "\"");
	}
	const gas = nodeLaserSheetCuttingGasId(wsi4.graph.targets(vertex)[0]);
	if (gas === undefined) {
		return wsi4.throwError("sheetNodeCuttingGas(): Missing gas");
	}

	// Note:  When this script is evaluated the graph might be in an "inconsistent" intermediate state.
	// Example:  A sheet's target nodes might have different cutting gases.
	//           Splitting of the common sheet source is performed as a result of this script.
	//           => The following assertion does not hold at the time this script is evaluated.
	/*
	assertDebug(
		() => wsi4.graph.targets(vertex)
			.every(v => nodeLaserSheetCuttingGasId(v) === gas),
		"Expecting consistent cutting gases for each target node",
	);
        */

	return gas;
}

/**
 * Compute processId of targets of sheet node
 *
 * @param vertex  A sheet node
 */
function sheetNodeTargetProcessId(vertex: Vertex): string {
	if (wsi4.node.workStepType(vertex) !== "sheet") {
		return wsi4.throwError("sheetNodeTargetProcessId(): Vertex has wrong workStepType \"" + wsi4.node.workStepType(vertex) + "\"");
	}
	if (wsi4.graph.targets(vertex).length < 1) {
		return wsi4.throwError("sheetNodeTargetProcessId(): Missing targets");
	}
	return wsi4.node.processId(wsi4.graph.targets(vertex)[0]);
}

function targetFitsOnSheet(target: Vertex, source: Vertex): boolean {
	if (wsi4.node.workStepType(target) !== "sheetCutting") {
		return wsi4.throwError("targetFitsOnSheet(): Target has wrong workStepType '" + wsi4.node.workStepType(target) + "\"");
	}
	if (wsi4.node.workStepType(source) !== "sheet") {
		return wsi4.throwError("targetFitsOnSheet(): Source has wrong workStepType \"" + wsi4.node.workStepType(source) + "\"");
	}
	const tTwoDimRep = wsi4.node.twoDimRep(target);
	if (tTwoDimRep === undefined) {
		return wsi4.throwError("targetFitsOnSheet(): Missing twoDimRep of target");
	}

	const partBb = wsi4.cam.util.boundingBox2(tTwoDimRep);
	const sheets = computeAllowedSheets(source);
	const nestingDistance = getSettingOrDefault("sheetNestingDistance");
	return sheets.some(sheet => {
		const partX = nestingDistance + bbDimensionX(partBb);
		const partY = nestingDistance + bbDimensionY(partBb);
		const sheetX = sheet.dimX;
		const sheetY = sheet.dimY;
		return partX <= sheetX && partY <= sheetY || partY <= sheetX && partX <= sheetY;
	});
}

/**
 * Check, if `source` could be a source vertex for `target`
 *
 * It can be a source vertex, if the following are true:
 *  - Merging is enabled (local setting `sheetNodeMergingEnabled`)
 *  - Process ids of targets of `source` are the same as process id of `target`
 *  - materials are the same
 *  - the thicknesses of `source` and its targets are equal to the thickness of `target`
 *  - if `target`'s [[ProcessType]] is `laserSheetCutting`: the cutting gases are the same
 */
function isSource(target: Vertex, source: Vertex): boolean {
	if (wsi4.node.workStepType(target) !== "sheetCutting") {
		return wsi4.throwError("isSource(): Target has wrong workStepType '" + wsi4.node.workStepType(target) + "\"");
	}
	if (wsi4.node.workStepType(source) !== "sheet") {
		return wsi4.throwError("isSource(): Source has wrong workStepType \"" + wsi4.node.workStepType(source) + "\"");
	}

	if (!getSharedDataEntry("sheetMergingEnabled")) {
		return false;
	}
	if (!targetFitsOnSheet(target, source)) {
		return false;
	}
	if (wsi4.node.processId(target) !== sheetNodeTargetProcessId(source)) {
		return false;
	}
	if (material(target) !== material(source)) {
		return false;
	}
	if (wsi4.node.processType(target) === "laserSheetCutting") {
		const targetGas = nodeLaserSheetCuttingGasId(target);
		if (targetGas === undefined) {
			return wsi4.throwError("isSource(): Missing laserCuttingGas");
		}
		if (targetGas !== sheetNodeCuttingGas(source)) {
			return false;
		}
	}
	return sameThickness(target, source);
}

export function findSheetCuttingSource(sheetCuttingVertex: Vertex): Vertex|undefined {
	const currentSources = wsi4.graph.sources(sheetCuttingVertex);
	const currentSheetVertex = (() => {
		if (currentSources.length === 1) {
			return currentSources[0];
		} else {
			return undefined;
		}
	})();

	const unrelatedMatchingSheet = wsi4.graph.vertices()
		.filter(vertex => wsi4.node.workStepType(vertex) === WorkStepType.sheet)
		.filter(vertex => !isEqual(vertex, currentSheetVertex))
		.find(sheetVertex => isSource(sheetCuttingVertex, sheetVertex));

	if (unrelatedMatchingSheet !== undefined) {
		return unrelatedMatchingSheet;
	} else if (currentSheetVertex !== undefined) {
		const relatedSheetCuttingVertices = wsi4.graph.targets(currentSheetVertex)
		// When an existing graph is modified (e.g. bulk-modification of sheet material) then there can be valid cases where
		// one or more target nodes of the (now) common sheet source are in processing state CreateWorkStep when this function
		// is reached.
		// Example:  Material of both a bend-laser-part and a laser-part are changed.
		//           For both vertices the state changes to CreateSources, but only in case of the laser-part this function is reached
		//           immediately.
		//           For the bend-laser-part the then current laser-source-node changes to CreateWorkStep.
		//           Depending on the order of results for the first set of launched futures this function might be reached for
		//           the laser-part's CreateSources result after the bend-laser-part's CreateSources future has been finished.
		//           Hence, there can be at least one node in state CreateWorkStep even though this wasn't the case when the
		//           laser-part's CreateSources future has been launched.
			.filter(vertex => wsi4.node.workStepType(vertex) !== WorkStepType.undefined);
		if (relatedSheetCuttingVertices.every(vertex => isSource(vertex, currentSheetVertex))) {
			return currentSheetVertex;
		} else {
			return undefined;
		}
	} else {
		return undefined;
	}
}
