import {
	Color,
	ProcessType,
	StrokeStyle,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	add2,
	normalize2,
	scale2,
	squareNorm2,
	sub2,
} from "./geometry_utils";
import {
	computeNestingFractions,
	sheetIdForVertex,
} from "./graph_utils";
import {
	cadFeatureTwoDimCentroid,
	computeLowerDieAffectDistanceMap,
	computeTappingCandidates,
	mappedBendMaterialId,
	mappedSheetCuttingMaterialId,
	nestingTargetBoxForVertex,
} from "./node_utils";
import {
	getSharedDataEntry,
} from "./shared_data";
import {
	addSheetTappingNumbersToScene,
	addSheetTappingTextToScene,
	TappingSceneSelectionEntry,
} from "./sheet_tapping_utils";
import {
	getTable,
} from "./table_utils";
import {
	tubeCuttingLayerDescriptor,
} from "./tubecutting_util";
import {
	accessAllUserData,
	collectNodeUserDataEntries,
	nodeUserDatumOrDefault,
} from "./userdata_utils";
import {
	assert,
	assertDebug,
	bbDimensionX,
	bbDimensionY,
} from "./utils";

interface SceneConfig
{
	bendLineShowLabels: boolean;
	bendLineShowAffectedSegments: boolean;
	sheetTappingLabelMode: "names"|"indices";
	tubeCuttingShowTubeContours: boolean;
	tubeCuttingShowVirtualCuts: boolean;
}

function completeSceneConfig(config: Partial<SceneConfig>): SceneConfig {
	return {
		bendLineShowLabels: config.bendLineShowLabels ?? false,
		bendLineShowAffectedSegments: config.bendLineShowAffectedSegments ?? false,
		sheetTappingLabelMode: config.sheetTappingLabelMode ?? "names",
		tubeCuttingShowTubeContours: config.tubeCuttingShowTubeContours ?? false,
		tubeCuttingShowVirtualCuts: config.tubeCuttingShowVirtualCuts ?? false,
	};
}

function generateUniqueSceneIdentifier(vertex: Vertex): string {
	const r = wsi4.util.toKey(wsi4.node.rootId(vertex));
	return (new Date())
		.toISOString() + "_" + r + "_";
}

function computeSceneSceneData(vertex: Vertex): SceneSceneData {
	// FIXME: Handle tube material (see #2405)
	const materialIds = collectNodeUserDataEntries("sheetMaterialId", vertex, accessAllUserData);
	const globalMaterial = (() => {
		if (materialIds.length !== 1) {
			return "";
		}
		return materialIds[0]!;
	})();
	const material = (() => {
		const materialIds = collectNodeUserDataEntries("sheetMaterialId", vertex, accessAllUserData);
		if (materialIds.length !== 1) {
			return "";
		}
		const materialId = materialIds[0]!;
		const workStepType = wsi4.node.workStepType(vertex);
		const mappedMaterialId = workStepType === WorkStepType.sheetBending ? mappedBendMaterialId(materialId)
			: workStepType === WorkStepType.sheetCutting ? mappedSheetCuttingMaterialId(materialId)
				: materialId;
		return mappedMaterialId ?? "";
	})();
	const thickness = wsi4.node.sheetThickness(vertex) ?? 0.;
	return {
		// Note: must conform to wscam's interface
		material: material,
		thickness: thickness,
		identifier: generateUniqueSceneIdentifier(vertex),
		comment: "",
		globalMaterial: globalMaterial,
	};
}

function styleForLayer(layer: Layer): SceneStyle {
	const colorMap = [
		Color.closedContour,
		Color.closedContour,
		Color.engraving,
		Color.green,
		Color.cyan,
		Color.blue,
		Color.magenta,
		Color.white,
		Color.red,
	];
	const color = (layer.number >= 0 && layer.number < colorMap.length) ? colorMap[layer.number]! : Color.closedContour;
	const style: SceneStyle = {
		strokeWidth: 1,
		strokeColor: color,
	};
	return style;
}

function addLayeredToScene(scene: Scene, layered: Layered, ldsToSkip: readonly number[]): Scene {
	wsi4.geo.util.layers(layered)
		.filter(layer => ldsToSkip.every(ld => ld !== layer.descriptor))
		.forEach(layer => {
			const style = styleForLayer(layer);
			scene = wsi4.geo.util.addLayersToScene(scene, layered, [ layer.descriptor ], style);
		});
	return scene;
}

