import {
	ProcessType,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	getTable,
} from "./table_utils";
import {
	assert,
	computeArrayIntersection,
} from "./utils";

/**
 * Mapping of Process -> associated parent-Process
 */
export const parentProcessMap: {[index in ProcessType]: ProcessType} = {
	undefined: ProcessType.undefined,
	manufacturing: ProcessType.undefined,
	semiManufactured: ProcessType.undefined,
	sheetCutting: ProcessType.removalOperation,
	laserSheetCutting: ProcessType.sheetCutting,
	joining: ProcessType.manufacturing,
	externalPart: ProcessType.semiManufactured,
	userDefinedTube: ProcessType.semiManufactured,
	sheet: ProcessType.semiManufactured,
	userDefinedBaseType: ProcessType.undefined,
	assembling: ProcessType.joining,
	forceFitting: ProcessType.joining,
	joiningByWelding: ProcessType.joining,
	joiningByBrazing: ProcessType.joining,
	bonding: ProcessType.joining,
	autogenousWelding: ProcessType.joiningByWelding,
	arcWelding: ProcessType.joiningByWelding,
	gasShieldedWelding: ProcessType.joiningByWelding,
	migWelding: ProcessType.gasShieldedWelding,
	magWelding: ProcessType.gasShieldedWelding,
	tigWelding: ProcessType.gasShieldedWelding,
	plasmaWelding: ProcessType.gasShieldedWelding,
	laserWelding: ProcessType.joiningByWelding,
	weldingWithPressure: ProcessType.joiningByWelding,
	resistanceWelding: ProcessType.weldingWithPressure,
	studWelding: ProcessType.weldingWithPressure,
	forming: ProcessType.manufacturing,
	bendForming: ProcessType.forming,
	bendingWithoutTool: ProcessType.bendForming,
	dieBending: ProcessType.sheetBending,
	sheetMetalFolding: ProcessType.sheetBending,
	cutting: ProcessType.manufacturing,
	removalOperation: ProcessType.cutting,
	plasmaSheetCutting: ProcessType.sheetCutting,
	waterJetSheetCutting: ProcessType.sheetCutting,
	machining: ProcessType.cutting,
	milling: ProcessType.machining,
	turning: ProcessType.machining,
	drilling: ProcessType.machining,
	threading: ProcessType.machining,
	userDefinedMachining: ProcessType.machining,
	userDefinedThreading: ProcessType.userDefinedMachining,
	userDefinedCountersinking: ProcessType.userDefinedMachining,
	mechanicalDeburring: ProcessType.machining,
	automaticMechanicalDeburring: ProcessType.mechanicalDeburring,
	manualMechanicalDeburring: ProcessType.mechanicalDeburring,
	cleaning: ProcessType.cutting,
	coating: ProcessType.manufacturing,
	sprayPainting: ProcessType.coating,
	powderCoating: ProcessType.coating,
	transport: ProcessType.manufacturing,
	packaging: ProcessType.manufacturing,
	sheetBending: ProcessType.bendForming,
	slideGrinding: ProcessType.machining,
	sheetTapping: ProcessType.machining,
	tube: ProcessType.semiManufactured,
	tubeCutting: ProcessType.removalOperation,
};

/**
 * @returns true if child is a sub-process of parent
 *
 * Note: if parent === child this function returns false
 */
export function isSubProcess(parent: ProcessType, child: ProcessType): boolean {
	const mapValue = parentProcessMap[child];
	return mapValue === parent || (mapValue !== ProcessType.undefined && isSubProcess(parent, mapValue));
}

/**
 * Mapping of Process -> associated WorkStepType
 */
