import {
	GraphNodeProcessingState,
	ProcessType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	isAttachment,
	isCadFeature,
	isScrewThreadUniqueMembers,
	isUserDefinedScalePrice,
} from "qrc:/js/lib/generated/typeguard";
import {
	BendLineEngravingMode,
	isBendLineEngravingMode,
} from "./bend_line_engraving_mode";

import {
	getSettingOrDefault,
} from "./settings_table";
import {
	getTable,
} from "./table_utils";
import {
	exhaustiveStringTuple,
	isArray,
	isBoolean,
	isInstanceOf,
	isNumber,
	isString,
} from "./utils";

export interface SheetTappingDataEntry {
	cadFeature: CadFeature;
	screwThread: ScrewThreadUniqueMembers;
}

export function isSheetTappingDataEntry(arg: unknown): arg is SheetTappingDataEntry {
	return isInstanceOf<SheetTappingDataEntry>(arg, {
		cadFeature: isCadFeature,
		screwThread: isScrewThreadUniqueMembers,
	});
}

/**
 * Maps node UserData entries to the associated type
 *
 * Each UserData entry is optional (`undefined`) by default.
 * This map defines the type for actual entries.
 *
 * Note: This interface should never be instantiated.
 * It can be considered a map of available node-UserData-entry-keys to the corresponding types
 */
export interface NodeUserDataEntries {
	attachments: Attachment[];
	bendLineEngravingMode: BendLineEngravingMode;
	comment: string;
	deburrDoubleSided: boolean;
	fixedRotations: number[];
	laserSheetCuttingGasId: string;
	numCountersinks: number;
	numThreads: number;
	sheetFilterSheetIds: string[];
	sheetMaterialId: string;
	sheetTappingData: SheetTappingDataEntry[];
	testReportRequired: boolean;
	tubeMaterialId: string;
	tubeSpecificationId: string;
	userDefinedMaterialCostsPerPiece: number;
	userDefinedScalePrices: UserDefinedScalePrice[];
	userDefinedSetupTime: number;
	userDefinedUnitTimePerPiece: number;
}

export interface NodeUserDataContext {
	workStepType: WorkStepType;

	processType: ProcessType;

	isInitialNode: boolean;
}

type TypeGuardMap<Entries> = {
	readonly[index in keyof Entries]: (arg: unknown) => arg is Entries[index];
};

type CompatibilityFunctionMap<Entries> = {
	readonly[index in keyof Entries]: (context: Readonly<NodeUserDataContext>) => boolean;
};

/**
 * Each entry is implicitly considered "optional".
 * Hence, the respective accessor functions always return `undefined` or the actual type, if set.
 *
 * The job of these type-guards is to check a values type, if the entry is actually set (i.e. non-undefined).
 */
const nodeTypeGuardMap: TypeGuardMap<NodeUserDataEntries> = {
	sheetMaterialId: isString,
	comment: isString,
	userDefinedMaterialCostsPerPiece: isNumber,
	userDefinedSetupTime: isNumber,
	userDefinedUnitTimePerPiece: isNumber,
	laserSheetCuttingGasId: isString,
	attachments: (arg) : arg is Attachment[] => isArray(arg, isAttachment),
	testReportRequired: isBoolean,
	fixedRotations: (arg) : arg is number[] => isArray(arg, isNumber),
	deburrDoubleSided: isBoolean,
	numThreads: isNumber,
	numCountersinks: isNumber,
	sheetTappingData: (arg): arg is SheetTappingDataEntry[] => isArray(arg, isSheetTappingDataEntry),
	userDefinedScalePrices: (arg): arg is UserDefinedScalePrice[] => isArray(arg, isUserDefinedScalePrice),
	tubeMaterialId: isString,
	tubeSpecificationId: isString,
	sheetFilterSheetIds: (arg: unknown): arg is string[] => isArray(arg, isString),
	bendLineEngravingMode: isBendLineEngravingMode,
};

function isSheetMaterialAndTestReportWst(wst: WorkStepType): boolean {
	return wst === WorkStepType.sheetCutting || wst === WorkStepType.sheetBending;
}

const noCommentWsts: WorkStepType[] = [
	WorkStepType.sheet,
	WorkStepType.tube,
];

