import {
	TableType,
} from "qrc:/js/lib/generated/enum";

import {
	tolerances,
} from "./constants";
import {
	getTable,
	laserSheetCuttingSpeed,
} from "./table_utils";
import {
	assert,
	getRowForThreshold,
} from "./utils";
import {
	getSettingOrDefault,
} from "./settings_table";

export interface LaserSheetCuttingCalcParams {
	twoDimRep: TwoDimRepresentation;
	thickness: number;
	sheetCuttingMaterialId: string;
	laserSheetCuttingGasId: string;
	engravings: Segment[];
}

// Return value can be used as follows:
// * Contour area < value       : laser sheet cutting speed slow-down by factor 0.5
// * Contour area < 0.7 * value : laser sheet cutting speed slow-down by factor 0.25
// * Contour area < 0.35 * value: laser sheet cutting is not possible for the contour
// See https://gitlab.hq.wsoptics.de/src/wsi4/issues/417
function computeThresholdArea(thickness: number): number {
	return 0.25 * thickness * thickness * Math.PI;
}

function computeSlowDownFactor(area: number, thickness: number): number {
	const threshold = computeThresholdArea(thickness);
	if (area < 0.7 * threshold) {
		return 4.;
	}
	if (area < threshold) {
		return 2.;
	}
	return 1.;
}

function computeContourTime(polygon: Polygon, thickness: number, vMax: number, pierceTime: number): number {
	// Approx. max. acceleration [mm/s^2]
	const aMax = 5000;
	const cutTime = wsi4.cam.laser.computeContourTime(polygon, vMax, aMax);

	const volume = wsi4.geo.util.volume(polygon);
	const factor = computeSlowDownFactor(volume, thickness);
	return factor * cutTime + pierceTime;
}

function laserSheetCuttingPierceTime(thickness: number, sheetCuttingMaterialId: string, laserSheetCuttingGasId: string): number|undefined {
	const table = getTable(TableType.laserSheetCuttingPierceTime);
	const filteredPierceTimes = table.filter(element => element.sheetCuttingMaterialId === sheetCuttingMaterialId && element.laserSheetCuttingGasId === laserSheetCuttingGasId);
	filteredPierceTimes.sort((lhs, rhs) => lhs.thickness < rhs.thickness ? -1 : lhs.thickness > rhs.thickness ? 1 : 0);

	const row = getRowForThreshold(filteredPierceTimes, element => element.thickness >= thickness - tolerances.thickness);
	if (row === undefined) {
		wsi4.util.error("laserSheetCuttingPierceTime(): There is no pierce time table for sheetCuttingMaterialId \"" +
				sheetCuttingMaterialId + "\", laserSheetCuttingGasId \"" + laserSheetCuttingGasId + "\" and thickness " + thickness);
		return undefined;
	}
	if (typeof row.time !== "number") {
		return wsi4.throwError("Missing property \"time\"");
	}
	return row.time;
}

export function computeLaserSheetCuttingUnitTimePerPiece(params: Readonly<LaserSheetCuttingCalcParams>): number {
	const iops = wsi4.cam.util.extractInnerOuterPolygons(params.twoDimRep);
	assert(iops !== undefined, "InnerOuterPolygons missing");

	const speed = laserSheetCuttingSpeed(params.thickness, params.sheetCuttingMaterialId, params.laserSheetCuttingGasId);
	assert(speed !== undefined, "Expecting valid laser sheet cutting speed");

	const pierceTime = laserSheetCuttingPierceTime(params.thickness, params.sheetCuttingMaterialId, params.laserSheetCuttingGasId);
	assert(pierceTime !== undefined, "Expecting valid laser sheet cutting pierce time");

	const computePolygonTime = (polygon: Polygon) => computeContourTime(polygon, params.thickness, speed, pierceTime);

	// Convert from m/s^2 to mm/s^2.
	const maxA = getSettingOrDefault("laserSheetCuttingAMax") * 1000;

	// Convert from m/min to mm/s.
	const maxV = getSettingOrDefault("laserSheetCuttingVMax") * (1000 / 60);

	const cuttingTime = iops.reduce((outerAcc, iop) => {
		// Generate TSP solution through the centroids of all inner polygons to account for travelling to all holes.
		const points = wsi4.geo.util.innerPolygons(iop)
			.map(p => wsi4.geo.util.centroid(p));
		const order = wsi4.geo.util.tsp(points);
		const tspPoints = order.map(i => points[i]);
		// This is the time it roughly takes to travel along all inner points.
		const pointsTravelTime = wsi4.cam.laser.computePointsTime(tspPoints, maxV, maxA);
		// We cross the diagonal of the bounding box to take travelling from part to part into account.
		const bbox = wsi4.geo.util.boundingBox2d(iop);
		const partTravelTime = wsi4.cam.laser.computePointsTime([
			bbox.lower,
			bbox.upper,
		], maxV, maxA);
		return pointsTravelTime + partTravelTime + wsi4.geo.util.innerPolygons(iop)
			.reduce((innerAcc, ip) => innerAcc + computePolygonTime(ip)
				, outerAcc + computePolygonTime(wsi4.geo.util.outerPolygon(iop)));
	}, 0);

	const engravingTime = wsi4.cam.laser.computeSegmentsTime(params.engravings, maxV, maxA);

	return cuttingTime + engravingTime;
}
