/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */

/*
 WARNING WARNING WARNING WARNING!!!!!!
 The following code IS NOT PRODUCTION READY and is only intended for internal use right now. 
 If it was for production, it would need to be refactored and optimized. IT WOULD NOT HAVE BEEN APPROVED FOR PROD USE IN ITS CURRENT STATE.
 Instead of checking for intersections between BimIfcObject it should probably check for intersections between BimProductMesh instances instead.
 A BimIfcObject is a collection of BimProductMesh instances. Each BimProductMesh instance points to its parent BimIfcObject instance. 
 Hence knowing which BimProductMesh instances intersect with each other would be enough to determine which BimIfcObject instances intersect with each other.
 Might even be that we should work directly with BimVertexData instance.
 Generic math methods (if any) should be moved to the math module.
 The entire interface (unless moved into IfcPRoductMesh class) should probably be written as a class instead as that is what
 we do in the rest of the codebase.

 Glancing at the code it looks like there is a lot of allocations going on in the hotpath. This is usually a bad idea in performance critical code.
 We have code elsewhere that can be used to make it easier to process BimVertexData instances a triangles which is probably what we should be using here.

 This code originates from another repository where we have graphical tests. Those tests should be converted to unit tests and moved into this repository.
 */

import {
    Vector3,
    BoundingSphere,
    BoundingBox,
    BoundingInfo,
    Ray,
    IntersectionInfo,
    Plane,
    Matrix,
    Epsilon,
    Scalar
} from './babylonjs-import';
import { BimIfcMesh } from './bim-ifc-mesh';
import { BimIfcObject } from './bim-ifc-object';

enum ContainmentResult {
    Contained,
    Intersects,
    None
}

/**
 * Checks intersections of an IFC products against an array of IFC products.
 * Only for internal use. Subject to change. Use of these method in production applications is not supported. If used, it is at your own risk.
 * Such code will very likely break in future versions.
 * @alpha
 * @internal
 * @hidden
 * @param cutter Main mesh that will be intersection tested against the clippies
 * @param cuttees Array of meshes to validate against the clipper mesh.
 * @returns
 */
export function _bimIfcObjectIntersect(cutter: BimIfcObject, cuttees: BimIfcObject[]): BimIfcObject[] {
    if (!cutter.hasGeometry) {
        return [];
    }

    const intersecting = [];
    for (const cuttee of cuttees) {
        if (cutterGeometricallyIntersectsCuttee(cutter, cuttee)) {
            intersecting.push(cuttee);
        }
    }
    return intersecting;
}

// Cache corners of bounding box.
const cc = [
    Vector3.Zero(),
    Vector3.Zero(),
    Vector3.Zero(),
    Vector3.Zero(),
    Vector3.Zero(),
    Vector3.Zero(),
    Vector3.Zero(),
    Vector3.Zero()
];

/**
 * Geometrically intersects or contains the cuttee.
 * @param cutter Bim object to cut whit.
 * @param cuttee Bim object to be cut.
 * @returns true if the cuttee is intersecting or is contained in cutter mesh.
 */
function cutterGeometricallyIntersectsCuttee(cutter: BimIfcObject, cuttee: BimIfcObject): boolean {
    if (!cuttee.hasGeometry) false;

    const cutterBoundingInfo = cutter.boundingInfo();
    const cutteeBoundingInfo = cuttee.boundingInfo();
    // Test Bounding sphere intersection
    if (!BoundingSphere.Intersects(cutterBoundingInfo.boundingSphere, cutteeBoundingInfo.boundingSphere)) {
        // Bounding spheres dose not intersect, cuttee to far from cutter
        return false;
    }
    // Test AABB intersection
    if (!BoundingBox.Intersects(cutterBoundingInfo.boundingBox, cutteeBoundingInfo.boundingBox)) {
        // Bounding box dose not intersect, cuttee to far from cutter
        return false;
    }

    // Test if cutter mesh completely contains cuttee bound
    const cutterMeshes: BimIfcMesh[] = cutter.rawProduct.m.map((m) => cutter.mesh(m));
    for (const cutterMesh of cutterMeshes) {
        if (boundingBoxContainsInMesh(cutterMesh, cutteeBoundingInfo)) {
            return true; // no more tests required, cuttee is completely on the inside of the cutter mesh.
        }
    }

    // Test cuttee triangle-sphere and triangle-edges agist cutter mesh.
    const cutteeMeshes = cuttee.rawProduct.m.map((m) => cuttee.mesh(m));
    for (const cutteeMesh of cutteeMeshes) {
        for (const cutterMesh of cutterMeshes) {
            const result = geometricallyContains(cutterMesh, cutteeMesh, cutteeBoundingInfo.boundingSphere.center);
            if (result === ContainmentResult.Contained) {
                return true; // cuttee mesh are contained in cutter mesh.
            } else if (result === ContainmentResult.Intersects) {
                return true; // cuttee mesh are intersecting cutter mesh.
            }
        }
    }

    // Nope, cuttee dose is not contained, nor intersecting cutter mesh.
    return false;
}