const nodeCompatibilityFunctionMap: CompatibilityFunctionMap<NodeUserDataEntries> = {
	sheetMaterialId: context => context.isInitialNode && isSheetMaterialAndTestReportWst(context.workStepType),
	testReportRequired: context => context.isInitialNode && isSheetMaterialAndTestReportWst(context.workStepType),
	comment: context => noCommentWsts.every(wst => wst !== context.workStepType),
	userDefinedMaterialCostsPerPiece: () => true,
	userDefinedSetupTime: () => true,
	userDefinedUnitTimePerPiece: () => true,
	laserSheetCuttingGasId: context => context.processType === ProcessType.laserSheetCutting,
	attachments: () => true,
	fixedRotations: context => context.workStepType === WorkStepType.sheetCutting && getSettingOrDefault("sheetCuttingFixedRotationsEnabled"),
	deburrDoubleSided: context => context.processType === ProcessType.automaticMechanicalDeburring || context.processType === ProcessType.manualMechanicalDeburring,
	numThreads: context => context.processType === ProcessType.userDefinedThreading,
	numCountersinks: context => context.processType === ProcessType.userDefinedCountersinking,
	sheetTappingData: context => context.processType === ProcessType.sheetTapping,
	userDefinedScalePrices: () => true,
	tubeMaterialId: context => context.isInitialNode && context.workStepType === WorkStepType.tubeCutting,
	tubeSpecificationId: context => context.isInitialNode && context.workStepType === WorkStepType.tubeCutting,
	sheetFilterSheetIds: context => context.workStepType === WorkStepType.sheetCutting,
	bendLineEngravingMode: context => context.workStepType === WorkStepType.sheetCutting || context.workStepType === WorkStepType.sheetBending,
};

/**
 * @returns Array of UserData entry keys
 */
export function nodeUserDataEntries(): Array<keyof NodeUserDataEntries> {
	return exhaustiveStringTuple<keyof NodeUserDataEntries>()(
		"attachments",
		"bendLineEngravingMode",
		"comment",
		"deburrDoubleSided",
		"fixedRotations",
		"laserSheetCuttingGasId",
		"numCountersinks",
		"numThreads",
		"sheetFilterSheetIds",
		"sheetMaterialId",
		"sheetTappingData",
		"testReportRequired",
		"tubeMaterialId",
		"tubeSpecificationId",
		"userDefinedMaterialCostsPerPiece",
		"userDefinedScalePrices",
		"userDefinedSetupTime",
		"userDefinedUnitTimePerPiece",
	);
}

/**
 * Narrows unknown value to UserData entry key
 */
export function isNodeUserDataEntry(arg: unknown): arg is keyof NodeUserDataEntries {
	return nodeUserDataEntries()
		.some(key => key === arg);
}

/**
 * @param key Key of the UserData entry
 * @param value The value to check
 * @returns True if [[value]] matches the type associated with [[key]]
 */
export function isNodeUserDataEntryType<Key extends keyof NodeUserDataEntries>(key: Key, value: unknown): value is NodeUserDataEntries[Key] {
	return nodeTypeGuardMap[key](value);
}

/**
 * @param key Key of the UserData entry
 * @param context Context for the associated node
 * @returns true if UserData entry for key is compatible to associated node
 */
export function isCompatibleToNodeUserDataEntryImpl<Key extends keyof NodeUserDataEntries>(key: Key, context: Readonly<NodeUserDataContext>): boolean {
	return nodeCompatibilityFunctionMap[key](context);
}

/**
 * A node's processing state will be reset to the mapped value in case an associated UserData value is changed
 */
export const nodeUserDataProcessingStateMap: {[index in keyof NodeUserDataEntries]: GraphNodeProcessingState} = {
	sheetMaterialId: GraphNodeProcessingState.UpdateWorkStep,
	comment: GraphNodeProcessingState.Finished,
	userDefinedMaterialCostsPerPiece: GraphNodeProcessingState.Finished,
	userDefinedSetupTime: GraphNodeProcessingState.Finished,
	userDefinedUnitTimePerPiece: GraphNodeProcessingState.Finished,
	laserSheetCuttingGasId: GraphNodeProcessingState.CreateSources,
	attachments: GraphNodeProcessingState.Finished,
	testReportRequired: GraphNodeProcessingState.Finished,
	fixedRotations: GraphNodeProcessingState.CreateSources,
	deburrDoubleSided: GraphNodeProcessingState.Finished,
	numThreads: GraphNodeProcessingState.Finished,
	numCountersinks: GraphNodeProcessingState.Finished,
	sheetTappingData: GraphNodeProcessingState.Finished,
	userDefinedScalePrices: GraphNodeProcessingState.Finished,
	tubeMaterialId: GraphNodeProcessingState.UpdateWorkStep,
	tubeSpecificationId: GraphNodeProcessingState.UpdateWorkStep,
	sheetFilterSheetIds: GraphNodeProcessingState.CreateSources,
	// For now engravings are ignored by nestor, so there is no need to recompute sources.
	bendLineEngravingMode: GraphNodeProcessingState.Finished,
};

/**
 * Note: This interface should never be instantiated.
 */
interface GraphUserDataEntries {
	attachments: Attachment[];
	name: string;
}

const graphTypeGuardMap: TypeGuardMap<GraphUserDataEntries> = {
	attachments: (arg: unknown) : arg is Attachment[] => isArray(arg, isAttachment),
	name: (arg: unknown) : arg is string => isString(arg),
};

const graphUserDataKeyMap: {[index in keyof GraphUserDataEntries]: string} = {
	attachments: "attachments",
	name: "name",
};

