import {
	isCameraOrientation3,
} from "qrc:/js/lib/generated/typeguard";
import {
	getArticleName,
} from "./graph_utils";
import {
	assert,
	hasOptionalPropertyT,
	hasPropertyT,
	isArray,
	isString,
} from "./utils";

export interface AssemblyIdentifier {
	assembly: Assembly;
	identifier: string;
	// If the Assembly is associated with a joining, its joining identifier is stored here
	joiningIdentifier: string | undefined;
}

export interface ExportableJoiningStepEntry extends StringIndexedInterface {
	assemblyIdentifier: string;
}

export interface ExportableJoiningStep extends StringIndexedInterface {
	entries: ExportableJoiningStepEntry[]; // These are identifiers for assemblies to be added
	cameraOrientation: CameraOrientation3 | undefined;
	comment: string;
}

export interface ExportableJoining {
	// The joining identifier this joining belongs to.  Note that this is empty for the root joining.
	joiningIdentifier: string;
	exportableJoiningSteps: ExportableJoiningStep[];
}

// Creating joining instructions from a Vertex may lead to its child vertices again having joining instructions.
// Instead of having a recursive data structure that duplicates data for each assembly, we map sub-joining via a joiningIdentifier that provides a many-to-one mechanism.
export interface GlobalJoiningsData extends StringIndexedInterface {
	joinings: ExportableJoining[];
}

export interface JoiningsData extends StringIndexedInterface {
	globalData: GlobalJoiningsData;
	nodeVendorDatas: GltfNodeVendorData[];
}

interface ExportableJoiningsData {
	exportableJoinings: ExportableJoining[];
	assemblyIdentifiers: AssemblyIdentifier[];
}

// Create an exportable joinings data.
export function createJoiningsData(vertex: Vertex): JoiningsData | undefined {
	const ejd: ExportableJoiningsData = {
		exportableJoinings: [],
		assemblyIdentifiers: [],
	};
	appendExportableJoiningsData(vertex, ejd);
	assert((ejd.assemblyIdentifiers.length === 0) === (ejd.exportableJoinings.length === 0), "Generated exportable joinings half-defined");
	// Map the root assembly to the root joining if possible.
	if (ejd.assemblyIdentifiers.length === 0) {
		// No joining instructions generated.
		return undefined;
	}
	const assembly = wsi4.node.assembly(vertex);
	assert(assembly !== undefined, "assembly undefined yet generated joining");
	ejd.assemblyIdentifiers.push({
		assembly: assembly,
		identifier: "root",
		joiningIdentifier: wsi4.util.toKey(vertex),
	});
	const nvd = ejd.assemblyIdentifiers.map((ai: AssemblyIdentifier): GltfNodeVendorData => ({
		assembly: ai.assembly,
		data: {
			identifier: ai.identifier,
			joiningIdentifier: ai.joiningIdentifier,
		},
	}));
	return {
		globalData: {
			joinings: ejd.exportableJoinings,
		},
		nodeVendorDatas: nvd,
	};
}

export function appendExportableJoiningsData(vertex: Vertex, exportableJoiningsData: ExportableJoiningsData): void {
	const assembly = wsi4.node.assembly(vertex);
	if (wsi4.node.workStepType(vertex) !== "joining" || assembly === undefined) {
		return;
	}
	const joining = wsi4.node.joining(vertex);
	if (joining === undefined) {
		return;
	}
	for (const sourceVertex of wsi4.graph.sources(vertex)) {
		if (wsi4.node.workStepType(sourceVertex) !== "joining") {
			continue;
		}
		appendExportableJoiningsData(sourceVertex, exportableJoiningsData);
	}

	const assemblyIdentifiers: AssemblyIdentifier[] = [];
	const exportableJoiningSteps = joining.joiningSteps.map((step: JoiningStep): ExportableJoiningStep => {
		const entries = step.entries.map((entry: JoiningStepEntry): ExportableJoiningStepEntry => {
			const sjv = getSubJoiningVertex(vertex, entry.assembly);
			assemblyIdentifiers.push({
				assembly: entry.assembly,
				identifier: wsi4.util.toKey(entry.assembly),
				joiningIdentifier: sjv === undefined ? undefined : wsi4.util.toKey(sjv),
			});
			return {
				assemblyIdentifier: wsi4.util.toKey(entry.assembly),
			};
		});
		return {
			entries: entries,
			cameraOrientation: step.cameraOrientation,
			comment: step.comment,
		};
	});

	// Append the generated joining to the array of all joinings we produce.
	exportableJoiningsData.exportableJoinings.push({
		joiningIdentifier: wsi4.util.toKey(vertex),
		exportableJoiningSteps: exportableJoiningSteps,
	});

	const combinedAssemblyIdentifiers = exportableJoiningsData.assemblyIdentifiers.concat(assemblyIdentifiers);
	combinedAssemblyIdentifiers.sort((lhs, rhs): number => lhs.identifier < rhs.identifier ? -1 : lhs.identifier > rhs.identifier ? 1 : 0);

	exportableJoiningsData.assemblyIdentifiers = combinedAssemblyIdentifiers.filter((v, index, self) => self.indexOf(v) === index);
}

