import {
	checkGraphAxioms,
} from "qrc:/js/lib/axioms";
import {
	ProcessType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	isStringIndexedInterface,
} from "qrc:/js/lib/generated/typeguard";
import {
	addConstAssembly,
	appendNode,
	changeNodeProcessId,
	changeNodeUserData,
	deleteNode,
} from "qrc:/js/lib/graph_manipulator";
import {
	getArticleName,
	isComponentArticle,
} from "qrc:/js/lib/graph_utils";
import {
	getProcessTypes,
} from "qrc:/js/lib/process_utils";
import {
	createUpdatedSharedData,
} from "qrc:/js/lib/shared_data";
import {
	toVertex,
} from "qrc:/js/lib/ui_utils";
import {
	createUpdatedNodeUserData,
	isCompatibleToNodeUserDataEntry,
	UserData,
} from "qrc:/js/lib/userdata_utils";
import {
	assert,
	getKeysOfObject,
	isBoolean,
	isEqual,
	rootIdToVertex,
} from "qrc:/js/lib/utils";
import {
	createCalcCache,
	CalcCache,
} from "qrc:/js/src/export_calc_cache";
import {
	createPngFutures,
} from "qrc:/js/lib/node_utils";
import {
	Base64ResourceMap,
	GraphRepresentation,
} from "qrc:/js/lib/erp_interface";
import {
	getSettingOrDefault,
} from "qrc:/js/lib/settings_table";
import {
	createManufacturingStateCache,
} from "qrc:/js/lib/manufacturing_state_util";
import {
	front,
	itemAt,
} from "qrc:/js/lib/array_util";

import {
	AdditionalProcesses,
	AdditionalProcessUserDataEntry,
	AddOrRemoveAdditionalProcess,
	PartCreationInformation,
	ProjectObject,
	RootIdWithGraphNodeWritableAttributes,
	VertexKeyWithChangeData,
} from "./cli_shop_interface";
import {
	createProjectObj as createProjectObjectImpl,
} from "./cli_shop_project_json";
import {
	createCsv as createBomCsv,
	createEntry as createBomEntry,
} from "./export_bom";
import {
	createGraphRepresentation,
	ScaleDataConfig,
} from "./export_graph_representation";
import {
	applyGraphNodeWritableAttributesToRootIds,
} from "./cli_shop_graph_manipulator";
import {
	createInternalCalc,
} from "./export_calculation";

interface ShopBackendProjectData {
	projectObject: ProjectObject;
	graphRepresentation: GraphRepresentation|undefined;
	bomCsv: string|undefined;
	bomResources: BomResources|undefined;
	calcHtml: string;
}

export interface BomResourceEntry {
	name: string;
	base64Data: string;
}

export interface BomResources {
	automaticMechanicalDeburring: BomResourceEntry[];
	dieBending: BomResourceEntry[];
	laserSheetCutting: BomResourceEntry[];
	manualMechanicalDeburring: BomResourceEntry[];
	userDefinedCountersinking: BomResourceEntry[];
	userDefinedThreading: BomResourceEntry[];
	sheetTapping: BomResourceEntry[];
}

