// Note: All functions in this module must conform to DocumentGraphHandler's script engine API.
import {
	isStringIndexedInterface,
} from "qrc:/js/lib/generated/typeguard";
import {
	GraphNodeProcessingState,
	TableType,
} from "qrc:/js/lib/generated/enum";

import {
	isSignatureWst,
} from "./workstep_util";
import {
	createNodeUserDataStringIndex,
	isCompatibleToNodeUserDataEntryImpl,
	isNodeUserDataEntryType,
	nodeUserDataEntries,
	NodeUserDataEntries,
	nodeUserDataProcessingStateMap,
	NodeUserDataContext,
} from "./userdata_config";
import {
	assert,
	assertDebug,
	getKeysOfObject,
	isEqual,
	isString,
} from "./utils";
import {
	min as computeMinProcessingState,
} from "./graphnodeprocessingstate_utils";
import {
	getTable,
} from "./table_utils";

/**
 * Typedef for UserData
 */
export declare type UserData = StringIndexedInterface;

export function nodeUserDataContextForVertex(vertex: Vertex): NodeUserDataContext {
	return {
		workStepType: wsi4.node.workStepType(vertex),
		processType: wsi4.node.processType(vertex),
		isSignatureNode: isSignatureWst(wsi4.node.workStepType(vertex), wsi4.graph.article(vertex).map(v => wsi4.node.workStepType(v))),
		isInitialNode: wsi4.node.isInitial(vertex),
	};
}

/**
 * Get UserData entry for given key and vertex.
 *
 * Note: If UserData entry is incompatible to vertex an error is thrown.
 */
export function getNodeUserDataEntry<Key extends keyof NodeUserDataEntries>(key: Key, vertex: Vertex, userData?: StringIndexedInterface): NodeUserDataEntries[Key]|undefined {
	if (!isCompatibleToNodeUserDataEntry(key, vertex)) {
		return wsi4.throwError("Get UserData entry \"" + key + "\" for incompatible vertex with key \"" + wsi4.util.toKey(vertex) + "\"");
	}
	const context = nodeUserDataContextForVertex(vertex);
	userData = userData !== undefined ? userData : wsi4.node.userData(vertex);
	const value = userData[createNodeUserDataStringIndex(key, context)];
	return isNodeUserDataEntryType(key, value) ? value : undefined;
}

/**
 * Get UserData entry for given key and vertex.
 *
 * @param key UserData entry key
 * @param vertex Vertex of the node to access
 * @returns Value for the UserData [[key]]
 *
 * Note: If UserData entry is incompatible to vertex an error is thrown.
 * Note: If UserData entry is not available for vertex an error is thrown.
 */
export function getNodeUserDataEntryOrThrow<Key extends keyof NodeUserDataEntries>(key: Key, vertex: Vertex, userData?: StringIndexedInterface): NodeUserDataEntries[Key] {
	const value = getNodeUserDataEntry(key, vertex, userData);
	return isNodeUserDataEntryType(key, value) ? value : wsi4.throwError("No UserData entry for key \"" + key + "\" and vertex \"" + wsi4.util.toKey(vertex) + "\"");
}

/**
 * Create new UserData for given key and vertex
 *
 * @param key UserData entry key
 * @param vertex Vertex of the node to modify
 * @param value The value to set
 * @param userData If set, this object will be updated and returned.  If unset, the associated node's UserData is queried, updated and returned.
 * @returns Updated UserData object
 *
 * Note: If UserData entry is incompatible to vertex an error is thrown.
 * Note: This function does *not* change the underlying graph
 */
export function createUpdatedNodeUserData<Key extends keyof NodeUserDataEntries>(key: Key, vertex: Vertex, value: NodeUserDataEntries[Key], userData?: StringIndexedInterface): StringIndexedInterface {
	if (!isCompatibleToNodeUserDataEntry(key, vertex)) {
		return wsi4.throwError("Set UserData entry \"" + key + "\" for incompatible vertex with key \"" + wsi4.util.toKey(vertex) + "\"");
	}
	const context = nodeUserDataContextForVertex(vertex);
	userData = userData !== undefined ? userData : wsi4.node.userData(vertex);
	userData[createNodeUserDataStringIndex(key, context)] = value;
	return userData;
}

