import {
	Color,
} from "qrc:/js/lib/generated/enum";

/**
 * Multiplication of Matrix with Vector
 */
export function multiply3(matrix: Matrix3, vec: Vector3): Vector3 {
	const me = matrix.entries;
	const ve = vec.entries;
	return {
		entries: [
			me[0] * ve[0] + me[3] * ve[1] + me[6] * ve[2],
			me[1] * ve[0] + me[4] * ve[1] + me[7] * ve[2],
			me[2] * ve[0] + me[5] * ve[1] + me[8] * ve[2],
		],
	};
}

/**
 * Multiplication of Matrix with Vector
 * Note that matrix is considered to be in column-major format and multiplied from the left with the (column) vector.
 * (This is of course equivalent to it being in row-major format and being multiplied from the right to the (row) vector.)
 */
export function multiply4(matrix: Matrix4, vec: Vector4): Vector4 {
	const me = matrix.entries;
	const ve = vec.entries;
	return {
		entries: [
			me[0] * ve[0] + me[4] * ve[1] + me[8] * ve[2] + me[12] * ve[3],
			me[1] * ve[0] + me[5] * ve[1] + me[9] * ve[2] + me[13] * ve[3],
			me[2] * ve[0] + me[6] * ve[1] + me[10] * ve[2] + me[14] * ve[3],
			me[3] * ve[0] + me[7] * ve[1] + me[11] * ve[2] + me[15] * ve[3],
		],
	};
}

/**
 * Cross product
 */
export function cross(first: Vector3, sec: Vector3): Vector3 {
	const fe = first.entries;
	const se = sec.entries;
	return {
		entries: [
			fe[1] * se[2] - fe[2] * se[1],
			fe[2] * se[0] - fe[0] * se[2],
			fe[0] * se[1] - fe[1] * se[0],
		],
	};
}

/**
 * Scalar multipliciation
 */
export function scalarMult(l: number, vec: Vector3): Vector3 {
	const ve = vec.entries;
	return {
		entries: [
			l * ve[0],
			l * ve[1],
			l * ve[2],
		],
	};
}

/**
 * Sumation of vectors
 */
export function sum(first: Vector3, sec: Vector3): Vector3 {
	const fe = first.entries;
	const se = sec.entries;
	return {
		entries: [
			fe[0] + se[0],
			fe[1] + se[1],
			fe[2] + se[2],
		],
	};
}
/**
 * Scalar product
 */
export function scalarProduct(first: Vector3, sec: Vector3): number {
	const fe = first.entries;
	const se = sec.entries;
	return fe[0] * se[0] + fe[1] * se[1] + fe[2] * se[2];
}

/**
 * Normalize vector
 */
export function normalize(vec: Vector3): Vector3 {
	return scalarMult(1 / Math.sqrt(scalarProduct(vec, vec)), vec);
}

/**
 * Scales v by n
 */
export function scale2(n: number, v: Vector2): Vector2 {
	return {
		entries: [
			n * v.entries[0],
			n * v.entries[1],
		],
	};
}

/**
 * Dot product of two vectors
 */
export function dot2(lhs: Vector2, rhs: Vector2): number {
	const le = lhs.entries;
	const re = rhs.entries;
	return le[0] * re[0] + le[1] * re[1];
}

/**
 * Determinant of the matrix resulting from lhs and rhs
 */
export function determinant2(lhs: Vector2, rhs: Vector2): number {
	const le = lhs.entries;
	const re = rhs.entries;
	return le[0] * re[1] - le[1] * re[0];
}

/**
 * The square v's norm
 */
export function squareNorm2(v: Vector2): number {
	return dot2(v, v);
}

/**
 * Normalize vector
 */
export function normalize2(v: Vector2): Vector2 {
	return scale2(1 / Math.sqrt(squareNorm2(v)), v);
}

/**
 * Add lhs to rhs
 */
export function add2(lhs: Vector2, rhs: Vector2): Vector2 {
	return {
		entries: [
			lhs.entries[0] + rhs.entries[0],
			lhs.entries[1] + rhs.entries[1],
		],
	};
}

/**
 * Subtract rhs from lhs
 */
