import {
	GraphNodeProcessingState,
	ProcessType,
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	isAttachment,
	isLaserSheetCuttingGasUniqueMembers,
	isPackagingUniqueMembers,
	isPoint3,
	isScrewThreadUniqueMembers,
	isSheetMaterialUniqueMembers,
	isStringIndexedInterface,
	isTubeMaterialUniqueMembers,
	isTubeSpecificationUniqueMembers,
	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,
	getKeysOfObject,
	isArray,
	isBoolean,
	isNumber,
	isString,
} from "./utils";

export interface SheetTappingDataEntry {
	center3: Point3;
	screwThread: ScrewThreadUniqueMembers;
}

export function isTappingDataEntry(arg: unknown): arg is SheetTappingDataEntry {
	const typeGuardMap: {[index in keyof SheetTappingDataEntry]: (arg: unknown) => arg is SheetTappingDataEntry[index]} = {
		center3: isPoint3,
		screwThread: isScrewThreadUniqueMembers,
	};
	return isStringIndexedInterface(arg) && getKeysOfObject(typeGuardMap)
		.every(key => typeGuardMap[key](arg[key]));
}

export interface ArticleUserData {
	/**
	 * Article name
	 *
	 * User editable name that might be pre-set by us.
	 */
	name: string;

	/**
	 * Article specific comment
	 *
	 * Empty by default.
	 */
	comment: string;

	/**
	 * User-defined / thirdparty part number
	 *
	 * ("Artikelnummer")
	 *
	 * Note: There are no guarantees for this value to be unique.
	 */
	externalPartNumber: string;

	/**
	 * User-defined / thirdparty drawing number
	 *
	 * ("Zeichnungsnummer")
	 *
	 * Note: There are no guarantees for this value to be unique.
	 */
	externalDrawingNumber: string;

	/**
	 * User-defined / thirdparty revision number
	 *
	 * ("Revisionsnummer")
	 *
	 * Note: There are no guarantees for this value to be unique.
	 */
	externalRevisionNumber: string;
}

const articleUserDataTypeGuardMap: TypeGuardMap<ArticleUserData> = {
	name: isString,
	comment: isString,
	externalPartNumber: isString,
	externalDrawingNumber: isString,
	externalRevisionNumber: isString,
};

export function isArticleUserDataEntry<Key extends keyof ArticleUserData>(key: Key, value: unknown): value is ArticleUserData[Key] {
	return articleUserDataTypeGuardMap[key](value);
}

function isArticleUserData(arg: unknown): arg is ArticleUserData {
	return isStringIndexedInterface(arg) && getKeysOfObject(articleUserDataTypeGuardMap)
		.every(key => articleUserDataTypeGuardMap[key](arg[key]));
}

