/**
 * Adding of data comprises three phases:
 *
 * 0. Pre-processing / classification:
 * 	- STEP / DocumentGraph is classified as such (valid / invalid)
 * 	- DXF is converted to TwoDimRep (if layered is valid)
 * 	- DXF is converted to layer-view specific data (if layered is invalid)
 * 1. Add valid input data to graph
 * 	Valid input is added to the graph (if possible)
 * (optional: flatten graph)
 * 2. Post-processing / forwarding
 * 	- Forward results from phase 0
 * 	- Post-process results from phase 1
 * 	- Combine forwared and post-processed results
 */

import {
	AddResultStatus,
	Color,
	FileType,
	InputType,
	TwoDimImportResultType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	isStringIndexedInterface,
	isTwoDimImportResultEngravingInvalid,
	isTwoDimImportResultPartInvalid,
	isTwoDimImportResultSuccess,
} from "qrc:/js/lib/generated/typeguard";
import {
	addToGraph,
	deleteNode,
} from "qrc:/js/lib/graph_manipulator";
import {
	assert,
	assertDebug,
	getKeysOfObject,
	isArray,
	isNumber,
	isObject,
	isString,
} from "qrc:/js/lib/utils";

export interface InputEntry {
	importId: string;
	data: string;
	name?: string;
	thickness?: number;
}

export function isInputEntry(arg: unknown): arg is InputEntry {
	const map: {[index in keyof Required<InputEntry>]: (arg: unknown) => arg is InputEntry[index]} = {
		importId: isString,
		data: isString,
		name: (arg): arg is string|undefined => arg === undefined || isString(arg),
		thickness: (arg): arg is number | undefined => arg === undefined || isNumber(arg),
	};
	return isStringIndexedInterface(arg) && getKeysOfObject(map)
		.every(key => map[key](arg[key]));
}

export interface TwoDimInput {
	layered: Layered,
	cuttingLds: number[],
	engravingLds: number[],
	thickness: number,
	importId: string,
	name: string,
}

export function isTwoDimInput(arg: unknown): arg is TwoDimInput {
	const map: {[index in keyof TwoDimInput]: (arg: unknown) => arg is TwoDimInput[index]} = {
		layered: (arg): arg is Layered => isObject(arg),
		cuttingLds: (arg): arg is number[] => isArray(arg, isNumber),
		engravingLds: (arg): arg is number[] => isArray(arg, isNumber),
		importId: isString,
		name: (arg): arg is string => isString(arg),
		thickness: (arg): arg is number => isNumber(arg),
	};
	return isStringIndexedInterface(arg) && getKeysOfObject(map)
		.every(key => map[key](arg[key]));
}

export interface LayerViewData {
	descriptor: number,
	name: string,
	svg: string,
	color: Color|undefined;
}

export interface AddSuccess {
	tag: "AddSuccess"
	importId: string;
	vertexKeys: string[];
}

export interface InvalidLayered {
	tag: "InvalidLayered";
	importId: string;
	name: string;
	layered: Layered;
	cuttingLds: number[],
	engravingLds: number[],
	layerViewDatas: LayerViewData[];
}

export interface InvalidInput {
	tag: "InvalidInput";
	importId: string;
}

export type ExternalAddResult = AddSuccess | InvalidLayered | InvalidInput;

interface ValidInput {
	tag: "ValidInput";
	importId: string;
	input: Input;
}

type ResultPhase0 = ValidInput | InvalidLayered | InvalidInput;

function layerColor(layer: Layer) : Color|undefined {
	const colorMap = [
		Color.closedContour,
		Color.closedContour,
		Color.yellow,
		Color.green,
		Color.cyan,
		Color.blue,
		Color.magenta,
		Color.white,
		Color.red,
	];
	return (layer.number >= 0 && layer.number < colorMap.length) ? colorMap[layer.number] : undefined;
}

