import {
	ProcessType,
	TableType,
} from "qrc:/js/lib/generated/enum";
import {
	createUpdatedTable,
	getSettingOrDefault,
} from "qrc:/js/lib/settings_table";
import {
	getDefaultTable,
	getMutableTable,
	getTable,
	setInternalTable,
	TableTypeMap,
} from "qrc:/js/lib/table_utils";
import {
	front,
} from "qrc:/js/lib/array_util";

// This should not be required any more.
// At program start all tables that are not present at all are set to their default values.
function addDefaultTableIfMissing(type: TableType): void {
	if (wsi4.tables.isInternalTable(type) || wsi4.tables.isExternalTable(type)) {
		return;
	}
	wsi4.tables.setInternalTable(wsi4.tables.getDefault(type));
}

function replaceOrAddInternalTable(type: TableType) {
	if (wsi4.tables.isExternalTable(type)) {
		return;
	}
	wsi4.tables.setInternalTable(wsi4.tables.getDefault(type));
}

// Fix process id (see #1565)
function fixProcessId(): void {
	if (wsi4.tables.isExternalTable(TableType.process)) {
		return;
	}
	const oldTable = getTable(TableType.process);
	const newTable = oldTable.filter(row => row.identifier !== "mechanicalDeburring");
	if (newTable.length === oldTable.length) {
		return;
	}
	const fixedRow = getDefaultTable(TableType.process)
		.find(row => row.identifier === "mechanicalDeburringId");
	if (fixedRow === undefined) {
		return;
	}
	newTable.push(fixedRow);
	setInternalTable(TableType.process, newTable);
}

// * Add new ProcessType:s automatic and manual mechanical deburring to
//     * process table
//     * process rate table
//     * process setup time fallback table
// * Deactivate mechanicalDeburring base process
// * Add new tables
//     * automaticMechanicalDeburringMaterial
//     * automaticMechanicalDeburringParameters
function applyDeburringRelatedChanges(): void {
	// Extend / modify process table
	(() => {
		if (wsi4.tables.isExternalTable(TableType.process)) {
			return;
		}
		// Removing existing row as this table row is modified (active is changed)
		const newTable = getTable(TableType.process)
			.filter(row => row.type !== ProcessType.mechanicalDeburring);
		const addProcessIfMissing = (newRow: Process | undefined) => {
			if (newRow === undefined) {
				return;
			}
			if (newTable.some(row => row.identifier === newRow.identifier)) {
				return;
			}
			newTable.push(newRow);
		};
		const defaultTable = getDefaultTable(TableType.process);
		addProcessIfMissing(defaultTable.find(row => row.type === ProcessType.mechanicalDeburring));
		addProcessIfMissing(defaultTable.find(row => row.type === ProcessType.automaticMechanicalDeburring));
		addProcessIfMissing(defaultTable.find(row => row.type === ProcessType.manualMechanicalDeburring));
		setInternalTable(TableType.process, newTable);
	})();

	// Utility function to extend a process-refererencing table
	const addRowIfMissing = <Row extends {processId: string}>(processType: ProcessType, defaultTable: readonly Readonly<Row>[], targetTable: Row[]) => {
		const process = getTable(TableType.process)
			.find(row => row.type === processType);
		if (process === undefined) {
			return;
		}
		const defaultRow = defaultTable.find(row => row.processId === process.identifier);
		if (defaultRow === undefined) {
			return;
		}
		if (targetTable.some(row => row.processId === defaultRow.processId)) {
			return;
		}
		targetTable.push(defaultRow);
	};

	// Extend process rate table
	(() => {
		if (wsi4.tables.isExternalTable(TableType.processRate)) {
			return;
		}
		const defaultTable = getDefaultTable(TableType.processRate);
		const newTable = Array.from(getTable(TableType.processRate));
		addRowIfMissing(ProcessType.automaticMechanicalDeburring, defaultTable, newTable);
		addRowIfMissing(ProcessType.manualMechanicalDeburring, defaultTable, newTable);
		setInternalTable(TableType.processRate, newTable);
	})();

	// Extend process setup time table
	(() => {
		if (wsi4.tables.isExternalTable(TableType.processSetupTimeFallback)) {
			return;
		}
		const defaultTable = getDefaultTable(TableType.processSetupTimeFallback);
		const newTable = Array.from(getTable(TableType.processSetupTimeFallback));
		addRowIfMissing(ProcessType.automaticMechanicalDeburring, defaultTable, newTable);
		addRowIfMissing(ProcessType.manualMechanicalDeburring, defaultTable, newTable);
		setInternalTable(TableType.processSetupTimeFallback, newTable);
	})();

	// Add new tables
	addDefaultTableIfMissing(TableType.automaticMechanicalDeburringMaterial);
	addDefaultTableIfMissing(TableType.automaticMechanicalDeburringParameters);
}

