import {
	back,
	front,
} from "./array_util";
import {
	tolerances,
} from "./constants";
import {
	squareNorm2,
	sub2,
} from "./geometry_utils";
import {
	getSettingOrDefault,
} from "./settings_table";
import {
	getMutableTable,
	getTable,
} from "./table_utils";
import {
	assert,
} from "./utils";

/*
 * @param bendLineData BendLineData for the bend
 * @flangeLength BendLineFlangeLength for the bend
 * @extraFlangeLength "safety distance" that is added to the geometrically computed minimum
 *
 * @return Approx. max. opening width a lower die must have to ensure that none of the bend flanges slides into the tool.
 *
 * As of now the elastic rebound of the part is not considered (a slightly larger bend angle is required to achieve the actual bend angle).
 */
export function maxLowerDieOpeningWidth(
	bendLineData: Readonly<BendLineData>,
	flangeLength: Readonly<BendLineFlangeLength>,
	extraFlangeLength: number,
): number {
	// Max. Angle that is considered manufacturable without splitting up the bend processes into two bends
	//
	// Example for two-step-bend:
	// Seamed edge with resulting angle of 180°; first bend's resulting angle: 130°; second bend's resulting angle: 180°
	//
	// Considered max angle:  153.00°
	const maxSingleBendAngle = 0.85 * Math.PI;

	//    o/2   o/2
	//  |<--->|<--->|
	//
	//  ......|
	//  \ξ    |     /
	//   \    |    /
	//    \   |   /
	// fl0 \  |  / fl1
	//      \β|β/
	//       \|/
	//        \ α
	//         \
	//          \
	// fl0, fl1:  bend legs
	// o = opening width of the sheet
	// α = bend-angle
	// β = 0.5 * (π - α)
	// ξ = 0.5 * π - β = 0.5 * α

	const bendAngle = Math.min(maxSingleBendAngle, Math.abs(bendLineData.bendAngle));
	assert(bendAngle >= 0, "bendAngle must be non-negative");
	const factor = Math.cos(.5 * bendAngle);
	const fl0 = Math.max(0, (flangeLength.flangeLengthLhs - extraFlangeLength));
	const fl1 = Math.max(0, (flangeLength.flangeLengthRhs - extraFlangeLength));
	const a = factor * fl0;
	const b = factor * fl1;
	return 2 * Math.min(a, b);
}

function dieChoiceMatchesDieUnitConstraintsImpl<Unit>(
	bendLineData: Readonly<BendLineData>,
	dies: readonly Readonly<UpperDie|LowerDie>[],
	units: readonly Readonly<Unit>[],
	extractDieId: (unit: Unit) => string,
	extractDimXSum: (unit: Unit) => number,
) {
	const firstSegment = front(bendLineData.segments);
	const lastSegment = back(bendLineData.segments);
	const bendLineLength = Math.sqrt(
		squareNorm2(sub2(
			firstSegment.content.from,
			lastSegment.content.to,
		)),
	);
	return dies.length === 0 || dies.some(die => bendLineLength <= units.filter(row => extractDieId(row) === die.identifier)
		.reduce((acc, unit) => acc + extractDimXSum(unit), 0));
}

function dieChoiceMatchesUpperDieUnitConstraints(
	bendDieChoice: Readonly<BendDieChoice>,
	bendLineData: Readonly<BendLineData>,
	upperDies: readonly Readonly<UpperDie>[],
	upperDieUnits: readonly Readonly<UpperDieUnit>[],
): boolean {
	const dies = upperDies.filter(row => row.upperDieGroupId === bendDieChoice.upperDieGroupId);
	return dieChoiceMatchesDieUnitConstraintsImpl(
		bendLineData,
		dies,
		upperDieUnits,
		unit => unit.upperDieId,
		unit => unit.multiplicity * unit.dimX,
	);
}

function dieChoiceMatchesLowerDieUnitConstraints(
	bendDieChoice: Readonly<BendDieChoice>,
	bendLineData: Readonly<BendLineData>,
	lowerDies: readonly Readonly<LowerDie>[],
	lowerDieUnits: readonly Readonly<LowerDieUnit>[],
): boolean {
	const dies = lowerDies.filter(row => row.lowerDieGroupId === bendDieChoice.lowerDieGroupId);
	return dieChoiceMatchesDieUnitConstraintsImpl(
		bendLineData,
		dies,
		lowerDieUnits,
		unit => unit.lowerDieId,
		unit => unit.multiplicity * unit.dimX,
	);
}

function mappedBendMaterialId(sheetMaterialId: string): string|undefined {
	const materialMappings = getTable("sheetBendingMaterialMapping");
	const found = materialMappings.find(row => row.sheetMaterialId === sheetMaterialId);
	if (!found || typeof found.sheetBendingMaterialId !== "string") {
		wsi4.util.error("mappedBendMaterialId(): Material not mapped to bend-material.");
		return undefined;
	}
	return found.sheetBendingMaterialId;
}

export interface DieChoiceTables {
	upperDieGroups: readonly Readonly<UpperDieGroup>[];
	lowerDieGroups: readonly Readonly<LowerDieGroup>[];
	upperDies: readonly Readonly<UpperDie>[];
	lowerDies: readonly Readonly<LowerDie>[];
	upperDieUnits: readonly Readonly<UpperDieUnit>[];
	lowerDieUnits: readonly Readonly<LowerDieUnit>[];
	bendDeductions: readonly Readonly<BendDeduction>[];
}

