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

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;
}

// 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;
}

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 = {
			entries: step.entries.map((entry: JoiningStepEntry): InternalJoiningStepEntry => convertJoiningStepEntry(entry)),
			cameraOrientation: step.cameraOrientation,
			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) => ({
		identifier: entry.identifier,
		assembly: wsi4.util.toKey(entry.assembly),
		subJoining: entry.subJoining === undefined ? undefined : createExternalJoining(entry.subJoining),
	});
	const convertStep = (step: InternalJoiningStep) => ({
		entries: step.entries.map(entry => convertEntry(entry)),
		cameraOrientation: step.cameraOrientation,
		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)),
	};
}