function computeBomResources(article: readonly Readonly<Vertex>[], itemNumber: number, graphRep: Readonly<GraphRepresentation>): BomResources {
	const articleName = (() => {
		const articleName = getArticleName(front(article));
		return articleName === undefined ? "unknown" : articleName;
	})();
	const fileNameBase = itemNumber.toString() + "_" + articleName;

	const extractProcessSpecificAttachments = (processType: ProcessType): BomResourceEntry[] => {
		const vertex = article.find(v => wsi4.node.processType(v) === processType);
		if (vertex === undefined) {
			return [];
		}

		const vertexKey = wsi4.util.toKey(vertex);
		const attachments = graphRep.resources.attachments[vertexKey];
		if (attachments === undefined) {
			return [];
		} else {
			return attachments.map(attachment => ({
				name: fileNameBase + "_" + attachment.name,
				base64Data: attachment.data,
			}));
		}
	};

	const extractProcessSpecificCadData = (processType: ProcessType, fileExtention: string, resourceMap: Readonly<Base64ResourceMap>): BomResourceEntry[] => {
		const vertex = article.find(v => wsi4.node.processType(v) === processType);
		if (vertex === undefined) {
			return [];
		}

		const vertexKey = wsi4.util.toKey(vertex);
		const data = resourceMap[vertexKey];
		if (data === undefined) {
			return [];
		} else {
			return [
				{
					name: fileNameBase + "." + fileExtention,
					base64Data: data,
				},
			];
		}
	};

	const extractProcessSpecificResources = (processType: ProcessType): BomResourceEntry[] => {
		const result: BomResourceEntry[] = [];
		result.push(...extractProcessSpecificAttachments(processType));
		result.push(...extractProcessSpecificCadData(processType, "dxf", graphRep.resources.dxfs));
		result.push(...extractProcessSpecificCadData(processType, "geo", graphRep.resources.geos));
		result.push(...extractProcessSpecificCadData(processType, "step", graphRep.resources.inputSteps));
		return result;
	};

	return {
		automaticMechanicalDeburring: extractProcessSpecificResources(ProcessType.automaticMechanicalDeburring),
		dieBending: extractProcessSpecificResources(ProcessType.dieBending),
		laserSheetCutting: extractProcessSpecificResources(ProcessType.laserSheetCutting),
		manualMechanicalDeburring: extractProcessSpecificResources(ProcessType.manualMechanicalDeburring),
		userDefinedCountersinking: extractProcessSpecificResources(ProcessType.userDefinedCountersinking),
		userDefinedThreading: extractProcessSpecificResources(ProcessType.userDefinedThreading),
		sheetTapping: extractProcessSpecificResources(ProcessType.sheetTapping),
	};
}

function createBomCsvAndResources(graphRep: GraphRepresentation, calcCache?: CalcCache): [ string, BomResources ] {
	const bomEntriesAndResources = wsi4.graph.articles()
		.filter(article => isComponentArticle(article))
		.map((article, index) => ({
			bomEntry: createBomEntry(article, index, calcCache),
			resources: computeBomResources(article, index, graphRep),
		}));
	const bomCsv = createBomCsv(bomEntriesAndResources.map(bear => bear.bomEntry), getSettingOrDefault("csvLocale"));
	const bomResources = bomEntriesAndResources.reduce((acc: BomResources, bear) => {
		getKeysOfObject(acc)
			.forEach(key => acc[key].push(...bear.resources[key]));
		return acc;
	}, {
		automaticMechanicalDeburring: [],
		dieBending: [],
		laserSheetCutting: [],
		manualMechanicalDeburring: [],
		userDefinedCountersinking: [],
		userDefinedThreading: [],
		sheetTapping: [],
	});
	return [
		bomCsv,
		bomResources,
	];
}

export interface ShopBackendDataConfig {
	bomEnabled: boolean;
	graphRepEnabled: boolean;
}

export function isShopBackendDataConfig(input: unknown): input is ShopBackendDataConfig {
	const map: {[index in keyof ShopBackendDataConfig]: (arg: unknown) => arg is ShopBackendDataConfig[index]} = {
		bomEnabled: isBoolean,
		graphRepEnabled: isBoolean,
	};
	return isStringIndexedInterface(input) && getKeysOfObject(map)
		.every(key => map[key](input[key]));
}

const defaultShopBackendDataConfig: ShopBackendDataConfig = {
	bomEnabled: true,
	graphRepEnabled: true,
};

/**
 * Utilized by shop (*not* webui)
 *
 * @param knownVertices projectObject-resources will *not* be computed for associated nodes
 *
 * Pre-condition:  All underlying vertices are export ready
 */