function layerToSvg(layered: Layered, layer: Layer, viewPort: Box2): string {
	const lc = layerColor(layer);
	const color = lc === undefined || lc === Color.white ? Color.closedContour : lc;
	const style: SceneStyle = {
		strokeWidth: 2,
		strokeColor: color,
	};

	const scene = wsi4.geo.util.layersToScene(
		layered,
		[ layer.descriptor ],
		style,
	);
	return wsi4.util.arrayBufferToString(
		wsi4.geo.util.renderScene(
			scene,
			FileType.svg,
			{
				viewPort: viewPort,
			},
		),
	);
}

function computeLayerViewData(layered: Layered): LayerViewData[] {
	const viewPort = wsi4.geo.util.boundingBox2d(layered);
	return wsi4.geo.util.layers(layered)
		.map(layer => ({
			descriptor: layer.descriptor,
			name: layer.name,
			svg: wsi4.geo.util.isLayerEmpty(layered, layer.descriptor) ? "" : layerToSvg(layered, layer, viewPort),
			color: layerColor(layer),
		}));
}

function preProcessLayered(twoDimInput: Readonly<TwoDimInput>): ResultPhase0 {
	const tolerances = [
		0.001,
		0.002,
		0.005,
		0.01,
		0.02,
		0.05,
		0.1,
	];

	for (const tol of tolerances) {
		const result = wsi4.cam.util.twoDimRepFromLayered(
			twoDimInput.layered,
			twoDimInput.cuttingLds,
			twoDimInput.engravingLds,
			tol,
		);
		if (result.type === TwoDimImportResultType.success) {
			const resultContent = result.content;
			assert(isTwoDimImportResultSuccess(resultContent));
			const content: InputContentTwoDimRep = {
				twoDimRep: resultContent.twoDimRep,
				thickness: twoDimInput.thickness,
				name: twoDimInput.name,
				id: twoDimInput.importId,
			};
			return {
				tag: "ValidInput",
				importId: twoDimInput.importId,
				input: {
					type: InputType.twoDimRep,
					content: content,
				},
			};
		} else {
			const resultContent = result.content;
			assert(isTwoDimImportResultPartInvalid(resultContent) || isTwoDimImportResultEngravingInvalid(resultContent));
		}
	}

	return {
		tag: "InvalidLayered",
		importId: twoDimInput.importId,
		name: twoDimInput.name,
		layered: twoDimInput.layered,
		cuttingLds: twoDimInput.cuttingLds,
		engravingLds: twoDimInput.engravingLds,
		layerViewDatas: computeLayerViewData(twoDimInput.layered),
	};
}

function processInputPhase0(inputEntry: Readonly<InputEntry>): ResultPhase0 {
	const buffer = wsi4.util.fromBase64(wsi4.util.stringToArrayBuffer(inputEntry.data));
	const type = wsi4.classifier.classify(buffer);
	switch (type) {
		case InputType.twoDimRep:
		case InputType.undefined: return ({
			tag: "InvalidInput",
			importId: inputEntry.importId,
		});
		case InputType.assembly: {
			const content: InputContentAssembly = {
				id: inputEntry.importId,
				data: buffer,
			};
			return {
				tag: "ValidInput",
				importId: inputEntry.importId,
				input: {
					type: type,
					content: content,
				},
			};
		}
		case InputType.documentGraph: {
			const content: InputContentDocumentGraph = {
				id: inputEntry.importId,
				data: buffer,
			};
			return {
				tag: "ValidInput",
				importId: inputEntry.importId,
				input: {
					type: type,
					content: content,
				},
			};
		}
		case InputType.layered: {
			const layered = wsi4.geo.util.createLayered(buffer);
			if (layered === undefined) {
				return {
					tag: "InvalidInput",
					importId: inputEntry.importId,
				};
			} else {
				const cuttingLds = wsi4.geo.util.layers(layered)
					.map(layer => layer.descriptor);
				const engravingLds: number[] = [];
				return preProcessLayered({
					layered: layered,
					cuttingLds: cuttingLds,
					engravingLds: engravingLds,
					thickness: inputEntry.thickness ?? 1,
					name: inputEntry.name ?? "",
					importId: inputEntry.importId,
				});
			}
		}
	}
}

