import {
	isAttachment,
	isSegment,
	isSheetTappingDataEntry,
} from "qrc:/js/lib/generated/typeguard";
import {
	ManufacturingState,
} from "qrc:/js/lib/manufacturing_state";
import {
	findActiveProcess,
} from "qrc:/js/lib/process";
import {
	exhaustiveStringTuple,
	getKeysOfObject,
	hasOptionalPropertyT,
	isArray,
	isBoolean,
	isInstanceOf,
	isNumber,
	isObject,
	isString,
} from "qrc:/js/lib/utils";
import {
	isThroughHole,
	ThroughHole,
} from "qrc:/js/lib/sheet_tapping_utils";
import {
	ProcessType,
} from "qrc:/js/lib/generated/enum";

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 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;
	editingState: NodeEditingStateEntry[];
	hasTwoDimInput: boolean;
	importId: string | 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;
	throughHoles: ThroughHole[];
	selection: SheetTappingDataEntry[];
	svg: string;
}

export function isTappingNodeData(arg: unknown): arg is TappingNodeData {
	return isInstanceOf<TappingNodeData>(arg, {
		partVertexKey: isString,
		throughHoles: (n: unknown): n is ThroughHole[] => isArray(n, isThroughHole),
		selection: (n: unknown): n is SheetTappingDataEntry[] => isArray(n, isSheetTappingDataEntry),
		svg: isString,
	});
}

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

interface MechanicalDeburring {
	tag: "mechanicalDeburring";
	addNode: boolean;
	deburrDoubleSided: boolean;
}

function isMechanicalDeburring(arg: unknown): arg is MechanicalDeburring {
	return isInstanceOf<MechanicalDeburring>(arg, {
		tag: (a: unknown): a is "mechanicalDeburring" => a === "mechanicalDeburring",
		addNode: isBoolean,
		deburrDoubleSided: isBoolean,
	});
}

interface SlideGrinding {
	tag: "slideGrinding";
	addNode: boolean;
}

function isSlideGrinding(arg: unknown): arg is SlideGrinding {
	return isInstanceOf<SlideGrinding>(arg, {
		tag: (a: unknown): a is "slideGrinding" => a === "slideGrinding",
		addNode: isBoolean,
	});
}

interface Threading {
	tag: "threading";
	addNode: boolean;
	numThreads: number;
}

function isThreading(arg: unknown): arg is Threading {
	return isInstanceOf<Threading>(arg, {
		tag: (a: unknown): a is "threading" => a === "threading",
		addNode: isBoolean,
		numThreads: isNumber,
	});
}

interface Countersinking {
	tag: "countersinking";
	addNode: boolean;
	numCountersinks: number;
}

function isCountersinking(arg: unknown): arg is Countersinking {
	return isInstanceOf<Countersinking>(arg, {
		tag: (a: unknown): a is "countersinking" => a === "countersinking",
		addNode: isBoolean,
		numCountersinks: isNumber,
	});
}

interface Tapping {
	tag: "tapping";
	addNode: boolean;
	sheetTappingData: SheetTappingDataEntry[];
}

function isTapping(arg: unknown): arg is Tapping {
	return isInstanceOf<Tapping>(arg, {
		tag: (a: unknown): a is "tapping" => a === "tapping",
		addNode: isBoolean,
		sheetTappingData: (a: unknown): a is SheetTappingDataEntry[] => isArray(a, isSheetTappingDataEntry),
	});
}

export type AdditionalProcessInformation = MechanicalDeburring | SlideGrinding | Countersinking | Threading | Tapping;

function isAdditionalProcessInformation(arg: unknown): arg is AdditionalProcessInformation {
	return exhaustiveStringTuple<AdditionalProcessInformation["tag"]>()(
		"mechanicalDeburring",
		"slideGrinding",
		"threading",
		"countersinking",
		"tapping",
		// eslint-disable-next-line array-callback-return
	).some(tag => {
		switch (tag) {
			case "mechanicalDeburring": return isMechanicalDeburring(arg);
			case "slideGrinding": return isSlideGrinding(arg);
			case "threading": return isThreading(arg);
			case "countersinking": return isCountersinking(arg);
			case "tapping": return isTapping(arg);
		}
	});
}

export type AdditionalProcess = AdditionalProcessInformation["tag"];

export function isAdditionalProcess(p: unknown): p is AdditionalProcess {
	return exhaustiveStringTuple<AdditionalProcess>()("mechanicalDeburring", "threading", "countersinking", "slideGrinding", "tapping").some(e => e === p);
}