/**
 * If configured, split bend line segments in case they exceed a certain threshold
 *
 * For an engravingLength of 0 the bend line segments are not split at all.
 * For an engravingLength > 0 the bend line segments are split in case
 * a segment's length exceeds 4 x engravingLength.
 */
function splitBendLineSegmentsIfRequired(inputSegments: Segment[], engravingLength: number): Segment[] {
	const result: Segment[] = [];
	inputSegments.forEach(segment => {
		const from = segment.content.from;
		const to = segment.content.to;
		const u = sub2(to, from);
		const l = Math.sqrt(squareNorm2(u));
		if (engravingLength === 0 || l < 4 * engravingLength) {
			result.push(segment);
		} else {
			const v = scale2(engravingLength, normalize2(u));
			result.push({
				type: "line",
				content: {
					from: from,
					to: add2(from, v),
				},
			});
			result.push({
				type: "line",
				content: {
					from: sub2(to, v),
					to: to,
				},
			});
		}
	});
	return result;
}

function addBendLineEngravingsOnDemand(vertex: Vertex, baseScene: Scene): Scene {
	assertDebug(() => wsi4.node.workStepType(vertex) === "sheetCutting" || wsi4.node.workStepType(vertex) === "sheetBending", "Pre-condition violated");
	const engravingMode = nodeUserDatumOrDefault("bendLineEngravingMode", vertex);
	if (engravingMode === "none") {
		return baseScene;
	}

	const bendLineData = wsi4.node.computeBendLineData(vertex);
	assert(bendLineData !== undefined, "Expecting valid bend line data for sheet bending node");

	const maxEngravingLength = getSharedDataEntry("bendLineEngravingLength");
	const addBendLineEngravings = (scene: Scene, strokeStyle: StrokeStyle, pred: (bld: BendLineData) => boolean) => {
		const segments: Segment[] = [];
		bendLineData.filter(bld => pred(bld)).forEach(bld => {
			segments.push(...splitBendLineSegmentsIfRequired(bld.segments, maxEngravingLength));
		});
		return wsi4.geo.util.addSegmentsToScene(scene, segments, {
			strokeColor: Color.engraving,
			strokeWidth: 1,
			strokeStyle: strokeStyle,
			// Bend lines currently have a z-value of 3
			zValue: 4,
		});
	};

	switch (engravingMode) {
		case "upwardOnly": {
			return addBendLineEngravings(baseScene, StrokeStyle.continuous, bld => bld.bendAngle >= 0.);
		}
		case "downwardOnly": {
			return addBendLineEngravings(baseScene, StrokeStyle.dashed, bld => bld.bendAngle < 0.);
		}
		case "all": {
			let scene = baseScene;
			scene = addBendLineEngravings(scene, StrokeStyle.continuous, bld => bld.bendAngle >= 0.);
			scene = addBendLineEngravings(scene, StrokeStyle.dashed, bld => bld.bendAngle < 0.);
			return scene;
		}
	}
}

/**
 * Array of SceneObjectData, indexed by BendDescriptor:s.
 */
function computeBendSceneObjectDatas(vertex: Vertex): Array<SceneObjectData> {
	const dieChoiceArray = wsi4.node.dieChoiceMap(vertex);
	if (dieChoiceArray === undefined) {
		wsi4.util.error("computeBendSceneObjectDatas(): dieChoiceArray missing");
		return [];
	}
	const bendLineDataArray = wsi4.node.computeBendLineData(vertex);
	if (!bendLineDataArray) {
		wsi4.util.error("computeBendSceneObjectDatas(): bendLineData missing");
		return [];
	}
	if (dieChoiceArray.length !== bendLineDataArray.length) {
		wsi4.util.error("computeBendSceneObjectDatas(): inconsistent bend data");
		return [];
	}

	const upperDieGroups = getTable(TableType.upperDieGroup);
	const lowerDieGroups = getTable(TableType.lowerDieGroup);

	return dieChoiceArray.map(entry => {
		const bendLineData = bendLineDataArray.find(bld => bld.bendDescriptor === entry.bendDescriptor);
		if (bendLineData === undefined) {
			wsi4.util.info(JSON.stringify(bendLineDataArray));
			wsi4.throwError("Cannot find BendDescriptor " + entry.bendDescriptor.toString() + " in bendLineData");
		}

		const getExportId = (table: readonly Readonly<UpperDieGroup|LowerDieGroup>[], id: string) => {
			const entry = table.find(row => row.identifier === id);
			return entry === undefined ? "" : entry.exportIdentifier;
		};

		return {
			zValue: 1,
			upperDieGroup: getExportId(upperDieGroups, entry.bendDieChoice.upperDieGroupId),
			lowerDieGroup: getExportId(lowerDieGroups, entry.bendDieChoice.lowerDieGroupId),
			bendAngle: bendLineData.bendAngle,
			innerRadius: entry.bendDieChoice.baseClass.innerRadius,
			sharpDeduction: entry.bendDieChoice.sharpDeduction,
		};
	});
}