export function sub2(lhs: Vector2, rhs: Vector2): Vector2 {
	return add2(lhs, scale2(-1, rhs));
}

/**
 * Compute direction of camera
 */
export function computeCameraDirection(camera: Camera3): Vector3 {
	return {
		entries: [
			camera.center.entries[0] - camera.eye.entries[0],
			camera.center.entries[1] - camera.eye.entries[1],
			camera.center.entries[2] - camera.eye.entries[2],
		],
	};
}

/**
 * Rotate vector around x-axis
 *
 * @param vector The vector
 * @param angle Angle for rotation around x-axis [rad]
 * @returns rotated vector
 */
export function rotateX(vector: Vector3, angle: number): Vector3 {
	// Note: matrix appears to be transposed due to column-major format
	const matrix: Matrix3 = {
		entries: [
			1,
			0,
			0,
			0,
			Math.cos(angle),
			Math.sin(angle),
			0,
			-Math.sin(angle),
			Math.cos(angle),
		],
	};
	return multiply3(matrix, vector);
}

/**
 * Rotate vector around y-axis
 *
 * @param vector The vector
 * @param angle Angle for rotation around y-axis [rad]
 * @returns rotated vector
 */
export function rotateY(vector: Vector3, angle: number): Vector3 {
	// Note: matrix appears to be transposed due to column-major format
	const matrix: Matrix3 = {
		entries: [
			Math.cos(angle),
			0,
			-Math.sin(angle),
			0,
			1,
			0,
			Math.sin(angle),
			0,
			Math.cos(angle),
		],
	};
	return multiply3(matrix, vector);
}

/**
 * Rotate vector around z-axis
 *
 * @param vector The vector
 * @param angle Angle for rotation around z-axis [rad]
 * @returns rotated vector
 */
export function rotateZ(vector: Vector3, angle: number): Vector3 {
	// Note: matrix appears to be transposed due to column-major format
	const matrix: Matrix3 = {
		entries: [
			Math.cos(angle),
			Math.sin(angle),
			0,
			-Math.sin(angle),
			Math.cos(angle),
			0,
			0,
			0,
			1,
		],
	};
	return multiply3(matrix, vector);
}

/**
 * Rotate vector around x-, y-, and z-axis in this order
 *
 * @param vector The vector
 * @param angleX Angle for rotation around x-axis [rad]
 * @param angleY Angle for rotation around y-axis [rad]
 * @param angleZ Angle for rotation around z-axis [rad]
 * @returns rotated vector
 */
export function rotate(vector: Vector3, angleX: number, angleY: number, angleZ: number): Vector3 {
	return rotateZ(rotateY(rotateX(vector, angleX), angleY), angleZ);
}

export function squareNorm3(first: Vector3, second: Vector3): number {
	const f = first.entries;
	const s = second.entries;
	return (f[0] - s[0]) * (f[0] - s[0]) + (f[1] - s[1]) * (f[1] - s[1]) + (f[2] - s[2]) * (f[2] - s[2]);
}

/**
 * Convenience function to create an assembly from an Iop and a given depth
 */
export function assemblyFromIop(iop: InnerOuterPolygon, depth: number, name = ""): Assembly {
	return wsi4.geo.assembly.fromIop(iop, depth, name);
}

export function extractRotationMatrix(projectiveMatrix: Matrix4): Matrix3 {
	const e = projectiveMatrix.entries;
	return {
		entries: [
			e[0],
			e[1],
			e[2],
			e[4],
			e[5],
			e[6],
			e[8],
			e[9],
			e[10],
		],
	};
}

export function extractTranslationVector(projectiveMatrix: Matrix4): Vector3 {
	const e = projectiveMatrix.entries;
	return {
		entries: [
			e[12],
			e[13],
			e[14],
		],
	};
}

export interface Dimensions {
	x: number,
	y: number,
	z: number,
}

/**
 * Compute assembly dimensions in WCS
 */
