import {
	Times,
} from "qrc:/js/lib/calc_times";
import {
	ProcessType,
} from "qrc:/js/lib/generated/enum";
import {
	computeArticleMass,
	isComponentArticle,
	sheetIdForVertex,
} from "qrc:/js/lib/graph_utils";
import {
	assert,
	exhaustiveStringTuple,
	toCsvString,
} from "qrc:/js/lib/utils";
import {
	accessAllUserData,
	collectNodeUserDataEntries,
	nodeUserDatumOrDefault,
	hasManufacturingCostOverride,
	isCompatibleToNodeUserDataEntry,
	nodeUserDatum,
} from "qrc:/js/lib/userdata_utils";
import {
	grossTubeConsumptionForVertex,
	tubeForVertex,
} from "qrc:/js/lib/node_utils";

import {back, front} from "../lib/array_util";
import {articleUserDatumImpl} from "../lib/article_userdata_config";
import {
	computeRecursiveArticleScaleSellingPrice,
	computeRecursiveArticleScaleCosts,
	computeArticleSheetConsumption,
} from "./export_calc_costs";
import {
	CalcCache,
} from "./export_calc_cache";
import {
	computeNodeTimes,
	getFixedMultiplicity,
} from "./export_calc_times";

/**
 * Corresponds to a component article and consists of data that apply to the respective article
 */
export interface BomEntry {
	/**
	 * Numerical index of the entry
	 */
	itemNumber: number;

	/**
	 * Name of the corresponding article
	 */
	articleName: string;

	/**
	 * Part number of the corresponding article (if any).
	 *
	 * This value is user-defined.  There is no guarantee for this value to be non-empty or unique.
	 */
	articlePartNumber: string;

	/**
	 * Drawing number of the corresponding article (if any).
	 *
	 * This value is user-defined.  There is no guarantee for this value to be non-empty or unique.
	 */
	articleDrawingNumber: string;

	/**
	 * Revision number of the corresponding article (if any).
	 *
	 * This value is user-defined.  There is no guarantee for this value to be non-empty or unique.
	 */
	articleRevisionNumber: string;

	/**
	 * Comment for the corresponding article
	 */
	articleComment: string;

	/**
	 * Multiplicity of the corresponding article
	 */
	articleMultiplicity: number;

	/**
	 * Setup costs of the corresponding article
	 */
	articleSetupCosts: number | undefined;

	/**
	 * Material costs of the corresponding article
	 *
	 * Note:  Value includes [[multiplicity]]
	 */
	articleMaterialCosts: number | undefined;

	/**
	 * Unit costs of the corresponding article
	 *
	 * Note:  Value includes [[multiplicity]]
	 */
	articleUnitCosts: number | undefined;

	/**
	 * Mass of one instance of the article (if any)
	 */
	articleMass: number | undefined;

	/**
	 * Selling price
	 *
	 * Note:  Value includes [[multiplicity]]
	 */
	articleSellingPrice: number | undefined;

	/**
	 * If laser sheet cutting is part of the associated article
	 */
	laserSheetCuttingRequired: boolean;

	/**
	 * If die bending is part of the associated article
	 */
	dieBendingRequired: boolean;

	/**
	 * If automatic mechanical deburring is part of the associated article
	 */
	automaticMechanicalDeburringRequired: boolean;

	/**
	 * If manual mechanical deburring is part of the associated article
	 */
	manualMechanicalDeburringRequired: boolean;

	/**
	 * If user defined threading is part of the associated article
	 */
	userDefinedThreadingRequired: boolean;

	/**
	 * If user defined counter sinking is part of the associated article
	 */
	userDefinedCountersinkingRequired: boolean;

	/**
	 * If slide grinding is part of the associated article
	 */
	slideGrindingRequired: boolean;

	/**
	 * Sheet thickness of the associated article (if any)
	 */
	sheetThickness: number | undefined;

	/**
	 * Id of the associated sheet material (if any)
	 */
	sheetMaterialId: string | undefined;

	/**
	 * Id of the associated sheet material (if any)
	 *
	 * Legacy value; same as [[ sheetMaterialId ]]
	 */
	globalMaterialId: string | undefined;

	/**
	 * Id of the associated sheet (if any)
	 */
	sheetId: string | undefined;

