/*
 * Note: All functions in this module must conform to DocumentGraphHandler's script engine API.
 *
 * Functions exported in this module coorespond to DocumentGraph's processing state.
 *
 * All functions share the same signature which is the current Vertex.
 * The return type depends on the processing state.
 */

import {
	tolerances,
} from "qrc:/js/lib/constants";
import {
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	isProcess,
} from "qrc:/js/lib/generated/typeguard";
import {
	assumeGraphAxioms,
} from "qrc:/js/lib/axioms";
import {
	getDeducedDataEntryOrThrow,
} from "qrc:/js/lib/deduceddata_utils";
import {
	computeAllowedSheets,
} from "qrc:/js/lib/graph_utils";
import {
	computeBendDieChoiceCandidates,
	tubeForVertex,
} from "qrc:/js/lib/node_utils";
import {
	getSharedDataEntry,
} from "qrc:/js/lib/shared_data";
import {
	getTable,
} from "qrc:/js/lib/table_utils";
import {
	assert,
	assertDebug,
	bbDimensionX,
	bbDimensionY,
	isArray,
	isEqual,
} from "qrc:/js/lib/utils";
import {
	getSettingOrDefault,
} from "qrc:/js/lib/settings_table";
import {
	extractUserSelection,
} from "qrc:/js/lib/sheet_util";
import {
	createPolygonPath,
} from "qrc:/js/lib/part_creation";
import {
	nodeUserDatumOrDefault,
} from "qrc:/js/lib/userdata_utils";

function sortSheets(input: Array<Sheet>): Array<Sheet> {
	const sheetPriorities = getTable(TableType.sheetPriority);
	const cache = new Map<string, number>();
	const getPriority = (sheet: Sheet): number => {
		if (cache.has(sheet.identifier)) {
			return cache.get(sheet.identifier)!;
		} else {
			const sheetPriority = sheetPriorities.find(row => row.sheetId === sheet.identifier);
			const priority = sheetPriority === undefined ? 0 : sheetPriority.priority;
			cache.set(sheet.identifier, priority);
			return priority;
		}
	};
	input.sort((lhs, rhs) => {
		const lhsPrio = getPriority(lhs);
		const rhsPrio = getPriority(rhs);
		if (lhsPrio === rhsPrio) {
			const lhsVolume = lhs.dimX * lhs.dimY;
			const rhsVolume = rhs.dimX * rhs.dimY;
			return Math.abs(lhsVolume - rhsVolume) < tolerances.volume * tolerances.volume ? 0 : lhsVolume > rhsVolume ? -1 : 1;
		} else {
			return rhsPrio - lhsPrio;
		}
	});
	return input;
}

function pickNestingTargetBox(vertex: Vertex): Box2 | undefined {
	assert(wsi4.node.workStepType(vertex) === WorkStepType.sheet, "Expecting sheet WST");

	const preselection = computeAllowedSheets(vertex);
	const sheet = sortSheets(extractUserSelection(vertex, preselection)).shift();
	if (sheet === undefined) {
		return undefined;
	}

	return {
		lower: {
			entries: [
				0.,
				0.,
			],
		},
		upper: {
			entries: [
				sheet.dimX,
				sheet.dimY,
			],
		},
	};
}

/**
 * Get NestorConfiguration from shared data
 */
function getNestorConfig(): CamNestorConfig {
	return {
		timeout: getSharedDataEntry("nestorTimeLimit"),
		numRelevantNestings: 0,
	};
}

function extractLargestIopWithVolume(twoDimRep: TwoDimRepresentation): [InnerOuterPolygon, number] {
	const iops = wsi4.cam.util.extractInnerOuterPolygons(twoDimRep);
	assert(iops.length > 0, "Expecting at least one IOP");
	return iops.map((iop): [InnerOuterPolygon, number] => [
		iop,
		wsi4.geo.util.volume(wsi4.geo.util.outerPolygon(iop)),
	])
		.sort(([
			_lhsIop,
			lhsVolume,
		], [
			_rhsIop,
			rhsVolume,
		]) => rhsVolume - lhsVolume)[0]!;
}

function computeNestorInputParts(sheetVertex: Vertex): CamNestorInputPart[] {
	assert(wsi4.node.workStepType(sheetVertex) === WorkStepType.sheet, "Expecting sheet WST");

	const sheetCuttingVertices = wsi4.graph.targets(sheetVertex);
	assertDebug(
		() => sheetCuttingVertices.every(vertex => wsi4.node.workStepType(vertex) === WorkStepType.sheetCutting),
		"Expecting sheetCutting targets only",
	);

	// Sorting parts so the order is independent from the order of vertices
	// This is required for reproducible nesting results
	return sheetCuttingVertices.map((vertex): [CamNestorInputPart, number] => {
		const twoDimRep = wsi4.node.twoDimRep(vertex);
		assert(twoDimRep !== undefined, "Expecting valid TwoDimRep");
		const [
			iop,
			volume,
		] = extractLargestIopWithVolume(twoDimRep);
		const multiplicity = wsi4.node.multiplicity(vertex);
		return [
			{
				iop: iop,
				minCount: multiplicity,
				maxCount: multiplicity,
				fixedRotations: nodeUserDatumOrDefault("fixedRotations", vertex) ?? [],
			},
			volume,
		];
	})
		.sort(([
			_lhsPart,
			lhsVolume,
		], [
			_rhsPart,
			rhsVolume,
		]) => rhsVolume - lhsVolume)
		.map(([
			part,
			_volume,
		]) => part);
}