function createBendSpecificScene(vertex: Vertex, config: Readonly<SceneConfig>, scene: Scene): Scene {
	const bendLineData = wsi4.node.computeBendLineData(vertex);
	assert(bendLineData !== undefined, "Expecting valid bend line data");

	const objectDatas = computeBendSceneObjectDatas(vertex);
	scene = wsi4.geo.util.addBendLinesToScene(scene, config.bendLineShowLabels, bendLineData, objectDatas);
	scene = addBendLineEngravingsOnDemand(vertex, scene);
	if (config.bendLineShowAffectedSegments) {
		const twoDimRep = wsi4.node.twoDimRep(vertex);
		assert(twoDimRep !== undefined, "Expecting valid TwoDimRepresentation");

		const affectDistanceMap = computeLowerDieAffectDistanceMap(vertex);
		const segments = wsi4.cam.bend.affectedSegments(twoDimRep, affectDistanceMap);
		scene = wsi4.geo.util.addSegmentsToScene(scene, segments, {
			strokeWidth: 2,
			strokeColor: Color.blue,
		});

		const a = wsi4.cam.util.extractOverlappingAreas(twoDimRep);
		if (a.length !== 0) {
			const style: SceneStyle = {
				strokeWidth: 1,
				strokeColor: Color.magenta,
			};
			scene = wsi4.geo.util.addInnerOuterPolygonsToScene(scene, a, style);
		}
	}

	return scene;
}

function createSheetTappingSpecificScene(vertex: Vertex, config: SceneConfig, scene: Scene): Scene {
	assertDebug(() => wsi4.node.processType(vertex) === "sheetTapping", "Pre-condition violated");

	// For now legacy graphs are supported where an 2D input sheet parts have no underlying part.
	// In these cases sheet tapping is not supported.
	if (wsi4.node.part(vertex) === undefined) {
		wsi4.util.warn("Sheet tapping not supported for loaded node.");
		return scene;
	}

	assertDebug(() => wsi4.node.part(vertex) !== undefined, "Pre-condition violated");

	switch (config.sheetTappingLabelMode) {
		case "names": {
			const screwThreads = getTable("screwThread");
			const entries: TappingSceneSelectionEntry[] = nodeUserDatumOrDefault("sheetTappingData", vertex)
				.map(entry => {
					const screwThread = screwThreads.find(row => row.identifier === entry.screwThread.identifier);
					assert(screwThread !== undefined, "Expecting valid screw thread");
					return {
						cadFeature: entry.cadFeature,
						screwThread: screwThread,
						center2: cadFeatureTwoDimCentroid(vertex, entry.cadFeature),
					};
				});
			return addSheetTappingTextToScene(scene, entries);
		}
		case "indices": {
			const candidates = computeTappingCandidates(vertex);
			return addSheetTappingNumbersToScene(scene, candidates);
		}
	}
}