	/**
	 * Consumption of the associated sheet (if any)
	 */
	sheetConsumption: number | undefined;

	/**
	 * Laser sheet cutting gas id (if any)
	 */
	laserSheetCuttingGasId: string | undefined;

	/**
	 * Comma-separated list of fixed rotations (if any)
	 *
	 * Value is undefined if
	 * - there there are no fixed rotations
	 * - the datum does not apply to the respective article
	 *
	 * Each value represents a rotation relative to the part's default orientation in degree.
	 */
	laserSheetCuttingFixedRotations: string | undefined;

	/**
	 * If a test report is required for the corresponding article
	 */
	sheetTestReportRequired: boolean | undefined;

	/**
	 * Number of user defined threads (if any)
	 */
	userDefinedThreadingNumThreads: number | undefined;

	/**
	 * Number of user defined counter sinks (if any)
	 */
	userDefinedCountersinkingNumSinks: number | undefined;

	/**
	 * If mechanical deburring should be applied on both sides (if any)
	 */
	mechanicalDeburringDeburrDoubleSided: boolean | undefined;

	/**
	 * Comment of the laser sheet cutting node (if any)
	 */
	laserSheetCuttingComment: string | undefined;

	/**
	 * Comment of the die bending node (if any)
	 */
	dieBendingComment: string | undefined;

	/**
	 * Comment of the automatic mechanical deburring node (if any)
	 */
	automaticMechanicalDeburringComment: string | undefined;

	/**
	 * Comment of the manual mechanical deburring node (if any)
	 */
	manualMechanicalDeburringComment: string | undefined;

	/**
	 * Comment of the user defined threading node (if any)
	 */
	userDefinedThreadingComment: string | undefined;

	/**
	 * Comment of the user defined countersinking node (if any)
	 */
	userDefinedCountersinkingComment: string | undefined;

	/**
	 * Comment of the slide grinding node (if any)
	 */
	slideGrindingComment: string | undefined;

	/**
	 * Setup time for process type automatical mechanical deburring (if any)
	 *
	 * Value represents time in seconds.
	 */
	automaticMechanicalDeburringSetupTime: number | undefined;

	/**
	 * Unit time for process type automatical mechanical deburring (if any)
	 *
	 * Value represents time in seconds.
	 *
	 * Value includes [[multiplicity]].
	 */
	automaticMechanicalDeburringUnitTime: number | undefined;

	/**
	 * Setup time for process type manual mechanical deburring (if any)
	 *
	 * Value represents time in seconds.
	 */
	manualMechanicalDeburringSetupTime: number | undefined;

	/**
	 * Unit time for process type manual mechanical deburring (if any)
	 *
	 * Value represents time in seconds.
	 *
	 * Value includes [[multiplicity]].
	 */
	manualMechanicalDeburringUnitTime: number | undefined;

	/**
	 * Setup time for process type automatical mechanical deburring (if any)
	 *
	 * Value represents time in seconds.
	 */
	slideGrindingSetupTime: number | undefined;

	/**
	 * Unit time for process type automatical mechanical deburring (if any)
	 *
	 * Value represents time in seconds.
	 *
	 * Value includes [[multiplicity]].
	 */
	slideGrindingUnitTime: number | undefined;

	/**
	 * Setup time for process type die bending (if any)
	 *
	 * Value represents time in seconds.
	 */
	dieBendingSetupTime: number | undefined;

	/**
	 * Unit time for process type die bending (if any)
	 *
	 * Value represents time in seconds.
	 *
	 * Value includes [[multiplicity]].
	 */
	dieBendingUnitTime: number | undefined;

	/**
	 * Setup time for process type laser sheet cutting (if any)
	 *
	 * Value represents time in seconds.
	 */
	laserSheetCuttingSetupTime: number | undefined;

	/**
	 * Unit time for process type laser sheet cutting (if any)
	 *
	 * Value represents time in seconds.
	 *
	 * Value includes [[multiplicity]].
	 */
	laserSheetCuttingUnitTime: number | undefined;

	/**
	 * Setup time for process type user defined countersinking (if any)
	 *
	 * Value represents time in seconds.
	 */
	userDefinedCountersinkingSetupTime: number | undefined;