/**
 * Checks if bound is contained in mesh.
 * @param cutterMesh The cutter mesh.
 * @param cutteeBoundingInfo the cuttee boundingInfo.
 * @returns Whether the bounding box is completely inside the mesh or not.
 */
function boundingBoxContainsInMesh(cutterMesh: BimIfcMesh, cutteeBoundingInfo: BoundingInfo): boolean {
    const cutteeMin = cutteeBoundingInfo.boundingBox.minimumWorld.clone();
    const cutteeMax = cutteeBoundingInfo.boundingBox.maximumWorld.clone();

    const babylonToLocal = cutterMesh.transform.clone().invert(); //matrix from babylon space to cutter geometry object space.
    // Transform from world to local coords
    Vector3.TransformCoordinatesToRef(cutteeMin, babylonToLocal, cutteeMin);
    Vector3.TransformCoordinatesToRef(cutteeMax, babylonToLocal, cutteeMax);

    // NOTE: It is a confusing that cc has documentation stating that it is a cached BBOX and we use
    // variables like cutteeMin and cutteeMax (signifying a AABB). However the implentation is actually treating
    // cc as a "cube" with 8 corners and it is (after transformation) not guaranteed to be a AABB. This is intentional
    // but very confusing. Renaming of variables and documentation is strongly suggested.
    cc[0].copyFrom(cutteeMin);
    cc[1].set(cutteeMin.x, cutteeMin.y, cutteeMax.z);
    cc[2].set(cutteeMax.x, cutteeMin.y, cutteeMin.z);
    cc[3].set(cutteeMax.x, cutteeMin.y, cutteeMax.z);
    cc[4].set(cutteeMin.x, cutteeMax.y, cutteeMin.z);
    cc[5].set(cutteeMin.x, cutteeMax.y, cutteeMax.z);
    cc[6].set(cutteeMax.x, cutteeMax.y, cutteeMin.z);
    cc[7].copyFrom(cutteeMax);

    // The 8 corners are used as line segments whit start and end points.
    // The mesh has to contain all 4 line segments to be consider containing the cutteeBoundingBox.
    if (!geometricallyContainsLine(cutterMesh, cc[0], cc[1])) return false;
    if (!geometricallyContainsLine(cutterMesh, cc[2], cc[3])) return false;
    if (!geometricallyContainsLine(cutterMesh, cc[4], cc[5])) return false;
    if (!geometricallyContainsLine(cutterMesh, cc[6], cc[7])) return false;

    return true;
}

/**
 * Checks if a mesh geometrically contains the line defined by start and end vectors.
 * @param mesh The mesh to check against.
 * @param start The start vector of the line.
 * @param end The end vector of the line.
 * @returns True if the mesh contains the line segment, false otherwise.
 */
function geometricallyContainsLine(mesh: BimIfcMesh, start: Vector3, end: Vector3): boolean {
    // Calculate the direction vector from start to end and normalize it.
    const direction = end.subtract(start).normalize();
    const ray = new Ray(start, direction, undefined, 0);
    const distance = Vector3.Distance(start, end);

    // Get intersections of the ray with the mesh and check if the ray is parallel to any face.
    const [intersections, isParallel] = getRayIntersections(mesh, ray, distance);
    if (isParallel) {
        return true;
    }

    // If there are no intersections, the mesh does not contain the line segment.
    if (intersections.length === 0) {
        return false;
    }

    // Sort intersections by distance from the ray origin.
    intersections.sort((a, b) => a.distance - b.distance);

    // Check for intersections close to min, max, and between min and max.
    const hasHitAtStart = intersections.some((i) => i.distance < 1.5);
    const hasHitAtEnd = intersections.some((i) => Math.abs(i.distance - distance) < 1.5);
    const hasHitBetweenStartAndEnd = intersections.some((i) => i.distance > 1.5 && i.distance <= distance - 1);

    // If no intersections are within the distance, check the parity of intersections.
    if (!intersections.some((i) => i.distance <= distance)) {
        if (intersections.length % 2 > 0) {
            return true;
        }
        return false;
    } else if (hasHitAtEnd && !hasHitBetweenStartAndEnd && intersections.length % 2 > 0) {
        return true;
    }

    // If there are no hits at min or max, the mesh does not contain the line segment.
    if (!hasHitAtStart && !hasHitAtEnd) {
        return false;
    }

    // If there is only one intersection at end or start, the mesh does not contain the line segment.
    if (intersections.length === 1 && (hasHitAtStart || hasHitAtEnd)) {
        return false;
    }

    // If there are hits between end and start, the mesh does not contain the line segment.
    if (hasHitBetweenStartAndEnd) {
        return false;
    }

    // If there are hits at both end and start, the mesh contains the line segment.
    if (hasHitAtStart && hasHitAtEnd) {
        return true;
    }

    // Test the opposite point (start if end was hit, and vice versa).
    let pointToTest = start;
    if (hasHitAtStart) {
        pointToTest = end;
    }

    // Create a new ray from the opposite point.
    ray.origin = pointToTest;
    const [result, isParallel2] = getRayIntersections(mesh, ray, distance);

    // If the new ray is parallel, assume the mesh contains the line segment.
    if (isParallel2) return true;

    // Sort the new intersections by distance.
    result.sort((a, b) => a.distance - b.distance);
    // Check the parity of the new intersections.
    if (result.length % 2 === 0) {
        return false;
    }

    return true;
}