function addDimensionConstraintsTable(): void {
	addDefaultTableIfMissing(TableType.dimensionConstraints);
}

function fixPackagingTimePerArticle(): void {
	if (wsi4.tables.isExternalTable(TableType.packaging)) {
		return;
	}
	const table = getTable(TableType.packaging);
	if (table.some(row => row.tea !== 2.0)) {
		// User has modified the table so keeping as is
		return;
	}
	table.map((row): Packaging => Object.assign(row, {tea: 0.}));
	setInternalTable(TableType.packaging, table);
}

// Adding the new table AutomaticMechanicalDeburringMaterial breaks table consistency for users with custom sheetMaterial table entries.
// To address this issue this function updates the automatic mechanical deburring tables so for each sheet material there is an entry.
function fillAutomaticDeburringTablesFromSheetMaterial(): void {
	if (wsi4.tables.isExternalTable(TableType.automaticMechanicalDeburringMaterial) || wsi4.tables.isExternalTable(TableType.automaticMechanicalDeburringParameters)) {
		return;
	}
	const sheetMaterialTable = getTable(TableType.sheetMaterial);
	const oldDeburringMatTable = (() => {
		const table = getTable(TableType.automaticMechanicalDeburringMaterial);
		return table.length === 0 ? getDefaultTable(TableType.automaticMechanicalDeburringMaterial) : table;
	})();
	if (sheetMaterialTable.every(sheetMatRow => oldDeburringMatTable.some(deburringMatRow => sheetMatRow.identifier === deburringMatRow.sheetMaterialId))) {
		// Tables are ok
		return;
	}
	const newDeburringMatTable: AutomaticMechanicalDeburringMaterial[] = sheetMaterialTable.map(row => ({
		sheetMaterialId: row.identifier,
		automaticMechanicalDeburringMaterialId: front(oldDeburringMatTable).automaticMechanicalDeburringMaterialId,
	}));
	const newDeburringParamTable = getTable(TableType.automaticMechanicalDeburringParameters)
		.filter(lhs => newDeburringMatTable.some(rhs => lhs.automaticMechanicalDeburringMaterialId === rhs.automaticMechanicalDeburringMaterialId));
	if (newDeburringMatTable.length === 0) {
		// Cannot apply fix
		return;
	}
	setInternalTable(TableType.automaticMechanicalDeburringMaterial, newDeburringMatTable);
	setInternalTable(TableType.automaticMechanicalDeburringParameters, newDeburringParamTable);
}

function addLaserSheetCuttingMaxThicknessTable() {
	addDefaultTableIfMissing(TableType.laserSheetCuttingMaxThickness);
}

function addDefaultRowIfMissing<Type extends keyof TableTypeMap>(type: Type, isMatchingRow: (row: Readonly<TableTypeMap[Type]>) => boolean) {
	if (wsi4.tables.isExternalTable(type)) {
		return;
	}
	const table = Array.from(getTable(type));
	if (table.some(isMatchingRow)) {
		return;
	}
	const defaultRow = getDefaultTable(type)
		.find(isMatchingRow);
	if (defaultRow === undefined) {
		return;
	}
	table.push(defaultRow);
	setInternalTable(type, table);
}

function addProcessTypeUserDefinedMachining() {
	addDefaultRowIfMissing(TableType.process, row => row.type === ProcessType.userDefinedMachining);
	addDefaultRowIfMissing(TableType.process, row => row.type === ProcessType.userDefinedThreading);
	addDefaultRowIfMissing(TableType.process, row => row.type === ProcessType.userDefinedCountersinking);

	// Search for previously set table rows...
	const udmProcess = getTable(TableType.process)
		.find(row => row.type === ProcessType.userDefinedMachining);
	const udtProcess = getTable(TableType.process)
		.find(row => row.type === ProcessType.userDefinedThreading);
	const udcProcess = getTable(TableType.process)
		.find(row => row.type === ProcessType.userDefinedCountersinking);

	// ... and leave if there are none as this indicates that user defined external tables are involved
	if (udmProcess === undefined || udtProcess === undefined || udcProcess === undefined) {
		return;
	}

	addDefaultRowIfMissing(TableType.processRate, row => row.processId === udmProcess.identifier);
	addDefaultRowIfMissing(TableType.processRate, row => row.processId === udtProcess.identifier);
	addDefaultRowIfMissing(TableType.processRate, row => row.processId === udcProcess.identifier);

	addDefaultRowIfMissing(TableType.processSetupTimeFallback, row => row.processId === udmProcess.identifier);
	addDefaultRowIfMissing(TableType.processSetupTimeFallback, row => row.processId === udtProcess.identifier);
	addDefaultRowIfMissing(TableType.processSetupTimeFallback, row => row.processId === udcProcess.identifier);

	addDefaultRowIfMissing(TableType.processUnitTimeFallback, row => row.processId === udmProcess.identifier);
	addDefaultRowIfMissing(TableType.processUnitTimeFallback, row => row.processId === udtProcess.identifier);
	addDefaultRowIfMissing(TableType.processUnitTimeFallback, row => row.processId === udcProcess.identifier);
}

