/*
 * Functions that are called when adding new data to wsi4
 *
 * Note: All API calls must conform to DocumentGraphHandler's script engine
 */

import {
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	isLaserSheetCuttingGasUniqueMembers,
	isPoint3,
	isScrewThreadUniqueMembers,
	isSheetMaterialUniqueMembers,
	isSheetTappingDataEntry,
	isStringIndexedInterface,
	isTubeMaterialUniqueMembers,
	isTubeSpecificationUniqueMembers,
} from "qrc:/js/lib/generated/typeguard";
import {
	migrateDeducedData,
} from "qrc:/js/lib/deduceddata_utils";
import {
	createUpdatedNodeUserDataImpl,
	nodeUserDatumImpl,
} from "qrc:/js/lib/userdata_config";
import {
	createUpdatedNodeUserData,
	isCompatibleToNodeUserDataEntry,
	nodeUserDatumOrDefault,
	UserData,
} from "qrc:/js/lib/userdata_utils";
import {
	assert,
	assertDebug,
	isArray,
	isBoolean,
	isInstanceOf,
	isNumber,
	isString,
} from "qrc:/js/lib/utils";
import {
	computeTappingCandidates,
} from "qrc:/js/lib/node_utils";
import {
	point3To2,
	squareNorm2,
	sub2,
} from "qrc:/js/lib/geometry_utils";
import {
	front,
} from "qrc:/js/lib/array_util";
import {
	getArticleSignatureVertex,
} from "qrc:/js/lib/graph_utils";

interface UserDataMapEntry {
	vertex: Vertex;
	userData: UserData;
}

function migrateTestReport(inputVertex: Vertex, inputUserData: Readonly<StringIndexedInterface>): StringIndexedInterface {
	const testReportKey = "testReportRequired";
	if (!isCompatibleToNodeUserDataEntry("testReportRequired", inputVertex) || isBoolean(inputUserData[testReportKey])) {
		return inputUserData;
	} else {
		const sheetVertex = wsi4.graph.reaching(inputVertex)
			.find(vertex => wsi4.node.workStepType(vertex) === WorkStepType.sheet);
		if (sheetVertex === undefined) {
			return inputUserData;
		}

		const sheetNodeUserData = wsi4.node.userData(sheetVertex);
		if (isBoolean(sheetNodeUserData[testReportKey])) {
			const result: StringIndexedInterface = Object.assign({}, inputUserData);
			result[testReportKey] = sheetNodeUserData[testReportKey];
			return result;
		} else {
			return inputUserData;
		}
	}
}

function migrateCalcParams(vertex: Vertex, userData: StringIndexedInterface): StringIndexedInterface {
	if (nodeUserDatumImpl("userDefinedMaterialCostsPerPiece", userData) === undefined) {
		const materialCostsPerPiece = userData["materialCostsPerPiece"] ?? userData["materialCosts"] ?? userData["price"];
		if (isNumber(materialCostsPerPiece)) {
			userData = createUpdatedNodeUserData("userDefinedMaterialCostsPerPiece", vertex, materialCostsPerPiece, userData);
		}
	}

	if (nodeUserDatumImpl("userDefinedSetupTime", userData) === undefined) {
		const setupTime = userData["setupTime"];
		if (isNumber(setupTime)) {
			userData = createUpdatedNodeUserData("userDefinedSetupTime", vertex, setupTime, userData);
		}
	}

	if (nodeUserDatumImpl("userDefinedUnitTimePerPiece", userData) === undefined) {
		const unitTimePerPiece = userData["unitTimePerPiece"] ?? userData["unitTime"];
		if (isNumber(unitTimePerPiece)) {
			userData = createUpdatedNodeUserData("userDefinedUnitTimePerPiece", vertex, unitTimePerPiece, userData);
		}
	}

	return userData;
}

function migrateOldKeysAndTypes(vertex: Vertex, userData: StringIndexedInterface): StringIndexedInterface {
	if (isCompatibleToNodeUserDataEntry("sheetMaterialId", vertex) && nodeUserDatumImpl("sheetMaterialId", userData) === undefined) {
		const obj = userData["material"];
		if (isSheetMaterialUniqueMembers(obj)) {
			userData = createUpdatedNodeUserDataImpl("sheetMaterialId", obj.identifier, userData);
		}
	}
	if (wsi4.node.workStepType(vertex) === "sheetCutting" && userData["laserSheetCuttingGasId"] === undefined) {
		const obj = userData["laserCuttingGas"];
		if (isLaserSheetCuttingGasUniqueMembers(obj)) {
			userData["laserSheetCuttingGasId"] = obj.identifier;
		}
	}
	if (isCompatibleToNodeUserDataEntry("tubeMaterialId", vertex) && nodeUserDatumImpl("tubeMaterialId", userData) === undefined) {
		const obj = userData["tubeMaterial"];
		if (isTubeMaterialUniqueMembers(obj)) {
			userData = createUpdatedNodeUserDataImpl("tubeMaterialId", obj.identifier, userData);
		}
	}
	if (isCompatibleToNodeUserDataEntry("tubeSpecificationId", vertex) && nodeUserDatumImpl("tubeSpecificationId", userData) === undefined) {
		const obj = userData["tubeSpecification"];
		if (isTubeSpecificationUniqueMembers(obj)) {
			userData = createUpdatedNodeUserDataImpl("tubeSpecificationId", obj.identifier, userData);
		}
	}
	return userData;
}