/**
 * Gets the intersections of a ray with a mesh and checks if the ray is parallel to any face.
 * @param mesh The mesh to check against.
 * @param ray The ray to test for intersections.
 * @param rayLength The length of the ray (default is 0).
 * @returns A tuple containing the list of intersections and a boolean indicating if the ray is parallel to any face.
 */
function getRayIntersections(mesh: BimIfcMesh, ray: Ray, rayLength = 0): [IntersectionInfo[], boolean] {
    const v1 = Vector3.Zero();
    const v2 = Vector3.Zero();
    const v3 = Vector3.Zero();
    const vd = mesh.vertexData;
    const iLen = vd.indices.length;
    const intersections: IntersectionInfo[] = [];

    for (let i = 0; i < iLen; i += 3) {
        const i1 = vd.indices[i];
        const i2 = vd.indices[i + 1];
        const i3 = vd.indices[i + 2];

        // Set the vertices of the triangle.
        v1.set(vd.positions[i1 * 3], vd.positions[i1 * 3 + 1], vd.positions[i1 * 3 + 2]);
        v2.set(vd.positions[i2 * 3], vd.positions[i2 * 3 + 1], vd.positions[i2 * 3 + 2]);
        v3.set(vd.positions[i3 * 3], vd.positions[i3 * 3 + 1], vd.positions[i3 * 3 + 2]);

        // Check if the ray intersects the triangle.
        const intersectionInfo = ray.intersectsTriangle(v1, v2, v3);
        if (intersectionInfo) {
            let distance = intersectionInfo.distance;
            if (Math.abs(distance) < 0.01) distance = 0;

            // Add the intersection if it's not already in the list.
            if (distance >= 0 && !intersections.some((i) => Math.abs(i.distance - distance) < 0.1)) {
                intersections.push(intersectionInfo);
            }
        } else {
            // Check if the ray is parallel to the triangle's plane.
            const plane = Plane.FromPoints(v1, v2, v3);
            const rayStart = ray.origin;
            const rayEnd = ray.origin.add(ray.direction.scale(rayLength));
            const signedDistanceStart = plane.signedDistanceTo(ray.origin);
            const signedDistanceEnd = plane.signedDistanceTo(ray.origin.add(ray.direction.scale(rayLength)));

            if (Math.abs(signedDistanceStart) < 1 && Math.abs(signedDistanceEnd) < 1) {
                const toPlaneMatrix = new Matrix();
                Matrix.RotationAlignToRef(Vector3.Forward(), plane.normal, toPlaneMatrix, true);
                const v12d = Vector3.TransformCoordinates(v1, toPlaneMatrix);
                const v22d = Vector3.TransformCoordinates(v2, toPlaneMatrix);
                const v32d = Vector3.TransformCoordinates(v3, toPlaneMatrix);

                // Check if the ray start and end points are within the triangle.
                if (!isPointInTriangle(rayStart, v12d, v22d, v32d)) {
                    continue;
                }

                if (!isPointInTriangle(rayEnd, v12d, v22d, v32d)) {
                    continue;
                }

                // If the ray is parallel to the plane and intersects the triangle, return true.
                return [[], true];
            }
        }
    }

    // Return the list of intersections and false indicating the ray is not parallel.
    return [intersections, false];
}

/**
 * Check if point is in triangle.
 * @param point Point to test.
 * @param p1 Triangle corner.
 * @param p2  Triangle corner.
 * @param p3  Triangle corner.
 * @returns True if point is inside the triangle.
 */