function addProcessTypeSlideGrinding() {
	addDefaultRowIfMissing(TableType.process, row => row.type === ProcessType.slideGrinding);
	const process = getTable(TableType.process)
		.find(row => row.type === ProcessType.slideGrinding);
	if (process === undefined) {
		return;
	}
	addDefaultRowIfMissing(TableType.processRate, row => row.processId === process.identifier);
	addDefaultRowIfMissing(TableType.processSetupTimeFallback, row => row.processId === process.identifier);
	addDefaultRowIfMissing(TableType.processUnitTimeFallback, row => row.processId === process.identifier);
}

function addScrewThreadTable() {
	addDefaultTableIfMissing(TableType.screwThread);
}

function addProcessTypeSheetTapping() {
	addDefaultRowIfMissing(TableType.process, row => row.type === ProcessType.sheetTapping);
	const process = getTable(TableType.process)
		.find(row => row.type === ProcessType.sheetTapping);
	if (process === undefined) {
		return;
	}
	addDefaultRowIfMissing(TableType.processRate, row => row.processId === process.identifier);
	addDefaultRowIfMissing(TableType.processSetupTimeFallback, row => row.processId === process.identifier);
}

function addTappingTimeParametersTable() {
	addDefaultTableIfMissing(TableType.tappingTimeParameters);
}

function extendTransportationCostsTable() {
	if (wsi4.tables.isExternalTable(TableType.transportationCosts)) {
		return;
	}

	const packagingTable = getTable(TableType.packaging);
	const transporationCostsTable = getMutableTable(TableType.transportationCosts);
	transporationCostsTable.forEach((transportationCosts, index) => {
		const packaging = packagingTable.find(row => row.identifier === transportationCosts.packagingId);
		if (packaging === undefined) {
			// Valid case as user tables can be inconsistent at this point - user will receive a warning when the gui shows up.
			transportationCosts.identifier = "transport-" + index.toFixed(0);
		} else {
			transportationCosts.identifier = "transport-" + packaging.identifier;
			transportationCosts.name = packaging.name;
		}
	});
	setInternalTable(TableType.transportationCosts, transporationCostsTable);
}

function addExportIdentifierToDieGroupTable(tableType: "upperDieGroup" | "lowerDieGroup") {
	if (wsi4.tables.isExternalTable(tableType)) {
		return;
	}

	const defaultTable = getDefaultTable(tableType);
	const updatedTable = getMutableTable(tableType)
		.map(rowToUpdate => {
			const defaultRow = defaultTable.find(row => row.identifier === rowToUpdate.identifier);
			if (defaultRow !== undefined) {
				rowToUpdate.exportIdentifier = defaultRow.exportIdentifier;
			}
			return rowToUpdate;
		});
	setInternalTable(tableType, updatedTable);
}

function addExportIdentifierToUpperDieGroupTable() {
	addExportIdentifierToDieGroupTable(TableType.upperDieGroup);
}

function addExportIdentifierToLowerDieGroupTable() {
	addExportIdentifierToDieGroupTable(TableType.lowerDieGroup);
}

function addTubeTables() {
	addDefaultTableIfMissing(TableType.tubeProfile);
	addDefaultTableIfMissing(TableType.tubeMaterial);
	addDefaultTableIfMissing(TableType.tubeMaterialDensity);
	addDefaultTableIfMissing(TableType.tubeSpecification);
	addDefaultTableIfMissing(TableType.tube);
}

function addProcessUserDefinedTube() {
	// ProcessType is deprecated; nothing is left to do
	return;
}

function removeDeprecatedSurchargesIfDefault() {
	if (wsi4.tables.isExternalTable(TableType.surcharge)) {
		return;
	} else {
		setInternalTable(TableType.surcharge, getTable(TableType.surcharge)
			.filter(row => row.type !== "globalDelta" || row.value === 0)
			.filter(row => row.type !== "articleDelta" || row.value === 0));
	}
}