export function createShopBackendProjectData(knownVertices: Vertex[], config?: ShopBackendDataConfig): ShopBackendProjectData {
	config = config ?? defaultShopBackendDataConfig;
	const manufacturingStateCache = createManufacturingStateCache();

	const scaleDataConfig: ScaleDataConfig = {
		type: "actualMultiplicity",
	};

	const calcCache = createCalcCache();

	const pngFutures = createPngFutures(wsi4.graph.vertices());

	const graphRep = (() => {
		if (config.graphRepEnabled || config.bomEnabled) {
			return createGraphRepresentation(scaleDataConfig, pngFutures, calcCache);
		} else {
			return undefined;
		}
	})();

	const [
		bomCsv,
		bomResources,
	] = (() => {
		if (config.bomEnabled) {
			assert(graphRep !== undefined, "Expecting valid graphRep");
			return createBomCsvAndResources(graphRep, calcCache);
		} else {
			return [
				undefined,
				undefined,
			];
		}
	})();

	const html = (() => {
		const content = createInternalCalc(pngFutures, calcCache);
		return wsi4.documentCreator.renderIntoHtml(content, {orientation: "portrait" });
	})();

	return {
		projectObject: createProjectObjectImpl(
			knownVertices,
			pngFutures,
			undefined,
			calcCache,
			manufacturingStateCache,
		),
		graphRepresentation: graphRep,
		bomCsv: bomCsv,
		bomResources: bomResources,
		calcHtml: html,
	};
}

export function serializeProject(): string {
	const buffer = wsi4.graph.serialize();
	return wsi4.util.arrayBufferToString(wsi4.util.toBase64(buffer));
}

function findSource(initiatingNode: Vertex, process: ProcessType) {
	if (wsi4.node.workStepType(initiatingNode) !== WorkStepType.sheetCutting) {
		return wsi4.throwError("findSource(): Wrong workStepType of source node");
	}

	const r = wsi4.graph.reachable(initiatingNode);
	const additionalProcessOrder: Array<Array<ProcessType>> = [
		[
			ProcessType.automaticMechanicalDeburring,
			ProcessType.manualMechanicalDeburring,
		],
		[ ProcessType.slideGrinding ],
		[ ProcessType.userDefinedCountersinking ],
		[
			ProcessType.sheetTapping,
			ProcessType.userDefinedThreading,
		],
	];
	const index = additionalProcessOrder.findIndex(a => a.some(p => p === process));
	assert(index !== -1);
	if (index === 0) {
		return initiatingNode;
	}
	for (let i = 0; i < index; ++i) {
		const as = itemAt(index - 1 - i, additionalProcessOrder);
		for (const p of as) {
			const s = r.find(r => wsi4.node.processType(r) === p);
			if (s !== undefined) {
				return s;
			}
		}
	}
	return initiatingNode;
}

export function addMechanicalDeburringNode(initiatingNode: Vertex, userDataEntries?: Array<AdditionalProcessUserDataEntry>): boolean {
	let source = findSource(initiatingNode, ProcessType.automaticMechanicalDeburring);
	if (isEqual(source, initiatingNode)) {
		source = findSource(initiatingNode, ProcessType.manualMechanicalDeburring);
	}
	const sourceRootId = wsi4.node.rootId(source);
	const hasDeburringTarget =
wsi4.graph.targets(source)
	.some(s => wsi4.node.processType(s) === ProcessType.automaticMechanicalDeburring || wsi4.node.processType(s) === ProcessType.manualMechanicalDeburring);
	if (!hasDeburringTarget) {
		if (!appendNode(rootIdToVertex(sourceRootId))) {
			wsi4.util.error("addMechanicalDeburringNode(): Injecting failed");
			return false;
		}
	}
	const currentTargetVertices = wsi4.graph.targets(rootIdToVertex(sourceRootId));
	if (currentTargetVertices.length !== 1) {
		return wsi4.throwError("addMechanicalDeburringNode(): Injecting failed");
	}
	const target = currentTargetVertices[0]!;
	const targetRootId = wsi4.node.rootId(target);
	if (!hasDeburringTarget) {
		const processes = getProcessTypes();
		let processId: string|undefined = undefined;
		if (processes.some(p => p === ProcessType.manualMechanicalDeburring)) {
			processId = "manualMechanicalDeburringId";
		}
		if (processes.some(p => p === ProcessType.automaticMechanicalDeburring)) {
			processId = "automaticMechanicalDeburringId";
		}
		if (processId === undefined) {
			return wsi4.throwError("addMechanicalDeburringNode(): No deburring process found");
		}
		if (!changeNodeProcessId(target, processId, true)) {
			return false;
		}
	}
	if (userDataEntries !== undefined) {
		const userData =
userDataEntries.reduce((u: UserData, entry: AdditionalProcessUserDataEntry) => createUpdatedNodeUserData(entry.key, rootIdToVertex(targetRootId), entry.value, u),
	wsi4.node.userData(rootIdToVertex(targetRootId)));
		if (!changeNodeUserData(rootIdToVertex(targetRootId), userData)) {
			return false;
		}
	}
	checkGraphAxioms();
	return true;
}