// For one of this node's joining-step-entry`s this function checks all sources of the node
// if there is a source-node of workStepType Joining with an assembly that is isomorphic to
// joining-step-entry's joining.
//
// Note: vertex must be associated with a node with underlying workStepType Joining.
//
// Return: The sub-joining if available along with the associated vertex;  undefined else
export function getSubJoiningVertex(vertex: Vertex, stepEntryAssembly: Assembly): Vertex | undefined {
	const sourceVertices = wsi4.graph.sources(vertex);
	for (const sourceVertex of sourceVertices) {
		if (wsi4.node.workStepType(sourceVertex) !== "joining") {
			continue;
		}

		// A node of type Joining should have an assembly
		const assembly = wsi4.node.assembly(sourceVertex);
		if (assembly === undefined) {
			continue;
		}
		if (!wsi4.geo.util.isIsomorphic(stepEntryAssembly, assembly)) {
			continue;
		}

		return sourceVertex;
	}

	// No sub-joining available.
	return undefined;
}

/*
 * Note that everything below here is deprecated and should be removed once #1779 is fully implemented!
 */

function assemblyIdentifier(vertex: Vertex, stepEntryAssembly: Assembly): string | undefined {
	const sourceVertices = wsi4.graph.sources(vertex);
	for (const sourceVertex of sourceVertices) {
		const asm = wsi4.node.assembly(sourceVertex);
		if (!asm) {
			continue;
		}
		if (wsi4.geo.util.isIsomorphic(asm, stepEntryAssembly)) {
			return getArticleName(sourceVertex);
		}
	}
	return undefined;
}

export declare interface InternalJoiningStepEntry {
	// Note: this is the associated step-model's name
	identifier: string;
	assembly: Assembly;
	subJoining: InternalJoining | undefined;
}

export declare interface InternalJoiningStep {
	entries: Array<InternalJoiningStepEntry>;
	cameraOrientation?: CameraOrientation3;
	comment: string;
}

// Internal joining provides actual Assembly:s. Hence it cannot be used independently from wsi4. For external usage see ExternalJoining.
export declare interface InternalJoining {
	joiningSteps: Array<InternalJoiningStep>;
}

// Create nested joining from underlying serialized wscam::Joining.
// The resulting object consists of modified joinings that can be used independently from wsi4 since Assembly:s are replaced by keys respectively.
export function createInternalJoining(vertex: Vertex): InternalJoining | undefined {
	const convertJoiningStepEntry = (entry: JoiningStepEntry): InternalJoiningStepEntry => {
		const identifier = assemblyIdentifier(vertex, entry.assembly);
		const subJoining = (() => {
			const subJoiningVertex = getSubJoiningVertex(vertex, entry.assembly);
			if (subJoiningVertex === undefined) {
				return undefined;
			}
			return createInternalJoining(subJoiningVertex);
		})();
		const result: InternalJoiningStepEntry = {
			assembly: entry.assembly,
			identifier: identifier === undefined ? wsi4.util.translate("Unknown") : identifier,
			subJoining: subJoining,
		};
		return result;
	};

	const convertJoiningStep = (step: JoiningStep) => {
		const result: InternalJoiningStep = step.cameraOrientation !== undefined ? {
			entries: step.entries.map((entry: JoiningStepEntry): InternalJoiningStepEntry => convertJoiningStepEntry(entry)),
			cameraOrientation: step.cameraOrientation,
			comment: step.comment,
		} : {
			entries: step.entries.map((entry: JoiningStepEntry): InternalJoiningStepEntry => convertJoiningStepEntry(entry)),
			comment: step.comment,
		};
		return result;
	};

	const joining = wsi4.node.joining(vertex);
	if (joining === undefined) {
		return undefined;
	}
	const steps = joining.joiningSteps.map(step => convertJoiningStep(step));
	return {
		joiningSteps: steps,
	};
}

