import {
	BendDieChoiceType,
	Feature,
	ProcessType,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";

import {
	tolerances,
} from "./constants";
import {
	getArticleName,
	getAssociatedSheetMaterialId,
	sheetIdForVertex,
} from "./graph_utils";
import {
	computeLowerDieAffectDistanceMapImpl,
	computeMaxBendLineNetLength,
	mappedSheetCuttingMaterialId,
	maxLowerDieOpeningWidth,
	nodeLaserSheetCuttingGasId,
	tubeForVertex,
} from "./node_utils";
import {getSettingOrDefault} from "./settings_table";
import {
	getTable,
} from "./table_utils";
import {
	isValidNodeUserDataValue,
	nodeUserDataEntries,
} from "./userdata_config";
import {
	getNodeUserDataEntryOrThrow,
	isCompatibleToNodeUserDataEntry,
} from "./userdata_utils";
import {
	assert,
	assertDebug,
	bbDimensionX,
	bbDimensionY,
	bbDimensionZ,
	computeBox3,
} from "./utils";

export interface Constraints {
	maxDimensions: boolean;
	sheetAvailability: boolean;
	minContourSize: boolean;
	dataConsistent: boolean;
	dataCompleteness: boolean;
	dataValidity: boolean;
	bendDie: boolean;
	// Sheet thickness of bend deduction table row does not match actual sheet thickness
	bendThickness: boolean;
	transportSource: boolean;
	userDefinedProcessId: boolean;
	maxBendLineNetLength: boolean;
	// Underlying sheet thickness too large
	maxSheetThickness: boolean;
	// Bend areas overlap
	bendAreasNotOverlapping: boolean;
	sheetMaterialAvailability: boolean;
	profileSupport: boolean;
	tubeAvailability: boolean;
	tubeNestingAvailability: boolean;
	// The tube cutting process must be able to process the associated tube (if any)
	tubeCuttingProcessCompatibility: boolean;
	// Enforce tube detection feature
	tubeDetectionLicensed: boolean;
	contourInBend: boolean;
	bendFlangeTooShort: boolean;
}

type Constraint = keyof Constraints;

function violatesProcessMaxDimensionConstraints(processId: string, bbox: Box3): boolean {
	const constraints = getTable(TableType.dimensionConstraints)
		.find(row => row.processId === processId);
	const lowerLimits = constraints === undefined ? [] : [
		constraints.minX,
		constraints.minY,
		constraints.minZ,
	].sort((lhs, rhs) => rhs - lhs);
	const upperLimits = constraints === undefined ? [] : [
		constraints.maxX,
		constraints.maxY,
		constraints.maxZ,
	].sort((lhs, rhs) => rhs - lhs);
	const dims = [
		bbDimensionX(bbox),
		bbDimensionY(bbox),
		bbDimensionZ(bbox),
	].sort((lhs, rhs) => rhs - lhs);

	// For sheet metal parts the are quite a few cases where the thickess deviation starts at the 4th decimal position (e.g. 1.0004000)
	const eps = 0.001;
	return lowerLimits.some((limit, index) => dims[index]! < limit - eps) || upperLimits.some((limit, index) => limit + eps < dims[index]!);
}

function violatesTwoDimRepDimConstraints(processId: string, twoDimRep: TwoDimRepresentation, thickness: number): boolean {
	return violatesProcessMaxDimensionConstraints(processId, computeBox3(twoDimRep, thickness));
}

function violatesVertexTwoDimRepDimConstraints(vertex: Vertex): boolean {
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	const thickness = wsi4.node.sheetThickness(vertex);
	return twoDimRep !== undefined
		&& thickness !== undefined
		&& wsi4.node.workStepType(vertex) !== WorkStepType.sheet
		&& violatesTwoDimRepDimConstraints(wsi4.node.processId(vertex), twoDimRep, thickness);
}

function violatesVertexInputAssemblyDimConstraints(vertex: Vertex): boolean {
	const assembly = (() => {
		const outputAsm = wsi4.node.assembly(vertex);
		return outputAsm === undefined ? wsi4.node.inputAssembly(vertex) : outputAsm;
	})();
	return assembly !== undefined && violatesProcessMaxDimensionConstraints(wsi4.node.processId(vertex), wsi4.geo.util.boundingBox3d(assembly));
}

export function violatesMaxDimensionConstraints(vertex: Vertex): boolean {
	// For nodes where the underlying geometry can be considered a "flat" (sheet-metal) part, the dimension constraint check is limited to
	// the respective TwoDimRepresentation.
	// This prevents potential false-positives due to axis aligned 3D bounding boxes for parts where the underlying 2D geometry is computed
	// via the unfold-logic.  In these cases a (rather) right 2D bounding box is computed implicitly since the TwoDimRepresentation has been
	// transformed accordingly at the time of its creation.

	const wst = wsi4.node.workStepType(vertex);
	if (wst === WorkStepType.sheet) {
		// Boundary check is obsolete since dimension related issues should lead to invalid nestings
		return false;
	} else if (wst === WorkStepType.sheetCutting) {
		return violatesVertexTwoDimRepDimConstraints(vertex);
	} else if (wst === WorkStepType.userDefined) {
		const processType = wsi4.node.processType(vertex);
		if (processType === ProcessType.automaticMechanicalDeburring
			|| processType === ProcessType.manualMechanicalDeburring
			|| processType === ProcessType.sheetTapping) {
			return violatesVertexTwoDimRepDimConstraints(vertex);
		} else {
			return violatesVertexTwoDimRepDimConstraints(vertex) || violatesVertexInputAssemblyDimConstraints(vertex);
		}
	} else {
		return violatesVertexTwoDimRepDimConstraints(vertex) || violatesVertexInputAssemblyDimConstraints(vertex);
	}
}

function checkConstraintsLaserSheetCutting(vertex: Vertex): Constraint[] {
	const sheetMaterialId = getAssociatedSheetMaterialId(vertex);
	const sheetCuttingMaterialId = mappedSheetCuttingMaterialId(sheetMaterialId);
	if (sheetCuttingMaterialId === undefined) {
		// Cannot check laser sheet cutting specific constraints without mapped material.
		// Note: Not considered an error at this point.
		//       This is covered by the constraint `sheetMaterialAvailability`.
		return [];
	}

	const result: Constraint[] = [];
	const materialGasCombinationCheck = (): Constraint[] => {
		const laserSheetCuttingGasId = nodeLaserSheetCuttingGasId(vertex);
		if (laserSheetCuttingGasId === undefined) {
			return wsi4.throwError("Laser sheet cutting gas id not available.");
		}
		// Both cutting speed and pierce time table are valid choices to check the consistency here
		const cuttingSpeedTable = getTable(TableType.laserSheetCuttingSpeed);
		if (cuttingSpeedTable.some(element => element.sheetCuttingMaterialId === sheetCuttingMaterialId && element.laserSheetCuttingGasId === laserSheetCuttingGasId)) {
			return [];
		}
		return [ "dataConsistent" ];
	};
	result.push(...materialGasCombinationCheck());

	const minContourSizeCheck = (): Constraint[] => {
		const thickness = wsi4.node.sheetThickness(vertex);
		if (thickness === undefined) {
			return wsi4.throwError("checkConstraintsLaserSheetCutting(): Thickness not available");
		}
		const twoDimRepId = wsi4.node.twoDimRep(vertex);
		if (twoDimRepId === undefined) {
			return wsi4.throwError("checkConstraintsLaserSheetCutting(): TwoDimRepresentation not available");
		}
		const iops = wsi4.cam.util.extractInnerOuterPolygons(twoDimRepId);
		if (iops === undefined) {
			return wsi4.throwError("checkConstraintsLaserSheetCutting(): InnerOuterPolygons not available");
		}
		const laserSheetCuttingGasId = getNodeUserDataEntryOrThrow("cuttingGas", vertex).identifier;
		const minAreaRow = Array.from(getTable(TableType.laserSheetCuttingMinArea))
			.sort((lhs, rhs) => (lhs.thickness < rhs.thickness ? -1 : lhs.thickness > rhs.thickness ? 1 : lhs.area < rhs.area ? -1 : lhs.area > rhs.area ? 1 : 0))
			.find(row => (row.sheetCuttingMaterialId === sheetCuttingMaterialId && laserSheetCuttingGasId === row.laserSheetCuttingGasId && thickness <= row.thickness));
		if (minAreaRow === undefined) {
			wsi4.util.warn(`minContourSizeCheck(): No min. contour size entry found in table "${TableType.laserSheetCuttingMinArea}" for thickness ${
				thickness}, sheetCuttingMaterialId "${sheetCuttingMaterialId}", and laserSheetCuttingGasId "${laserSheetCuttingGasId}"`);
			return [];
		}
		const invalidContourFound = iops.some(iop => (wsi4.geo.util.volume(wsi4.geo.util.outerPolygon(iop)) < minAreaRow.area || wsi4.geo.util.innerPolygons(iop)
			.some(p => wsi4.geo.util.volume(p) < minAreaRow.area)));
		if (invalidContourFound) {
			return [ "minContourSize" ];
		}
		return [];
	};
	result.push(...minContourSizeCheck());

	const maxThicknessCheck = (): Constraint[] => {
		const thickness = wsi4.node.sheetThickness(vertex);
		assert(thickness !== undefined, "Expecting valid thickness");
		const laserSheetCuttingGasId = getNodeUserDataEntryOrThrow("cuttingGas", vertex).identifier;
		const maxThicknessTableRow = getTable(TableType.laserSheetCuttingMaxThickness)
			.find(row => row.sheetCuttingMaterialId === sheetCuttingMaterialId && row.laserSheetCuttingGasId === laserSheetCuttingGasId);
		if (maxThicknessTableRow === undefined) {
			return [];
		} else if ((thickness - tolerances.thickness) <= maxThicknessTableRow.maxThickness) {
			return [];
		} else {
			return [ "maxSheetThickness" ];
		}
	};
	result.push(...maxThicknessCheck());
	return result;
}

function sheetConstraintViolated(vertex: Vertex): boolean {
	if (wsi4.node.workStepType(vertex) !== "sheet") {
		return true;
	}

	const twoDimRep = wsi4.node.twoDimRep(vertex);
	if (twoDimRep === undefined) {
		return true;
	}

	const sheetId = sheetIdForVertex(vertex);
	return sheetId === undefined;
}

function checkConstraintsSheetCutting(vertex: Vertex): Constraint[] {
	const result: Constraint[] = [];
	const sheetAvailabilityCheck = (): Constraint[] => {
		const sources = wsi4.graph.sources(vertex);
		if (sources.length !== 1) {
			// Expecting one source of WorkStepType sheet
			return [ "sheetAvailability" ];
		}
		if (sheetConstraintViolated(sources[0]!)) {
			return [ "sheetAvailability" ];
		}
		return [];
	};
	result.push(...sheetAvailabilityCheck());
	if (wsi4.node.processType(vertex) === ProcessType.laserSheetCutting) {
		result.push(...checkConstraintsLaserSheetCutting(vertex));
	}
	return result;
}

function checkConstraintsTubeCutting(vertex: Vertex): Constraint[] {
	assertDebug(
		() => wsi4.node.workStepType(vertex) === WorkStepType.tubeCutting,
		"Pre-condition violated",
	);

	const result: Constraint[] = [];

	if (!wsi4.isFeatureEnabled(Feature.tubeDetection)) {
		result.push("tubeDetectionLicensed");
	}

	const profileGeometry = wsi4.node.tubeProfileGeometry(vertex);
	const profileAvailabilityCheck = (): Constraint[] => {
		if (profileGeometry !== undefined) {
			return [];
		}

		return [ "profileSupport" ];

	};
	result.push(...profileAvailabilityCheck());

	const tube = profileGeometry === undefined ? undefined : tubeForVertex(vertex);
	const tubeAvailabilityCheck = (): Constraint[] => {
		if (tube !== undefined) {
			return [];
		}

		return [ "tubeAvailability" ];

	};
	result.push(...tubeAvailabilityCheck());

	const tubeCuttingProcessCompatibilityCheck = (): Constraint[] => {
		if (tube === undefined) {
			// Check is only meaningful if there is a matching tube
			return [];
		}

		const processId = wsi4.node.processId(vertex);
		const isCompatible = getTable(TableType.tubeCuttingProcessMapping)
			.some(row => row.processId === processId && row.tubeMaterialId === tube.tubeMaterialId);
		if (!isCompatible) {
			return [ "tubeCuttingProcessCompatibility" ];
		}

		return [];
	};
	result.push(...tubeCuttingProcessCompatibilityCheck());

	return result;
}

function checkConstraintsSheet(vertex: Vertex): Constraint[] {
	const result: Constraint[] = [];
	const sheetAvailabilityCheck = (): Constraint[] => sheetConstraintViolated(vertex) ? [ "sheetAvailability" ] : [];
	result.push(...sheetAvailabilityCheck());
	return result;
}

function checkConstraintsUserDefinedBase(vertex: Vertex): Constraint[] {
	const result: Constraint[] = [];
	const nameEmptyCheck = (): Constraint[] => {
		const name = getArticleName(vertex);
		if (!name.length || name.length === 0) {
			return [ "dataCompleteness" ];
		}
		return [];
	};
	result.push(...nameEmptyCheck());
	return result;
}

function checkConstraintsTube(vertex: Vertex): Constraint[] {
	const result: Constraint[] = [];
	const nestingCheck = (): Constraint[] => {
		if (wsi4.node.workStepType(vertex) !== WorkStepType.tube || wsi4.node.tubeNestingResult(vertex) !== undefined) {
			return [];
		}

		return [ "tubeNestingAvailability" ];
	};
	result.push(...nestingCheck());
	return result;
}

function bendLineLengthConstraintViolated(vertex: Vertex): boolean {
	const bendLineData = wsi4.node.computeBendLineData(vertex);
	if (bendLineData === undefined) {
		return wsi4.throwError("Expecting bend line data");
	}

	const thickness = wsi4.node.sheetThickness(vertex);
	if (thickness === undefined) {
		return wsi4.throwError("Expecting sheet thickness");
	}

	const sheetMaterialId = getAssociatedSheetMaterialId(vertex);
	const materialMapping = getTable(TableType.sheetBendingMaterialMapping)
		.find(row => row.sheetMaterialId === sheetMaterialId);
	if (materialMapping === undefined) {
		wsi4.util.error("bendLineLengthConstraintViolated(): Tables inconsistent.  Expecting mapped bend material for sheetMaterialId " + sheetMaterialId);
		return false;
	}

	const bendLineConstraint = Array.from(getTable(TableType.bendLineConstraint))
		.filter(row => row.sheetBendingMaterialId === materialMapping.sheetBendingMaterialId)
		.filter(row => row.thickness - tolerances.thickness <= thickness)
		.sort((lhs, rhs) => lhs.thickness - rhs.thickness)
		.pop();

	// Missing entry is not considered an error
	return bendLineConstraint !== undefined && computeMaxBendLineNetLength(vertex) > bendLineConstraint.maxNetLength;
}

function overlappingBendAreasConstraintViolated(vertex: Vertex): boolean {
	const twoDimRep = wsi4.node.twoDimRep(vertex);
	assert(twoDimRep !== undefined, "Expecting twoDimRep");
	return wsi4.cam.util.extractOverlappingAreas(twoDimRep).length !== 0;
}

// ! We allow an error of this much for the thickness of a sheet compared to the dies it is bent with without warning
/* !
 * If the error is greater, we consider the "bendThickness" constraint as violated
 */
const bendThicknessThreshold = 0.3;

function checkConstraintsSheetBending(vertex: Vertex): Constraint[] {
	const result: Constraint[] = [];
	const dieChoices = wsi4.node.dieChoiceMap(vertex);
	if (dieChoices === undefined) {
		return wsi4.throwError("checkConstraintsSheetBending(): Missing die choices");
	}
	const thickness = wsi4.node.sheetThickness(vertex);
	if (thickness === undefined) {
		return wsi4.throwError("checkConstraintsSheetBending(): missing thickness in BendDieChoice");
	}

	const bendLineData = wsi4.node.computeBendLineData(vertex);
	assert(bendLineData !== undefined);

	const twoDimRep = wsi4.node.twoDimRep(vertex);
	assert(twoDimRep !== undefined);

	const bendDieAffectDistances = computeLowerDieAffectDistanceMapImpl(dieChoices, bendLineData, thickness);
	const affectedSegments = wsi4.cam.bend.affectedSegments(twoDimRep, bendDieAffectDistances);

	if (dieChoices.length === 0 || dieChoices.some(entry => entry.bendDieChoice.type === BendDieChoiceType.neutralAxis)) {
		result.push("bendDie");
	}
	if (dieChoices.some(entry => Math.abs(entry.bendDieChoice.thickness - thickness) > bendThicknessThreshold)) {
		result.push("bendThickness");
	}
	if (bendLineLengthConstraintViolated(vertex)) {
		result.push("maxBendLineNetLength");
	}
	if (overlappingBendAreasConstraintViolated(vertex)) {
		result.push("bendAreasNotOverlapping");
	}
	if (affectedSegments.length > 0) {
		result.push("contourInBend");
	}
	{
		const flangeLengths = wsi4.node.computeBendLineFlangeLengths(vertex);
		assert(flangeLengths !== undefined, "Expecting valid flange lengths");

		const extraFlangeLength = getSettingOrDefault("bendFlangeSafetyDistance");
		const lowerDieGroups = getTable(TableType.lowerDieGroup);
		const minFlangeLengthUndercut = dieChoices.some(dieChoice => {
			if (dieChoice.bendDieChoice.type === "neutralAxis") {
				return false;
			}

			const lowerDieGroup = lowerDieGroups.find(row => row.identifier === dieChoice.bendDieChoice.lowerDieGroupId);
			if (lowerDieGroup === undefined) {
				// Ignoring unknown die choice at this point
				return false;
			}

			const bld = bendLineData.find(entry => entry.bendDescriptor === dieChoice.bendDescriptor);
			assert(bld !== undefined, "Bend data inconsistent");

			const flangeLength = flangeLengths.find(entry => entry.bendDescriptor === dieChoice.bendDescriptor);
			assert(flangeLength !== undefined, "Bend data inconsistent");

			const maxOpeningWidth = maxLowerDieOpeningWidth(bld, flangeLength, extraFlangeLength);
			return maxOpeningWidth < lowerDieGroup.openingWidth;
		});
		if (minFlangeLengthUndercut) {
			result.push("bendFlangeTooShort");
		}
	}
	return result;
}

function checkConstraintsUserDefined(vertex: Vertex): Constraint[] {
	const result: Constraint[] = [];
	const processId = wsi4.node.processId(vertex);
	if (processId === "") {
		// Missing process for a user defined node
		result.push("userDefinedProcessId");
	}
	const processType = wsi4.node.processType(vertex);
	if (processType === ProcessType.transport) {
		const sources = wsi4.graph.sources(vertex);
		if (sources.length !== 1 || wsi4.node.workStepType(sources[0]!) !== "packaging") {
			result.push("transportSource");
		}
	}
	return result;
}

function checkWstSpecificConstraints(vertex: Vertex): Constraint[] {
	switch (wsi4.node.workStepType(vertex)) {
		case WorkStepType.undefined: return [];
		case WorkStepType.joining: return [];
		case WorkStepType.packaging: return [];
		case WorkStepType.sheet: return checkConstraintsSheet(vertex);
		case WorkStepType.sheetBending: return checkConstraintsSheetBending(vertex);
		case WorkStepType.sheetCutting: return checkConstraintsSheetCutting(vertex);
		case WorkStepType.transform: return [];
		case WorkStepType.tube: return checkConstraintsTube(vertex);
		case WorkStepType.tubeCutting: return checkConstraintsTubeCutting(vertex);
		case WorkStepType.userDefined: return checkConstraintsUserDefined(vertex);
		case WorkStepType.userDefinedBase: return checkConstraintsUserDefinedBase(vertex);
	}
}

function violatesDataValidityConstraint(vertex: Vertex): boolean {
	return nodeUserDataEntries()
		.filter(key => isCompatibleToNodeUserDataEntry(key, vertex))
		.some(key => !isValidNodeUserDataValue(key, getNodeUserDataEntryOrThrow(key, vertex)));
}

function violatesSheetMaterialConstraint(vertex: Vertex): boolean {
	if (isCompatibleToNodeUserDataEntry("sheetMaterial", vertex)) {
		const material = getNodeUserDataEntryOrThrow("sheetMaterial", vertex);
		const materials = getTable(TableType.sheetMaterial);
		return materials.every(row => row.identifier !== material.identifier);
	} else {
		return false;
	}
}

export function checkConstraints(vertex: Vertex): Constraints {
	const constraints: Constraints = {
		maxDimensions: false,
		sheetAvailability: false,
		minContourSize: false,
		dataConsistent: false,
		dataCompleteness: false,
		dataValidity: false,
		bendDie: false,
		bendThickness: false,
		transportSource: false,
		userDefinedProcessId: false,
		maxBendLineNetLength: false,
		maxSheetThickness: false,
		bendAreasNotOverlapping: false,
		sheetMaterialAvailability: false,
		profileSupport: false,
		tubeAvailability: false,
		tubeNestingAvailability: false,
		tubeCuttingProcessCompatibility: false,
		tubeDetectionLicensed: false,
		contourInBend: false,
		bendFlangeTooShort: false,
	};
	constraints.maxDimensions = violatesMaxDimensionConstraints(vertex);
	constraints.dataValidity = violatesDataValidityConstraint(vertex);
	constraints.sheetMaterialAvailability = violatesSheetMaterialConstraint(vertex);
	checkWstSpecificConstraints(vertex)
		.forEach(key => {
			constraints[key] = true;
		});
	return constraints;
}