	/**
	 * Unit time for process type user defined countersinking (if any)
	 *
	 * Value represents time in seconds.
	 *
	 * Value includes [[multiplicity]].
	 */
	userDefinedCountersinkingUnitTime: number | undefined;

	/**
	 * Setup time for process type user defined threading (if any)
	 *
	 * Value represents time in seconds.
	 */
	userDefinedThreadingSetupTime: number | undefined;

	/**
	 * Unit time for process type user defined threading (if any)
	 *
	 * Value represents time in seconds.
	 *
	 * Value includes [[multiplicity]].
	 */
	userDefinedThreadingUnitTime: number | undefined;

	/**
	 * Consumption of the associated tube (if any)
	 */
	tubeConsumption: number | undefined;

	/**
	 * Comment of the tube cutting node (if any)
	 */
	tubeCuttingComment: string | undefined;

	/**
	 * ID of the tube cutting process (if any)
	 */
	tubeCuttingProcessId: string | undefined;

	/**
	 *
	 * If tube cutting is part of the associated article
	 */
	tubeCuttingRequired: boolean | undefined;

	/**
	 * Setup time for tube cutting (if any)
	 *
	 * Value represents time in seconds.
	 */
	tubeCuttingSetupTime: number | undefined;

	/**
	 * Unit time for tube cutting(if any)
	 *
	 * Value represents time in seconds.
	 *
	 * Value includes [[multiplicity]].
	 */
	tubeCuttingUnitTime: number | undefined;

	/**
	 * ID of the associated tube (if any)
	 */
	tubeId: string | undefined;
}

export function entryKeys() : (keyof BomEntry)[] {
	return exhaustiveStringTuple<keyof BomEntry>()(
		"articleMaterialCosts",
		"articleMultiplicity",
		"articleName",
		"articlePartNumber",
		"articleDrawingNumber",
		"articleRevisionNumber",
		"articleComment",
		"articleSellingPrice",
		"articleSetupCosts",
		"articleUnitCosts",
		"articleMass",
		"automaticMechanicalDeburringComment",
		"automaticMechanicalDeburringRequired",
		"automaticMechanicalDeburringSetupTime",
		"automaticMechanicalDeburringUnitTime",
		"dieBendingComment",
		"dieBendingRequired",
		"dieBendingSetupTime",
		"dieBendingUnitTime",
		"sheetMaterialId",
		"globalMaterialId",
		"itemNumber",
		"laserSheetCuttingComment",
		"laserSheetCuttingFixedRotations",
		"laserSheetCuttingGasId",
		"laserSheetCuttingRequired",
		"laserSheetCuttingSetupTime",
		"laserSheetCuttingUnitTime",
		"manualMechanicalDeburringComment",
		"manualMechanicalDeburringRequired",
		"manualMechanicalDeburringSetupTime",
		"manualMechanicalDeburringUnitTime",
		"mechanicalDeburringDeburrDoubleSided",
		"sheetConsumption",
		"sheetId",
		"sheetTestReportRequired",
		"sheetThickness",
		"slideGrindingComment",
		"slideGrindingRequired",
		"slideGrindingSetupTime",
		"slideGrindingUnitTime",
		"tubeConsumption",
		"tubeCuttingComment",
		"tubeCuttingProcessId",
		"tubeCuttingRequired",
		"tubeCuttingSetupTime",
		"tubeCuttingUnitTime",
		"tubeId",
		"userDefinedCountersinkingComment",
		"userDefinedCountersinkingNumSinks",
		"userDefinedCountersinkingRequired",
		"userDefinedCountersinkingSetupTime",
		"userDefinedCountersinkingUnitTime",
		"userDefinedThreadingComment",
		"userDefinedThreadingNumThreads",
		"userDefinedThreadingRequired",
		"userDefinedThreadingSetupTime",
		"userDefinedThreadingUnitTime",
	);
}

function extractLaserSheetCuttingGasId(article: readonly Readonly<Vertex>[]): string | undefined {
	const vertex = article.find(vertex => wsi4.node.processType(vertex) === ProcessType.laserSheetCutting);
	if (vertex === undefined) {
		return undefined;
	}
	return nodeUserDatum("laserSheetCuttingGasId", vertex);
}