// Note: Cannot use part_creation convenience function since nesting
//       results potentially differs for translated boundaries
function quadPathForNestor(dimX: number, dimY: number): Segment[] {
	return createPolygonPath([
		[
			0,
			0,
		],
		[
			dimX,
			0,
		],
		[
			dimX,
			dimY,
		],
		[
			0,
			dimY,
		],
	]);
}

// Note: Cannot use part_creation convenience function since nesting
//       results potentially differs for translated boundaries
function quadPolyForNestor(dimX: number, dimY: number): Polygon {
	const path = quadPathForNestor(dimX, dimY);

	const iop = wsi4.geo.util.createIop(path);
	assert(iop !== undefined, "Expecting valid InnerOuterPolygon");
	assertDebug(() => wsi4.geo.util.innerPolygons(iop).length === 0, "Expecting no inner polygon(s)");

	const result = wsi4.geo.util.outerPolygon(iop);
	assert(result !== undefined, "Expecting valid outer polygon");

	return result;
}

/**
 * Compute information need for UpdateWorkStep of sheet node
 */
function computeUpdateWorkStepInputSheet(sheetVertex: Vertex): CamWorkStepUpdateInputSheet | undefined {
	assumeGraphAxioms([ "deducedDataConsistent" ]);
	assert(wsi4.node.workStepType(sheetVertex) === WorkStepType.sheet, "Expecting sheet WST");

	const sheetCuttingVertices = wsi4.graph.targets(sheetVertex);
	assertDebug(
		() => sheetCuttingVertices.every(vertex => wsi4.node.workStepType(vertex) === WorkStepType.sheetCutting),
		"Expecting sheetCutting targets only",
	);

	const parts = computeNestorInputParts(sheetVertex);
	const targetBox = pickNestingTargetBox(sheetVertex);
	if (targetBox === undefined) {
		assertDebug(() => {
			const deducedData = wsi4.node.deducedData(sheetVertex);
			const b = getDeducedDataEntryOrThrow("targetsFitOnSheet", sheetVertex, deducedData);
			return !b;
		}, "Wrong filling.");
		return undefined;
	}

	const nestorInput = {
		parts: parts,
		// Note: The translatory offset of the target box potentially affects the nestor result
		targetBoundary: quadPolyForNestor(
			bbDimensionX(targetBox),
			bbDimensionY(targetBox),
		),
		nestingDistance: getSettingOrDefault("sheetNestingDistance"),
	};
	const nestorConfig = getNestorConfig();

	return {
		nestorInput: nestorInput,
		nestorConfig: nestorConfig,
	};
}

function computeDieChoiceMap(vertex: Vertex): DieChoiceMapEntry[] {
	const dieChoiceMapAlternatives = computeBendDieChoiceCandidates(vertex);
	const oldDieChoiceMap = wsi4.node.dieChoiceMap(vertex);
	if (oldDieChoiceMap !== undefined && oldDieChoiceMap.every(oldMapEntry => {
		const candidatesEntry = dieChoiceMapAlternatives.find(candidatesEntry => candidatesEntry.bendDescriptor === oldMapEntry.bendDescriptor);
		if (candidatesEntry === undefined) {
			return false;
		} else {
			return candidatesEntry.bendDieChoices.some(element => isEqual(element, oldMapEntry.bendDieChoice));
		}
	})) {
		return oldDieChoiceMap;
	} else {
		return dieChoiceMapAlternatives.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]!,
			};
		});
	}
}

function computeUpdateWorkStepInputTube(profileVertex: Vertex): CamWorkStepUpdateInputTube {
	assertDebug(() => wsi4.node.workStepType(profileVertex) === WorkStepType.tube, "Pre-condition violated");

	const targets = wsi4.graph.targets(profileVertex);
	assertDebug(() => targets.length === 1, "Expecting exactly one target vertex");

	const tubeCuttingVertex = targets[0]!;
	assertDebug(() => wsi4.node.workStepType(tubeCuttingVertex) === WorkStepType.tubeCutting, "Expecting target vertex to be of type tube cutting");

	// In case of no matching tube a length of 0 is expected to result in an invalid nesting
	const tubeDimX = tubeForVertex(tubeCuttingVertex)?.dimX ?? 0;

	const nestingDistance = getSettingOrDefault("tubeNestingDistance");
	const clampingLength = getSettingOrDefault("tubeClampingLength");
	const netLength = Math.max(0, tubeDimX - clampingLength);
	return {
		length: netLength,
		multiplicity: wsi4.node.multiplicity(tubeCuttingVertex),
		nestingDistance: nestingDistance,
	};
}

// Returns process table
export function createWorkStep(_vertex: Vertex): HandlerPreProcessCreateWorkStep {
	const processTable = getTable(TableType.process);
	if (!isArray<Process>(processTable, isProcess)) {
		return wsi4.throwError("Process table invalid");
	}
	return {
		processTable: processTable,
	};
}

export function updateWorkStep(vertex: Vertex): DieChoiceMapEntry[] | CamWorkStepUpdateInputSheet | CamWorkStepUpdateInputTube | undefined {
	const workStepType = wsi4.node.workStepType(vertex);
	if (workStepType === WorkStepType.sheetBending) {
		return computeDieChoiceMap(vertex);
	} else if (workStepType === WorkStepType.sheet) {
		return computeUpdateWorkStepInputSheet(vertex);
	} else if (workStepType === WorkStepType.tube) {
		return computeUpdateWorkStepInputTube(vertex);
	} else {
		return undefined;
	}
}

export function fillFromSources(_vertex: Vertex): undefined {
	return undefined;
}

export function afterFillFromSources(_vertex: Vertex): undefined {
	return undefined;
}