export const workStepTypeMap: {[index in ProcessType]: WorkStepType} = {
	undefined: WorkStepType.undefined,
	manufacturing: WorkStepType.userDefined,
	semiManufactured: WorkStepType.undefined,
	sheetCutting: WorkStepType.sheetCutting,
	laserSheetCutting: WorkStepType.sheetCutting,
	joining: WorkStepType.joining,
	externalPart: WorkStepType.userDefinedBase,
	userDefinedTube: WorkStepType.userDefinedBase,
	sheet: WorkStepType.sheet,
	userDefinedBaseType: WorkStepType.undefined,
	assembling: WorkStepType.joining,
	forceFitting: WorkStepType.joining,
	joiningByWelding: WorkStepType.joining,
	joiningByBrazing: WorkStepType.joining,
	bonding: WorkStepType.joining,
	autogenousWelding: WorkStepType.joining,
	arcWelding: WorkStepType.joining,
	gasShieldedWelding: WorkStepType.joining,
	migWelding: WorkStepType.joining,
	magWelding: WorkStepType.joining,
	tigWelding: WorkStepType.joining,
	plasmaWelding: WorkStepType.joining,
	laserWelding: WorkStepType.joining,
	weldingWithPressure: WorkStepType.joining,
	resistanceWelding: WorkStepType.joining,
	studWelding: WorkStepType.joining,
	forming: WorkStepType.userDefined,
	bendForming: WorkStepType.userDefined,
	bendingWithoutTool: WorkStepType.userDefined,
	dieBending: WorkStepType.sheetBending,
	sheetMetalFolding: WorkStepType.sheetBending,
	cutting: WorkStepType.userDefined,
	removalOperation: WorkStepType.userDefined,
	plasmaSheetCutting: WorkStepType.sheetCutting,
	waterJetSheetCutting: WorkStepType.sheetCutting,
	machining: WorkStepType.userDefined,
	milling: WorkStepType.userDefined,
	turning: WorkStepType.userDefined,
	drilling: WorkStepType.userDefined,
	threading: WorkStepType.userDefined,
	userDefinedMachining: WorkStepType.userDefined,
	userDefinedThreading: WorkStepType.userDefined,
	userDefinedCountersinking: WorkStepType.userDefined,
	mechanicalDeburring: WorkStepType.userDefined,
	automaticMechanicalDeburring: WorkStepType.userDefined,
	manualMechanicalDeburring: WorkStepType.userDefined,
	cleaning: WorkStepType.userDefined,
	coating: WorkStepType.userDefined,
	sprayPainting: WorkStepType.userDefined,
	powderCoating: WorkStepType.transform,
	transport:	WorkStepType.userDefined,
	packaging:	WorkStepType.packaging,
	sheetBending: WorkStepType.sheetBending,
	slideGrinding: WorkStepType.userDefined,
	sheetTapping: WorkStepType.userDefined,
	tube: WorkStepType.tube,
	tubeCutting: WorkStepType.tubeCutting,
};

function isSubTreeActive(targetProcess: Readonly<Process>, processes: readonly Readonly<Process>[]): boolean {
	const parentProcess = processes.find(process => process.identifier === targetProcess.parentIdentifier);
	if (parentProcess === undefined) {
		return targetProcess.childrenActive;
	} else {
		return targetProcess.childrenActive && isSubTreeActive(parentProcess, processes);
	}
}

/**
 * @returns true if process is active and all parent processes have their childProcesses enabled
 */
export function isAvailableProcess(targetProcess: Readonly<Process>, processes?: readonly Readonly<Process>[]): boolean {
	processes = processes ?? getTable(TableType.process);
	const parentProcess = processes.find(process => process.identifier === targetProcess.parentIdentifier);
	if (parentProcess === undefined) {
		return targetProcess.active;
	} else {
		return targetProcess.active && isSubTreeActive(parentProcess, processes);
	}
}

/**
 * Information that is required to check all graph constraints without the need to access the actual graph
 */
export interface GraphConstraintsContext {
	reachingWsts: WorkStepType[];
	reachableWsts: WorkStepType[];
}

function isFlatSheetMetalPart(context: Readonly<GraphConstraintsContext>): boolean {
	return context.reachingWsts.some(wst => wst === WorkStepType.sheetCutting) &&
		context.reachingWsts.every(wst => wst !== WorkStepType.sheetBending) &&
		context.reachingWsts.every(wst => wst !== WorkStepType.joining);
}

function canApplyDeburringSubProcess(context: Readonly<GraphConstraintsContext>): boolean {
	return isFlatSheetMetalPart(context);
}

function canApplySheetTappingProcess(context: Readonly<GraphConstraintsContext>): boolean {
	return isFlatSheetMetalPart(context);
}