function isPointInTriangle(point: Vector3, p1: Vector3, p2: Vector3, p3: Vector3): boolean {
    // Calculate vectors.
    const v0x = p3.x - p1.x;
    const v0y = p3.y - p1.y;
    const v1x = p2.x - p1.x;
    const v1y = p2.y - p1.y;
    const v2x = point.x - p1.x;
    const v2y = point.y - p1.y;

    // Compute dot products.
    const dot00 = v0x * v0x + v0y * v0y;
    const dot01 = v0x * v1x + v0y * v1y;
    const dot02 = v0x * v2x + v0y * v2y;
    const dot11 = v1x * v1x + v1y * v1y;
    const dot12 = v1x * v2x + v1y * v2y;

    // Compute barycentric coordinates.
    const denom = dot00 * dot11 - dot01 * dot01;

    // Check if the denominator is zero.
    if (Scalar.WithinEpsilon(denom, 0, Epsilon)) {
        return true; // The points are collinear or the triangle is degenerate.
    }

    // Compute the inverse of the denominator.
    const invDenom = 1 / denom;
    const u = (dot11 * dot02 - dot01 * dot12) * invDenom;
    const v = (dot00 * dot12 - dot01 * dot02) * invDenom;

    // Check if point is in triangle.
    return u >= 0 && v >= 0 && u + v <= 1;
}

/**
 * Check if cutter mesh contains cuttee mesh.
 * @param cutterMesh Mesh to test if it encapsulate the cuttee.
 * @param cutteeMesh Mesh to check if it contains.
 * @param center A center coordinate to ame the corner vectors towards.
 * @param tolerance in millimeters. (Since the mesh is in object space)
 * @returns Result.
 */