function sheetScene(vertex: Vertex, twoDimRep: TwoDimRepresentation, emptyScene: Scene, style: SceneStyle) : Scene {
	assert(wsi4.node.processType(vertex) === ProcessType.sheet, "Expecting sheet vertex.");
	const table = getTable(TableType.sheet);
	const sheetId = sheetIdForVertex(vertex, table);
	if (sheetId === undefined) {
		// Handling gracefully here to make scene computation more robust for incomplete graphs.
		return emptyScene;
	}
	const targetBox = nestingTargetBoxForVertex(vertex);
	assert(targetBox !== undefined, "Execting valid target box");
	// Sheet modulus table entry is not mandatory.
	// Using neutral default value as fallback.
	const sheetModulusRow: Readonly<{
		xModulus: number,
		yModulus: number,
		applyToAll: boolean,
	}> = getTable(TableType.sheetModulus)
		.find(row => row.sheetId === sheetId) ?? {
		xModulus: 0,
		yModulus: 0,
		applyToAll: true,
	};
	const nestingFractions = computeNestingFractions(twoDimRep, targetBox, sheetModulusRow);

	const targetBoxDimX = bbDimensionX(targetBox);
	const targetBoxDimY = bbDimensionY(targetBox);

	const nestingDissections = nestingFractions.map((fraction): NestingDissection => {
		const d:NestingDissection = {
			nestingDescriptor: fraction.nestingDescriptor,
		};
		if (fraction.effectiveDimX < targetBoxDimX) {
			d.xDissection = fraction.effectiveDimX;
		}
		if (fraction.effectiveDimY < targetBoxDimY) {
			d.yDissection = fraction.effectiveDimY;
		}
		return d;
	});
	const entry = table.find(row => row.identifier === sheetId);
	assert(entry !== undefined);
	const extraText = entry.name + (entry.description.length === 0 ? "" : " (" + entry.description + ")");
	return wsi4.cam.util.addNestingToScene(emptyScene, twoDimRep, nestingDissections, extraText, style);
}

/**
 * Create a [[Scene]] representing a given vertex's 2d view.
 *
 * @param vertex The Vertex to generate the Scene for
 * @param partialConfig Configuration for the scene
 * @returns The 2d representation of vertex in a Scene
 */
export function sceneForVertex(vertex: Vertex, partialConfig: Partial<SceneConfig>): Scene {
	const config = completeSceneConfig(partialConfig);
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	const layered = wsi4.node.layered(vertex);
	assert((twoDimRep === undefined) !== (layered === undefined), "Expecting either layered or twoDimRep");

	const emptyScene = (() => {
		const sceneSceneData = computeSceneSceneData(vertex);
		return wsi4.geo.util.createScene(sceneSceneData);
	})();

	const processType = wsi4.node.processType(vertex);

	const baseScene = (() => {
		if (twoDimRep !== undefined) {
			const style: SceneStyle = {
				strokeWidth: 1,
				strokeColor: Color.closedContour,
			};
			if (processType === ProcessType.sheet) {
				return sheetScene(vertex, twoDimRep, emptyScene, style);
			} else {
				const iops = wsi4.cam.util.extractInnerOuterPolygons(twoDimRep);
				return wsi4.geo.util.combineScenes([
					wsi4.geo.util.addInnerOuterPolygonsToScene(emptyScene, iops, style),
					wsi4.cam.util.extractEngravings(twoDimRep),
				]);
			}
		} else {
			assert(layered !== undefined);
			if (processType === ProcessType.tubeCutting) {
				const ldsToSkip = (() => {
					if (config.tubeCuttingShowTubeContours) {
						return [];
					} else {
						return [ tubeCuttingLayerDescriptor("tubeOutlines", layered) ];
					}
				})();
				return addLayeredToScene(emptyScene, layered, ldsToSkip);
			} else {
				return addLayeredToScene(emptyScene, layered, []);
			}
		}
	})();

	if (processType === ProcessType.sheetCutting || processType === ProcessType.laserSheetCutting) {
		return addBendLineEngravingsOnDemand(vertex, baseScene);
	} else if (processType === ProcessType.dieBending) {
		return createBendSpecificScene(vertex, config, baseScene);
	} else if (processType === ProcessType.sheetTapping) {
		return createSheetTappingSpecificScene(vertex, config, baseScene);
	} else {
		return baseScene;
	}
}

/**
 * Create a [[Scene]] representing a given vertex's 2d view.
 *
 * @param vertex The Vertex to generate the Scene for
 * @returns The 2d representation of vertex in a Scene
 *
 * Convenience function utilizing the default scene config.
 */
export function defaultSceneForVertex(vertex: Vertex): Scene {
	return sceneForVertex(vertex, {});
}