/**
 * Check if a process can be applied to a vertex in respect of the current graph
 *
 * Note: the associated node's data might be incomplete so these functions should not rely on it.
 * E. g. check if a process can be applied by looking at the existing graph
 */
const graphConstraintsFunctionMap: {[index in ProcessType]: (context: Readonly<GraphConstraintsContext>) => boolean} = {
	undefined: () => true,
	manufacturing: () => true,
	semiManufactured: () => true,
	sheetCutting: () => true,
	laserSheetCutting: () => true,
	joining: () => true,
	externalPart: () => true,
	userDefinedTube: () => true,
	sheet: () => true,
	userDefinedBaseType: () => true,
	assembling: () => true,
	forceFitting: () => true,
	joiningByWelding: () => true,
	joiningByBrazing: () => true,
	bonding: () => true,
	autogenousWelding: () => true,
	arcWelding: () => true,
	gasShieldedWelding: () => true,
	migWelding: () => true,
	magWelding: () => true,
	tigWelding: () => true,
	plasmaWelding: () => true,
	laserWelding: () => true,
	weldingWithPressure: () => true,
	resistanceWelding: () => true,
	studWelding: () => true,
	forming: () => true,
	bendForming: () => true,
	bendingWithoutTool: () => true,
	dieBending: () => true,
	sheetMetalFolding: () => true,
	cutting: () => true,
	removalOperation: () => true,
	plasmaSheetCutting: () => true,
	waterJetSheetCutting: () => true,
	machining: () => true,
	milling: () => true,
	turning: () => true,
	drilling: () => true,
	threading: () => true,
	userDefinedMachining: () => true,
	userDefinedThreading: () => true,
	userDefinedCountersinking: () => true,
	mechanicalDeburring: () => true,
	automaticMechanicalDeburring: canApplyDeburringSubProcess,
	manualMechanicalDeburring: canApplyDeburringSubProcess,
	cleaning: () => true,
	coating: () => true,
	sprayPainting: () => true,
	powderCoating: () => true,
	transport:	() => false,
	packaging:	() => false,
	sheetBending: () => true,
	slideGrinding: () => true,
	sheetTapping: canApplySheetTappingProcess,
	tube: () => true,
	tubeCutting: () => true,
};

/**
 * Must not depend on the graph (i.e. must not use wsi4.graph / wsi4.node)
 *
 * @returns true if process can be applied to vertex
 */
export function matchesGraphConstraints(type: ProcessType, context: Readonly<GraphConstraintsContext>): boolean {
	return graphConstraintsFunctionMap[type](context);
}

/**
 * Compute context for a virtual target node of referenceVertex
 */
export function computeGraphContext(vertex: Vertex): GraphConstraintsContext {
	const referenceReachingWsts = wsi4.graph.reaching(vertex)
		.map(v => wsi4.node.workStepType(v));
	const referenceReachableWsts = wsi4.graph.reachable(vertex)
		.map(v => wsi4.node.workStepType(v));
	return {
		reachingWsts: referenceReachingWsts,
		reachableWsts: referenceReachableWsts,
	};
}

/**
 * Compute context for a virtual source node of referenceVertex
 */
export function computeVirtualSourceGraphContext(referenceVertex: Vertex): GraphConstraintsContext {
	const referenceReachingWsts = wsi4.graph.reaching(referenceVertex)
		.map(vertex => wsi4.node.workStepType(vertex));
	const referenceReachableWsts = wsi4.graph.reachable(referenceVertex)
		.map(vertex => wsi4.node.workStepType(vertex));
	return {
		reachingWsts: referenceReachingWsts,
		reachableWsts: [
			...referenceReachableWsts,
			wsi4.node.workStepType(referenceVertex),
		],
	};
}

/**
 * Compute common context for virtual source nodes for all reference vertices
 */
export function computeVirtualSourcesGraphContext(referenceVertices: readonly Vertex[]): GraphConstraintsContext {
	const reachingWsts = computeArrayIntersection(referenceVertices.map(referenceVertex => wsi4.graph.reaching(referenceVertex)
		.map(vertex => wsi4.node.workStepType(vertex))));
	const reachableWsts = computeArrayIntersection(referenceVertices.map(referenceVertex => {
		const wsts = wsi4.graph.reachable(referenceVertex)
			.map(vertex => wsi4.node.workStepType(vertex));
		wsts.push(wsi4.node.workStepType(referenceVertex));
		return wsts;
	}));
	return {
		reachingWsts: reachingWsts,
		reachableWsts: reachableWsts,
	};
}