function geometricallyContains(
    cutterMesh: BimIfcMesh,
    cutteeMesh: BimIfcMesh,
    center: Vector3,
    tolerance = 5
): ContainmentResult {
    const v1 = Vector3.Zero();
    const v2 = Vector3.Zero();
    const v3 = Vector3.Zero();
    const otherMeshPoint = Vector3.Zero();

    // Invert the transformation matrix to get from IFC space to local geometry object space.
    const ifcToLocal = cutterMesh.transform.clone().invert();
    // Combined transformation matrix from cuttee mesh to local mesh.
    // The direction of the matrix is from cuttee IFC to cutter local object space.
    // (Important Optimization suggestion: If the geometry of the cuttee has more indices than the cutter, the matrix direction should be inverted for optimizing the amount of triangles transformed to the other mesh coordinate system.)
    const meshLocalToIfcToLocal = cutteeMesh.transform.multiply(ifcToLocal);

    const cutterVertexData = cutterMesh.vertexData;
    const cutteeVertexData = cutteeMesh.vertexData;

    // Transform the center point to local space.
    const localCenter = Vector3.TransformCoordinates(center, cutterMesh.transform.clone().invert());

    // Generate a random direction vector for the ray.
    const randomDirection = new Vector3(-1 + Math.random() * 2, -1 + Math.random() * 2, -1 + Math.random() * 2).scale(
        0.01
    );
    const ray = new Ray(Vector3.Zero(), Vector3.Up(), undefined, 0);

    let numContained = 0;
    let numNotContained = 0;
    let numOnSurface = 0;

    // Iterate over each vertex of the cuttee mesh.
    for (let i = 0; i < cutteeVertexData.positions.length; i += 3) {
        otherMeshPoint.set(
            cutteeVertexData.positions[i],
            cutteeVertexData.positions[i + 1],
            cutteeVertexData.positions[i + 2]
        );
        // Transform the vertex to local mesh space.
        Vector3.TransformCoordinatesToRef(otherMeshPoint, meshLocalToIfcToLocal, otherMeshPoint);
        ray.origin = otherMeshPoint;

        // Calculate the ray direction from the local center to the current vertex.
        localCenter.subtractToRef(otherMeshPoint, ray.direction);
        ray.direction.addInPlace(randomDirection);
        ray.direction.normalize();

        const intersections: IntersectionInfo[] = [];
        let isCoplanar = false;

        let distanceToMesh = Infinity;

        // TODO: Use utility function to avoid duplication of code
        // VertexDataUtils.forEachTriangle(cutterVertexData, (p1, p2, p3) => {});
        // Iterate over each triangle in the mesh.
        for (let j = 0; j < cutterVertexData.indices.length; j += 3) {
            const i1 = cutterVertexData.indices[j];
            const i2 = cutterVertexData.indices[j + 1];
            const i3 = cutterVertexData.indices[j + 2];
            v1.set(
                cutterVertexData.positions[i1 * 3],
                cutterVertexData.positions[i1 * 3 + 1],
                cutterVertexData.positions[i1 * 3 + 2]
            );
            v2.set(
                cutterVertexData.positions[i2 * 3],
                cutterVertexData.positions[i2 * 3 + 1],
                cutterVertexData.positions[i2 * 3 + 2]
            );
            v3.set(
                cutterVertexData.positions[i3 * 3],
                cutterVertexData.positions[i3 * 3 + 1],
                cutterVertexData.positions[i3 * 3 + 2]
            );

            // Calculate the distance between the vertex and the plane of the triangle.
            const plane = Plane.FromPoints(v1, v2, v3);
            const distanceToPlane = Math.abs(plane.signedDistanceTo(otherMeshPoint));
            const minDistanceToVertex = Math.min(
                Vector3.Distance(otherMeshPoint, v1),
                Vector3.Distance(otherMeshPoint, v2),
                Vector3.Distance(otherMeshPoint, v3)
            );
            const toPlaneMatrix = new Matrix();
            Matrix.RotationAlignToRef(plane.normal, Vector3.Forward(), toPlaneMatrix, false);
            const v12d = Vector3.TransformCoordinates(v1, toPlaneMatrix);
            const v22d = Vector3.TransformCoordinates(v2, toPlaneMatrix);
            const v32d = Vector3.TransformCoordinates(v3, toPlaneMatrix);
            const otherMeshPointOnPlane = Vector3.TransformCoordinates(otherMeshPoint, toPlaneMatrix);

            // Check if the vertex is in the triangle and closer than the current closest distance.
            if (isPointInTriangle(otherMeshPointOnPlane, v12d, v22d, v32d) && distanceToPlane < distanceToMesh) {
                distanceToMesh = distanceToPlane;
            }

            if (minDistanceToVertex < distanceToMesh) {
                distanceToMesh = minDistanceToVertex;
            }
        }

        // Check if the vertex is on the surface of the mesh.
        if (distanceToMesh < 1) {
            numOnSurface++;
        }

        // TODO: Use utility function to avoid duplication of code
        // VertexDataUtils.forEachTriangle(cutterVertexData, (p1, p2, p3) => {});
        // Check for intersections between the ray and the triangles of the mesh.
        for (let j = 0; j < cutterVertexData.indices.length; j += 3) {
            const i1 = cutterVertexData.indices[j];
            const i2 = cutterVertexData.indices[j + 1];
            const i3 = cutterVertexData.indices[j + 2];
            v1.set(
                cutterVertexData.positions[i1 * 3],
                cutterVertexData.positions[i1 * 3 + 1],
                cutterVertexData.positions[i1 * 3 + 2]
            );
            v2.set(
                cutterVertexData.positions[i2 * 3],
                cutterVertexData.positions[i2 * 3 + 1],
                cutterVertexData.positions[i2 * 3 + 2]
            );
            v3.set(
                cutterVertexData.positions[i3 * 3],
                cutterVertexData.positions[i3 * 3 + 1],
                cutterVertexData.positions[i3 * 3 + 2]
            );

            // Check if the ray intersects the triangle.
            const intersectionInfo = ray.intersectsTriangle(v1, v2, v3);

            if (intersectionInfo) {
                intersections.push(intersectionInfo);
            } else {
                // Check if the vertex is coplanar with the triangle.
                const plane = Plane.FromPoints(v1, v2, v3);
                const signedDistance = plane.signedDistanceTo(otherMeshPoint);

                if (Math.abs(signedDistance) <= tolerance) {
                    const toPlaneMatrix = new Matrix();
                    Matrix.RotationAlignToRef(Vector3.Forward(), plane.normal, toPlaneMatrix, true);
                    const v12d = Vector3.TransformCoordinates(v1, toPlaneMatrix);
                    const v22d = Vector3.TransformCoordinates(v2, toPlaneMatrix);
                    const v32d = Vector3.TransformCoordinates(v3, toPlaneMatrix);
                    const otherMeshPointOnPlane = Vector3.TransformCoordinates(otherMeshPoint, toPlaneMatrix);

                    if (isPointInTriangle(otherMeshPointOnPlane, v12d, v22d, v32d)) {
                        isCoplanar = true;
                        break;
                    }
                }
            }
        }

        // Skip coplanar vertices.
        if (isCoplanar) {
            continue;
        }

        // Filter intersections based on distance.
        const positiveIntersections = intersections.filter((i) => i.distance >= -tolerance);
        const nearZeroIntersections = intersections.filter((i) => Math.abs(i.distance) <= tolerance);

        // Determine containment status based on intersections.
        if (positiveIntersections.length % 2 === 0 && nearZeroIntersections.length === 0 && distanceToMesh > 1) {
            numNotContained++;
            continue;
        }

        if (distanceToMesh > 1) {
            numContained++;
        }
    }

    // Determine final containment result.
    if (numContained === 0 && numOnSurface > 0) {
        if (geometricallyIntersectsWithTolerance(cutterMesh, cutteeMesh)) {
            return ContainmentResult.Intersects;
        }
    }

    if (numNotContained > 0 && numContained > 0) {
        return ContainmentResult.Intersects;
    }

    if (numNotContained === 0) {
        return ContainmentResult.Contained;
    }

    if (numContained === 0 && numNotContained === 0 && numOnSurface > 0) {
        return ContainmentResult.Contained;
    }

    return ContainmentResult.None;
}

