import {
	ProcessType,
} from "qrc:/js/lib/generated/enum";
import {
	isAttachment,
	isSheetMaterial,
	isSegment,
	isStringIndexedInterface,
} from "qrc:/js/lib/generated/typeguard";
import {
	ManufacturingState,
} from "qrc:/js/lib/manufacturing_state";
import {
	isNodeUserDataEntry,
	isNodeUserDataEntryType,
	isSheetTappingDataEntry,
	NodeUserDataEntries,
	SheetTappingDataEntry,
} from "qrc:/js/lib/userdata_config";
import {
	getKeysOfObject,
	hasOptionalPropertyT,
	hasPropertyT,
	isArray,
	isBoolean,
	isInstanceOf,
	isNumber,
	isObject,
	isString,
} from "qrc:/js/lib/utils";
import {
	isTappingCandidate,
	TappingCandidate,
} from "qrc:/js/lib/sheet_tapping_utils";

export interface InputEntry {
	importId: string;
	data: string;
	name?: string;
	thickness?: number;
}

export function isInputEntry(arg: unknown): arg is InputEntry {
	return isInstanceOf<InputEntry>(arg, {
		importId: isString,
		data: isString,
		name: (arg): arg is string | undefined => arg === undefined || isString(arg),
		thickness: (arg): arg is number | undefined => arg === undefined || isNumber(arg),
	});
}

/**
 * UserData as needed in shop
 */
export interface ShopUserData {
	material: SheetMaterial | undefined;
	doubleSidedMechanicalDeburring: boolean | undefined;
}

export interface ScalePrice {
	scaleValue: number;
	sellingPrice: number;
}

export interface NodeObject {
	vertex: string;
	rootIdKey: string;
	nodeIdKey: string;
	name: string;
	assemblyId: string|undefined;
	processId: string;
	processType: ProcessType;
	workStepType: WorkStepType;
	processTypeTranslation: string;
	userData: ShopUserData;
	importMultiplicity: number|undefined;
	sources: Array<string>;
	targets: Array<string>;
	thickness: number | undefined;
	width: number | undefined;
	height: number | undefined;
	actualManufacturingState: ManufacturingState;
	virtualManufacturingState: ManufacturingState;
	forcedProcessType: boolean;
	// Applies to matching nodes only; undefined if feature is disabled
	testReportRequired: boolean | undefined;
	// Applies to sheet nodes only; undefined if feature is disabled
	testReportPrice: number | undefined;
	fixedRotations: number[] | undefined;
	// Note: this applies to sheetCuttingNodes
	deburringPreviewPrices: Array<number> | undefined;
	// Note: this applices to (auto. / man.) deburring nodes
	deburringPrice: number | undefined;
	// Applies to sheet cutting nodes only
	cuttingGasId: string | undefined;
	// Applies to initial nodes that are not userDefinedBase only
	flipSide: boolean | undefined;
	// Applies to initial nodes that are not userDefinedBase only
	comment: string | undefined;
	// Applies only to userdefinedThreading nodes
	numThreads: number | undefined;
	// Applies only to userdefinedCountersinking nodes
	numCountersinks: number | undefined;
	// Note: this applies to sheetCuttingNodes
	slideGrindingPreviewPrice: number | undefined;
}

export interface ArticleObject {
	name: string;
	externalPartNumber: string;
	externalDrawingNumber: string;
	externalRevisionNumber: string;
	multiplicity: number;
	nodes: Array<NodeObject>;
	approxPrice: number | undefined;
	scalePrices: ScalePrice[];
}

export interface ResourceEntry {
	[index: string]: string;
}

export interface AttachmentMap {
	[index: string]: Array<Attachment>;
}

export interface TappingNodeData {
	/**
	 * Vertex key of the node the underlying CadFeatures refer to.
	 */
	partVertexKey: string;
	candidates: TappingCandidate[];
	selection: SheetTappingDataEntry[];
	svg: string;
}

export function isTappingNodeData(arg: unknown): arg is TappingNodeData {
	const typeGuardMap: {[key in keyof TappingNodeData]: (arg: unknown) => arg is TappingNodeData[key]} = {
		partVertexKey: isString,
		candidates: (n: unknown) : n is TappingCandidate[] => isArray(n, isTappingCandidate),
		selection: (n: unknown) : n is SheetTappingDataEntry[] => isArray(n, isSheetTappingDataEntry),
		svg: isString,
	};
	return isStringIndexedInterface(arg) && getKeysOfObject(typeGuardMap)
		.every(key => typeGuardMap[key](arg[key]));
}

export interface CanForceSheetPartEntry {
	vertexKey: string;
	canForce: boolean;
}

