/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */
import { Vector3, Ray, BoundingInfo } from './babylonjs-import';
import { NormalType } from './bim-format-types';
import { setMax, setMin, intersectRayWithAAABBToRef } from '../math';
import { decodePackedNormal } from './PackedNormal';
const v1 = Vector3.Zero();
const v2 = Vector3.Zero();
const v3 = Vector3.Zero();
/** Representation of an IFC mesh */
export class BimIfcMesh {
    vertexData;
    transform;
    toMeterScale;
    static _tmp = {
        aabbMin: Vector3.Zero(),
        aabbMax: Vector3.Zero(),
        out: Vector3.Zero(),
        normal: Vector3.Zero(),
        boundingInfo: new BoundingInfo(Vector3.Zero(), Vector3.Zero())
    };
    constructor(vertexData, transform, toMeterScale) {
        this.vertexData = vertexData;
        this.transform = transform;
        this.toMeterScale = toMeterScale;
    }
    /**
     * Perform intersection testing on a {@link BimIfcMesh}.
     * @param o Intersection options. See {@link BimIfcMeshIntersectionOptions}.
     * @returns Intersection info. Will be empty if no intersection was found.
     */
    intersect(o) {
        const intersections = [];
        const t = this.transform;
        let ray;
        if ('canvasCoordinate' in o) {
            ray = Ray.Zero();
            ray = o.camera.getScene().createPickingRay(o.canvasCoordinate.x, o.canvasCoordinate.y, t, o.camera);
        }
        else {
            ray = Ray.Transform(o.ray, this.transform.clone().invert());
        }
        const aabbMax = setMin(BimIfcMesh._tmp.aabbMax);
        const aabbMin = setMax(BimIfcMesh._tmp.aabbMin);
        const vd = this.vertexData;
        const iLen = vd.indices.length;
        // Intersecting a triangle ie expensive so we only do it if we are required to
        // if not we replace the intersect method with a noop. (gets rid of if statement in tight for loop)
        // We always build a AABB even if no geometry intersection is performed.
        const intersectTriangle = o.isGeometryIntersectionEnabled === undefined || o.isGeometryIntersectionEnabled
            ? this.intersectTriangle.bind(this)
            : this.intersectTriangleNoOp.bind(this);
        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]);
            aabbMax.maximizeInPlace(v1);
            aabbMax.maximizeInPlace(v2);
            aabbMax.maximizeInPlace(v3);
            aabbMin.minimizeInPlace(v1);
            aabbMin.minimizeInPlace(v2);
            aabbMin.minimizeInPlace(v3);
            intersectTriangle(ray, v1, v2, v3, i / 3, t, intersections);
        }
        if (intersections.length === 0 && o.isAABBIntersectionEnabled) {
            const distance = intersectRayWithAAABBToRef(ray, aabbMin, aabbMax, BimIfcMesh._tmp.out);
            if (distance !== undefined) {
                intersections.push({
                    position: Vector3.TransformCoordinates(BimIfcMesh._tmp.out, t),
                    // TODO Not correct normal!
                    normal: Vector3.TransformNormal(ray.direction.negate(), t).normalize(),
                    distance: distance
                });
            }
        }
        if (intersections.length === 0 && o.isCenterIntersectionFallbackEnabled) {
            BimIfcMesh._tmp.boundingInfo.reConstruct(aabbMin, aabbMax);
            const center = BimIfcMesh._tmp.boundingInfo.boundingSphere.centerWorld;
            const distance = ray.origin.subtract(center).length();
            intersections.push({
                position: Vector3.TransformCoordinates(center, t),
                // TODO Not correct normal! But in this case there is none.
                normal: Vector3.TransformNormal(ray.direction.negate(), t).normalize(),
                distance: distance
            });
        }
        intersections.sort((a, b) => a.distance - b.distance);
        return intersections;
    }
    /**
     * Finds all intersections of a Ray object and a this geometry.
     * If a Vector2 object is provided, a screen-spaced Ray form pixel will be used for intersection test.
     * @param screenCoordinateOrRay Vector2 | Ray. Ray needs to be in babylon-coordinate-system/world-space.
     * @param sceneOrOptions Scene.
     * @returns Intersection info. Will be empty if no intersection was found.
     */
    getIntersectionPoints(screenCoordinateOrRay, sceneOrOptions) {
        if (screenCoordinateOrRay instanceof Ray) {
            return this.intersect({ ray: screenCoordinateOrRay });
        }
        if (sceneOrOptions === undefined) {
            throw new Error('Not implemented');
        }
        else if (!sceneOrOptions.activeCamera) {
            throw new Error('Scene has no active camera');
        }
        return this.intersect({ canvasCoordinate: screenCoordinateOrRay, camera: sceneOrOptions.activeCamera });
    }
    /**
     * Checks if bounds of object are inside of geometry.
     * @param bound SimpleBoundingBox in world space.
     * @alpha
     */
    geometricallyContainsBoundingBox(bound) {
        const babylonToLocal = this.transform.clone().invert(); //matrix from babylon space to this geometry object space
        // set bbox coords to v1 and v2
        v1.set(bound[0], bound[1], bound[2]);
        v2.set(bound[3], bound[4], bound[5]);
        // transform from world to local coords
        Vector3.TransformCoordinatesToRef(v1, babylonToLocal, v1);
        Vector3.TransformCoordinatesToRef(v2, babylonToLocal, v2);
        // calculate bbox vector and distance.
        const direction = v2.subtract(v1).normalize();
        const distance = Vector3.Distance(v1, v2);
        const ray = new Ray(v1.clone(), direction);
        const intersections = this.getRayIntersections(ray);
        // if even intersection count = ray got origin outside the geometry
        if (intersections.length % 2 === 0) {
            return false;
        }
        // filter out all intersections closer to origin than the size of the bbox
        const intersectionsBeyondBbox = intersections.filter((i) => i.distance >= distance);
        if (intersectionsBeyondBbox.length % 2 === 0) {
            return false;
        }
        // else there is a odd count, and the bbox is completely on inside of mesh
        return true;
    }
    /**
     * Checks if bounds of object are inside of geometry.
     * @param bound SimpleBoundingBox in world space.
     * @alpha
     */
    geometricallyIntersectingBoundingBox(bound) {
        const babylonToLocal = this.transform.clone().invert(); //matrix from babylon space to this geometry object space
        // set bbox coords to v1 and v2
        v1.set(bound[0], bound[1], bound[2]);
        v2.set(bound[3], bound[4], bound[5]);
        // transform from world to local coords
        Vector3.TransformCoordinatesToRef(v1, babylonToLocal, v1);
        Vector3.TransformCoordinatesToRef(v2, babylonToLocal, v2);
        // calculate bbox vector and distance.
        const direction = v2.subtract(v1).normalize();
        const distance = Vector3.Distance(v1, v2);
        const ray = new Ray(v1.clone(), direction, distance);
        const intersections = this.getRayIntersections(ray);
        // If no intersection, bounding box is not intersecting
        if (intersections.length === 0) {
            return false;
        }
        // else there is a intersection
        return true;
    }
    /**
     * Checks if all vertices of mesh are completely contained by the geometry.
     * @param mesh Mesh of geometry to test.
     * @alpha
     */
    geometricallyContains(mesh) {
        const ifcToLocal = this.transform.clone().invert(); //matrix from IFC space to this geometry object space
        const meshLocalToIfcToLocal = mesh.transform.multiply(ifcToLocal); // from mesh to ifc and to local
        const vd = mesh.vertexData;
        const iLen = vd.indices.length;
        for (let i = 0; i < iLen; i += 3) {
            const i1 = vd.indices[i];
            const i2 = vd.indices[i + 1];
            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]);
            Vector3.TransformCoordinatesToRef(v1, meshLocalToIfcToLocal, v1);
            Vector3.TransformCoordinatesToRef(v2, meshLocalToIfcToLocal, v2);
            const direction = v2.subtract(v1).normalize();
            const distance = Vector3.Distance(v1, v2);
            const ray = new Ray(v1.clone(), direction, distance);
            let intersections = this.getRayIntersections(ray);
            // if even intersection count = ray got origin outside the geometry
            if (intersections.length % 2 === 0) {
                return false;
            }
            // filter out all intersections closer to origin than the size of the bbox
            let intersectionsBeondBbox = intersections.filter((i) => i.distance >= distance);
            if (intersectionsBeondBbox.length % 2 === 0) {
                return false;
            }
            // to be sure we need to test another edge as well
            const i3 = vd.indices[i + 2];
            v1.set(vd.positions[i1 * 3], vd.positions[i1 * 3 + 1], vd.positions[i1 * 3 + 2]);
            v3.set(vd.positions[i3 * 3], vd.positions[i3 * 3 + 1], vd.positions[i3 * 3 + 2]);
            Vector3.TransformCoordinatesToRef(v1, meshLocalToIfcToLocal, v1);
            Vector3.TransformCoordinatesToRef(v3, meshLocalToIfcToLocal, v3);
            ray.length = Vector3.Distance(v1, v3);
            direction.copyFrom(v3.subtractInPlace(v1).normalize());
            intersections = this.getRayIntersections(ray);
            // if even intersection count = ray got origin outside the geometry
            if (intersections.length % 2 === 0) {
                return false;
            }
            // filter out all intersections closer to origin than the size of the bbox
            intersectionsBeondBbox = intersections.filter((i) => i.distance >= distance);
            if (intersectionsBeondBbox.length % 2 === 0) {
                return false;
            }
        }
        return false;
    }
    /**
     * Checks if there is a vertex from the mesh on the inside and outside of the geometry.
     * @param mesh Mesh of geometry to test.
     * @alpha
     */
    geometricallyIntersects(mesh) {
        const ifcToLocal = this.transform.clone().invert(); //matrix from IFC space to this geometry object space
        const meshLocalToIfcToLocal = mesh.transform.multiply(ifcToLocal); // from mesh to ifc and to local
        const vd = mesh.vertexData;
        const iLen = vd.indices.length;
        for (let i = 0; i < iLen; i += 3) {
            const i1 = vd.indices[i];
            const i2 = vd.indices[i + 1];
            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]);
            Vector3.TransformCoordinatesToRef(v1, meshLocalToIfcToLocal, v1);
            Vector3.TransformCoordinatesToRef(v2, meshLocalToIfcToLocal, v2);
            const direction = v2.subtract(v1).normalize();
            const distance = Vector3.Distance(v1, v2);
            const ray = new Ray(v1.clone(), direction, distance);
            if (this.rayIntersects(ray)) {
                return true;
            }
            // to be sure we need to test another edge as well
            const i3 = vd.indices[i + 2];
            v1.set(vd.positions[i1 * 3], vd.positions[i1 * 3 + 1], vd.positions[i1 * 3 + 2]);
            v3.set(vd.positions[i3 * 3], vd.positions[i3 * 3 + 1], vd.positions[i3 * 3 + 2]);
            Vector3.TransformCoordinatesToRef(v1, meshLocalToIfcToLocal, v1);
            Vector3.TransformCoordinatesToRef(v3, meshLocalToIfcToLocal, v3);
            ray.length = Vector3.Distance(v1, v3);
            direction.copyFrom(v3.subtractInPlace(v1).normalize());
            if (this.rayIntersects(ray)) {
                return true;
            }
        }
        return false;
    }
    barycentricToPoint(v1, v2, v3, bu, bv) {
        const bw = 1 - (bu + bv);
        return new Vector3(bu * v1.x + bv * v2.x + bw * v3.x, bu * v1.y + bv * v2.y + bw * v3.y, bu * v1.z + bv * v2.z + bw * v3.z);
    }
    getRayIntersections(ray) {
        const vd = this.vertexData;
        const iLen = vd.indices.length;
        const intersections = [];
        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]);
            const intersectionInfo = ray.intersectsTriangle(v1, v2, v3);
            if (intersectionInfo && intersectionInfo.distance > 0) {
                intersections.push(intersectionInfo);
            }
        }
        return intersections;
    }
    rayIntersects(ray) {
        const vd = this.vertexData;
        const iLen = vd.indices.length;
        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]);
            const intersectionInfo = ray.intersectsTriangle(v1, v2, v3);
            if (intersectionInfo && intersectionInfo.distance > 0) {
                return true;
            }
        }
        return false;
    }
    intersectTriangleNoOp(ray, vertex1, vertex2, vertex3, triangleIndex, t, dstIntersections
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    ) { }
    intersectTriangle(ray, vertex1, vertex2, vertex3, triangleIndex, t, dstIntersections) {
        const intersectionInfo = ray.intersectsTriangle(vertex1, vertex2, vertex3);
        // Filter out backface/behind-origin hits and self-intersections (distance <= epsilon)
        if (intersectionInfo && intersectionInfo.distance > 1e-6) {
            const intersectionPoint = this.barycentricToPoint(vertex1, vertex2, vertex3, intersectionInfo.bu, intersectionInfo.bv);
            const vd = this.vertexData;
            let packedNormal;
            if (vd.normalType === NormalType.PerFace) {
                packedNormal = vd.packedNormals[triangleIndex];
            }
            else {
                const firstVertexIndex = vd.indices[triangleIndex * 3];
                packedNormal = vd.packedNormals[firstVertexIndex];
            }
            const dn = decodePackedNormal[packedNormal];
            const objectSpaceNormal = BimIfcMesh._tmp.normal.set(dn.x, dn.y, dn.z);
            const position = Vector3.TransformCoordinates(intersectionPoint, t);
            const rayOriginBjsSpace = Vector3.TransformCoordinatesToRef(ray.origin, t, v1);
            dstIntersections.push({
                position: position,
                normal: Vector3.TransformNormal(objectSpaceNormal, t).normalize(),
                distance: Vector3.Distance(position, rayOriginBjsSpace)
            });
        }
    }
}
//# sourceMappingURL=bim-ifc-mesh.js.map