/**
 * Flags for accessing user data of specific nodes (to be used as a flag set)
 *
 * @constant {object}
 * @property {number} self - access user data of the node itself
 * @property {number} reaching - access user data of the nodes reaching the vertex
 * @property {number} reachable - access user data of the nodes reachable from the vertex
 * @property {number} article - access user data of the nodes of the article of the node
 */
export const userDataAccess = {
	self: 1,
	reaching: 2,
	reachable: 4,
	article: 8,
};

/**
 * @constant Access user data for all possible UserDataAccessFlags
 */
export const accessAllUserData = userDataAccess.self | userDataAccess.reaching | userDataAccess.reachable;

/**
 * @constant Access user data of the vertex and an reaching vertices
 */
export const accessSelfAndReachable = userDataAccess.self | userDataAccess.reachable;

const isCloseNodeUserDataFunctionMap: {[index in keyof NodeUserDataEntries]: (lhs: NodeUserDataEntries[index], rhs: NodeUserDataEntries[index]) => boolean} = {
	sheetMaterial: (lhs, rhs) => lhs.identifier === rhs.identifier,
	comment: isEqual,
	materialCostsPerPiece: isEqual,
	setupTime: isEqual,
	unitTimePerPiece: isEqual,
	cuttingGas: (lhs, rhs) => lhs.identifier === rhs.identifier,
	packaging: (lhs, rhs) => lhs.identifier === rhs.identifier,
	transportDistance: isEqual,
	importId: isEqual,
	attachments: isEqual,
	testReportRequired: isEqual,
	fixedRotations: isEqual,
	deburrDoubleSided: isEqual,
	numThreads: isEqual,
	numCountersinks: isEqual,
	sheetTappingData: (lhsEntries, rhsEntries) => lhsEntries.length === rhsEntries.length && lhsEntries.every((lhsEntry, index) => {
		const rhsEntry = rhsEntries[index]!;
		return lhsEntry.screwThread.identifier === rhsEntry.screwThread.identifier
			&& isEqual(lhsEntry.center3, rhsEntry.center3);
	}),
	articleUserData: isEqual,
	userDefinedScalePrices: isEqual,
	tubeMaterial: (lhs, rhs) => lhs === undefined && rhs === undefined ? true : lhs === undefined || rhs === undefined ? false : lhs.identifier === rhs.identifier,
	tubeSpecification: (lhs, rhs) => lhs === undefined && rhs === undefined ? true : lhs === undefined || rhs === undefined ? false : lhs.identifier === rhs.identifier,
	sheetFilterSheetIds: (lhs, rhs) => {
		const sort = (vec: string[]) => vec.filter((v, index, self) => self.indexOf(v) === index);
		return isEqual(sort(lhs), sort(rhs));
	},
	bendLineEngravingMode: isEqual,
};

/**
 * Check if two user data entries are close
 *
 * For user data entries that hold table values this check is limited to the unique members respectively.
 * All other entries are checked for equality.
 */
function isCloseNodeUserDataEntry<Key extends keyof NodeUserDataEntries>(key: Key, lhs: NodeUserDataEntries[Key], rhs: NodeUserDataEntries[Key]): boolean {
	// XXX Caution!
	// As of both lhs and rhs will *not* be type-checked by the comiler as one would expect *on the call site*.
	// Instead, both lhs and rhs can be *any* node user data property type.
	assertDebug(() => isNodeUserDataEntryType(key, lhs), `Pre-condition violated: lhs is incompatible to key ${key}: ${JSON.stringify(lhs)}`);
	assertDebug(() => isNodeUserDataEntryType(key, lhs), `Pre-condition violated: rhs is incompatible to key ${key}: ${JSON.stringify(rhs)}`);
	return isCloseNodeUserDataFunctionMap[key](lhs, rhs);
}

/**
 * Collect UserData for all connected GraphNode~s (depending on access flags)
 *
 * @param key - Node UserData entry key for entries to collect
 * @param vertex - Vertex number
 * @param accessFlag - for which connected vertices to collect user data from
 */
