/**
 * 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 {
	Color,
	FileType,
	InputType,
	TwoDimImportResultType,
} from "qrc:/js/lib/generated/enum";
import {
	isStringIndexedInterface,
	isTwoDimImportResultEngravingInvalid,
	isTwoDimImportResultPartInvalid,
	isTwoDimImportResultSuccess,
} from "qrc:/js/lib/generated/typeguard";
import {
	addToGraph,
	deleteArticles,
} from "qrc:/js/lib/graph_manipulator";
import {
	getAllSubJoinings,
} from "qrc:/js/lib/graph_utils";
import {
	assert,
	assertDebug,
	getKeysOfObject,
	isArray,
	isNumber,
	isObject,
	isString,
} from "qrc:/js/lib/utils";
import {
	finalizeImportedGraph,
} from "qrc:/js/lib/graphimport";
import {
	ExternalInputEntry,
} from "qrc:/js/lib/external_input";
import {
	loadExternalInput,
	NormalizedInputEntry,
} from "qrc:/js/lib/normalized_input";
import {
	getDefaultLayeredSelection,
} from "qrc:/js/lib/layered_utils";
import {
	defaultSvgResolution,
} from "qrc:/js/lib/scene_utils";

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

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),
		scaleFactor: isNumber,
	};
	return isStringIndexedInterface(arg) && getKeysOfObject(map)
		.every(key => map[key](arg[key]));
}

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

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[];
	thickness: number;
	scaleFactor: number;
	boundingBox: Box2;
}

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

export type ExternalAddResult = AddSuccess | InvalidLayered | InvalidInput;

interface ValidGraphCreatorInput {
	tag: "ValidGraphCreatorInput";
	importId: string;
	graphCreatorInput: GraphCreatorInput;
}

interface SerializedGraphInput {
	tag: "SerializedGraphInput";
	importId: string;
	buffer: ArrayBuffer;
}

type ResultPhase0 = ValidGraphCreatorInput | SerializedGraphInput | 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,
			{
				resolution: defaultSvgResolution(),
				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),
			boundingBox: wsi4.geo.util.layerBoundingBox(layered, layer.descriptor),
		}));
}

function preProcessLayered(twoDimInput: Readonly<TwoDimInput>, firstTime: boolean): ResultPhase0 {
	const bbox = wsi4.geo.util.boundingBox2d(twoDimInput.layered);
	if (firstTime && wsi4.geo.util.layers(twoDimInput.layered).filter(l => !wsi4.geo.util.isLayerEmpty(twoDimInput.layered, l.descriptor)).length > 1) {
		const defaultSelection = getDefaultLayeredSelection(twoDimInput.layered);
		return {
			tag: "InvalidLayered",
			importId: twoDimInput.importId,
			name: twoDimInput.name,
			layered: twoDimInput.layered,
			cuttingLds: defaultSelection.cuttingLds,
			engravingLds: defaultSelection.engravingLds,
			layerViewDatas: computeLayerViewData(twoDimInput.layered),
			thickness: twoDimInput.thickness,
			scaleFactor: twoDimInput.scaleFactor,
			boundingBox: bbox,
		};
	}
	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,
			twoDimInput.scaleFactor,
		);
		if (result.type === TwoDimImportResultType.success) {
			const resultContent = result.content;
			assert(isTwoDimImportResultSuccess(resultContent));
			const content: GraphCreatorInputTwoDimRep = {
				twoDimRep: resultContent.twoDimRep,
				thickness: twoDimInput.thickness,
				assemblyName: twoDimInput.name,
				importId: twoDimInput.importId,
				multiplicity: 1,
			};
			return {
				tag: "ValidGraphCreatorInput",
				importId: twoDimInput.importId,
				graphCreatorInput: {
					type: "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),
		thickness: twoDimInput.thickness,
		scaleFactor: twoDimInput.scaleFactor,
		boundingBox: bbox,
	};
}

function processInputPhase0(inputEntry: Readonly<NormalizedInputEntry>): ResultPhase0 {
	const buffer = inputEntry.data;
	const type = wsi4.classifier.classify(buffer);
	switch (type) {
		case InputType.twoDimRep:
		case InputType.undefined: return ({
			tag: "InvalidInput",
			importId: inputEntry.importId,
			status: "unsupportedFormat",
		});
		case InputType.assembly: {
			const content: GraphCreatorInputStep = {
				importId: inputEntry.importId,
				data: buffer,
				multiplicity: 1,
			};
			return {
				tag: "ValidGraphCreatorInput",
				importId: inputEntry.importId,
				graphCreatorInput: {
					type: "step",
					content: content,
				},
			};
		}
		case InputType.documentGraph: {
			const result: SerializedGraphInput = {
				tag: "SerializedGraphInput",
				importId: inputEntry.importId,
				buffer: buffer,
			};
			return result;
		}
		case InputType.layered: {
			const layered = wsi4.geo.util.createLayered(buffer);
			if (layered === undefined) {
				return {
					tag: "InvalidInput",
					importId: inputEntry.importId,
					status: "undefinedError",
				};
			} else {
				return preProcessLayered({
					layered: layered,
					cuttingLds: wsi4.geo.util.layers(layered).map(layer => layer.descriptor),
					engravingLds: [],
					thickness: inputEntry.sheetThickness ?? 1,
					name: inputEntry.terminalArticleAttributes.articleName ?? "",
					importId: inputEntry.importId,
					scaleFactor: 1.,
				}, true);
			}
		}
	}
}

function flattenGraph() {
	const subjoinings = getAllSubJoinings();
	if (subjoinings.length === 0) {
		return;
	}
	deleteArticles(subjoinings);
}

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

interface ResultPhase1 {
	importId: string,
	status: AddResultStatus,
	rootIds: GraphNodeRootId[],
}

function addGraphs(inputs: readonly Readonly<SerializedGraphInput>[]): ResultPhase1[] {
	const results: ResultPhase1[] = [];
	inputs.forEach(input => {
		const result = wsi4.sl.graph.deserialize(input.buffer);
		if (result.graph === undefined) {
			results.push({
				importId: input.importId,
				status: result.status,
				rootIds: [],
			});
			return;
		}

		const graph = finalizeImportedGraph(result.graph, input.importId);
		const rootIds = wsi4.sl.graph.vertices(graph)
			.filter(v => wsi4.sl.node.importId(v, graph))
			.map(v => {
				assertDebug(() => wsi4.sl.node.importId(v, graph) === input.importId, `Import ID inconsistent; expected: ${input.importId}; actual: ${wsi4.sl.node.importId(v, graph) ?? "undefined"}`);
				return wsi4.sl.node.rootId(v, graph);
			});

		wsi4.graphManipulator.merge(graph);

		results.push({
			importId: input.importId,
			status: "success",
			rootIds: rootIds,
		});
	});
	return results;
}

function processInputPhase1(resultsPhase0: readonly Readonly<ValidGraphCreatorInput>[], cadImportConfig: Readonly <CadImportConfig>, importIdNameMap: Map<string, string>): ResultPhase1[] {
	return addToGraph(resultsPhase0.map(obj => obj.graphCreatorInput), cadImportConfig, importIdNameMap)
		.map(result => ({
			importId: result.importId,
			status: result.status,
			rootIds: result.vertices.map(vertex => wsi4.node.rootId(vertex)),
		}));
}

function processInputPhase2(
	resultsPhase0: readonly Readonly<ResultPhase0>[],
	resultsPhase1: readonly Readonly<ResultPhase1>[],
): ExternalAddResult[] {
	// eslint-disable-next-line array-callback-return
	const group0 = resultsPhase0.filter((obj): obj is InvalidLayered | InvalidInput => {
		switch (obj.tag) {
			case "InvalidInput":
			case "InvalidLayered": return true;
			case "ValidGraphCreatorInput":
			case "SerializedGraphInput": return false;
		}
	});
	const group1 = resultsPhase1.map((obj): AddSuccess | InvalidInput => {
		if (obj.status === "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 {
			const result: ExternalAddResult = {
				tag: "InvalidInput",
				importId: obj.importId,
				status: obj.status,
			};
			return result;
		}
	});
	return [
		...group0,
		...group1,
	];
}

function handlePhase0Results(resultsPhase0: readonly Readonly<ResultPhase0>[], cadImportConfig: Readonly<CadImportConfig>, importIdNameMap: Map<string, string>, flattenGraphEnabled: boolean): ExternalAddResult[] {
	const resultsPhase1: ResultPhase1[] = [];
	resultsPhase1.push(...addGraphs(resultsPhase0.filter((result): result is SerializedGraphInput => result.tag === "SerializedGraphInput")));
	resultsPhase1.push(...processInputPhase1(resultsPhase0.filter((result): result is ValidGraphCreatorInput => result.tag === "ValidGraphCreatorInput"), cadImportConfig, importIdNameMap));

	if (flattenGraphEnabled) {
		flattenGraph();
	}

	return processInputPhase2(
		resultsPhase0,
		resultsPhase1,
	);
}

function createImportIdNameMap(inputs: readonly Readonly<NormalizedInputEntry>[]): Map<string, string> {
	const result = new Map<string, string>();
	inputs.forEach(input => {
		const articleName = input.terminalArticleAttributes?.articleName;
		if (articleName !== undefined) {
			result.set(input.importId, articleName);
		}
	});
	return result;
}

function addDataImpl(externalInputs: readonly Readonly<ExternalInputEntry>[], cadImportConfig: Readonly<CadImportConfig>, flattenGraphEnabled: boolean): ExternalAddResult[] {
	const loadedInputs = loadExternalInput(externalInputs);
	const importIdNameMap = createImportIdNameMap(loadedInputs);

	const resultsPhase0 = loadedInputs.map(input => processInputPhase0(input));
	const resultsPhase2 = handlePhase0Results(resultsPhase0, cadImportConfig, importIdNameMap, flattenGraphEnabled);

	assertDebug(
		() => loadedInputs.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: ExternalInputEntry[], cadImportConfig: Readonly<CadImportConfig> = {}): ExternalAddResult[] {
	return addDataImpl(inputObjects, cadImportConfig, false);
}

export function addDataAndFlattenGraph(inputObjects: ExternalInputEntry[], cadImportConfig: Readonly<CadImportConfig> = {}): ExternalAddResult[] {
	return addDataImpl(inputObjects, cadImportConfig, true);
}

export function addLayereds(inputs: readonly Readonly<TwoDimInput>[], cadImportConfig: Readonly<CadImportConfig> = {}): ExternalAddResult[] {
	const importIdNameMap = (() => {
		const result = new Map<string, string>();
		inputs.forEach(obj => result.set(obj.importId, obj.name));
		return result;
	})();
	const resultsPhase0 = inputs.map(input => preProcessLayered(input, false));
	return handlePhase0Results(resultsPhase0, cadImportConfig, importIdNameMap, false);
}