function extractLaserSheetCuttingFixedRotations(article: readonly Readonly<Vertex>[]): string | undefined {
	const vertex = article.find(vertex => wsi4.node.processType(vertex) === ProcessType.laserSheetCutting);
	if (vertex === undefined || !isCompatibleToNodeUserDataEntry("fixedRotations", vertex)) {
		return undefined;
	}

	const fixedRotations = nodeUserDatumOrDefault("fixedRotations", vertex);
	if (fixedRotations === undefined) {
		return undefined;
	}

	if (fixedRotations.length === 0) {
		return undefined;
	} else {
		return fixedRotations.join(",");
	}
}

function extractTestReportRequired(article: readonly Readonly<Vertex>[]): boolean | undefined {
	const vertex = article.find(vertex => isCompatibleToNodeUserDataEntry("testReportRequired", vertex));
	if (vertex === undefined) {
		return undefined;
	} else {
		return nodeUserDatumOrDefault("testReportRequired", vertex);
	}
}

function extractUserDefinedThreadingNumThreads(article: readonly Readonly<Vertex>[]): number | undefined {
	const vertex = article.find(vertex => wsi4.node.processType(vertex) === ProcessType.userDefinedThreading);
	if (vertex === undefined || !isCompatibleToNodeUserDataEntry("numThreads", vertex)) {
		return undefined;
	} else {
		return nodeUserDatumOrDefault("numThreads", vertex);
	}
}

function extractUserDefinedCountersinkingNumSinks(article: readonly Readonly<Vertex>[]): number | undefined {
	const vertex = article.find(vertex => wsi4.node.processType(vertex) === ProcessType.userDefinedCountersinking);
	if (vertex === undefined || !isCompatibleToNodeUserDataEntry("numCountersinks", vertex)) {
		return undefined;
	} else {
		return nodeUserDatumOrDefault("numCountersinks", vertex);
	}
}

function extractMechanicalDeburringDeburrDoubleSided(article: readonly Readonly<Vertex>[]): boolean | undefined {
	const vertex = article.find(vertex => {
		const processType = wsi4.node.processType(vertex);
		return processType === ProcessType.automaticMechanicalDeburring || processType === ProcessType.manualMechanicalDeburring;
	});
	if (vertex === undefined || !isCompatibleToNodeUserDataEntry("deburrDoubleSided", vertex)) {
		return undefined;
	} else {
		return nodeUserDatumOrDefault("deburrDoubleSided", vertex);
	}
}

function requiresProcessType(article: readonly Readonly<Vertex>[], processType: ProcessType): boolean {
	return article.some(vertex => wsi4.node.processType(vertex) === processType);
}

function extractSheetMaterialId(article: readonly Readonly<Vertex>[]): string | undefined {
	const values = collectNodeUserDataEntries("sheetMaterialId", front(article), accessAllUserData);
	assert(values.length <= 1, "Expecting at most one user data entry for sheetMaterial");
	if (values.length === 1) {
		return values[0]!;
	} else {
		return undefined;
	}
}

function extractSheetId(article: readonly Readonly<Vertex>[]): string | undefined {
	const vertex = [
		...article,
		...wsi4.graph.reaching(front(article)),
	].find(vertex => wsi4.node.processType(vertex) === ProcessType.sheet);
	if (vertex === undefined) {
		return undefined;
	} else {
		return sheetIdForVertex(vertex);
	}
}

function computeSheetConsumption(article: readonly Readonly<Vertex>[]): number | undefined {
	return computeArticleSheetConsumption(article);
}

function extractSheetThickness(article: readonly Readonly<Vertex>[]): number | undefined {
	return article.reduce((acc: number | undefined, vertex) => acc === undefined ? wsi4.node.sheetThickness(vertex) : acc, undefined);
}

function extractProcessSpecificComment(article: readonly Readonly<Vertex>[], processType: ProcessType): string | undefined {
	const vertex = article.find(vertex => wsi4.node.processType(vertex) === processType);
	if (vertex === undefined) {
		return undefined;
	} else {
		return nodeUserDatumOrDefault("comment", vertex);
	}
}

function computeProcessTime(
	article: readonly Readonly<Vertex>[],
	processType: ProcessType,
	extractTime: (times: Times) => number | undefined,
	cache?: CalcCache,
): number | undefined {
	const vertex = article.find(vertex => wsi4.node.processType(vertex) === processType);
	if (vertex === undefined) {
		return undefined;
	} else {
		const times = computeNodeTimes(vertex, getFixedMultiplicity(vertex), cache);
		return times === undefined ? undefined : extractTime(times);
	}
}