function isGraphUserDataEntryType<Key extends keyof GraphUserDataEntries>(key: Key, arg: unknown): arg is GraphUserDataEntries[Key] {
	return graphTypeGuardMap[key](arg);
}

/**
 * Access graph UserData entry for given key
 *
 * @param key UserData entry key
 * @param userData UserData object that should be accessed
 * @returns Value if present, undefined else
 */
export function getGraphUserDataEntry<Key extends keyof GraphUserDataEntries>(key: Key, userData?: StringIndexedInterface): GraphUserDataEntries[Key]|undefined {
	userData = userData === undefined ? wsi4.graph.userData() : userData;
	const value = userData[graphUserDataKeyMap[key]];
	return isGraphUserDataEntryType(key, value) ? value : undefined;
}

/**
 * Create updated graph UserData
 *
 * @param key UserData entry key
 * @param value Value to set for key's entry
 * @returns Updated UserData
 *
 * Note: This function does *not* modify the underlying graph
 */
export function createUpdatedGraphUserData<Key extends keyof GraphUserDataEntries>(key: Key, value: GraphUserDataEntries[Key], userData?: StringIndexedInterface): StringIndexedInterface {
	userData = userData === undefined ? wsi4.graph.userData() : userData;
	userData[graphUserDataKeyMap[key]] = value;
	return userData;
}

type ValidatedNodeUserDataEntries = Pick<NodeUserDataEntries, "sheetMaterialId"
| "laserSheetCuttingGasId"
| "sheetFilterSheetIds"
| "sheetTappingData"
| "tubeMaterialId"
| "tubeSpecificationId"
>

export function validatedNodeUserDataKeys(): (keyof ValidatedNodeUserDataEntries)[] {
	return exhaustiveStringTuple<keyof ValidatedNodeUserDataEntries>()(
		"sheetMaterialId",
		"laserSheetCuttingGasId",
		"sheetFilterSheetIds",
		"sheetTappingData",
		"tubeMaterialId",
		"tubeSpecificationId",
	);
}

/**
 * Functions to check if a set user data entry is valid.
 *
 * For user data values that correspond to table entries check, that there are matching table entries.
 *
 * Invalid values will result in the violation of a generic data consistency constraint.
 */
const isValidNodeUserDataValueFunctionMap: {[index in keyof ValidatedNodeUserDataEntries]: (arg: ValidatedNodeUserDataEntries[index] | undefined) => boolean} = {
	sheetMaterialId: arg => arg !== undefined && getTable("sheetMaterial").some(row => row.identifier === arg),
	laserSheetCuttingGasId: arg => arg !== undefined && getTable("laserSheetCuttingGas").some(row => row.identifier === arg),
	sheetTappingData: arg => {
		const screwThreads = getTable("screwThread");
		return arg === undefined || arg.every(entry => screwThreads.some(row => row.identifier === entry.screwThread.identifier));
	},
	tubeMaterialId: arg => arg !== undefined && getTable("tubeMaterial").some(row => row.identifier === arg),
	tubeSpecificationId: arg => arg !== undefined && getTable("tubeSpecification").some(row => row.identifier === arg),
	sheetFilterSheetIds: arg => {
		if (arg === undefined) {
			return true;
		}
		const sheets = getTable("sheet");
		return arg.every(entry => sheets.some(s => s.identifier === entry));
	},
};

/**
 * Check if a user data entry is valid.
 *
 * Type-checking is *not* part of this function (use isNodeUserDataEntryType() instead).
 *
 * For user data values that correspond to table entries check, that there are matching table entries.
 *
 * Invalid values will result in the violation of a generic data consistency constraint.
 */
export function isValidNodeUserDataValue<Key extends keyof ValidatedNodeUserDataEntries>(key: Key, value?: ValidatedNodeUserDataEntries[Key]): boolean {
	return isValidNodeUserDataValueFunctionMap[key](value);
}

/**
 * Minimal node UserData access wrapper
 */
export function nodeUserDatumImpl<Key extends keyof NodeUserDataEntries>(key: Key, userData: StringIndexedInterface): NodeUserDataEntries[Key] | undefined {
	const value = userData[key];
	if (value === undefined || !isNodeUserDataEntryType(key, value)) {
		return undefined;
	} else {
		return value;
	}
}

/**
 * Minimal node UserData access wrapper
 */
export function createUpdatedNodeUserDataImpl<Key extends keyof NodeUserDataEntries>(key: Key, value: NodeUserDataEntries[Key], userData: StringIndexedInterface): StringIndexedInterface {
	if (value !== undefined) {
		userData[key] = value;
	} else {
		const keys = Object.keys(userData);
		if (keys.some(k => k === key)) {
			const obj: StringIndexedInterface = {};
			keys.filter(k => k !== key)
				.forEach(k => {
					obj[k] = userData[k];
				});
			userData = obj;
		}
	}
	return userData;
}