function removeMechanicalDeburringNode(initiatingNode: Vertex): boolean {
	let source = findSource(initiatingNode, ProcessType.automaticMechanicalDeburring);
	if (isEqual(source, initiatingNode)) {
		source = findSource(initiatingNode, ProcessType.manualMechanicalDeburring);
	}
	const currentTargetVertices = wsi4.graph.targets(source);
	if (currentTargetVertices.length !== 1) {
		// no target added so removing was not necessary
		return true;
	}
	const target = currentTargetVertices[0]!;
	if (wsi4.node.processType(target) !== ProcessType.manualMechanicalDeburring && wsi4.node.processType(target) !== ProcessType.automaticMechanicalDeburring) {
		// no target with process type mechanical deburring
		// we return true because removing was not necessary
		return true;
	}
	if (!deleteNode(target)) {
		return false;
	}
	checkGraphAxioms();
	return true;
}

export function addSheetTappingNode(initiatingNode: Vertex, userDataEntries?: Array<AdditionalProcessUserDataEntry>): boolean {
	const source = findSource(initiatingNode, ProcessType.sheetTapping);
	const sourceRootId = wsi4.node.rootId(source);
	const hasTappingTarget = wsi4.graph.targets(source)
		.some(s => wsi4.node.processType(s) === ProcessType.sheetTapping);
	if (!hasTappingTarget) {
		if (!appendNode(rootIdToVertex(sourceRootId))) {
			wsi4.util.error("addSheetTappingNode(): Injecting failed");
			return false;
		}
	}
	const currentTargetVertices = wsi4.graph.targets(rootIdToVertex(sourceRootId));
	if (currentTargetVertices.length !== 1) {
		return wsi4.throwError("addSheetTappingNode(): Injecting failed");
	}
	const target = currentTargetVertices[0]!;
	const targetRootId = wsi4.node.rootId(target);
	if (!hasTappingTarget) {
		const processes = getProcessTypes();
		const processId: string|undefined = processes.some(p => p === ProcessType.sheetTapping) ? "sheetTappingId" : undefined;
		if (processId === undefined) {
			return wsi4.throwError("addSheetTappingNode(): No tapping process found");
		}
		if (!changeNodeProcessId(target, processId, true)) {
			return false;
		}
	}
	if (userDataEntries !== undefined) {
		const userData = userDataEntries.reduce((u: UserData,
			entry: AdditionalProcessUserDataEntry) => createUpdatedNodeUserData(entry.key, rootIdToVertex(targetRootId), entry.value, u),
		wsi4.node.userData(rootIdToVertex(targetRootId)));
		if (!changeNodeUserData(rootIdToVertex(targetRootId), userData)) {
			return false;
		}
	}
	checkGraphAxioms();
	return true;
}

function removeSheetTappingNode(initiatingNode: Vertex): boolean {
	const source = findSource(initiatingNode, ProcessType.sheetTapping);
	const currentTargetVertices = wsi4.graph.targets(source);
	if (currentTargetVertices.length !== 1) {
		wsi4.util.error("removeSheetTappingNode(): Wrong number of targets:" + currentTargetVertices.length.toString());
		return false;
	}
	const target = currentTargetVertices[0]!;
	if (wsi4.node.processType(target) !== ProcessType.sheetTapping) {
		// no target with process type sheetTapping
		// we return true because removing was not necessary
		return true;
	}
	if (!deleteNode(target)) {
		return false;
	}
	checkGraphAxioms();
	return true;
}