export function computeWcsDimensions(assembly: Assembly): Dimensions {
	// Assembly bounding box is in local coordinates
	const localV = ((): Vector3 => {
		const bbox = wsi4.geo.util.boundingBox3d(assembly);
		return {
			entries: [
				bbox.upper.entries[0] - bbox.lower.entries[0],
				bbox.upper.entries[1] - bbox.lower.entries[1],
				bbox.upper.entries[2] - bbox.lower.entries[2],
			],
		};
	})();

	const globalV = (() => {
		const wcs = wsi4.geo.assembly.worldCoordinateSystem(assembly);
		const projMatrix = wsi4.geo.util.toProjectiveTransformationMatrix(wcs);
		const rotMatrix = extractRotationMatrix(projMatrix);
		return multiply3(rotMatrix, localV);
	})();

	return {
		x: Math.abs(globalV.entries[0]),
		y: Math.abs(globalV.entries[1]),
		z: Math.abs(globalV.entries[2]),
	};
}

export function createEmptyScene(): Scene {
	return wsi4.geo.util.createScene({
		material: "",
		thickness: 0,
		identifier: "",
		comment: "",
		globalMaterial: "",
	});
}

export function addIopsToScene(inputScene: Scene, iops: readonly Readonly<InnerOuterPolygon>[], styleInput?: Readonly<SceneStyle>): Scene {
	const style = styleInput ?? {
		strokeWidth: 1,
		strokeColor: Color.closedContour,
	};
	return wsi4.geo.util.addInnerOuterPolygonsToScene(inputScene, iops, style);
}

export function renderScene(scene: Scene, fileType: FileType, renderSettingsInput?: RenderSceneSettings): ArrayBuffer {
	const settings = renderSettingsInput ?? {};
	return wsi4.geo.util.renderScene(scene, fileType, settings);
}

export function transformPoint3(point: Readonly<Point3>, transformation: CoordinateSystem3): Point3 {
	const projMatrix = wsi4.geo.util.toProjectiveTransformationMatrix(transformation);

	let tmp: Vector4 = {
		entries: [
			point.entries[0],
			point.entries[1],
			point.entries[2],
			1,
		],
	};
	tmp = multiply4(projMatrix, tmp);

	if (tmp.entries[3] === 0) {
		wsi4.throwError("Point transformation invalid");
	}

	return {
		entries: [
			tmp.entries[0] / tmp.entries[3],
			tmp.entries[1] / tmp.entries[3],
			tmp.entries[2] / tmp.entries[3],
		],
	};
}

/**
 * @param point The point
 * @param transformation Typically the TwoDimRep's transformation that is obtained by wsi4.node.twoDimRepTransformation()
 * @returns The transformed point
 */
export function point2To3(point: Point2, transformation: CoordinateSystem3): Point3 {
	const projMatrix = wsi4.geo.util.toProjectiveTransformationMatrix(transformation);

	let tmp: Vector4 = {
		entries: [
			point.entries[0],
			point.entries[1],
			0,
			1,
		],
	};
	tmp = multiply4(projMatrix, tmp);

	if (tmp.entries[3] === 0) {
		wsi4.throwError("Point transformation invalid");
	}

	return {
		entries: [
			tmp.entries[0] / tmp.entries[3],
			tmp.entries[1] / tmp.entries[3],
			tmp.entries[2] / tmp.entries[3],
		],
	};
}

/**
 * @param point The point
 * @param transformation Typically the TwoDimRep's transformation that is obtained by wsi4.node.twoDimRepTransformation()
 * @returns The transformed point
 */
export function point3To2(point: Point3, transformation: CoordinateSystem3): Point2 {
	const inverseTransformation = wsi4.geo.util.invertCoordinateSystem(transformation);
	const inverseProjMatrix = wsi4.geo.util.toProjectiveTransformationMatrix(inverseTransformation);

	let tmp: Vector4 = {
		entries: [
			point.entries[0],
			point.entries[1],
			point.entries[2],
			1,
		],
	};
	tmp = multiply4(inverseProjMatrix, tmp);

	if (tmp.entries[3] === 0) {
		wsi4.throwError("Point transformation invalid");
	}

	return {
		entries: [
			tmp.entries[0] / tmp.entries[3],
			tmp.entries[1] / tmp.entries[3],
		],
	};
}