export interface TubeCuttingDataEntry {
	vertexKey: string;
	processId: string;
	dimX: number;
	tubeProfileGeometry: TubeProfileGeometry;
	// Value is undefined if there is no matching tube
	tubeMaterialId: string | undefined;
	// Value is undefined if there is no matching tube
	tubeSpecificationId: string | undefined;
}

export interface ProjectObject {
	creator: string;
	projectName: string;
	articles: Array<ArticleObject>;
	resources: ResourcesObject;
	minCompletionTime: number|undefined;
	netSellingPrice: number|undefined;
}

export interface ResourcesObject {
	attachments: AttachmentMap;
	gltfs: ResourceEntry;
	pngs: ResourceEntry;
	svgs: ResourceEntry;
	technicalDrawings: Record<string, string[]>;
	sheetTappingNodeData: TappingNodeData[];
	tubeCuttingData: TubeCuttingDataEntry[];
	canForceSheetPartData: CanForceSheetPartEntry[];
	problematicGeometryGlfts: ResourceEntry;
}

/**
 * Material table entry in shop configuration
 */
export type ShopConfigMaterialEntry = Array<SheetMaterial>;

/**
 * Sheets table entry in shop configuration
 */
export type ShopConfigSheetsEntry = Array<Sheet>;

export interface ShopConfigCuttingGasConfiguration {
	thickness: number;
	sheetMaterial: SheetMaterial;
	cuttingGasId: string;
}

export interface AdditionalProcesses {
	mechanicalDeburring: Array<ProcessType>;
	threading: Array<ProcessType>;
	countersinking: Array<ProcessType>;
	slideGrinding: Array<ProcessType>;
	tapping: Array<ProcessType>;
}

export function getAdditionalProcesses(): AdditionalProcesses {
	return {
		mechanicalDeburring: [
			ProcessType.automaticMechanicalDeburring,
			ProcessType.manualMechanicalDeburring,
		],
		slideGrinding: [ ProcessType.slideGrinding ],
		threading: [ ProcessType.userDefinedThreading ],
		countersinking: [ ProcessType.userDefinedCountersinking ],
		tapping: [ ProcessType.sheetTapping ],
	};
}

type AdditionalProcess = keyof AdditionalProcesses;

export function isAdditionalProcess(p: unknown): p is AdditionalProcess {
	for (const key of Object.keys(getAdditionalProcesses())) {
		if (p === key) {
			return true;
		}
	}
	return false;
}

export type AllowedAdditionalProcesses = Array<AdditionalProcess>;

export interface ShopConfigSettings {
	fixedRotationsEnabled: boolean;
	testReportRequired: boolean;
}

export interface MinLowerDieOpeningWidth {
	thickness: number;
	openingWidth: number;
}

export interface TubeProfileEntry {
	identifier: string;
	name: string;
	description: string;
	svg: string;
	geometry: TubeProfileGeometryRectangular|TubeProfileGeometryCircular;
	geometryType: TubeProfileGeometryType;
}

/**
 * Shop Configuration interface
 */
export interface ShopConfig {
	allowedAdditionalProcesses: AllowedAdditionalProcesses;
	allowedCuttingGasConfigurations: Array<ShopConfigCuttingGasConfiguration>;
	laserSheetCuttingMaterial: Array<SheetCuttingMaterialMapping>;
	material: ShopConfigMaterialEntry;
	settings: ShopConfigSettings;
	sheets: ShopConfigSheetsEntry;
	laserSheetCuttingMinAreas: Array<LaserSheetCuttingMinArea>;
	minLowerDieOpeningWidths: Array<MinLowerDieOpeningWidth>;
	tubeProfiles: TubeProfileEntry[];
	tubeMaterials: TubeMaterial[];
	tubeSpecifications: TubeSpecification[];
	tubes: Tube[];
	tubeCuttingProcesses: Process[];
	minimalScrewDiameter: number;
}

export interface AdditionalProcessUserDataEntry {
	key: keyof NodeUserDataEntries;
	value: NodeUserDataEntries[keyof NodeUserDataEntries];
}

export type AddOrRemoveAdditionalProcess = {
	process: keyof AdditionalProcesses;
	// if false, remove the node, if true add it
	addNode : boolean;
	userDataEntries?: Array<AdditionalProcessUserDataEntry>;
};

function isAdditionalProcessUserDataEntry(obj: unknown): obj is AdditionalProcessUserDataEntry {
	if (!isStringIndexedInterface(obj)) {
		return false;
	}
	const key = obj["key"];
	if (!isNodeUserDataEntry(key)) {
		return false;
	}
	const value = obj["value"];
	return isNodeUserDataEntryType(key, value);
}

function isAddOrRemoveAdditionalProcess(obj: unknown): obj is AddOrRemoveAdditionalProcess {
	return typeof obj === "object"
		&& obj !== null
		&& hasPropertyT(obj, "process", isAdditionalProcess)
		&& hasPropertyT(obj, "addNode", isBoolean)
		&& hasOptionalPropertyT(obj, "userDataEntries", (t: unknown): t is Array<AdditionalProcessUserDataEntry> => isArray(t, isAdditionalProcessUserDataEntry));
}