/**
 * 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 {
	sheetMaterial: SheetMaterialUniqueMembers;
	comment: string;
	materialCostsPerPiece: number|undefined;
	setupTime: number|undefined;
	unitTimePerPiece: number|undefined;
	cuttingGas: LaserSheetCuttingGasUniqueMembers;
	packaging: PackagingUniqueMembers;
	transportDistance: number;
	importId: string|undefined;
	attachments: Attachment[];
	testReportRequired: boolean;
	fixedRotations: number[];
	deburrDoubleSided: boolean;
	numThreads: number;
	numCountersinks: number;
	sheetTappingData: SheetTappingDataEntry[];
	articleUserData: ArticleUserData;
	userDefinedScalePrices: UserDefinedScalePrice[];
	tubeMaterial: TubeMaterialUniqueMembers|undefined,
	tubeSpecification: TubeSpecificationUniqueMembers|undefined,
	sheetFilterSheetIds: string[];
	bendLineEngravingMode: BendLineEngravingMode;
}

export interface NodeUserDataContext {
	workStepType: WorkStepType;

	processType: ProcessType;

	isSignatureNode: boolean;

	isInitialNode: boolean;
}

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

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

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

function isOptionalNumber(arg: unknown): arg is number|undefined {
	return arg === undefined || isNumber(arg);
}

const nodeTypeGuardMap: TypeGuardMap<NodeUserDataEntries> = {
	sheetMaterial: isSheetMaterialUniqueMembers,
	comment: isString,
	materialCostsPerPiece: isOptionalNumber,
	setupTime: isOptionalNumber,
	unitTimePerPiece: isOptionalNumber,
	cuttingGas: isLaserSheetCuttingGasUniqueMembers,
	packaging: isPackagingUniqueMembers,
	transportDistance: isNumber,
	importId: (arg) : arg is string | undefined => (arg === undefined || isString(arg)),
	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, isTappingDataEntry),
	articleUserData: isArticleUserData,
	userDefinedScalePrices: (arg): arg is UserDefinedScalePrice[] => isArray(arg, isUserDefinedScalePrice),
	tubeMaterial: (arg): arg is TubeMaterialUniqueMembers|undefined => arg === undefined || isTubeMaterialUniqueMembers(arg),
	tubeSpecification: (arg): arg is TubeSpecificationUniqueMembers|undefined => arg === undefined || isTubeSpecificationUniqueMembers(arg),
	sheetFilterSheetIds: (arg: unknown): arg is string[] => isArray(arg, isString),
	bendLineEngravingMode: isBendLineEngravingMode,
};

const createNodeUserDataStringIndexFunctionMap: CreateUserDataStringIndexFunctionMap<NodeUserDataEntries> = {
	sheetMaterial: () => "material",
	comment: () => "comment",
	materialCostsPerPiece: context => (context.workStepType === WorkStepType.userDefinedBase ? "price" : "materialCostsPerPiece"),
	setupTime: () => "setupTime",
	unitTimePerPiece: () => "unitTimePerPiece",
	cuttingGas: () => "laserCuttingGas",
	packaging: () => "packaging",
	transportDistance: () => "km",
	importId: () => "importId",
	attachments: () => "attachments",
	testReportRequired: () => "testReportRequired",
	fixedRotations: () => "fixedRotations",
	deburrDoubleSided: () => "deburrDoubleSided",
	numThreads: () => "numThreads",
	numCountersinks: () => "numCountersinks",
	sheetTappingData: () => "sheetTappingData",
	articleUserData: () => "articleUserData",
	userDefinedScalePrices: () => "userDefinedScalePrices",
	tubeMaterial: () => "tubeMaterial",
	tubeSpecification: () => "tubeSpecification",
	sheetFilterSheetIds: () => "sheetFilterSheetIds",
	bendLineEngravingMode: () => "bendLineEngravingMode",
};

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

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

const nodeCompatibilityFunctionMap: CompatibilityFunctionMap<NodeUserDataEntries> = {
	sheetMaterial: context => context.isInitialNode && isSheetMaterialAndTestReportWst(context.workStepType),
	testReportRequired: context => context.isInitialNode && isSheetMaterialAndTestReportWst(context.workStepType),
	comment: context => noCommentWsts.every(wst => wst !== context.workStepType),
	materialCostsPerPiece: () => true,
	setupTime: () => true,
	unitTimePerPiece: () => true,
	cuttingGas: context => context.processType === ProcessType.laserSheetCutting,
	packaging: context => context.workStepType === WorkStepType.packaging,
	transportDistance: context => context.processType === ProcessType.transport,
	importId: () => true,
	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,
	// Note:  This should work for incomplete graphs as well as the WorkStepType prioritization defining signature nodes matches the occurence of nodes when initially creating the graph.
	// Example:  sheetBending is created prior to sheetCutting
	articleUserData: context => context.isSignatureNode,
	userDefinedScalePrices: () => true,
	tubeMaterial: context => context.isInitialNode && context.workStepType === WorkStepType.tubeCutting,
	tubeSpecification: context => context.isInitialNode && context.workStepType === WorkStepType.tubeCutting,
	sheetFilterSheetIds: context => context.workStepType === WorkStepType.sheetCutting,
	bendLineEngravingMode: context => context.workStepType === WorkStepType.sheetCutting,
};

/**
 * @param key Key of the UserData entry
 * @param context Context of the node the UserData entry belongs to
 * @returns Key of the UserData object
 *
 * Note: UserData string-indices are considered an implementation detail of this script module
 * Note: This function should not be used outside the user-data-functionality-world.  This function is exported as it is required by the initial-value-module.
 */
export function createNodeUserDataStringIndex<Key extends keyof NodeUserDataEntries>(key: Key, context: Readonly<NodeUserDataContext>): string {
	return createNodeUserDataStringIndexFunctionMap[key](context);
}

