// Note: All functions in this module must conform to DocumentGraphHandler's script engine API.
import {
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	DeducedData,
	getDeducedDataEntryOrThrow,
} from "qrc:/js/lib/deduceddata_utils";
import {
	nodeLaserSheetCuttingGasId,
} from "qrc:/js/lib/node_utils";
import {
	getSettingOrDefault,
} from "qrc:/js/lib/settings_table";
import {
	getSharedDataEntry,
} from "qrc:/js/lib/shared_data";
import {
	computeAllowedSheets,
} from "qrc:/js/lib/table_utils";
import {
	accessSelfAndReachable,
	collectNodeUserDataEntries,
	getNodeUserDataEntryOrThrow,
} from "qrc:/js/lib/userdata_utils";
import {
	assert,
	bbDimensionX,
	bbDimensionY,
	computeArrayIntersection,
	isEqual,
	thicknessEquivalence,
} from "qrc:/js/lib/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);
}

function targetFitsOnSheet(target: Vertex, source: Vertex, deducedDataOfSource: DeducedData): 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) + "\"");
	}

	if (!getDeducedDataEntryOrThrow("targetsFitOnSheet", source, deducedDataOfSource)) {
		// no sheet is allowed because targets are too big
		return false;
	}

	const tTwoDimRep = wsi4.node.twoDimRep(target);
	if (tTwoDimRep === undefined) {
		return wsi4.throwError("targetFitsOnSheet(): Missing twoDimRep of target");
	}

	const sheetSheetFilterSheetIds = getDeducedDataEntryOrThrow("sheetFilterSheetIds", source, deducedDataOfSource);
	const sheetCuttingSheetFilterSheetIds = getNodeUserDataEntryOrThrow("sheetFilterSheetIds", target);
	if (sheetSheetFilterSheetIds !== undefined && sheetCuttingSheetFilterSheetIds.length !== 0) {
		if (computeArrayIntersection([
			sheetCuttingSheetFilterSheetIds,
			sheetSheetFilterSheetIds,
		]).length === 0) {
			// no intersection between sheet filter, so don't merge them
			return false;
		}
	}

	const partBb = wsi4.cam.util.boundingBox2(tTwoDimRep);
	const nestingDistance = getSettingOrDefault("sheetNestingDistance");
	const dimX = bbDimensionX(partBb);
	const dimY = bbDimensionY(partBb);
	const minDimX = nestingDistance + Math.max(dimX, dimY);
	const minDimY = nestingDistance + Math.min(dimX, dimY);

	const sheetMaterial = getDeducedDataEntryOrThrow("sheetMaterial", source, deducedDataOfSource);
	const sheetThickness = wsi4.node.sheetThickness(source);
	assert(sheetThickness !== undefined, "targetFitsOnSheet(): Missing thickness");
	return computeAllowedSheets(sheetThickness, sheetMaterial.identifier, minDimX, minDimY).length > 0;
}

/**
 * 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 (!sameThickness(target, source)) {
		return false;
	}

	const deducedDataOfSource = wsi4.node.deducedData(source);
	// deduced data has to be filled
	assert(Object.keys(deducedDataOfSource).length > 0, "Deduced data is not filled for sheet node");

	const processId = getDeducedDataEntryOrThrow("sheetCuttingProcessId", source, deducedDataOfSource);
	if (wsi4.node.processId(target) !== processId) {
		return false;
	}

	const sheetMaterial = getDeducedDataEntryOrThrow("sheetMaterial", source, deducedDataOfSource);
	if (material(target) !== sheetMaterial.identifier) {
		return false;
	}

	const laserSheetCuttingGas = getDeducedDataEntryOrThrow("laserSheetCuttingGas", source, deducedDataOfSource);
	if (laserSheetCuttingGas !== undefined) {
		assert(wsi4.node.processType(target) === "laserSheetCutting", "isSource(): Wrong process type.");
		const targetGas = nodeLaserSheetCuttingGasId(target);
		assert(targetGas !== undefined, "isSource(): Missing laserCuttingGas");
		if (targetGas !== laserSheetCuttingGas.identifier) {
			return false;
		}
	}
	return targetFitsOnSheet(target, source, deducedDataOfSource);
}

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;
	}
}