/**
 * Compute context for a virtual target node of referenceVertex
 */
export function computeVirtualTargetGraphContext(referenceVertex: Vertex): GraphConstraintsContext {
	const referenceReachingWsts = wsi4.graph.reaching(referenceVertex)
		.map(vertex => wsi4.node.workStepType(vertex));
	const referenceReachableWsts = wsi4.graph.reachable(referenceVertex)
		.map(vertex => wsi4.node.workStepType(vertex));
	return {
		reachingWsts: [
			...referenceReachingWsts,
			wsi4.node.workStepType(referenceVertex),
		],
		reachableWsts: referenceReachableWsts,
	};
}

/**
 * Compute common context for virtual target nodes for all reference vertices
 */
export function computeVirtualTargetsGraphContext(referenceVertices: readonly Vertex[]): GraphConstraintsContext {
	const reachingWsts = computeArrayIntersection(referenceVertices.map(referenceVertex => {
		const wsts = wsi4.graph.reaching(referenceVertex)
			.map(vertex => wsi4.node.workStepType(vertex));
		wsts.push(wsi4.node.workStepType(referenceVertex));
		return wsts;
	}));
	const reachableWsts = computeArrayIntersection(referenceVertices.map(referenceVertex => wsi4.graph.reachable(referenceVertex)
		.map(vertex => wsi4.node.workStepType(vertex))));
	return {
		reachingWsts: reachingWsts,
		reachableWsts: reachableWsts,
	};
}

export function isBaseWorkStepType(workStepType: WorkStepType): boolean {
	switch (workStepType) {
		case WorkStepType.undefined: return false;
		case WorkStepType.sheet: return true;
		case WorkStepType.sheetCutting: return false;
		case WorkStepType.joining: return false;
		case WorkStepType.tubeCutting: return false;
		case WorkStepType.sheetBending: return false;
		case WorkStepType.userDefined: return false;
		case WorkStepType.userDefinedBase: return true;
		case WorkStepType.packaging: return false;
		case WorkStepType.transform: return false;
		case WorkStepType.tube: return true;
	}
}

export function isBaseProcessType(processType: ProcessType): boolean {
	const mappedWst = workStepTypeMap[processType];
	return isBaseWorkStepType(mappedWst);
}

function distanceFromRoot(process: Process, table: Readonly<Process[]>): number {
	if (process.parentIdentifier.length === 0) {
		return 0;
	}
	const parent = table.find(p => p.identifier === process.parentIdentifier);
	assert(parent !== undefined);
	return distanceFromRoot(parent, table) + 1;
}

interface OrderingEntry {
	rootDist: number;
	process: Process;
}

function computeOrderingEntry(process: Process, table: Readonly<Process[]>): OrderingEntry {
	return {
		rootDist: distanceFromRoot(process, table),
		process: process,
	};
}

function compareStrings(lhs: string, rhs: string) {
	if (lhs < rhs) {
		return -1;
	} else if (lhs > rhs) {
		return 1;
	}
	return 0;
}

function compare(lhs: OrderingEntry, rhs: OrderingEntry) {
	if (lhs.rootDist === rhs.rootDist) {
		// Identifiers must be unique, so this must suffice
		assert(lhs.process.identifier !== rhs.process.identifier, "Process identifiers must be unique");
		return compareStrings(lhs.process.identifier, rhs.process.identifier);
	}
	return lhs.rootDist - rhs.rootDist;
}

export function computeOrderedProcesses(workStepType: WorkStepType) : Process[] {
	const table = getTable(TableType.process);
	return table.filter(row => workStepType === workStepTypeMap[row.type])
		.map(e => computeOrderingEntry(e, table))
		.sort(compare)
		.map(o => o.process);
}

export function findActiveProcess(type: ProcessType, table = getTable(TableType.process)): Process|undefined {
	return table.filter(process => process.type === type)
		.find(process => isAvailableProcess(process, table));
}
