import {
	TableType,
	TubeProfileGeometryType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	isTubeProfileGeometryCircular,
	isTubeProfileGeometryRectangular,
} from "qrc:/js/lib/generated/typeguard";
import { front } from "./array_util";
import {
	addToGraph,
	changeNodeProcessId,
	changeNodesUserData,
	changeNodeUserData,
	changeProcessIds,
} from "./graph_manipulator";
import {
	createCircTubeProfilePath,
	createRectTubeProfilePath,
} from "./part_creation";
import {
	findActiveProcess,
	workStepTypeMap,
} from "./process";
import {
	getTable,
	parseTubeProfileGeometry,
} from "./table_utils";
import {
	createUpdatedNodeUserData,
	isCompatibleToNodeUserDataEntry,
} from "./userdata_utils";
import {
	assert,
	isEqual,
	isString,
} from "./utils";

/**
 * Change material of all nodes that contain this UserDataEntry
 *
 * @param sheetMaterial The new material
 * @returns true if successful
 */
export function changeMaterialGlobally(sheetMaterial: SheetMaterial): boolean {
	return changeNodesUserData(wsi4.graph.vertices()
		.filter(vertex => isCompatibleToNodeUserDataEntry("sheetMaterialId", vertex))
		.map(vertex => ({
			vertex: vertex,
			userData: createUpdatedNodeUserData("sheetMaterialId", vertex, sheetMaterial.identifier),
		})));
}

/**
 * Add a user-defined tube node to the current graph
 *
 * Pre-condition:  tubeId is valid
 * Pre-condition:  tube's profile-id is valid
 * Pre-condition:  There is an active process of type tubeCutting
 *
 * @param tubeId Id of the associated tube
 * @param dimX Target length of the underlying tube
 * @param name Target name of the underlying assembly
 * @param processId Id of the target process (if any)
 * @returns True if (1) node has been added successfully and (2) user data have been updated successfully
 */
export function addUserDefinedTubeNode(tubeId: string, dimX: number, name: string, processId?: string): boolean {
	const tube = getTable(TableType.tube)
		.find(row => row.identifier === tubeId);
	assert(tube !== undefined, "Tube identifier invalid:  " + tubeId);

	const iop = (() => {
		const profile = getTable(TableType.tubeProfile)
			.find(row => row.identifier === tube.tubeProfileId);
		assert(profile !== undefined, "Expecting matching profile");

		const [
			type,
			geometry,
		] = parseTubeProfileGeometry(profile.geometryJson);

		switch (type) {
			case TubeProfileGeometryType.circular: {
				assert(isTubeProfileGeometryCircular(geometry), "Tube profile geometry inconsistent");
				return wsi4.geo.util.createIop(createCircTubeProfilePath(geometry));

			}
			case TubeProfileGeometryType.rectangular: {
				assert(isTubeProfileGeometryRectangular(geometry), "Tube profile geometry inconsistent");
				return wsi4.geo.util.createIop(createRectTubeProfilePath(geometry));

			}
		}
	})();
	assert(iop !== undefined, "Could not create tube profile IOP");

	const processMapping = getTable(TableType.tubeCuttingProcessMapping)
		.find(row => row.tubeMaterialId === tube.tubeMaterialId && (processId === undefined || row.processId === processId));
	assert(processMapping !== undefined, "Expecting at least one valid tube cutting process mapping");

	const process = getTable(TableType.process)
		.find(row => row.identifier === processMapping.processId);
	assert(process !== undefined, "Expecting associated process");

	const vertex = addExtrusionWithProcess(
		iop,
		dimX,
		process,
		name,
	);
	if (vertex === undefined) {
		return false;
	}
	assert(wsi4.node.workStepType(vertex) === WorkStepType.tubeCutting, "Expecting tubeCutting for generated tube node");

	let userData = wsi4.node.userData(vertex);
	userData = createUpdatedNodeUserData("tubeMaterialId", vertex, tube.tubeMaterialId, userData);
	userData = createUpdatedNodeUserData("tubeSpecificationId", vertex, tube.tubeSpecificationId, userData);

	return changeNodeUserData(vertex, userData);
}