export interface DieChoiceParams {
	thickness: number;
	sheetMaterialId: string | undefined;
	bendLineData: BendLineData[];
	flangeLengths: BendLineFlangeLength[];
}

/**
 * Available BendDieChoice candidates for a specific bend
 */
export interface BendDieChoiceCandidatesEntry {
	bendDescriptor: number;
	bendDieChoices: BendDieChoice[];
}

export function computeDieChoiceCandidates(params: Readonly<DieChoiceParams>, tables?: Readonly<DieChoiceTables>): BendDieChoiceCandidatesEntry[] {
	const sheetBendingMaterialId = params.sheetMaterialId === undefined ? undefined : mappedBendMaterialId(params.sheetMaterialId);

	const thickness = params.thickness;
	const bendLineDataArray = params.bendLineData;

	const upperDieGroups = sheetBendingMaterialId === undefined ? [] : tables?.upperDieGroups ?? getTable("upperDieGroup");
	const lowerDieGroups = sheetBendingMaterialId === undefined ? [] : tables?.lowerDieGroups ?? getTable("lowerDieGroup");

	const upperDies = sheetBendingMaterialId === undefined ? [] : tables?.upperDies ?? getTable("upperDie");
	const lowerDies = sheetBendingMaterialId === undefined ? [] : tables?.lowerDies ?? getTable("lowerDie");

	const upperDieUnits = sheetBendingMaterialId === undefined ? [] : tables?.upperDieUnits ?? getTable("upperDieUnit");
	const lowerDieUnits = sheetBendingMaterialId === undefined ? [] : tables?.lowerDieUnits ?? getTable("lowerDieUnit");

	const unfilteredBendDeductions = sheetBendingMaterialId === undefined ? [] : tables?.bendDeductions ?? getTable("bendDeduction");
	const bendDeductions = unfilteredBendDeductions.filter(bendDeduction => bendDeduction.sheetBendingMaterialId === sheetBendingMaterialId
		// Allows filtering bend deductions by removing entries from the upper die group table
		&& upperDieGroups.find(upperDieGroup => upperDieGroup.identifier === bendDeduction.upperDieGroupId) !== undefined
		// Allows filtering bend deductions by removing entries from the lower die group table
		&& lowerDieGroups.find(lowerDieGroup => lowerDieGroup.identifier === bendDeduction.lowerDieGroupId) !== undefined);

	const extraFlangeLength = getSettingOrDefault("bendFlangeSafetyDistance");
	const bendLineFlangeLengths = params.flangeLengths;
	if (bendLineFlangeLengths === undefined) {
		wsi4.util.error("computeDieChoiceCandidates(): Flange lengths not available");
		return [];
	}

	const dieGroupPriorities = sheetBendingMaterialId === undefined ? [] : getMutableTable("dieGroupPriority")
		.filter(row => row.sheetBendingMaterialId === sheetBendingMaterialId
			&& row.sheetThickness <= (thickness + tolerances.thickness))
		.sort((lhs, rhs) => rhs.sheetThickness - lhs.sheetThickness)
		.filter((lhs, index, self) => index === self.findIndex(rhs => lhs.upperDieGroupId === rhs.upperDieGroupId && lhs.lowerDieGroupId === rhs.lowerDieGroupId));

	return bendLineDataArray
		.map((bendLineData: BendLineData) => {
			const flangeLength = bendLineFlangeLengths.find(entry => entry.bendDescriptor === bendLineData.bendDescriptor);
			assert(flangeLength !== undefined, "Bend data inconsistent");
			const maxOpeningWidth = maxLowerDieOpeningWidth(bendLineData, flangeLength, extraFlangeLength);
			const query = {
				thickness: thickness,
				bendAngle: Math.abs(bendLineData.bendAngle),
				maxOpeningWidth: maxOpeningWidth,
			};
			const constructedRadius = bendLineData.constructedInnerRadius;
			return {
				bendDescriptor: bendLineData.bendDescriptor,
				bendDieChoices: wsi4.cam.bend.selectDieGroups(bendDeductions, upperDieGroups, lowerDieGroups, dieGroupPriorities, query, constructedRadius)
					.filter(bendDieChoice => bendDieChoice.type === "neutralAxis" || dieChoiceMatchesUpperDieUnitConstraints(
						bendDieChoice,
						bendLineData,
						upperDies,
						upperDieUnits,
					))
					.filter(bendDieChoice => bendDieChoice.type === "neutralAxis" || dieChoiceMatchesLowerDieUnitConstraints(
						bendDieChoice,
						bendLineData,
						lowerDies,
						lowerDieUnits,
					)),
			};
		});
}

export function makeDieChoices(params: Readonly<DieChoiceParams>, tables?: DieChoiceTables): DieChoiceMapEntry[] {
	const candidates = computeDieChoiceCandidates(params, tables);
	return candidates.map(entry => {
		assert(entry.bendDieChoices.length > 0, "There must be at least the neutral axis pseudo tool");
		return {
			bendDescriptor: entry.bendDescriptor,
			bendDieChoice: entry.bendDieChoices[0]!,
		};
	});
}