function computeProcessSetupTime(article: readonly Readonly<Vertex>[], processType: ProcessType, calcCache?: CalcCache): number | undefined {
	return computeProcessTime(article, processType, (times: Times) => times.setup, calcCache);
}

function computeProcessUnitTime(article: readonly Readonly<Vertex>[], processType: ProcessType, calcCache?: CalcCache): number | undefined {
	return computeProcessTime(article, processType, (times: Times) => times.unit, calcCache);
}

function computeTubeConsumption(article: readonly Readonly<Vertex>[]): number | undefined {
	const vertex = article.find(vertex => wsi4.node.processType(vertex) === ProcessType.tubeCutting);
	if (vertex === undefined) {
		return undefined;
	} else {
		return grossTubeConsumptionForVertex(vertex);
	}
}

function extractTubeCuttingProcessId(article: readonly Readonly<Vertex>[]): string | undefined {
	const vertex = article.find(vertex => wsi4.node.processType(vertex) === ProcessType.tubeCutting);
	if (vertex === undefined) {
		return undefined;
	} else {
		return wsi4.node.processId(vertex);
	}
}

function extractTubeId(article: readonly Readonly<Vertex>[]): string | undefined {
	const vertex = article.find(vertex => wsi4.node.processType(vertex) === ProcessType.tubeCutting);
	if (vertex === undefined) {
		return undefined;
	} else {
		return tubeForVertex(vertex)?.identifier;
	}
}

/**
 * Create BOM entry for an article
 *
 * Assumption:  Underlying nodes are export ready
 */