/**
 * Checks if two meshes intersect with each other within a given tolerance.
 * @param mesh The first mesh.
 * @param otherMesh The second mesh.
 * @param tolerance The tolerance for intersection checks in millimeter.
 * @returns True if the meshes intersect within the given tolerance, otherwise false.
 */
function geometricallyIntersectsWithTolerance(mesh: BimIfcMesh, otherMesh: BimIfcMesh, tolerance = 5): boolean {
    const v1 = Vector3.Zero();
    const v2 = Vector3.Zero();
    const v3 = Vector3.Zero();
    const ref = Vector3.Zero();

    // Matrix from IFC space to this geometry object space.
    const ifcToLocal = mesh.transform.clone().invert();

    // Matrix from otherMesh to IFC and then to local space.
    const meshLocalToIfcToLocal = otherMesh.transform.multiply(ifcToLocal);

    // Get vertex data and indices length.
    const vd = otherMesh.vertexData;
    const iLen = vd.indices.length;
    const ray = new Ray(Vector3.Zero(), Vector3.One(), 1, 0);

    // Loop through otherMesh and transform each triangle to mesh local space.
    // TODO: Use utility function to avoid duplication of code
    // VertexDataUtils.forEachTriangle(cutterVertexData, (p1, p2, p3) => {});
    // Split triangle into 3 rays and check each ray against mesh.
    for (let i = 0; i < iLen; i += 3) {
        const i1 = vd.indices[i];
        const i2 = vd.indices[i + 1];
        const i3 = vd.indices[i + 2];
        v1.set(vd.positions[i1 * 3], vd.positions[i1 * 3 + 1], vd.positions[i1 * 3 + 2]);
        v2.set(vd.positions[i2 * 3], vd.positions[i2 * 3 + 1], vd.positions[i2 * 3 + 2]);
        v3.set(vd.positions[i3 * 3], vd.positions[i3 * 3 + 1], vd.positions[i3 * 3 + 2]);

        // Transform triangle vertices to local space.
        Vector3.TransformCoordinatesToRef(v1, meshLocalToIfcToLocal, v1);
        Vector3.TransformCoordinatesToRef(v2, meshLocalToIfcToLocal, v2);
        Vector3.TransformCoordinatesToRef(v3, meshLocalToIfcToLocal, v3);

        // Calculate the centroid and radius of the circumcircle of the triangle.
        calculateCentroidToRef(v1, v2, v3, ref);
        const radius = calculateMaxDistance(v1, v2, v3, ref);

        // Check if the triangle intersects with the sphere.
        if (!triangleSphere(mesh, ref, radius)) {
            continue;
        }

        // Setup first ray between v1 and v2.
        ray.origin.copyFrom(v1);
        ray.length = Vector3.Distance(v1, v2);
        ray.direction.copyFrom(v2).subtractInPlace(v1).normalize();
        if (deepRayIntersectsWithTolerance(mesh, ray, tolerance)) {
            return true;
        }

        // Setup ray for test between v1 and v3, change direction and length.
        ray.length = Vector3.Distance(v1, v3);
        ray.direction.copyFrom(v3).subtractInPlace(v1).normalize();
        if (deepRayIntersectsWithTolerance(mesh, ray, tolerance)) {
            return true;
        }

        // Setup ray for last edge between v2 and v3, change direction and length.
        ray.origin.copyFrom(v2);
        ray.length = Vector3.Distance(v2, v3);
        ray.direction.copyFrom(v3).subtractInPlace(v2).normalize();
        if (deepRayIntersectsWithTolerance(mesh, ray, tolerance)) {
            return true;
        }
    }

    return false;
}

/**
 * Checks if a given ray intersects with a mesh within a specified tolerance.
 * @param mesh The mesh to check against.
 * @param ray The ray to check for intersection.
 * @param tolerance The tolerance value for intersection checks. Default is 5 millimeters.
 * @returns True if the ray intersects with the mesh within the tolerance, otherwise false.
 */
