import {
	TableType,
} from "qrc:/js/lib/generated/enum";
import {
} from "./calc_costs";
import {
	tolerances,
} from "./constants";
import {
	getSettingOrDefault,
} from "./settings_table";
import {
	getSharedDataEntry,
} from "./shared_data";
import {
	getMutableTable,
	getSheetsInStock,
	getTable,
	isTubeInStock,
} from "./table_utils";
import {
	createUpdatedNodeUserDataImpl,
	isCompatibleToNodeUserDataEntryImpl,
	NodeUserDataContext,
	NodeUserDataEntries,
	nodeUserDatumImpl,
} from "./userdata_config";
import {
	nodeUserDataContextForVertex,
} from "./userdata_utils";
import {
	assert,
	exhaustiveStringTuple,
	lexicographicObjectLess,
} from "./utils";

// The primary motivation for using callbacks instead of pre-computed
// values is that pre-computation breaks certain tests that operate on
// fixtures that in turn break API function post-conditions that have
// been introduced in the meantime.
//
// Example:
// export_wsi4_file.ts processes a .wsi4 file that holds
// non-normalized IOPs in some of its `TwoDimRepresentation`s.
// This triggers an assertion when pre-fetching this twoDimRep.
// Since the respective UserData entry is part of the loaded
// graph there is no need to fetch this twoDimRep.
//
// This can be mitigated by fetching the TwoDimRepresentation on demand.

export interface InitialValueNodeUserDataContextSheetBending {
	type: "sheetBending";
	sheetThickness: () => number;
}

export interface InitialValueNodeUserDataContextSheetCutting {
	type: "sheetCutting";
	processId: () => string;
	multiplicity: () => number;
	sheetThickness: () => number;
	twoDimRep: () => TwoDimRepresentation;
	// Unset if vertex is not source of a sheet bending vertex
	sheetMaterialId: () => string | undefined;
}

export interface InitialValueNodeUserDataContextTubeCutting {
	type: "tubeCutting";
	profileExtrusionLength: () => number;
	tubeProfile: () => TubeProfile | undefined;
}

export interface InitialValueNodeUserDataContextDefault {
	type: "default";
}

export type InitialValueNodeUserDataContext = InitialValueNodeUserDataContextDefault
| InitialValueNodeUserDataContextSheetBending
| InitialValueNodeUserDataContextSheetCutting
| InitialValueNodeUserDataContextTubeCutting;

type Context = Readonly<InitialValueNodeUserDataContext>;

type InitialValueFunctionMap<Entries> = {
	readonly[index in keyof Entries]: (context: Readonly<Context>) => Entries[index] | undefined;
};

/**
 * Select material according to available sheets (if any)
 */
function computeInitialValueSheetMaterial(context: Context): string | undefined {
	assert(context.type === "sheetBending" || context.type === "sheetCutting", "Context invalid");
	const thickness = context.sheetThickness();
	const matchingSheets = getSheetsInStock()
		.filter(sheet => thickness !== undefined && Math.abs(sheet.thickness - thickness) < tolerances.thickness);
	const sheetMaterialTable = getTable(TableType.sheetMaterial);
	const defaultSheetMaterialId = getSharedDataEntry("defaultSheetMaterialId");

	// Prio 0: valid defaultSheetMaterialId present and sheet found that matches both the settings value and the node's sheet thickness
	// Prio 1: matching sheet available
	// Prio 2: valid defaultSheetMaterialId present
	if (defaultSheetMaterialId !== undefined && sheetMaterialTable.some(row => row.identifier === defaultSheetMaterialId) && matchingSheets.some(row => row.sheetMaterialId === defaultSheetMaterialId)) {
		return defaultSheetMaterialId;
	} else if (matchingSheets.length > 0) {
		const cheapestSheetId = (() => {
			const matchingSheetRelPrices = getTable(TableType.sheetPrice)
				.filter(sheetPrice => matchingSheets.some(sheet => sheet.identifier === sheetPrice.sheetId))
				.map(sheetPrice => {
					const sheet = matchingSheets.find(sheet => sheet.identifier === sheetPrice.sheetId);
					assert(sheet !== undefined, "Expecting valid sheet");
					const relSheetPrice = sheetPrice.pricePerSheet / (sheet.dimX * sheet.dimY);
					return {
						sheetId: sheet.identifier,
						relSheetPrice: relSheetPrice,
					};
				});
			assert(!wsi4.util.isDebug() || matchingSheetRelPrices.length === matchingSheets.length, "Expecting price for each sheet");
			assert(matchingSheetRelPrices.length > 0, "Expecting at least one matching sheet price");
			return matchingSheetRelPrices
				.reduce(
					(acc, sheetIdAndRelPrice) => {
						if (acc.relSheetPrice < sheetIdAndRelPrice.relSheetPrice) {
							return acc;
						} else {
							return sheetIdAndRelPrice;
						}
					},
					matchingSheetRelPrices[0]!)
				.sheetId;
		})();
		const sheetMaterial = (() => {
			const cheapestSheet = matchingSheets.find(sheet => sheet.identifier === cheapestSheetId);
			return sheetMaterialTable.find(row => row.identifier === cheapestSheet?.sheetMaterialId);
		})();
		return sheetMaterial?.identifier;
	} else {
		return undefined;
	}
}