export function createEntry(article: readonly Readonly<Vertex>[], itemNumber: number, calcCache?: CalcCache): BomEntry {
	assert(isComponentArticle(article), "Expecting component article");

	const multiplicity = wsi4.node.multiplicity(back(article));
	const articleCosts = (() => {
		const subGraph = [
			...article,
			...wsi4.graph.reaching(front(article)),
		];
		if (subGraph.some(vertex => hasManufacturingCostOverride(vertex))) {
			return undefined;
		} else {
			return computeRecursiveArticleScaleCosts(article, multiplicity, calcCache);
		}
	})();

	const articleUserData = wsi4.node.articleUserData(front(article));

	return {
		articleMaterialCosts: articleCosts === undefined ? undefined : articleCosts.material,
		articleMultiplicity: multiplicity,
		articleName: articleUserDatumImpl("name", articleUserData) ?? "",
		articlePartNumber: articleUserDatumImpl("externalPartNumber", articleUserData) ?? "",
		articleDrawingNumber: articleUserDatumImpl("externalDrawingNumber", articleUserData) ?? "",
		articleRevisionNumber: articleUserDatumImpl("externalRevisionNumber", articleUserData) ?? "",
		articleComment: articleUserDatumImpl("comment", articleUserData) ?? "",
		articleSellingPrice: computeRecursiveArticleScaleSellingPrice(article, multiplicity, calcCache),
		articleSetupCosts: articleCosts === undefined ? undefined : articleCosts.setup,
		articleUnitCosts: articleCosts === undefined ? undefined : articleCosts.unit,
		articleMass: computeArticleMass(article),
		automaticMechanicalDeburringComment: extractProcessSpecificComment(article, ProcessType.automaticMechanicalDeburring),
		automaticMechanicalDeburringRequired: requiresProcessType(article, ProcessType.automaticMechanicalDeburring),
		automaticMechanicalDeburringSetupTime: computeProcessSetupTime(article, ProcessType.automaticMechanicalDeburring, calcCache),
		automaticMechanicalDeburringUnitTime: computeProcessUnitTime(article, ProcessType.automaticMechanicalDeburring, calcCache),
		dieBendingComment: extractProcessSpecificComment(article, ProcessType.dieBending),
		dieBendingRequired: requiresProcessType(article, ProcessType.dieBending),
		dieBendingSetupTime: computeProcessSetupTime(article, ProcessType.dieBending, calcCache),
		dieBendingUnitTime: computeProcessUnitTime(article, ProcessType.dieBending, calcCache),
		sheetMaterialId: extractSheetMaterialId(article),
		globalMaterialId: extractSheetMaterialId(article),
		itemNumber: itemNumber,
		laserSheetCuttingComment: extractProcessSpecificComment(article, ProcessType.laserSheetCutting),
		laserSheetCuttingFixedRotations: extractLaserSheetCuttingFixedRotations(article),
		laserSheetCuttingGasId: extractLaserSheetCuttingGasId(article),
		laserSheetCuttingRequired: requiresProcessType(article, ProcessType.laserSheetCutting),
		laserSheetCuttingSetupTime: computeProcessSetupTime(article, ProcessType.laserSheetCutting, calcCache),
		laserSheetCuttingUnitTime: computeProcessUnitTime(article, ProcessType.laserSheetCutting, calcCache),
		manualMechanicalDeburringComment: extractProcessSpecificComment(article, ProcessType.manualMechanicalDeburring),
		manualMechanicalDeburringRequired: requiresProcessType(article, ProcessType.manualMechanicalDeburring),
		manualMechanicalDeburringSetupTime: computeProcessSetupTime(article, ProcessType.manualMechanicalDeburring, calcCache),
		manualMechanicalDeburringUnitTime: computeProcessUnitTime(article, ProcessType.manualMechanicalDeburring, calcCache),
		mechanicalDeburringDeburrDoubleSided: extractMechanicalDeburringDeburrDoubleSided(article),
		sheetConsumption: computeSheetConsumption(article),
		sheetId: extractSheetId(article),
		sheetTestReportRequired: extractTestReportRequired(article),
		sheetThickness: extractSheetThickness(article),
		slideGrindingComment: extractProcessSpecificComment(article, ProcessType.slideGrinding),
		slideGrindingRequired: requiresProcessType(article, ProcessType.slideGrinding),
		slideGrindingSetupTime: computeProcessSetupTime(article, ProcessType.slideGrinding, calcCache),
		slideGrindingUnitTime: computeProcessUnitTime(article, ProcessType.slideGrinding, calcCache),
		tubeConsumption: computeTubeConsumption(article),
		tubeCuttingComment: extractProcessSpecificComment(article, ProcessType.tubeCutting),
		tubeCuttingProcessId: extractTubeCuttingProcessId(article),
		tubeCuttingRequired: requiresProcessType(article, ProcessType.tubeCutting),
		tubeCuttingSetupTime: computeProcessSetupTime(article, ProcessType.tubeCutting, calcCache),
		tubeCuttingUnitTime: computeProcessUnitTime(article, ProcessType.tubeCutting, calcCache),
		tubeId: extractTubeId(article),
		userDefinedCountersinkingComment: extractProcessSpecificComment(article, ProcessType.userDefinedCountersinking),
		userDefinedCountersinkingNumSinks: extractUserDefinedCountersinkingNumSinks(article),
		userDefinedCountersinkingRequired: requiresProcessType(article, ProcessType.userDefinedCountersinking),
		userDefinedCountersinkingSetupTime: computeProcessSetupTime(article, ProcessType.userDefinedCountersinking, calcCache),
		userDefinedCountersinkingUnitTime: computeProcessUnitTime(article, ProcessType.userDefinedCountersinking, calcCache),
		userDefinedThreadingComment: extractProcessSpecificComment(article, ProcessType.userDefinedThreading),
		userDefinedThreadingNumThreads: extractUserDefinedThreadingNumThreads(article),
		userDefinedThreadingRequired: requiresProcessType(article, ProcessType.userDefinedThreading),
		userDefinedThreadingSetupTime: computeProcessSetupTime(article, ProcessType.userDefinedCountersinking, calcCache),
		userDefinedThreadingUnitTime: computeProcessUnitTime(article, ProcessType.userDefinedCountersinking, calcCache),
	};
}

/**
 * Create CSV representing the bill of materials
 *
 * Note:
 * The column order is not defined.
 * It is recommended to parse the CSV according to the exported column headers.
 */
export function createCsv(bomEntries: readonly Readonly<BomEntry>[], locale: string): string {
	const headers = entryKeys();
	const rows = bomEntries.map(bomEntry => entryKeys()
		.map(key => bomEntry[key]));

	return toCsvString([
		headers,
		...rows,
	], locale);
}