function deepRayIntersectsWithTolerance(mesh: BimIfcMesh, ray: Ray, tolerance = 5): boolean {
    const v1 = Vector3.Zero();
    const v2 = Vector3.Zero();
    const v3 = Vector3.Zero();
    const vertexData = mesh.vertexData;
    const iLen = vertexData.indices.length; // Total number of indices in the mesh.

    // TODO: Use utility function to avoid duplication of code
    // VertexDataUtils.forEachTriangle(cutterVertexData, (p1, p2, p3) => {});
    // Loop through all triangles in the mesh.
    for (let i = 0; i < iLen; i += 3) {
        const i1 = vertexData.indices[i];
        const i2 = vertexData.indices[i + 1];
        const i3 = vertexData.indices[i + 2];

        // Set the position of the vertices.
        v1.set(vertexData.positions[i1 * 3], vertexData.positions[i1 * 3 + 1], vertexData.positions[i1 * 3 + 2]);
        v2.set(vertexData.positions[i2 * 3], vertexData.positions[i2 * 3 + 1], vertexData.positions[i2 * 3 + 2]);
        v3.set(vertexData.positions[i3 * 3], vertexData.positions[i3 * 3 + 1], vertexData.positions[i3 * 3 + 2]);

        // Check if the ray intersects with the triangle.
        const intersectionInfo = ray.intersectsTriangle(v1, v2, v3);

        if (intersectionInfo) {
            const distance = intersectionInfo.distance; // Distance to the intersection point.

            // Remove intersections close to the ends of the line.
            if (distance > tolerance && distance < ray.length - tolerance) {
                const intersectionPoint = ray.origin.add(ray.direction.scale(distance)); // Calculate the intersection point.

                // Check if the intersection point is geometrically contained within the mesh.
                if (
                    !geometricallyContainsPoint(
                        mesh,
                        Vector3.TransformCoordinates(
                            intersectionPoint.subtract(ray.direction.scale(10)),
                            mesh.transform
                        )
                    ) &&
                    !geometricallyContainsPoint(
                        mesh,
                        Vector3.TransformCoordinates(intersectionPoint.add(ray.direction.scale(10)), mesh.transform)
                    )
                ) {
                    return false; // Return false if the point is not contained within the mesh.
                }

                return true; // Return true if the ray intersects within the tolerance.
            }
        }
    }
    return false; // Return false if no intersection is found.
}

/**
 * Checks if a point is geometrically contained within a mesh.
 * @param mesh The mesh to check against.
 * @param point The point to check.
 * @param tolerance The tolerance level for the containment check.
 * @returns True if the point is within the mesh, otherwise false.
 */
function geometricallyContainsPoint(mesh: BimIfcMesh, point: Vector3, tolerance = 5): boolean {
    const v1 = Vector3.Zero();
    const v2 = Vector3.Zero();
    const v3 = Vector3.Zero();
    const plane = new Plane(0, 0, 0, 0);

    const ifcToLocal = mesh.transform.clone().invert(); // Matrix from IFC space to this geometry object space.

    const meshVd = mesh.vertexData; // Vertex data of the mesh.

    const localPoint = Vector3.TransformCoordinates(point, ifcToLocal); // Transform the point to the local coordinate system of the mesh.
    const ray = new Ray(localPoint, Vector3.Up()); // Create a ray starting at the local point and pointing upwards.

    const intersections: IntersectionInfo[] = []; // Array to store intersection information.
    let isCoplanar = false; // Flag to detect if the point is coplanar with any triangle.

    // TODO: Use utility function to avoid duplication of code
    // VertexDataUtils.forEachTriangle(cutterVertexData, (p1, p2, p3) => {});
    for (let j = 0; j < meshVd.indices.length; j += 3) {
        const i1 = meshVd.indices[j];
        const i2 = meshVd.indices[j + 1];
        const i3 = meshVd.indices[j + 2];
        v1.set(meshVd.positions[i1 * 3], meshVd.positions[i1 * 3 + 1], meshVd.positions[i1 * 3 + 2]);
        v2.set(meshVd.positions[i2 * 3], meshVd.positions[i2 * 3 + 1], meshVd.positions[i2 * 3 + 2]);
        v3.set(meshVd.positions[i3 * 3], meshVd.positions[i3 * 3 + 1], meshVd.positions[i3 * 3 + 2]);

        const intersectionInfo = ray.intersectsTriangle(v1, v2, v3); // Check for intersection with the current triangle.
        if (intersectionInfo) {
            intersections.push(intersectionInfo); // If there is an intersection, add it to the intersections array.
        }

        plane.copyFromPoints(v1, v2, v3); // Update the plane from the triangle vertices.
        const signedDistance = plane.signedDistanceTo(localPoint); // Calculate the signed distance from the point to the plane.

        if (Math.abs(signedDistance) <= tolerance) {
            const toPlaneMatrix = new Matrix(); // Matrix to align the point to the plane.
            Matrix.RotationAlignToRef(Vector3.Left(), plane.normal, toPlaneMatrix, true); // Align the matrix to the plane normal.
            const v12d = Vector3.TransformCoordinates(v1, toPlaneMatrix);
            const v22d = Vector3.TransformCoordinates(v2, toPlaneMatrix);
            const v32d = Vector3.TransformCoordinates(v3, toPlaneMatrix);

            if (isPointInTriangle(localPoint, v12d, v22d, v32d)) {
                isCoplanar = true; // Check if the point lies within the triangle on the plane.
                break;
            }
        }
    }

    if (isCoplanar) {
        return false; // If the point is coplanar with any triangle, it is not geometrically contained.
    }

    const uniqueIntersections: IntersectionInfo[] = []; // Array to store unique intersections.
    for (const i of intersections) {
        if (!uniqueIntersections.some((ii) => Math.abs(i.distance - ii.distance) < 1)) {
            uniqueIntersections.push(i); // Add unique intersections based on distance.
        }
    }

    const positiveIntersections = uniqueIntersections.filter((i) => i.distance >= -tolerance); // Filter positive intersections.
    const nearZeroIntersections = uniqueIntersections.filter((i) => Math.abs(i.distance) <= tolerance); // Filter near-zero intersections.

    if (positiveIntersections.length % 2 === 0 && nearZeroIntersections.length === 0) {
        return false; // If the count of positive intersections is even and no intersections are near zero, the point is outside the mesh.
    }

    return true; // Otherwise, the point is inside the mesh.
}

