import { BimCamera } from './BimCamera';
import { BimVertexDataRepository } from './loader/bim-vertex-data-repository';
import { BimIfcLoader } from './loader/bim-ifc-loader';
import { BimPropertySetRepository } from './loader/bim-property-set-repository';
import { Materials } from './loader/materials';
import { TwinfinityViewer } from './loader/twinfinity-viewer';
import { setMax, setMin } from './math';
import { Vector3, Ray, Color3 } from './loader/babylonjs-import';
import { CoordinateTracker } from './tools/CoordinateTracker';
import { PickOptionType } from './loader/Selectables';
import { telemetry } from './Telemetry';
import { PickResultType } from './loader/PickResult';
import { PredefinedPointerButtonId } from './PointerInfoWithTimings';
import { isFailure } from './fail';
import { SeverityLevel } from '@microsoft/applicationinsights-web';
import { BimGeometricFeaturesRepository } from './loader/bim-geometric-features-repository';
const pointerDragThresholdTimeInMs = 200;
/** Adapts {@link TwinfinityViewer} to the {@link GeometryBuilderRenderControl} interface. */
class TwinfinityRenderControlOptions {
    _viewer;
    constructor(_viewer) {
        this._viewer = _viewer;
    }
    get isPaused() {
        return this._viewer.performance.isPaused;
    }
    set isPaused(value) {
        this._viewer.performance.isPaused = value;
    }
    render() {
        this._viewer.scene.render();
    }
}
/**
 * BimCoreApi is a generic API class. It is completely unaware about the Twinfinity API backend
 * and can therefore be used as a base for integration with a completely different backend. This must be used
 * in conjunction with a specialized implementation of {@link BimCoreApiClient}. {@link BimApi} is an actual
 * example of such an implementation.
 */