export function collectNodeUserDataEntries<Key extends keyof NodeUserDataEntries>(key: Key, vertex: Vertex, accessFlag: number): NodeUserDataEntries[Key][] {
	const contained = (flag: number, flagSet: number) => (flag & flagSet) === flag;
	const vertices: Vertex[] = [];
	if (contained(userDataAccess.self, accessFlag)) {
		vertices.push(vertex);
	}
	if (contained(userDataAccess.reaching, accessFlag)) {
		vertices.push(...wsi4.graph.reaching(vertex));
	}
	if (contained(userDataAccess.reachable, accessFlag)) {
		vertices.push(...wsi4.graph.reachable(vertex));
	}
	if (contained(userDataAccess.article, accessFlag)) {
		vertices.push(...wsi4.graph.article(vertex));
	}

	return vertices.filter((lhs, index, self) => self.findIndex(rhs => isEqual(lhs, rhs)) === index)
		.filter(v => isCompatibleToNodeUserDataEntry(key, v))
		.map(v => getNodeUserDataEntry(key, v))
		.filter((lhs, index, self) => self.findIndex(rhs => (lhs === undefined && rhs === undefined)
									|| (isNodeUserDataEntryType(key, lhs)
										&& isNodeUserDataEntryType(key, rhs)
										&& isCloseNodeUserDataEntry(key, lhs, rhs))) === index)
		.reduce((acc: NodeUserDataEntries[Key][], value) => {
			if (value === undefined) {
				return acc;
			} else {
				acc.push(value);
				return acc;
			}
		}, []);
}

/**
 * Type guard for UserData
 *
 * We cannot write "a is UserData" apparently...?!
 */
export function isUserData(a: unknown): a is UserData {
	return isStringIndexedInterface(a);
}

/**
 * Merge two UserDatas
 * NOTE: Entries already present are not overwritten
 *
 * @constant toExtend UserData to extend
 * @constant toMerge UserData to merge into toExtend (old entries are not overwritten)
 */
export function mergeUserData(toExtend: UserData, toMerge: UserData): UserData {
	// Note that we have to take toMerge first so its entries are overwritten by toExtend.
	return {...toMerge,
		...toExtend};
}

/**
 * Remove values in "oldUserData", that are not present in "newUserData" anymore
 */
export function removeInvalidUserData(oldUserData: UserData, newUserData: UserData): UserData {
	const result: UserData = {};
	for (const key of Object.keys(oldUserData)) {
		if (newUserData[key] !== undefined) {
			result[key] = oldUserData[key];
		}
	}
	return result;
}

/**
 * Merge [[ArticleAttributes]] of two graphnodes
 */
export function mergeArticleAttributes(lhs: ArticleAttributes, rhs: ArticleAttributes): ArticleAttributes {
	// FIXME attachments are overwritten (see #1428)
	const userData = mergeUserData(lhs.userData, rhs.userData);

	if (typeof lhs.userData["name"] !== "string") {
		userData["name"] = rhs.userData["name"];
	}
	return {
		userData: userData,
	};
}

export function hasManufacturingCostOverride(vertex: Vertex, userData?: Readonly<UserData>): boolean {
	return getNodeUserDataEntryOrThrow("userDefinedScalePrices", vertex, userData).length > 0;
}

/**
 * @param key Key of the UserData entry
 * @param vertex Vertex of the node to check
 * @returns true if UserData entry for key is compatible to vertex
 *
 * Note: This function should be called in the main script engine only.
 *       For incomplete graphs (aka when required in the internal engine)
 *       consider using the associated impl function.
 */
export function isCompatibleToNodeUserDataEntry<Key extends keyof NodeUserDataEntries>(key: Key, vertex: Vertex): boolean {
	const context = nodeUserDataContextForVertex(vertex);
	return isCompatibleToNodeUserDataEntryImpl(key, context);
}

function isNodeUserDataStringIndex(index: string, context: NodeUserDataContext): boolean {
	return nodeUserDataEntries()
		.some(key => createNodeUserDataStringIndex(key, context) === index);
}