function flattenGraph(): void {
	let unfinished = true;
	while (unfinished) {
		const vertex = wsi4.graph.vertices()
			.find(candidateVertex => {
				let result = true;
				result = result && wsi4.node.workStepType(candidateVertex) === WorkStepType.joining;
				result = result && wsi4.graph.reachable(candidateVertex)
					.some(vertex => wsi4.node.workStepType(vertex) === WorkStepType.joining);
				return result;
			});
		if (vertex !== undefined) {
			deleteNode(vertex);
		} else {
			unfinished = false;
		}
	}
}

/**
 * Exported so it can be tested
 */
export function privateFlattenGraph(): void {
	flattenGraph();
}

interface ResultPhase1 {
	importId: string,
	success: boolean,
	rootIds: GraphNodeRootId[],
}

function processInputPhase1(resultsPhase0: readonly Readonly<ValidInput>[]): ResultPhase1[] {
	return addToGraph(
		resultsPhase0.filter((obj): obj is ValidInput => obj.tag === "ValidInput")
			.map(obj => obj.input))
		.map(result => ({
			importId: result.id,
			success: result.status === AddResultStatus.success,
			rootIds: result.vertices.map(vertex => wsi4.node.rootId(vertex)),
		}));
}

function processInputPhase2(
	resultsPhase0: readonly Readonly<ResultPhase0>[],
	resultsPhase1: readonly Readonly<ResultPhase1>[],
): ExternalAddResult[] {
	const group0 = resultsPhase0.filter((obj): obj is InvalidLayered|InvalidInput => obj.tag !== "ValidInput");
	const group1 = resultsPhase1.map((obj): AddSuccess|InvalidInput => {
		if (obj.success) {
			const vertices = obj.rootIds.map(rootId => {
				const vertex = wsi4.node.vertexFromRootId(rootId);
				assert(vertex !== undefined, "Expecting matching vertex");
				return vertex;
			});
			return {
				tag: "AddSuccess",
				importId: obj.importId,
				vertexKeys: vertices.map(vertex => wsi4.util.toKey(vertex)),
			};
		} else {
			return {
				tag: "InvalidInput",
				importId: obj.importId,
			};
		}
	});
	return [
		...group0,
		...group1,
	];
}

function handlePhase0Results(resultsPhase0: readonly Readonly<ResultPhase0>[], flattenGraphEnabled: boolean): ExternalAddResult[] {
	const resultsPhase1 = processInputPhase1(resultsPhase0.filter((result): result is ValidInput => result.tag === "ValidInput"));

	if (flattenGraphEnabled) {
		flattenGraph();
	}

	return processInputPhase2(
		resultsPhase0,
		resultsPhase1,
	);
}

function addDataImpl(inputs: readonly Readonly<InputEntry>[], flattenGraphEnabled: boolean): ExternalAddResult[] {
	const resultsPhase0 = inputs.map(input => processInputPhase0(input));
	const resultsPhase2 = handlePhase0Results(resultsPhase0, flattenGraphEnabled);

	assertDebug(
		() => inputs.every(input => resultsPhase2.some(obj => obj.importId === input.importId)),
		"Expecting corresponding result for each input entry",
	);

	assertDebug(() => resultsPhase2.filter((obj): obj is AddSuccess => obj.tag === "AddSuccess")
		.every(entry => entry.vertexKeys.every(key => wsi4.graph.vertices()
			.some(vertex => key === wsi4.util.toKey(vertex)))), "Expecting valid vertices only");

	return resultsPhase2;
}

export function addData(inputObjects: InputEntry[]): ExternalAddResult[] {
	return addDataImpl(inputObjects, false);
}

export function addDataAndFlattenGraph(inputObjects: InputEntry[]): ExternalAddResult[] {
	return addDataImpl(inputObjects, true);
}

export function addLayereds(inputs: readonly Readonly<TwoDimInput>[]): ExternalAddResult[] {
	const resultsPhase0 = inputs.map(input => preProcessLayered(input));
	return handlePhase0Results(resultsPhase0, false);
}
