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 {
	getSettingOrDefault,
} from "./settings_table";
import {
	getTable,
} from "./table_utils";
import {
	getKeysOfObject,
	isArray,
	isBoolean,
	isEqual,
	isNumber,
	isString,
	parseJson,
	writeSetting,
} 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,
}

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),
};

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",
};

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,
};

/**
 * @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> {
	const keys: {[index in keyof NodeUserDataEntries]: undefined} = {
		sheetMaterial: undefined,
		comment: undefined,
		materialCostsPerPiece: undefined,
		setupTime: undefined,
		unitTimePerPiece: undefined,
		cuttingGas: undefined,
		packaging: undefined,
		transportDistance: undefined,
		importId: undefined,
		attachments: undefined,
		testReportRequired: undefined,
		fixedRotations: undefined,
		deburrDoubleSided: undefined,
		numThreads: undefined,
		numCountersinks: undefined,
		sheetTappingData: undefined,
		articleUserData: undefined,
		userDefinedScalePrices: undefined,
		tubeMaterial: undefined,
		tubeSpecification: undefined,
	};
	return getKeysOfObject(keys);
}

/**
 * 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,
};

/**
 * 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;
}

/**
 * Settings key function generators for each node UserData entry
 *
 * Note: This function does *not* require a vertex as it is necessary to read / write settings values independently from a vertex.
 * Example: settings-based default value definition for certain UserData entries.
 *
 * If a settings key depends on certain properties, the callbacks could be extended so they optionally take certain parameters (e. g. WorkStepType, ProcessType, processId, ...).
 */
const nodeUserDataSettingsKeyFunctionMap: {[index in keyof NodeUserDataEntries]: () => string} = {
	sheetMaterial: () => "globalMaterial",
	comment: () => "comment",
	materialCostsPerPiece: () => "materialCostsPerPiece",
	setupTime: () => "setupTime",
	unitTimePerPiece: () => "unitTimePerPiece",
	cuttingGas: () => "cuttingGas",
	packaging: () => "packaging",
	transportDistance: () => "transportDistance",
	importId: () => "importId",
	attachments: () => "attachments",
	testReportRequired: () => "testReportRequired",
	fixedRotations: () => "fixedRotations",
	deburrDoubleSided: () => "deburrDoubleSided",
	numThreads: () => "numThreads",
	numCountersinks: () => "numCountersinks",
	sheetTappingData: () => "sheetTappingData",
	articleUserData: () => "articleUserData",
	userDefinedScalePrices: () => "userDefinedScalePrices",
	tubeMaterial: () => "tubeMaterial",
	tubeSpecification: () => "tubeSpecification",
};

function createNodeUserDataSettingsKey<Key extends keyof NodeUserDataEntries>(key: Key): string {
	return nodeUserDataSettingsKeyFunctionMap[key]();
}

/**
 * Read UserData value from settings
 *
 * @param key Key of the userData entry to read
 * @returns The value if available; undefined else
 */
export function readNodeUserDataSettingsValue<Key extends keyof NodeUserDataEntries>(key: Key): NodeUserDataEntries[Key]|undefined {
	const json = wsi4.io.settings.read(createNodeUserDataSettingsKey(key));
	return json === undefined ? undefined : parseJson(json, (value: unknown): value is NodeUserDataEntries[Key] => isNodeUserDataEntryType(key, value));
}

/**
 * Write UserData value from settings
 *
 * @param key Key of the userData entry to write
 * @param value Value to write
 */
export function writeNodeUserDataSettingsValue<Key extends keyof NodeUserDataEntries>(key: Key, value: NodeUserDataEntries[Key]): void {
	writeSetting(createNodeUserDataSettingsKey(key), value);
}

/**
 * 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),
};

/**
 * 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);
}

const isCloseNodeUserDataFunctionMap: {[index in keyof NodeUserDataEntries]: (lhs: NodeUserDataEntries[index], rhs: NodeUserDataEntries[index]) => boolean} = {
	sheetMaterial: (lhs, rhs) => lhs.identifier === rhs.identifier,
	comment: isEqual,
	materialCostsPerPiece: isEqual,
	setupTime: isEqual,
	unitTimePerPiece: isEqual,
	cuttingGas: (lhs, rhs) => lhs.identifier === rhs.identifier,
	packaging: (lhs, rhs) => lhs.identifier === rhs.identifier,
	transportDistance: isEqual,
	importId: isEqual,
	attachments: isEqual,
	testReportRequired: isEqual,
	fixedRotations: isEqual,
	deburrDoubleSided: isEqual,
	numThreads: isEqual,
	numCountersinks: isEqual,
	sheetTappingData: (lhsEntries, rhsEntries) => lhsEntries.length === rhsEntries.length && lhsEntries.every((lhsEntry, index) => {
		const rhsEntry = rhsEntries[index];
		return lhsEntry.screwThread.identifier === rhsEntry.screwThread.identifier
			&& isEqual(lhsEntry.center3, rhsEntry.center3);
	}),
	articleUserData: isEqual,
	userDefinedScalePrices: isEqual,
	tubeMaterial: (lhs, rhs) => lhs === undefined && rhs === undefined ? true : lhs === undefined || rhs === undefined ? false : lhs.identifier === rhs.identifier,
	tubeSpecification: (lhs, rhs) => lhs === undefined && rhs === undefined ? true : lhs === undefined || rhs === undefined ? false : lhs.identifier === rhs.identifier,
};

/**
 * Check if two user data entries are close
 *
 * For user data entries that hold table values this check is limited to the unique members respectively.
 * All other entries are checked for equality.
 */
export function isCloseNodeUserDataEntry<Key extends keyof NodeUserDataEntries>(key: Key, lhs: NodeUserDataEntries[Key], rhs: NodeUserDataEntries[Key]): boolean {
	return isCloseNodeUserDataFunctionMap[key](lhs as never, rhs as never);
}