export class BimCoreApi {
    _bimBackendApi;
    _bimVertexDataRepository;
    _bimIfc;
    _beforeUnloadHandler;
    /**
     * Extension point for applications that need to run the renderloop outside their own
     * change handling (AngularJs is one such example). Assignment should be done before creation of
     * a {@link BimCoreApi} instance.
     * @example Example for Angular JS
     * ```typescript
     *  const bimApi: BimApi;
     *  BimCoreApi.runRenderLoop = this.ngZone.runOutSideAngular;
     * ```
     *  @beta
     * @param renderLoop Renderloop function will be provided by the viewer. Simply call it in your provided function
     */
    static runRenderLoop = (renderLoop) => {
        renderLoop();
    };
    /**
     * Exposes viewer internals. Use this to access BabylonJS API.
     * Disclaimer! Accessing BabylonJS internals is considered advanced usage and you are effectively bypassing {@link BimCoreApi} (and subclasses).
     * We cannot make guarantees that:
     *  * BabylonJS APIs will remain backwards compatible. Therefore, your application may break when BabylonJS is updated to a new version.
     *  * Babylonjs data that {@link BimCoreApi} creates, such as scene hierarchy, lights, camera etc., will remain backwards compatible between versions. Relying on this may break your application.
     *  * Your modifications to BabylonJS data (such as scene hierarchy, lights, cameras etc.) will not be changed by code in this package.
     *  * Other bugs will not occur as a result of direct usage of BabylonJS APIs.
     * {@link https://doc.babylonjs.com/api/classes/babylon.scene}
     * @example
     * ```typescript
     * api.viewer.scene.ambientColor = new Color3(1, 0, 0);
     * ```
     */
    viewer;
    /**
     * Creates a new instance of {@link BimCoreApi} or subclass of {@link BimCoreApi}.
     * @param _htmlElementOrId ID of the HTML element where the viewer should be created.
     * @param apiFactory Factory function to create the API instance.
     * @example
     * ```typescript
     * // Create an API instance.
     * const bimCoreApi = await BimCoreApi.create('viewer', canvas => new BimCoreApi(canvas, new BimApiClientTwinfinity()));
     * const bimApi = await BimCoreApi.create('viewer', canvas => new BimApi(canvas, new BimApiClientTwinfinity()));
     * ```
     */
    static async createApi(_htmlElementOrId, apiFactory) {
        // TODO Add Jquery PEP to DOM for touch?
        if (typeof _htmlElementOrId === 'string') {
            const domElement = document.getElementById(_htmlElementOrId);
            if (domElement === undefined) {
                throw new Error(`DOM canvas with id='${_htmlElementOrId}' does not exist`);
            }
            _htmlElementOrId = domElement;
        }
        if (!(_htmlElementOrId instanceof HTMLCanvasElement)) {
            const canvas = document.createElement('canvas');
            canvas.style.width = '100%';
            canvas.style.height = '100%';
            _htmlElementOrId.appendChild(canvas);
            _htmlElementOrId = canvas;
        }
        const api = apiFactory(_htmlElementOrId);
        await api.initialize();
        return api;
    }
    /** Constructor. Use {@link createApi} instead. */
    constructor(htmlCanvasElement, _bimBackendApi, options) {
        this._bimBackendApi = _bimBackendApi;
        telemetry.setAuthenticatedUserContext(telemetry.userId, _bimBackendApi.id);
        const material = new Materials();
        this.viewer = TwinfinityViewer.create(htmlCanvasElement, material, BimCoreApi.runRenderLoop);
        this.camera = new BimCamera(this.viewer, this);
        this._bimVertexDataRepository = new BimVertexDataRepository(_bimBackendApi, {
            wasmLoaderOptions: options?.wasmLoaderOptions
        });
        this._bimIfc = new BimIfcLoader(_bimBackendApi, new BimPropertySetRepository(_bimBackendApi), new BimGeometricFeaturesRepository(_bimBackendApi), this._bimVertexDataRepository, material, options?.twinInputResolver, new TwinfinityRenderControlOptions(this.viewer));
        if (options?.autoDisposeOnUnload !== false && typeof window !== 'undefined') {
            this._beforeUnloadHandler = () => this.dispose();
            window.addEventListener('beforeunload', this._beforeUnloadHandler);
        }
    }
    /**
     * @internal Returns an IFC structure.
     */
    get ifc() {
        return this._bimIfc;
    }
    /** Direct access to the methods for communicating with the backend. */
    get backend() {
        return this._bimBackendApi;
    }
    /** Exposes methods related to selection operations.
     *  For example, what the user actually clicked on.
     */
    get selectables() {
        return this.viewer.selectables;
    }
    /**
     * Subscribe to get access to pointer events (mouse, touchpad etc) and to determine
     * what (if anything) a mouse click interacted with (what object was 'picked' in the 3D scene).
     */
    get onPointerObservable() {
        return this.viewer.onPointerObservable;
    }
    /**
     * Sets the container of the current BimApi instance.
     * @param containerOrUrl Container object or absolute URL Example: https://bim.foo.com/sites/portal/projects/\{project name\}
     * @param options Loading options
     * @remarks This method previously returned a `BimQuery` instance. Use {@link foreach}, {@link products}, {@link filter} instead.
     */
    async setContainer(containerOrUrl, options) {
        this.clear();
        if (options === 'skip-ifc-load') {
            return;
        }
        let ifcChanges = [];
        let loadPropertySets = false;
        let loadGeometry = false;
        if (!options) {
            ifcChanges = await this._bimBackendApi.getIfcChanges(containerOrUrl);
        }
        else {
            if ('ifcChangePredicate' in options) {
                ifcChanges = (await this._bimBackendApi.getIfcChanges(containerOrUrl)).filter((ifcChange) => options.ifcChangePredicate(ifcChange));
                if (options.throwOnNoIfcChanges && ifcChanges.length === 0) {
                    throw new Error('options.ifcChangePredicate gave empty collection of BimIfcChange');
                }
                loadPropertySets = options.loadPropertySets;
                loadGeometry = options.loadGeometry;
            }
            else if ('ifcChanges' in options) {
                ifcChanges = options.ifcChanges
                    .filter((c) => c.load)
                    .map((c) => {
                    const { transform, change } = c;
                    return { ...change, ...{ transform } };
                });
                loadPropertySets = options.loadPropertySets;
                loadGeometry = options.loadGeometry;
            }
            else {
                throw new Error('Not a supported options object');
            }
            for (const ifc of ifcChanges) {
                ifc.loadOptions = options;
            }
        }
        telemetry.trackTrace({ message: 'Load ifcchanges....' });
        const loaderElementResult = await this.ifc.add(ifcChanges);
        const event = telemetry.startTrackEvent('Loaded ifcchanges');
        for (const lEr of loaderElementResult) {
            if (isFailure(lEr)) {
                telemetry.trackTrace({
                    message: `Failed to load IFC.`,
                    severityLevel: SeverityLevel.Error
                }, lEr);
                continue;
            }
        }
        event.stop();
        // If no IFC's have been loaded we get a default region bounding info
        // which in turn is used so setup the environment.
        this.viewer.addOrUpdateEnvironment(this.ifc.regionBoundingInfo);
        const loadPropertySetsPromise = loadPropertySets ? this.ifc.loadPropertySets() : Promise.resolve();
        if (loadGeometry) {
            const geometryBuilder = await this.ifc.geometryBuilder();
            await geometryBuilder.build(({ geometry }) => this.viewer.addOrReplaceMesh(geometry));
        }
        await loadPropertySetsPromise;
    }
    /**
     * Clears api (any loaded IFC files etc are unloaded).
     */
    clear() {
        this.ifc.clear();
        this.viewer.clear();
    }
    /**
     * Disposes the API and releases all resources including WASM memory and Babylon.js resources.
     * After calling this method, the API instance should not be used.
     */
    dispose() {
        if (this._beforeUnloadHandler && typeof window !== 'undefined') {
            window.removeEventListener('beforeunload', this._beforeUnloadHandler);
            this._beforeUnloadHandler = undefined;
        }
        this.clear();
        this._bimVertexDataRepository.dispose();
        this.viewer.dispose();
    }
    /**
     * Iterate the {@link BimIfcObject} instances and return those that match the specified predicate.
     * @param predicate Predicate. Items matching this will be returned in array format.
     * @returns Array with {@link BimIfcObject} instances matched by the specified _predicate_.
     */
    filter(predicate) {
        const ret = [];
        this._bimIfc.foreach((o, recursionOptions) => {
            if (predicate(o, recursionOptions)) {
                ret.push(o);
            }
        });
        return ret;
    }
    /**
     * Intersects all visible BIM objects using ray from origin towards direction
     * @param origin Vector3
     * @param direction Vector3
     * @param objects BimIfcObject[]
     * @returns Intersection[] | undefined
     * @example
     * ```typescript
     * // GetIntersections makes a hit test on all visible BIM objects, from origin in direction and return first object intersection.
     * const intersections = this.api.getIntersections([0, 0, 0], [1, 1, 1]);
     * ```
     * @example
     * ```typescript
     * // GetIntersections makes a hit test on a subset of  BIM objects, from origin in direction and return first object intersection.
     * const intersections = this.api.withSelection(myVisibleObjects, objects => this.api.getIntersections([0, 0, 0], [1, 1, 1]);
     * ```
     * @example
     * ```typescript
     * // GetIntersections makes a hit test on a subset of visible BIM objects, from origin in direction and return first object intersection.
     * const intersections = this.api.getIntersections([0, 0, 0], [1, 1, 1], myObjects);
     * ```
     */
    getIntersections(origin, direction, objects = []) {
        const viewer = this.viewer;
        if (objects?.length > 0) {
            return this.withVisibleBimObjects(objects, pick);
        }
        else {
            return pick();
        }
        function pick() {
            return viewer.camera.pick({ type: PickOptionType.Ray, ray: new Ray(origin, direction) }).hitInfo;
        }
    }
    /**
     * Makes it possible to perform API operations, that operate on the set of visible BIM objects, on a smaller set
     * of (still visible) BIM objects
     * @param visibleBimObjects - Visible objects to run operation on
     * @param action - Operation
     * @example
     * ```typescript
     * // GetIntersections makes a hit test on a subset of visible BIM objects, from origin in direction and return first object intersection.
     * const intersections = this.api.withVisibleBimObjects(myVisibleBimObjects, objects => api.getIntersections([0, 0, 0], [1, 1, 1]));
     * ```
     */
    withVisibleBimObjects(visibleBimObjects, action) {
        const visibleObjectSet = new Set();
        if (visibleBimObjects?.length > 0) {
            const objectsToTest = new Set([...visibleBimObjects]);
            // Show only objects that match this query
            this.foreach((o) => {
                if (o.isOnGpu()) {
                    if (o.visible()) {
                        visibleObjectSet.add(o);
                    }
                    o.visible(objectsToTest.has(o));
                }
            });
        }
        try {
            return action(visibleBimObjects);
        }
        finally {
            if (visibleBimObjects) {
                // Show objects that was visible before doing intersection test
                this.ifc.foreach((o) => {
                    o.visible(visibleObjectSet.has(o));
                });
            }
        }
    }
    /**
     * Loads all BIM property sets for all loaded files.
     * @deprecated Call {@link BimIfcLoader.loadPropertySets} on {@link ifc} instead.
     */
    loadPropertySets() {
        return this._bimIfc.loadPropertySets();
    }
    /**
     * This function zooms the camera to contain BIM objects in view frustum.
     * @param products Array of BIM objects.
     */
    zoomToExtent(products) {
        if (products.length === 0) {
            return;
        }
        const min = setMax(new Vector3());
        const max = setMin(new Vector3());
        // Note that we DO NOT recurse. It is assumed that the users want to zoom in on the given collection ONLY.
        for (const o of products) {
            for (const mesh of o.productMeshes) {
                mesh.aabb(min, max);
            }
        }
        this.camera.zoomToExtent([min.x, min.y, min.z, max.x, max.y, max.z]);
    }
    /**
     * Use this function to iterate over all BIM objects.
     * @param action This is a lambda function to evaluate on each BimIfcObject.
     * @example
     * ```typescript
     * api.foreach((o, recursionOptions) => {
     *     if (o.hasGeometry && o.class.id !== "IfcFlowSegment") {
     *         console.log(o.gid);
     *         o.visible(true);
     *     } else {
     *         o.visible(false);
     *     }
     * });
     *
     * ```
     */
    foreach(action) {
        this._bimIfc.foreach(action);
    }
    /**
     * Create a iterator that can be used to iterate over all {@link BimIfcObject}'s.
     * Same as foreach but with a iterator instead.
     * @returns Iterator that can be used to iterate over all {@link BimIfcObject}'s.
     * @example
     * ```typescript
     * for(const o of api.products()) {
     *     if (o.hasGeometry && o.class.id !== "IfcFlowSegment") {
     *         console.log(o.gid);
     *         o.visible(true);
     *     } else {
     *         o.visible(false);
     *     }
     * };
     *
     * ```
     */
    products() {
        return this._bimIfc.products();
    }
    /**
     * Updates the viewer with the latest visual changes on BIM objects. See {@link foreach} for an example.
     */
    async applyVisualChanges() {
        const geometryBuilder = await this.ifc.geometryBuilder();
        await geometryBuilder.build(({ geometry }) => this.viewer.addOrReplaceMesh(geometry));
    }
    /**
     * Sets a handler to event of given type
     * @deprecated Use {@link onPointerObservable} instead.
     * @param type Type of event. For now only "click" is supported
     * @param clickEventHandler Function that gets called when the event occurs
     */
    on(type, clickEventHandler) {
        const isGeometryIntersectionEnabled = clickEventHandler.length >= 2;
        this.onPointerObservable.add((pointerInfo) => {
            const mainButton = pointerInfo.twinfinity.button(PredefinedPointerButtonId.Main);
            if (mainButton.event === 'up' && mainButton.duration < pointerDragThresholdTimeInMs) {
                const pickResult = pointerInfo.twinfinity.pick(isGeometryIntersectionEnabled);
                const backwardsCompatibleHitInfo = isGeometryIntersectionEnabled ? pickResult.hitInfo : undefined;
                if (pickResult.type === PickResultType.Empty) {
                    clickEventHandler();
                }
                else if (pickResult.type === PickResultType.IfcProductMesh) {
                    clickEventHandler({ product: pickResult.ifcProductMesh.ifcProduct, mesh: pickResult.ifcProductMesh }, backwardsCompatibleHitInfo, pickResult.mesh);
                }
                else if (pickResult.type === PickResultType.Mesh) {
                    clickEventHandler(undefined, backwardsCompatibleHitInfo, pickResult.mesh);
                }
            }
        });
    }
    /** Gets a {@link CoordinateTracker} instance. */
    createCoordinateTracker() {
        return new CoordinateTracker(this);
    }
    /**
     * Camera is an instance of BimCamera and used for manipulating the 3D camera view of the BIM viewer.
     */
    camera;
    /**
     * Sets background color in viewer
     * @param color RGB in range 0-1
     * @example
     * ```typescript
     * api.setBackgroundColor([0.95, 0.95, 0.95]);
     * ```
     */
    setBackgroundColor(color) {
        this.viewer.isSkyboxEnabled = false;
        if (!(color instanceof Color3)) {
            color = Color3.FromArray(color);
        }
        const { clearColor } = this.viewer.scene;
        clearColor.r = color.r;
        clearColor.g = color.g;
        clearColor.b = color.b;
    }
    /**
     * Sets highlight color in viewer
     * @param color RGB in range 0-1
     * @param highlightIndex The highlight index tgo change, it's restricted to One, Two and Three
     * because it makes no sense to change the color of the Empty index (since it's only there to denote that a productMesh has no highlight).
     * The highlight index defaults to One
     * @example
     * ```typescript
     * api.setHighlightColor([1.0, 0.0, 0.0]); <-- Sets the color of the first highlight index to solid red
     * api.setHighlightColor([0.0, 1.0, 0.0], HighlightIndex.Two); <-- Sets the color of the second highlight index to solid green
     * ```
     */
    setHighlightColor(color, highlightIndex) {
        this.viewer.visualSettings.ifc.setHighlightColor(color, highlightIndex);
    }
    /**
     * Gets or sets a value indicating whether to show or hide skybox.
     */
    get isSkyboxEnabled() {
        return this.viewer.isSkyboxEnabled;
    }
    set isSkyboxEnabled(enabled) {
        this.viewer.isSkyboxEnabled = enabled;
    }
    /**
     * Gets or sets a value indicating whether to hide or show coordinate system axes.
     */
    get axes() {
        return this.viewer.axes;
    }
    set axes(isEnabled) {
        this.viewer.axes = isEnabled;
    }
    /**
     * Gets or sets options for 3D help grid visualization.
     */
    get grid() {
        return this.viewer.grid;
    }
    /** Called from {@link createApi}. Subclasses can implement if they need to initialize more data during API creation in {@link createApi} */
    async initialize() {
        return Promise.resolve();
    }
}
//# sourceMappingURL=BimCoreApi.js.map