export type AllowedAdditionalProcesses = Array<AdditionalProcess>;

export function getAdditionalProcess(tag: AdditionalProcess): Process | undefined {
	const getProcess = (key: ProcessType) => findActiveProcess(p => p.type === key);
	switch (tag) {
		case "mechanicalDeburring": return getProcess(ProcessType.automaticMechanicalDeburring) ?? getProcess(ProcessType.manualMechanicalDeburring);
		case "slideGrinding": return getProcess(ProcessType.slideGrinding);
		case "threading": return getProcess(ProcessType.userDefinedThreading);
		case "countersinking": return getProcess(ProcessType.userDefinedCountersinking);
		case "tapping": return getProcess(ProcessType.sheetTapping);
	}
}

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: TubeProfileGeometry;
}

/**
 * Shop Configuration interface
 */
export interface ShopConfig {
	allowedAdditionalProcesses: AllowedAdditionalProcesses;
	availableProcessesSheetCutting: Process[];
	availableProcessesTubeCutting: Process[];
	material: ShopConfigMaterialEntry;
	minLowerDieOpeningWidths: Array<MinLowerDieOpeningWidth>;
	minimalScrewDiameter: number;
	processConstraintsSheetMaterial: ProcessConstraintsSheetMaterial[];
	processConstraintsTubeMaterial: ProcessConstraintsTubeMaterial[];
	screwThreads: ScrewThread[];
	settings: ShopConfigSettings;
	sheetCuttingMotionParams: SheetCuttingMotionParameters[];
	sheetCuttingProcessMappings: SheetCuttingProcessMapping[];
	sheetCuttingProcesses: SheetCuttingProcess[];
	sheetCuttingThicknessConstraints: SheetCuttingThicknessConstraints[];
	sheets: ShopConfigSheetsEntry;
	tubeCuttingProcessMappings: TubeCuttingProcessMapping[];
	tubeCuttingProcesses: TubeCuttingProcess[];
	tubeMaterials: TubeMaterial[];
	tubeProfiles: TubeProfileEntry[];
	tubeSpecifications: TubeSpecification[];
	tubes: Tube[];
}

export interface TubeCuttingEditParams {
	processId: string;
	tubeMaterialId: string;
	tubeSpecificationId: string;
}

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

export interface SheetMetalPartEditParams {
	processIdSheetCutting: string;
	sheetMaterialId: string;
}

export function isSheetMetalPartEditParams(arg: unknown): arg is SheetMetalPartEditParams {
	return isInstanceOf<SheetMetalPartEditParams>(arg, {
		processIdSheetCutting: isString,
		sheetMaterialId: isString,
	});
}

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

export type GraphNodeWritableAttributes = Partial<PossibleWriteableAttributes>;

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,
		fixedRotations: (t: unknown): t is number[] => isArray(t, isNumber),
		sheetMetalPartEditParams: isSheetMetalPartEditParams,
		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: AdditionalProcessInformation[];
}

export function isVertexKeyWithChangeData(obj: unknown): obj is VertexKeyWithChangeData {
	return isInstanceOf<VertexKeyWithChangeData>(obj, {
		vertexKey: isString,
		attributes: isGraphNodeWritableAttributes,
		additionalProcesses: (arg: unknown): arg is AdditionalProcessInformation[] => isArray(arg, isAdditionalProcessInformation),
	});
}

export type PartCreationType = "flat" | "profile";

export function isPartCreationType(arg: unknown): arg is PartCreationType {
	return typeof arg === "string" && exhaustiveStringTuple<PartCreationType>()("flat", "profile").some(t => t === arg);
}

export interface PartCreationInformation {
	type: PartCreationType;
	segments: Segment[];
	thickness: number;
	name: string;
	sheetMaterialId: string;
	processIdSheetCutting: string;
	importId: string;
}

export function isPartCreationInformation(arg: unknown): arg is PartCreationInformation {
	return isInstanceOf<PartCreationInformation>(arg, {
		type: isPartCreationType,
		segments: (n: unknown): n is Segment[] => isArray(n, isSegment),
		thickness: isNumber,
		name: isString,
		sheetMaterialId: isString,
		processIdSheetCutting: isString,
		importId: isString,
	});
}

export interface ConfigFlags {
	tubesEnabled: boolean;
}

export function isConfigFlags(arg: unknown): arg is ConfigFlags {
	return isInstanceOf<ConfigFlags>(arg, {
		tubesEnabled: isBoolean,
	});
}