export declare interface ExternalJoiningStepEntry {
	// Note: this is the associated step-model's name
	identifier: string;
	assembly: string;
	subJoining?: ExternalJoining;
}

export function isExternalJoiningStepEntry(t: unknown): t is ExternalJoiningStepEntry {
	return typeof t === "object" && t !== null && hasPropertyT(t, "identifier", isString) && hasOptionalPropertyT(t, "subJoining", isExternalJoining);
}

export declare interface ExternalJoiningStep {
	entries: Array<ExternalJoiningStepEntry>;
	cameraOrientation?: CameraOrientation3;
	comment: string;
}

export function isExternalJoiningStep(t: unknown): t is ExternalJoiningStep {
	return typeof t === "object" && t !== null &&
		hasPropertyT(t, "entries", (ar: unknown): ar is Array<ExternalJoiningStepEntry> => isArray(ar, isExternalJoiningStepEntry)) &&
		hasOptionalPropertyT(t, "cameraOrientation", isCameraOrientation3) && hasPropertyT(t, "comment", isString);
}

/**
 * Opaque Assembly objects are replaced by ids so the resulting structure can be used independently from wsi4
 *
 * Note: this interface is compatible to UiJoiningRepresentation.
 * The only difference is the additional subJoining that is optionally part of a ExternalJoiningStepEntry which is required for joining instructions.
 */
export declare interface ExternalJoining {
	joiningSteps: Array<ExternalJoiningStep>;
}

export function isExternalJoining(t: unknown): t is ExternalJoining {
	return typeof t === "object" && t !== null && hasPropertyT(t, "joiningSteps", (ar: unknown): ar is Array<ExternalJoiningStep> => isArray(ar, isExternalJoiningStep));
}

export function createExternalJoining(joining: InternalJoining): ExternalJoining {
	const convertEntry = (entry: InternalJoiningStepEntry) => {
		if (entry.subJoining !== undefined) {
			return {
				identifier: entry.identifier,
				assembly: wsi4.util.toKey(entry.assembly),
				subJoining: createExternalJoining(entry.subJoining),
			};
		} else {
			return {
				identifier: entry.identifier,
				assembly: wsi4.util.toKey(entry.assembly),
			};
		}
	};
	const convertStep = (step: InternalJoiningStep) => {
		if (step.cameraOrientation !== undefined) {
			return {
				entries: step.entries.map(entry => convertEntry(entry)),
				cameraOrientation: step.cameraOrientation,
				comment: step.comment,
			};
		} else {
			return {
				entries: step.entries.map(entry => convertEntry(entry)),
				comment: step.comment,
			};
		}
	};
	return {joiningSteps: joining.joiningSteps.map(step => convertStep(step))};
}

function findAssembly(joining: Joining, assemblyString: string): Assembly {
	for (const step of joining.joiningSteps) {
		for (const entry of step.entries) {
			if (wsi4.util.toKey(entry.assembly) === assemblyString) {
				return entry.assembly;
			}
		}
	}
	return wsi4.throwError("findAssembly(): Missing assembly");
}

function toAssembly(vertex: Vertex, assemblyString: string): Assembly {
	const oldJoining = wsi4.node.joining(vertex);
	if (oldJoining === undefined) {
		return wsi4.throwError("toAssembly(): Missing joining");
	}
	return findAssembly(oldJoining, assemblyString);
}

function createJoiningStepEntry(vertex: Vertex, entry: ExternalJoiningStepEntry): JoiningStepEntry {
	return {
		assembly: toAssembly(vertex, entry.assembly),
	};
}

function createJoiningStep(vertex: Vertex, step: ExternalJoiningStep): JoiningStep {
	const entries = step.entries.map(element => createJoiningStepEntry(vertex, element));
	if (step.cameraOrientation === undefined) {
		return {
			entries: entries,
			comment: step.comment,
		};
	}
	return {
		entries: entries,
		cameraOrientation: step.cameraOrientation,
		comment: step.comment,
	};
}

export function createJoining(vertex: Vertex, joining: ExternalJoining): Joining {
	return {
		joiningSteps: joining.joiningSteps.map(element => createJoiningStep(vertex, element)),
	};
}