export function enforceSheetParts(vertices: Vertex[]): void {
	const verticesWithProcessIds = vertices.map(vertex => {
		const availableWsts = wsi4.node.checkWorkStepAvailability(vertex, [
			WorkStepType.sheetBending,
			WorkStepType.sheetCutting,
		]);
		if (availableWsts.length === 0) {
			return wsi4.throwError("Workstep must be convertible to sheetBending or sheetCutting");
		}
		const wst = front(availableWsts);
		const process = findActiveProcess(p => workStepTypeMap[p.type] === wst);
		if (process === undefined) {
			return wsi4.throwError("No process available for workStepType " + wst);
		}
		return {
			vertex: vertex,
			processId: process.identifier,
		};
	});
	changeProcessIds(verticesWithProcessIds);
}

export interface ChangeNodeProcessAndUserDataArg {
	vertex: Vertex
	processId: string;
	userData: StringIndexedInterface;
}

export function changeNodesProcessAndUserData(vertexArgs: readonly Readonly<ChangeNodeProcessAndUserDataArg>[]): boolean {
	const rootIdArgs = vertexArgs.map(arg => ({
		rootId: wsi4.node.rootId(arg.vertex),
		processId: arg.processId,
		userData: arg.userData,
	}));

	const rootIdsWithChangedProcess: GraphNodeRootId[] = [];
	for (const arg of rootIdArgs) {
		const vertex = wsi4.node.vertexFromRootId(arg.rootId);
		assert(vertex !== undefined, "Expecting valid vertex");
		const success = changeNodeProcessId(vertex, arg.processId);
		if (success) {
			rootIdsWithChangedProcess.push(arg.rootId);
		}
	}
	const success0 = rootIdsWithChangedProcess.length === vertexArgs.length;

	const verticesWithUserData = rootIdArgs.filter(arg => rootIdsWithChangedProcess.some(rootId => isEqual(arg.rootId, rootId)))
		.map(arg => {
			const vertex = wsi4.node.vertexFromRootId(arg.rootId);
			assert(vertex !== undefined, "Expecting valid vertex");
			return {
				vertex: vertex,
				userData: arg.userData,
			};
		});
	const success1 = changeNodesUserData(verticesWithUserData);

	return success0 && success1;
}

function generateUniqueImportId(): string {
	const existingIds = wsi4.graph.vertices()
		.map(v => wsi4.node.importId(v))
		.filter((id): id is string => isString(id));

	const prefix = "generated_";
	let suffix = 0;

	const buildId = () => prefix + suffix.toFixed(0);

	let result = buildId();
	while (existingIds.some(id => id === result)) {
		suffix += 1;
		result = buildId();
	}

	return result;
}

export function addExtrusion(iop: InnerOuterPolygon, depth: number, assemblyName?: string): Vertex | undefined {
	const importId = generateUniqueImportId();

	const content: GraphCreatorInputExtrusion = {
		importId: importId,
		innerOuterPolygon: iop,
		depth: depth,
		assemblyName: assemblyName ?? "",
		multiplicity: 1,
	};

	addToGraph([
		{
			type: "extrusion",
			content: content,
		},
	]);

	return wsi4.graph.vertices().find(v => wsi4.node.importId(v) === importId);
}

/**
 * Currently there is no guarantee that the generated node's underlying WorkStep and the submitted process are consistent.
 *
 * For corner cases (e.g. in case a geometry is both a legal sheet metal part and a tube) this would potentially require:
 * 	- Enforcing of WST for the extruded geometry
 * 	- Influence on the selection of the wscad::Part (currently the "best" Part is picked - other Parts are discarded)
 */
export function addExtrusionWithProcess(iop: InnerOuterPolygon, depth: number, process: Readonly<Process>, assemblyName?: string): Vertex | undefined {
	const rootId = (() => {
		const vertex = addExtrusion(iop, depth, assemblyName);
		if (vertex === undefined) {
			return undefined;
		}
		return wsi4.node.rootId(vertex);
	})();

	if (rootId === undefined) {
		return undefined;
	}

	{
		const vertex = wsi4.node.vertexFromRootId(rootId);
		assert(vertex !== undefined);
		changeNodeProcessId(vertex, process.identifier);
	}

	const vertex = wsi4.node.vertexFromRootId(rootId);
	assert(vertex !== undefined);
	return vertex;
}