/**
 * Checks if any triangle in the mesh intersects with a sphere.
 * @param mesh The mesh to check.
 * @param origin The center of the sphere.
 * @param radius The radius of the sphere.
 * @returns True if any triangle intersects with the sphere, otherwise false.
 */
function triangleSphere(mesh: BimIfcMesh, origin: Vector3, radius: number): boolean {
    const v1 = Vector3.Zero();
    const v2 = Vector3.Zero();
    const v3 = Vector3.Zero();
    const ref = Vector3.Zero();

    // Get vertex data and indices length.
    const vertexData = mesh.vertexData;
    const indexLength = vertexData.indices.length;

    // TODO: Use utility function to avoid duplication of code
    // VertexDataUtils.forEachTriangle(cutterVertexData, (p1, p2, p3) => {});
    // Iterate through each triangle in the mesh.
    for (let i = 0; i < indexLength; i += 3) {
        const i1 = vertexData.indices[i];
        const i2 = vertexData.indices[i + 1];
        const i3 = vertexData.indices[i + 2];

        // Set vector positions based on indices.
        v1.set(vertexData.positions[i1 * 3], vertexData.positions[i1 * 3 + 1], vertexData.positions[i1 * 3 + 2]);
        v2.set(vertexData.positions[i2 * 3], vertexData.positions[i2 * 3 + 1], vertexData.positions[i2 * 3 + 2]);
        v3.set(vertexData.positions[i3 * 3], vertexData.positions[i3 * 3 + 1], vertexData.positions[i3 * 3 + 2]);

        // Calculate centroid and maximum distance to the centroid.
        calculateCentroidToRef(v1, v2, v3, ref);
        const radius1 = calculateMaxDistance(v1, v2, v3, ref);

        const centroidDist = Vector3.Distance(ref, origin);

        // Check if the sphere intersects with the triangle.
        if (centroidDist < radius + radius1) {
            return true;
        }
    }
    return false;
}

/**
 * Calculate the maximum distance from the centroid to the vertices of a triangle.
 * @param v1 The first vertex of the triangle.
 * @param v2 The second vertex of the triangle.
 * @param v3 The third vertex of the triangle.
 * @param centroid The centroid of the triangle.
 * @returns The maximum distance from the centroid to a vertex.
 */
function calculateMaxDistance(v1: Vector3, v2: Vector3, v3: Vector3, centroid: Vector3): number {
    let maxDistance = Vector3.Distance(centroid, v1);
    maxDistance = Math.max(maxDistance, Vector3.Distance(centroid, v2));
    maxDistance = Math.max(maxDistance, Vector3.Distance(centroid, v3));
    return maxDistance;
}

/**
 * Calculate the centroid of a triangle and store the result in the reference vector.
 * @param v1 The first vertex of the triangle.
 * @param v2 The second vertex of the triangle.
 * @param v3 The third vertex of the triangle.
 * @param ref The vector to store the centroid.
 * @returns The vector containing the centroid.
 */
function calculateCentroidToRef(v1: Vector3, v2: Vector3, v3: Vector3, ref: Vector3): Vector3 {
    ref.copyFrom(v1);
    ref.addInPlace(v2);
    ref.addInPlace(v3);
    return ref.scaleInPlace(1 / 3); // Calculate average.
}