function addAdditionalNode(processType: ProcessType, processId: string, initiatingNode: Vertex, userDataEntries?: Array<AdditionalProcessUserDataEntry>): boolean {
	const source = findSource(initiatingNode, processType);
	const sourceRootId = wsi4.node.rootId(source);
	const hasNode = wsi4.graph.targets(source)
		.some(s => wsi4.node.processType(s) === processType);
	if (!hasNode) {
		if (!appendNode(rootIdToVertex(sourceRootId))) {
			wsi4.util.error("addAdditionalNode(): Injecting failed");
			return false;
		}
	}
	const currentTargetVertices = wsi4.graph.targets(rootIdToVertex(sourceRootId));
	if (currentTargetVertices.length !== 1) {
		return wsi4.throwError("addAdditionalNode(): Injecting failed");
	}
	const target = currentTargetVertices[0]!;
	const targetRootId = wsi4.node.rootId(target);
	if (!hasNode) {
		if (!changeNodeProcessId(target, processId, true)) {
			return false;
		}
	}
	if (userDataEntries !== undefined) {
		const vertex = rootIdToVertex(targetRootId);
		const userData = userDataEntries.reduce((u: UserData, entry: AdditionalProcessUserDataEntry) => createUpdatedNodeUserData(
			entry.key,
			vertex,
			entry.value,
			u,
		), wsi4.node.userData(vertex));
		if (!changeNodeUserData(vertex, userData)) {
			return false;
		}
	}
	checkGraphAxioms();
	return true;
}
function removeAdditionalNode(processType: ProcessType, initiatingNode: Vertex): boolean {
	const source = findSource(initiatingNode, processType);
	const currentTargetVertices = wsi4.graph.targets(source);
	if (currentTargetVertices.length !== 1) {
		// no target added so removing was not necessary
		return true;
	}
	const target = currentTargetVertices[0]!;
	if (wsi4.node.processType(target) !== processType) {
		// no target with process type processType
		// we return true because removing was not necessary
		return true;
	}
	if (!deleteNode(target)) {
		return false;
	}
	checkGraphAxioms();
	return true;
}

function addCountersinkingNode(initiatingNode: Vertex, userDataEntries?: Array<AdditionalProcessUserDataEntry>): boolean {
	return addAdditionalNode(ProcessType.userDefinedCountersinking, "userDefinedCountersinkingId", initiatingNode, userDataEntries);
}

function removeCountersinkingNode(initiatingNode: Vertex): boolean {
	return removeAdditionalNode(ProcessType.userDefinedCountersinking, initiatingNode);
}

function addThreadingNode(initiatingNode: Vertex, userDataEntries?: Array<AdditionalProcessUserDataEntry>): boolean {
	return addAdditionalNode(ProcessType.userDefinedThreading, "userDefinedThreadingId", initiatingNode, userDataEntries);
}
function removeThreadingNode(initiatingNode: Vertex): boolean {
	return removeAdditionalNode(ProcessType.userDefinedThreading, initiatingNode);
}
function addSlideGrindingNode(initiatingNode: Vertex, userDataEntries?: Array<AdditionalProcessUserDataEntry>): boolean {
	return addAdditionalNode(ProcessType.slideGrinding, "slideGrindingId", initiatingNode, userDataEntries);
}
function removeSlideGrindingNode(initiatingNode: Vertex): boolean {
	return removeAdditionalNode(ProcessType.slideGrinding, initiatingNode);
}

interface RootIdWithAdditionalProcesses {
	rootId: GraphNodeRootId;
	additionalProcesses: Array<AddOrRemoveAdditionalProcess>;
}

interface AddAndRemoveActions {
	add: (vertex: Vertex, userDataEntries?: Array<AdditionalProcessUserDataEntry>) => boolean;
	remove: (vertex: Vertex) => boolean;
}

type ProcessHandlingMap = {
	[index in keyof AdditionalProcesses]: AddAndRemoveActions;
};