function populateSheetCuttingMaterials() {
	if (wsi4.tables.isExternalTable(TableType.sheetCuttingMaterial)) {
		return;
	} else {
		const allIds = getTable(TableType.sheetCuttingMaterialMapping)
			.map(row => row.sheetCuttingMaterialId);
		const uniqueIds = Array.from(new Set(allIds));
		setInternalTable(TableType.sheetCuttingMaterial, uniqueIds.map((id): SheetCuttingMaterial => ({
			identifier: id,
			name: id,
		})));
	}
}

function populateSheetBendingMaterials() {
	if (wsi4.tables.isExternalTable(TableType.sheetBendingMaterial)) {
		return;
	} else {
		const allIds = getTable(TableType.sheetBendingMaterialMapping)
			.map(row => row.sheetBendingMaterialId);
		const uniqueIds = Array.from(new Set(allIds));
		setInternalTable(TableType.sheetBendingMaterial, uniqueIds.map((id): SheetBendingMaterial => ({
			identifier: id,
			name: id,
		})));
	}
}

function populateBendDeductions() {
	addDefaultTableIfMissing(TableType.bendDeduction);
}

function populateTubeRelatedTables() {
	if (getTable(TableType.process)
		.some(row => row.type === ProcessType.tubeCutting)) {
		return;
	}

	const defaultProcessTable = getDefaultTable(TableType.process);
	defaultProcessTable.filter(row => row.type === ProcessType.tubeCutting)
		.forEach(defaultProcess => {
			addDefaultRowIfMissing(TableType.process, otherProcess => defaultProcess.identifier === otherProcess.identifier);
			addDefaultRowIfMissing(TableType.processRate, processRate => defaultProcess.identifier === processRate.processId);
			addDefaultRowIfMissing(TableType.processSetupTimeFallback, setupTime => defaultProcess.identifier === setupTime.processId);
		});

	defaultProcessTable.filter(row => row.type === ProcessType.tube)
		.forEach(defaultProcess => addDefaultRowIfMissing(TableType.process, otherProcess => defaultProcess.identifier === otherProcess.identifier));

	// So far tubes were considered an experimental feature.
	// Since the default tube related tables have been changed substantially
	// the old table content is overwritten.
	replaceOrAddInternalTable(TableType.tubeProfile);
	replaceOrAddInternalTable(TableType.tubeMaterial);
	replaceOrAddInternalTable(TableType.tubeMaterialDensity);
	replaceOrAddInternalTable(TableType.tubeSpecification);
	replaceOrAddInternalTable(TableType.tubeCuttingProcess);
	replaceOrAddInternalTable(TableType.tubeCuttingProcessMapping);
	replaceOrAddInternalTable(TableType.tubeCuttingSpeed);
	replaceOrAddInternalTable(TableType.tubeCuttingPierceTime);
	replaceOrAddInternalTable(TableType.tube);
	replaceOrAddInternalTable(TableType.tubePrice);
	replaceOrAddInternalTable(TableType.tubeStock);
	replaceOrAddInternalTable(TableType.tubeMaterialScrapValue);
}

// Note: new entries must be appended;  array-index indicates associated migration version
const migrationFunctions = [
	fixProcessId,
	applyDeburringRelatedChanges,
	addDimensionConstraintsTable,
	fixPackagingTimePerArticle,
	fillAutomaticDeburringTablesFromSheetMaterial,
	addLaserSheetCuttingMaxThicknessTable,
	addProcessTypeUserDefinedMachining,
	addProcessTypeSlideGrinding,
	addScrewThreadTable,
	addProcessTypeSheetTapping,
	addTappingTimeParametersTable,
	extendTransportationCostsTable,
	addExportIdentifierToUpperDieGroupTable,
	addExportIdentifierToLowerDieGroupTable,
	addTubeTables,
	addProcessUserDefinedTube,
	removeDeprecatedSurchargesIfDefault,
	populateSheetCuttingMaterials,
	populateSheetBendingMaterials,
	populateBendDeductions,
	populateTubeRelatedTables,
];

export function currentVersion(): number {
	return migrationFunctions.length;
}

export function run() : void {
	const lastVersion = getSettingOrDefault("tableMigrationVersion");
	migrationFunctions.filter((_f, index) => index >= lastVersion)
		.forEach(f => f());
	setInternalTable(TableType.setting, createUpdatedTable("tableMigrationVersion", migrationFunctions.length));
}