function toNodeUserDataEntryKey(context: NodeUserDataContext, userDataIndex: string): keyof NodeUserDataEntries|undefined {
	return nodeUserDataEntries()
		.find(key => userDataIndex === createNodeUserDataStringIndex(key, context));
}

function toNodeUserDataEntryKeyOrThrow(context: NodeUserDataContext, userDataIndex: string): keyof NodeUserDataEntries {
	const result = toNodeUserDataEntryKey(context, userDataIndex);
	return result === undefined ? wsi4.throwError(`"${userDataIndex}" is not a valid UserData key for vertex ${JSON.stringify(context)}`) : result;
}

function toVertexOrThrow(vertexKey: unknown): Vertex {
	const vertex = wsi4.graph.vertices()
		.find(v => wsi4.util.toKey(v) === vertexKey);
	return vertex === undefined ? wsi4.throwError("Vertex key invalid: " + JSON.stringify(vertexKey)) : vertex;
}

/**
 * Compute new processing state resulting from the changes between two UserData objects
 *
 * @param vertexKey Key of the vertex of the associated node
 * @param oldUserData Unmodified UserData object
 * @param newUserData Modified UserData object
 * @returns Resulting processing state
 */
export function computeNodeProcessingState(vertexKey: unknown, oldUserData: unknown, newUserData: unknown): GraphNodeProcessingState {
	const vertex = toVertexOrThrow(vertexKey);
	const context = nodeUserDataContextForVertex(vertex);
	if (!isStringIndexedInterface(oldUserData)) {
		return wsi4.throwError("Input invalid");
	}
	if (!isStringIndexedInterface(newUserData)) {
		return wsi4.throwError("Input invalid");
	}
	const result = getKeysOfObject(newUserData)
		.filter((userDataIndex): userDataIndex is string => isString(userDataIndex))
		.filter(userDataIndex => isNodeUserDataStringIndex(userDataIndex, context))
		.map((userDataIndex): [
			string,
			keyof NodeUserDataEntries,
		]		=> [
			userDataIndex,
			toNodeUserDataEntryKeyOrThrow(context, userDataIndex),
		])
		.filter(([
			userDataIndex,
			key,
		]) => {
			const lhs = oldUserData[userDataIndex];
			const rhs = newUserData[userDataIndex];
			return !isNodeUserDataEntryType(key, lhs) || !isNodeUserDataEntryType(key, rhs) || !isCloseNodeUserDataEntry(key, lhs, rhs);
		})
		.map(([
			_,
			key,
		]) => key)
		.filter(key => isCompatibleToNodeUserDataEntry(key, vertex))
		.reduce((acc: GraphNodeProcessingState, key) => computeMinProcessingState(acc, nodeUserDataProcessingStateMap[key]), GraphNodeProcessingState.Finished);
	return result;
}

/**
 * Updates the UserData values that correspond to [[tube]]
 */
export function applyTubeToUserData(
	vertex: Vertex,
	tube: Readonly<Tube>,
	tubeMaterialsInput?: readonly Readonly<TubeMaterial>[],
	tubeSpecificationsInput?: readonly Readonly<TubeSpecification>[],
	userDataInput?: StringIndexedInterface,
): StringIndexedInterface {
	const tubeMaterials = tubeMaterialsInput ?? getTable(TableType.tubeMaterial);
	const tubeSpecifications = tubeSpecificationsInput ?? getTable(TableType.tubeSpecification);

	const tubeMaterial = tubeMaterials.find(row => row.identifier === tube.tubeMaterialId);
	assert(tubeMaterial !== undefined, "Tables inconsistent");

	const tubeSpecification = tubeSpecifications.find(row => row.identifier === tube.tubeSpecificationId);
	assert(tubeSpecification !== undefined, "Tables inconsistent");

	let userData = userDataInput ?? wsi4.node.userData(vertex);
	userData = createUpdatedNodeUserData("tubeMaterial", vertex, tubeMaterial, userData);
	userData = createUpdatedNodeUserData("tubeSpecification", vertex, tubeSpecification, userData);

	return userData;
}