/**
 * @returns Array of UserData entry keys
 */
export function nodeUserDataEntries(): Array<keyof NodeUserDataEntries> {
	return exhaustiveStringTuple<keyof NodeUserDataEntries>()(
		"sheetMaterial",
		"comment",
		"materialCostsPerPiece",
		"setupTime",
		"unitTimePerPiece",
		"cuttingGas",
		"packaging",
		"transportDistance",
		"importId",
		"attachments",
		"testReportRequired",
		"fixedRotations",
		"deburrDoubleSided",
		"numThreads",
		"numCountersinks",
		"sheetTappingData",
		"articleUserData",
		"userDefinedScalePrices",
		"tubeMaterial",
		"tubeSpecification",
		"sheetFilterSheetIds",
		"bendLineEngravingMode",
	);
}

/**
 * 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} = {
	sheetMaterial: GraphNodeProcessingState.UpdateWorkStep,
	comment: GraphNodeProcessingState.Finished,
	materialCostsPerPiece: GraphNodeProcessingState.Finished,
	setupTime: GraphNodeProcessingState.Finished,
	unitTimePerPiece: GraphNodeProcessingState.Finished,
	cuttingGas: GraphNodeProcessingState.CreateSources,
	packaging: GraphNodeProcessingState.FillFromSources,
	transportDistance: GraphNodeProcessingState.Finished,
	importId: GraphNodeProcessingState.Finished,
	attachments: GraphNodeProcessingState.Finished,
	testReportRequired: GraphNodeProcessingState.Finished,
	fixedRotations: GraphNodeProcessingState.CreateSources,
	deburrDoubleSided: GraphNodeProcessingState.Finished,
	numThreads: GraphNodeProcessingState.Finished,
	numCountersinks: GraphNodeProcessingState.Finished,
	sheetTappingData: GraphNodeProcessingState.Finished,
	articleUserData: GraphNodeProcessingState.Finished,
	userDefinedScalePrices: GraphNodeProcessingState.Finished,
	tubeMaterial: GraphNodeProcessingState.UpdateWorkStep,
	tubeSpecification: 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;
}

/**
 * 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
 */
const isValidNodeUserDataValueFunctionMap: {[index in keyof NodeUserDataEntries]: (arg: NodeUserDataEntries[index]) => boolean} = {
	sheetMaterial: (value: SheetMaterialUniqueMembers) => getTable(TableType.sheetMaterial)
		.some(row => row.identifier === value.identifier),
	comment: () => true,
	materialCostsPerPiece: () => true,
	setupTime: () => true,
	unitTimePerPiece: () => true,
	cuttingGas: (arg: LaserSheetCuttingGasUniqueMembers) => getTable(TableType.laserSheetCuttingGas)
		.some(row => row.identifier === arg.identifier),
	packaging: (arg: PackagingUniqueMembers) => getTable(TableType.packaging)
		.some(row => row.identifier === arg.identifier),
	transportDistance: () => true,
	importId: () => true,
	attachments: () => true,
	testReportRequired: () => true,
	fixedRotations: () => true,
	deburrDoubleSided: () => true,
	numThreads: () => true,
	numCountersinks: () => true,
	sheetTappingData: (arg: SheetTappingDataEntry[]) => arg.every(entry => isTappingDataEntry(entry)),
	articleUserData: () => true,
	userDefinedScalePrices: () => true,
	tubeMaterial: (value: TubeMaterialUniqueMembers|undefined) => value === undefined || getTable(TableType.tubeMaterial)
		.some(row => row.identifier === value.identifier),
	tubeSpecification: (value: TubeMaterialUniqueMembers|undefined) => value === undefined || getTable(TableType.tubeSpecification)
		.some(row => row.identifier === value.identifier),
	sheetFilterSheetIds: (value: string[]) => {
		const sheets = getTable(TableType.sheet);
		return value.every(entry => sheets.some(s => s.identifier === entry));
	},
	bendLineEngravingMode: () => true,
};

/**
 * 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
 */
export function isValidNodeUserDataValue<Key extends keyof NodeUserDataEntries>(key: Key, value: NodeUserDataEntries[Key]): boolean {
	const f = isValidNodeUserDataValueFunctionMap[key] as (arg: NodeUserDataEntries[Key]) => boolean;
	return f(value);
}