export interface TubeCuttingEditParams {
	processId: string;
	tubeId: string;
}

export function isTubeCuttingParams(arg: unknown): arg is TubeCuttingEditParams {
	return isInstanceOf<TubeCuttingEditParams>(arg, {
		processId: isString,
		tubeId: isString,
	});
}

export interface PossibleWriteableAttributes {
	articleName: string;
	articleExternalPartNumber: string;
	articleExternalDrawingNumber: string;
	articleExternalRevisionNumber: string;
	attachments: Array<Attachment>;
	flipSide: boolean;
	comment: string;
	cuttingGas: string;
	fixedRotations: Array<number>;
	sheetMaterial: SheetMaterial;
	importMultiplicity: number;
	testReportRequired: boolean;
	thickness: number;
	tubeCuttingEditParams: TubeCuttingEditParams;
	forceSheetMetalPart: boolean;
}

export type GraphNodeWritableAttributes = {
	[index in keyof PossibleWriteableAttributes]?: PossibleWriteableAttributes[index];
};

export function isGraphNodeWritableAttributes(obj: unknown): obj is GraphNodeWritableAttributes {
	type GraphNodeWritableAttributesTypeGuardMap = { readonly[index in keyof PossibleWriteableAttributes]: (arg: unknown) => arg is PossibleWriteableAttributes[index]; };
	const graphNodeWritableAttributesTypeGuards: GraphNodeWritableAttributesTypeGuardMap = {
		articleName: isString,
		articleExternalPartNumber: isString,
		articleExternalDrawingNumber: isString,
		articleExternalRevisionNumber: isString,
		attachments: (t: unknown) : t is Array<Attachment> => isArray(t, isAttachment),
		flipSide: isBoolean,
		comment: isString,
		cuttingGas: isString,
		fixedRotations: (t: unknown) : t is number[] => isArray(t, isNumber),
		sheetMaterial: isSheetMaterial,
		importMultiplicity: isNumber,
		testReportRequired: isBoolean,
		thickness: isNumber,
		tubeCuttingEditParams: isTubeCuttingParams,
		forceSheetMetalPart: isBoolean,
	};
	function hasOptionalValue<Key extends keyof GraphNodeWritableAttributes>(u: object, key: Key): boolean {
		return hasOptionalPropertyT(u, key, (value: unknown): value is GraphNodeWritableAttributes[Key] => graphNodeWritableAttributesTypeGuards[key](value));
	}

	if (!isObject(obj)) {
		return false;
	}

	return getKeysOfObject(graphNodeWritableAttributesTypeGuards)
		.every(key => hasOptionalValue(obj, key));
}

export declare interface RootIdWithGraphNodeWritableAttributes {
	rootId: GraphNodeRootId;
	attributes: GraphNodeWritableAttributes;
}

export interface VertexKeyWithChangeData {
	vertexKey: string;
	attributes: GraphNodeWritableAttributes;
	additionalProcesses: Array<AddOrRemoveAdditionalProcess>;
}

export function isVertexKeyWithChangeData(obj: unknown): obj is VertexKeyWithChangeData {
	return typeof obj === "object"
		&& obj !== null
		&& hasPropertyT(obj, "vertexKey", isString)
		&& hasPropertyT(obj, "attributes", isGraphNodeWritableAttributes)
		&& hasPropertyT(obj, "additionalProcesses", (t: unknown): t is Array<AddOrRemoveAdditionalProcess> => isArray(t, isAddOrRemoveAdditionalProcess));
}

export interface PartCreationInformation {
	segments: Segment[];
	thickness: number;
	name: string;
	material: SheetMaterial;
	gasId: string;
}

export function isPartCreationInformation(arg: unknown): arg is PartCreationInformation {
	const typeGuardMap: {[key in keyof PartCreationInformation]: (arg: unknown) => arg is PartCreationInformation[key]} = {
		segments: (n: unknown) : n is Segment[] => isArray(n, isSegment),
		thickness: isNumber,
		name: isString,
		material: isSheetMaterial,
		gasId: isString,
	};
	return isStringIndexedInterface(arg) && getKeysOfObject(typeGuardMap)
		.every(key => typeGuardMap[key](arg[key]));
}

export interface ConfigFlags {
	tubesEnabled: boolean;
}

export function isConfigFlags(arg: unknown): arg is ConfigFlags {
	const guardMap: {[index in keyof ConfigFlags]: (arg: unknown) => arg is ConfigFlags[index]} = {
		tubesEnabled: isBoolean,
	};
	return isStringIndexedInterface(arg) && getKeysOfObject(guardMap)
		.every(key => guardMap[key](arg[key]));
}