export function updateArticleUserData(): Array<UserDataMapEntry> {
	const result: UserDataMapEntry[] = [];
	wsi4.graph.articles().forEach(article => {
		if (isString(wsi4.node.articleUserData(article[0]!)["name"])) {
			return;
		}
		const vertex = getArticleSignatureVertex(article);
		const articleUserData = wsi4.node.userData(vertex)["articleUserData"];
		if (!isStringIndexedInterface(articleUserData)) {
			return;
		}
		result.push({
			vertex: vertex,
			userData: articleUserData,
		});
	});
	return result;
}

export function updateNodeUserData(): Array<UserDataMapEntry> {
	return wsi4.graph.vertices()
		.map(vertex => {
			let userData = wsi4.node.userData(vertex);
			userData = migrateTestReport(vertex, userData);
			userData = migrateOldKeysAndTypes(vertex, userData);
			userData = migrateCalcParams(vertex, userData);
			return {
				vertex: vertex,
				userData: userData,
			};
		});
}

interface SheetFilterMapEntry {
	sheetVertex: Vertex;
	sheetFilterSheetIds: string[];
}

export function migrateSheetFilterSheetIds(map: SheetFilterMapEntry[]): Array<UserDataMapEntry> {
	const userDataMapEntries: Array<UserDataMapEntry> = [];
	for (const entry of map) {
		assertDebug(() => wsi4.node.workStepType(entry.sheetVertex) === WorkStepType.sheet, "Wrong workStepType.");
		assert(entry.sheetFilterSheetIds !== undefined, "Missing sheet filter.");
		if (entry.sheetFilterSheetIds.length === 0) {
			continue;
		}
		for (const t of wsi4.graph.reachable(entry.sheetVertex)) {
			if (!isCompatibleToNodeUserDataEntry("sheetFilterSheetIds", t)) {
				continue;
			}
			const sheetFilterSheetIds = nodeUserDatumOrDefault("sheetFilterSheetIds", t);
			assert(sheetFilterSheetIds.length === 0);
			userDataMapEntries.push({
				vertex: t,
				userData: createUpdatedNodeUserData("sheetFilterSheetIds", t, entry.sheetFilterSheetIds),
			});
		}
	}
	return userDataMapEntries;
}

interface LegacySheetTappingDataEntry {
	center3: Point3;
	screwThread: ScrewThreadUniqueMembers;
}

function isLegacyTappingDataEntry(arg: unknown): arg is LegacySheetTappingDataEntry {
	return isInstanceOf<LegacySheetTappingDataEntry>(arg, {
		center3: isPoint3,
		screwThread: isScrewThreadUniqueMembers,
	});
}

/**
 * This is the second step of the migration of SheetTapping UserData.
 * The first step is the re-computation of the underlying wscad Parts as part of the file loading process.
 * (See fileloading.cpp)
 */
export function migrateSheetTappingUserData(): Array<UserDataMapEntry> {
	const legacyData = wsi4.graph.vertices()
		.filter(v => wsi4.node.processType(v) === "sheetTapping")
		.map((v): UserDataMapEntry => ({
			vertex: v,
			userData: wsi4.node.userData(v),
		}))
		.filter(entry => !isArray(entry.userData["sheetTappingData"], isSheetTappingDataEntry));

	const result: UserDataMapEntry[] = [];
	for (const entry of legacyData) {
		const oldData = entry.userData["sheetTappingData"];
		if (!isArray(oldData, isLegacyTappingDataEntry)) {
			// Not expected
			return [];
		}

		const cosys = wsi4.node.twoDimRepTransformation(entry.vertex);
		if (cosys === undefined) {
			// Not expected
			return [];
		}

		const candidates = computeTappingCandidates(entry.vertex);
		const newEntries: SheetTappingDataEntry[] = [];
		for (const oldEntry of oldData) {
			const center2 = point3To2(oldEntry.center3, cosys);
			candidates.sort((lhs, rhs) => squareNorm2(sub2(lhs.center2, center2)) - squareNorm2(sub2(rhs.center2, center2)));
			const tol = 1;
			if (candidates.length === 0 || squareNorm2(sub2(front(candidates).center2, center2)) > tol) {
				wsi4.util.warn("No matching sheet tapping candidate found for " + JSON.stringify(center2));
				return [];
			}

			const candidate = front(candidates);
			const screwThread = candidate.matchingScrewThreads.find(st => st.identifier === oldEntry.screwThread.identifier);
			if (screwThread === undefined) {
				wsi4.util.warn("No matching screw thread found for " + oldEntry.screwThread.identifier);
				return [];
			}

			newEntries.push({
				cadFeature: candidate.cadFeature,
				screwThreadId: screwThread.identifier,
			});
		}

		result.push({
			vertex: entry.vertex,
			userData: createUpdatedNodeUserData("sheetTappingData", entry.vertex, newEntries, entry.userData),
		});
	}
	return result;
}

export function updateDeducedData(): Array<UserDataMapEntry> {
	return wsi4.graph.vertices()
		.map(vertex => {
			let deducedData = wsi4.node.deducedData(vertex);
			// insert values if missing
			deducedData = migrateDeducedData(vertex, deducedData);
			return {
				vertex: vertex,
				userData: deducedData,
			};
		});
}