function findDefaultTube(profile: TubeProfile, minLength: number): Tube|undefined {
	const tubeStockTable = getTable(TableType.tubeStock);
	return getMutableTable(TableType.tube)
		.filter(row => row.dimX >= minLength)
		.sort((lhs, rhs) => lexicographicObjectLess(rhs, lhs))
		.find(row => row.tubeProfileId === profile.identifier && isTubeInStock(row.identifier, tubeStockTable));

}

function computeInitialValueTubeValue<T>(context: Context, mapTube: (tube: Tube) => T): T|undefined {
	assert(context.type === "tubeCutting", "Context invalid");
	const profile = context.tubeProfile();
	if (profile === undefined) {
		return undefined;
	}

	const clampingLength = getSettingOrDefault("tubeClampingLength");
	const minLength = clampingLength + context.profileExtrusionLength();
	const defaultTube = findDefaultTube(profile, minLength);
	if (defaultTube === undefined) {
		return undefined;
	}

	return mapTube(defaultTube);
}

function computeInitialValueTubeMaterial(context: Context): string | undefined {
	const map = (tube: Tube): TubeMaterialUniqueMembers => ({
		identifier: tube.tubeMaterialId,
	});
	return computeInitialValueTubeValue(context, map)?.identifier;
}

function computeInitialValueTubeSpecification(context: Context): string | undefined {
	const map = (tube: Tube): TubeSpecificationUniqueMembers => ({
		identifier: tube.tubeSpecificationId,
	});
	return computeInitialValueTubeValue(context, map)?.identifier;
}

type ComputedNodeUserDataEntries = Pick<NodeUserDataEntries, "sheetMaterialId"
| "tubeMaterialId"
| "tubeSpecificationId"
| "bendLineEngravingMode"
>

const initialValueNodeFunctionMap: InitialValueFunctionMap<ComputedNodeUserDataEntries> = {
	sheetMaterialId: computeInitialValueSheetMaterial,
	tubeMaterialId: computeInitialValueTubeMaterial,
	tubeSpecificationId: computeInitialValueTubeSpecification,
	bendLineEngravingMode: () => getSharedDataEntry("defaultBendLineEngravingMode"),
};

/**
 * @param key Key of the UserData entry
 * @param context Context of the node the value should be computed for
 * @returns Initial value depending on [[key]] and [context]]
 */
function computeNodeUserDataEntryInitialValue<Key extends keyof ComputedNodeUserDataEntries>(key: Key, context: Context): ComputedNodeUserDataEntries[Key] | undefined {
	return initialValueNodeFunctionMap[key](context);
}

/**
 * Create updated UserData object with default values added if they where missing
 *
 * Note: This function does *not* change the underlying graph
 */
export function insertInitialValuesIfMissingImpl(nodeUserDataContext: NodeUserDataContext, initialValueContext: Context, userData: StringIndexedInterface): StringIndexedInterface {
	const keys = exhaustiveStringTuple<keyof ComputedNodeUserDataEntries>()(
		"sheetMaterialId",
		"tubeMaterialId",
		"tubeSpecificationId",
		"bendLineEngravingMode",
	);
	keys.filter(key => isCompatibleToNodeUserDataEntryImpl(key, nodeUserDataContext))
		.filter(key => nodeUserDatumImpl(key, userData) === undefined)
		.forEach(key => {
			const value = computeNodeUserDataEntryInitialValue(key, initialValueContext);
			if (value !== undefined) {
				userData = createUpdatedNodeUserDataImpl(key, value, userData);
			}
		});
	return userData;
}

function initialValueContextForVertex(): InitialValueNodeUserDataContext {
	// This is considered sufficient for now.
	// If a more advanced context is required
	// at some point then this can be extended.
	return {
		type: "default",
	};
}

/**
 * Create updated UserData object with default values added if they where missing
 * @param vertex Vertex of the associated node
 * @param inputUserData UserData object to update;  using the associated node's userData if unset
 *
 * Note: This function does *not* change the underlying graph
 */
export function insertInitialValuesIfMissing(vertex: Vertex, inputUserData?: StringIndexedInterface): StringIndexedInterface {
	const nodeContext = nodeUserDataContextForVertex(vertex);
	const initialValueContext = initialValueContextForVertex();
	const userData = inputUserData ?? wsi4.node.userData(vertex);
	return insertInitialValuesIfMissingImpl(nodeContext, initialValueContext, userData);
}