function addOrRemoveAdditionalProcesses(data: RootIdWithAdditionalProcesses[]): boolean {
	const map: ProcessHandlingMap = {
		mechanicalDeburring: {
			add: addMechanicalDeburringNode,
			remove: removeMechanicalDeburringNode,
		},
		slideGrinding: {
			add: addSlideGrindingNode,
			remove: removeSlideGrindingNode,
		},
		threading: {
			add: addThreadingNode,
			remove: removeThreadingNode,
		},
		countersinking: {
			add: addCountersinkingNode,
			remove: removeCountersinkingNode,
		},
		tapping: {
			add: addSheetTappingNode,
			remove: removeSheetTappingNode,
		},
	};
	for (const d of data) {
		for (const a of d.additionalProcesses) {
			const vertex = wsi4.graph.vertices()
				.find(v => wsi4.util.toKey(wsi4.node.rootId(v)) === wsi4.util.toKey(d.rootId));
			assert(vertex !== undefined);
			if (a.addNode) {
				if (!map[a.process].add(vertex, a.userDataEntries)) {
					return false;
				}
			} else if (!map[a.process].remove(vertex)) {
				return false;
			}
		}
	}
	checkGraphAxioms();
	return true;
}

function vertexKeyChangeDataEmpty(data: VertexKeyWithChangeData): boolean {
	return Object.keys(data.attributes).length === 0 && Object.keys(data.additionalProcesses).length === 0;
}

export function applyChangesToVertices(data: VertexKeyWithChangeData[]): boolean {
	if (data.length === 0 || data.some(d => vertexKeyChangeDataEmpty(d))) {
		return false;
	}
	const rootIdsWithAdditionalProcesses: Array<RootIdWithAdditionalProcesses> = data.filter(d => Object.keys(d.additionalProcesses).length !== 0)
		.map(d => {
			const vertex = toVertex(d.vertexKey);
			if (vertex === undefined) {
				return wsi4.throwError("Invalid vertex key");
			}
			return {
				rootId: wsi4.node.rootId(vertex),
				additionalProcesses: d.additionalProcesses,
			};
		});
	const rootIdsWithGraphNodeWritableAttributes: Array<RootIdWithGraphNodeWritableAttributes> = data.filter(d => Object.keys(d.attributes).length !== 0)
		.map(d => {
			const vertex = toVertex(d.vertexKey);
			if (vertex === undefined) {
				return wsi4.throwError("Invalid vertex key");
			}
			return {
				rootId: wsi4.node.rootId(vertex),
				attributes: d.attributes,
			};
		});
	if (rootIdsWithAdditionalProcesses.length + rootIdsWithGraphNodeWritableAttributes.length === 0) {
		return wsi4.throwError("Implementation error");
	}
	if (rootIdsWithAdditionalProcesses.length !== 0) {
		// first add additional processes
		if (!addOrRemoveAdditionalProcesses(rootIdsWithAdditionalProcesses)) {
			return false;
		}
	}
	if (rootIdsWithGraphNodeWritableAttributes.length !== 0) {
		// then change attributes
		if (!applyGraphNodeWritableAttributesToRootIds(rootIdsWithGraphNodeWritableAttributes)) {
			return false;
		}
	}
	return true;
}

/*
 * Function should be called when starting a new wsi4-shop-session
 *
 * Note: this function is called by shop directly
 */
export function prepareUnsafeSession(nestorTimeLimit: number): boolean {
	wsi4.sharedData.write(createUpdatedSharedData("nestorTimeLimit", nestorTimeLimit));

	// Deactivate io script env. module
	// @ts-ignore
	wsi4.io = undefined;
	return wsi4.io === undefined;
}

export function addSimplePart(p: PartCreationInformation) : void {
	const iop = wsi4.geo.util.createIop(p.segments);
	assert(iop !== undefined, "Expecting valid iop");

	const assembly = wsi4.geo.assembly.fromIop(iop, p.thickness, p.name);
	const vertex = addConstAssembly(assembly);
	assert(vertex !== undefined);
	const rootId = wsi4.node.rootId(vertex);

	assert(isCompatibleToNodeUserDataEntry("sheetMaterial", rootIdToVertex(rootId)));
	changeNodeUserData(rootIdToVertex(rootId), createUpdatedNodeUserData("sheetMaterial", rootIdToVertex(rootId), p.material));
	for (const v of [
		rootIdToVertex(rootId),
		...wsi4.graph.reaching(rootIdToVertex(rootId)),
	]) {
		if (isCompatibleToNodeUserDataEntry("cuttingGas", v)) {
			changeNodeUserData(v, createUpdatedNodeUserData("cuttingGas", v,
				{
					identifier: p.gasId,
				}));
			break;
		}
	}
}
