import { VertexBuffer } from "../Buffers/buffer.js";
import { Camera } from "../Cameras/camera.js";

import { AddClipPlaneUniforms, BindClipPlane, PrepareStringDefinesForClipPlanes } from "../Materials/clipPlaneMaterialHelper.js";
import { EffectFallbacks } from "../Materials/effectFallbacks.js";
import { Material } from "../Materials/material.js";
import { BindBonesParameters, BindMorphTargetParameters, PrepareDefinesAndAttributesForMorphTargets, PushAttributesForInstances } from "../Materials/materialHelper.functions.js";
import { Color3, Color4 } from "../Maths/math.color.js";
import { ThinEffectLayer } from "./thinEffectLayer.js";
/**
 * @internal
 */
export class ThinSelectionOutlineLayer extends ThinEffectLayer {
    /**
     * Instantiates a new selection outline Layer and references it to the scene..
     * @param name The name of the layer
     * @param scene The scene to use the layer in
     * @param options Sets of none mandatory options to use with the layer (see IThinSelectionOutlineLayerOptions for more information)
     * @param dontCheckIfReady Specifies if the layer should disable checking whether all the post processes are ready (default: false). To save performance, this should be set to true and you should call `isReady` manually before rendering to the layer.
     */
    constructor(name, scene, options, dontCheckIfReady = false) {
        super(name, scene, options !== undefined ? !!options.forceGLSL : false);
        /**
         * The outline color
         */
        this.outlineColor = new Color3(1, 0.5, 0);
        /**
         * The thickness of the edges
         */
        this.outlineThickness = 2.0;
        /**
         * The strength of the occlusion effect (default: 0.8)
         */
        this.occlusionStrength = 0.8;
        /**
         * The occlusion threshold (default: 0.0001)
         */
        this.occlusionThreshold = 0.0001;
        /**
         * The width of the source texture
         */
        this.textureWidth = 0;
        /**
         * The height of the source texture
         */
        this.textureHeight = 0;
        /** @internal */
        this._meshUniqueIdToSelectionId = [];
        /** @internal */
        this._selection = [];
        this._instancedBufferSources = new Set();
        this._nextSelectionId = 1;
        // Adapt options
        this._options = {
            mainTextureRatio: 1.0,
            mainTextureFixedSize: 0,
            alphaBlendingMode: 2,
            camera: null,
            renderingGroupId: -1,
            forceGLSL: false,
            mainTextureType: 1,
            mainTextureFormat: 7,
            storeCameraSpaceZ: false,
            outlineMethod: 0,
            ...options,
        };
        // Fall back to a supported mask texture type if the device doesn't support rendering to float framebuffers
        // or linear filtering of float textures (e.g. OES_texture_float_linear missing on some iOS versions)
        if (this._options.mainTextureType === 1 && !(this._engine.getCaps().textureFloatRender && this._engine.getCaps().textureFloatLinearFiltering)) {
            this._options.mainTextureType = 2;
        }
        if (this._options.mainTextureType === 2 &&
            !(this._engine.getCaps().textureHalfFloatRender && this._engine.getCaps().textureHalfFloatLinearFiltering)) {
            this._options.mainTextureType = 0;
        }
        // When using an 8-bit render target, we cannot reliably store camera-space Z in the mask texture:
        // depth would be clamped/quantized, breaking occlusion comparisons. In that case, force-disable
        // storeCameraSpaceZ so the layer falls back to the supported behavior.
        if (this._options.storeCameraSpaceZ && this._options.mainTextureType === 0) {
            this._options.storeCameraSpaceZ = false;
        }
        // set clear color
        this.neutralColor = new Color4(0.0, this._options.storeCameraSpaceZ ? 0.0 : 1.0, 0.0, 1.0);
        // Initialize the layer
        this._init(this._options);
        // Do not render as long as no meshes have been added
        this._shouldRender = false;
        if (dontCheckIfReady) {
            // When dontCheckIfReady is true, we are in the new ThinXXX layer mode, so we must call _createTextureAndPostProcesses ourselves (it is called by EffectLayer otherwise)
            this._createTextureAndPostProcesses();
        }
    }
    /**
     * Gets the class name of the effect layer
     * @returns the string with the class name of the effect layer
     */
    getClassName() {
        return "SelectionOutlineLayer";
    }
    _isDef(val) {
        return val !== void 0 && val !== null;
    }
    /** @internal */
    _internalIsSubMeshReady(subMesh, useInstances, _emissiveTexture) {
        const engine = this._scene.getEngine();
        const mesh = subMesh.getMesh();
        const renderingMaterial = mesh._internalAbstractMeshDataInfo._materialForRenderPass?.[engine.currentRenderPassId];
        if (renderingMaterial) {
            return renderingMaterial.isReadyForSubMesh(mesh, subMesh, useInstances);
        }
        const material = subMesh.getMaterial();
        if (!material) {
            return false;
        }
        // selection outline layer is not compatible with custom materials
        // if (this._useMeshMaterial(subMesh.getRenderingMesh())) {
        //     return material.isReadyForSubMesh(subMesh.getMesh(), subMesh, useInstances);
        // }
        const defines = [];
        const attribs = [VertexBuffer.PositionKind];
        let uv1 = false;
        let uv2 = false;
        const color = false;
        // Alpha test
        if (material.needAlphaTestingForMesh(mesh)) {
            defines.push("#define ALPHATEST");
            if (mesh.isVerticesDataPresent(VertexBuffer.UVKind)) {
                attribs.push(VertexBuffer.UVKind);
                defines.push("#define UV1");
                uv1 = true;
            }
            if (mesh.isVerticesDataPresent(VertexBuffer.UV2Kind)) {
                attribs.push(VertexBuffer.UV2Kind);
                defines.push("#define UV2");
                uv2 = true;
            }
        }
        // Bones
        const fallbacks = new EffectFallbacks();
        if (mesh.useBones && mesh.computeBonesUsingShaders) {
            attribs.push(VertexBuffer.MatricesIndicesKind);
            attribs.push(VertexBuffer.MatricesWeightsKind);
            if (mesh.numBoneInfluencers > 4) {
                attribs.push(VertexBuffer.MatricesIndicesExtraKind);
                attribs.push(VertexBuffer.MatricesWeightsExtraKind);
            }
            defines.push("#define NUM_BONE_INFLUENCERS " + mesh.numBoneInfluencers);
            const skeleton = mesh.skeleton;
            if (skeleton && skeleton.isUsingTextureForMatrices) {
                defines.push("#define BONETEXTURE");
            }
            else {
                defines.push("#define BonesPerMesh " + (skeleton ? skeleton.bones.length + 1 : 0));
            }
            if (mesh.numBoneInfluencers > 0) {
                fallbacks.addCPUSkinningFallback(0, mesh);
            }
        }
        else {
            defines.push("#define NUM_BONE_INFLUENCERS 0");
        }
        // Morph targets
        const numMorphInfluencers = mesh.morphTargetManager
            ? PrepareDefinesAndAttributesForMorphTargets(mesh.morphTargetManager, defines, attribs, mesh, true, // usePositionMorph
            false, // useNormalMorph
            false, // useTangentMorph
            uv1, // useUVMorph
            uv2, // useUV2Morph
            color // useColorMorph
            )
            : 0;
        // Instances
        if (useInstances) {
            defines.push("#define INSTANCES");
            PushAttributesForInstances(attribs);
            if (subMesh.getRenderingMesh().hasThinInstances) {
                defines.push("#define THIN_INSTANCES");
            }
        }
        // Baked vertex animations
        const bvaManager = mesh.bakedVertexAnimationManager;
        if (bvaManager && bvaManager.isEnabled) {
            defines.push("#define BAKED_VERTEX_ANIMATION_TEXTURE");
            if (useInstances) {
                attribs.push("bakedVertexAnimationSettingsInstanced");
            }
        }
        // ClipPlanes
        PrepareStringDefinesForClipPlanes(material, this._scene, defines);
        // Selection ID
        if (useInstances) {
            attribs.push(ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName);
        }
        this._addCustomEffectDefines(defines);
        // Get correct effect
        const drawWrapper = subMesh._getDrawWrapper(undefined, true);
        const cachedDefines = drawWrapper.defines;
        const join = defines.join("\n");
        if (cachedDefines !== join) {
            const uniforms = [
                "world",
                "mBones",
                "viewProjection",
                "view",
                "morphTargetInfluences",
                "morphTargetCount",
                "boneTextureInfo",
                "diffuseMatrix",
                "morphTargetTextureInfo",
                "morphTargetTextureIndices",
                "bakedVertexAnimationSettings",
                "bakedVertexAnimationTextureSizeInverted",
                "bakedVertexAnimationTime",
                "bakedVertexAnimationTexture",
                "depthValues",
                "selectionId",
            ];
            AddClipPlaneUniforms(uniforms);
            drawWrapper.setEffect(this._engine.createEffect("selection", {
                attributes: attribs,
                uniformsNames: uniforms,
                uniformBuffersNames: [],
                samplers: ["diffuseSampler", "boneSampler", "morphTargets", "bakedVertexAnimationTexture"],
                defines: join,
                fallbacks: fallbacks,
                onCompiled: null,
                onError: null,
                indexParameters: { maxSimultaneousMorphTargets: numMorphInfluencers },
                shaderLanguage: this._shaderLanguage,
                extraInitializationsAsync: this._shadersLoaded
                    ? undefined
                    : async () => {
                        await this._importShadersAsync();
                        this._shadersLoaded = true;
                    },
            }, this._engine), join);
        }
        const effectIsReady = drawWrapper.effect.isReady();
        return effectIsReady && (this._dontCheckIfReady || (!this._dontCheckIfReady && this.isLayerReady()));
    }
    async _importShadersAsync() {
        if (this._shaderLanguage === 1 /* ShaderLanguage.WGSL */) {
            await Promise.all([
                import("../ShadersWGSL/selection.vertex.js"),
                import("../ShadersWGSL/selection.fragment.js"),
                import("../ShadersWGSL/glowMapMerge.vertex.js"),
                import("../ShadersWGSL/selectionOutline.fragment.js"),
            ]);
        }
        else {
            await Promise.all([
                import("../Shaders/selection.vertex.js"),
                import("../Shaders/selection.fragment.js"),
                import("../Shaders/glowMapMerge.vertex.js"),
                import("../Shaders/selectionOutline.fragment.js"),
            ]);
        }
        await super._importShadersAsync();
    }
    /**
     * Get the effect name of the layer.
     * @returns The effect name
     */
    getEffectName() {
        return ThinSelectionOutlineLayer.EffectName;
    }
    /** @internal */
    _createMergeEffect() {
        const defines = [];
        switch (this._options.outlineMethod) {
            case 0:
                defines.push("#define OUTLINELAYER_SAMPLING_TRIDIRECTIONAL");
                break;
            case 1:
                defines.push("#define OUTLINELAYER_SAMPLING_OCTADIRECTIONAL");
                break;
        }
        const join = defines.join("\n");
        return this._engine.createEffect({
            // glowMapMerge vertex is just a basic vertex shader for drawing a quad. so we reuse it here
            vertex: "glowMapMerge",
            // selection outline fragment does computation of outline with alpha channel for blending
            fragment: "selectionOutline",
        }, {
            attributes: [VertexBuffer.PositionKind],
            uniformsNames: ["screenSize", "outlineColor", "outlineThickness", "occlusionStrength", "occlusionThreshold"],
            samplers: ["maskSampler", "depthSampler"],
            defines: join,
            fallbacks: null,
            onCompiled: null,
            onError: null,
            shaderLanguage: this._shaderLanguage,
            extraInitializationsAsync: this._shadersLoaded
                ? undefined
                : async () => {
                    await this._importShadersAsync();
                    this._shadersLoaded = true;
                },
        }, this._engine);
    }
    /** @internal */
    _createTextureAndPostProcesses() {
        // we don't need to create a texture for this layer. since all computation is done in the merge effect
    }
    /**
     * Checks for the readiness of the element composing the layer.
     * @param subMesh the mesh to check for
     * @param useInstances specify whether or not to use instances to render the mesh
     * @returns true if ready otherwise, false
     */
    isReady(subMesh, useInstances) {
        const material = subMesh.getMaterial();
        const mesh = subMesh.getRenderingMesh();
        if (!material || !mesh || !this._selection) {
            return false;
        }
        return super._isSubMeshReady(subMesh, useInstances, null);
    }
    /** @internal */
    _canRenderMesh(_mesh, _material) {
        return true;
    }
    _renderSubMesh(subMesh, enableAlphaMode = false) {
        if (!this._internalShouldRender()) {
            return;
        }
        const material = subMesh.getMaterial();
        const ownerMesh = subMesh.getMesh();
        const replacementMesh = subMesh.getReplacementMesh();
        const renderingMesh = subMesh.getRenderingMesh();
        const effectiveMesh = subMesh.getEffectiveMesh();
        const scene = this._scene;
        const engine = scene.getEngine();
        effectiveMesh._internalAbstractMeshDataInfo._isActiveIntermediate = false;
        if (!material) {
            return;
        }
        // Do not block in blend mode.
        if (!this._canRenderMesh(renderingMesh, material)) {
            return;
        }
        // Culling
        let sideOrientation = material._getEffectiveOrientation(renderingMesh);
        const mainDeterminant = effectiveMesh._getWorldMatrixDeterminant();
        if (mainDeterminant < 0) {
            sideOrientation = sideOrientation === Material.ClockWiseSideOrientation ? Material.CounterClockWiseSideOrientation : Material.ClockWiseSideOrientation;
        }
        const reverse = sideOrientation === Material.ClockWiseSideOrientation;
        engine.setState(material.backFaceCulling, material.zOffset, undefined, reverse, material.cullBackFaces, undefined, material.zOffsetUnits);
        // Managing instances
        const batch = renderingMesh._getInstancesRenderList(subMesh._id, !!replacementMesh);
        if (batch.mustReturn) {
            return;
        }
        // Early Exit per mesh
        if (!this._shouldRenderMesh(renderingMesh)) {
            return;
        }
        const hardwareInstancedRendering = batch.hardwareInstancedRendering[subMesh._id] ||
            renderingMesh.hasThinInstances ||
            (!!renderingMesh._userInstancedBuffersStorage &&
                ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName in renderingMesh._userInstancedBuffersStorage.vertexBuffers);
        // When LOD transitions cause a different mesh to render with hardware instancing,
        // the LOD mesh's own instanceSelectionId must reflect the source mesh's value so
        // the "self" draw (representing the source mesh) gets the correct selection ID.
        if (hardwareInstancedRendering && renderingMesh._masterMesh && renderingMesh.instancedBuffers) {
            renderingMesh.instancedBuffers[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName] =
                renderingMesh._masterMesh.instancedBuffers?.[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName] ?? null;
        }
        this._setEmissiveTextureAndColor(renderingMesh, subMesh, material);
        this.onBeforeRenderMeshToEffect.notifyObservers(ownerMesh);
        // selection outline layer is not compatible with custom materials
        // if (this._useMeshMaterial(renderingMesh)) {
        //     subMesh.getMaterial()!._glowModeEnabled = true;
        //     renderingMesh.render(subMesh, enableAlphaMode, replacementMesh || undefined);
        //     subMesh.getMaterial()!._glowModeEnabled = false;
        // } else
        if (this._isSubMeshReady(subMesh, hardwareInstancedRendering, this._emissiveTextureAndColor.texture)) {
            const renderingMaterial = effectiveMesh._internalAbstractMeshDataInfo._materialForRenderPass?.[engine.currentRenderPassId];
            let drawWrapper = subMesh._getDrawWrapper();
            if (!drawWrapper && renderingMaterial) {
                drawWrapper = renderingMaterial._getDrawWrapper();
            }
            if (!drawWrapper) {
                return;
            }
            const effect = drawWrapper.effect;
            engine.enableEffect(drawWrapper);
            if (!hardwareInstancedRendering) {
                renderingMesh._bind(subMesh, effect, material.fillMode);
            }
            if (!renderingMaterial) {
                effect.setMatrix("viewProjection", scene.getTransformMatrix());
                if (this._options.storeCameraSpaceZ) {
                    effect.setMatrix("view", scene.getViewMatrix());
                }
                else {
                    const camera = this.camera || scene.activeCamera;
                    if (camera) {
                        const cameraIsOrtho = camera.mode === Camera.ORTHOGRAPHIC_CAMERA;
                        let minZ, maxZ;
                        if (cameraIsOrtho) {
                            minZ = !engine.useReverseDepthBuffer && engine.isNDCHalfZRange ? 0 : 1;
                            maxZ = engine.useReverseDepthBuffer && engine.isNDCHalfZRange ? 0 : 1;
                        }
                        else {
                            minZ = engine.useReverseDepthBuffer && engine.isNDCHalfZRange ? camera.minZ : engine.isNDCHalfZRange ? 0 : camera.minZ;
                            maxZ = engine.useReverseDepthBuffer && engine.isNDCHalfZRange ? 0 : camera.maxZ;
                        }
                        effect.setFloat2("depthValues", minZ, minZ + maxZ);
                    }
                }
                effect.setMatrix("world", effectiveMesh.getWorldMatrix());
            }
            else {
                renderingMaterial.bindForSubMesh(effectiveMesh.getWorldMatrix(), effectiveMesh, subMesh);
            }
            if (!renderingMaterial) {
                // Alpha test
                if (material && material.needAlphaTestingForMesh(effectiveMesh)) {
                    const alphaTexture = material.getAlphaTestTexture();
                    if (alphaTexture) {
                        effect.setTexture("diffuseSampler", alphaTexture);
                        effect.setMatrix("diffuseMatrix", alphaTexture.getTextureMatrix());
                    }
                }
                // Bones
                BindBonesParameters(renderingMesh, effect);
                // Morph targets
                BindMorphTargetParameters(renderingMesh, effect);
                if (renderingMesh.morphTargetManager && renderingMesh.morphTargetManager.isUsingTextureForTargets) {
                    renderingMesh.morphTargetManager._bind(effect);
                }
                // Baked vertex animations
                const bvaManager = subMesh.getMesh().bakedVertexAnimationManager;
                if (bvaManager && bvaManager.isEnabled) {
                    bvaManager.bind(effect, hardwareInstancedRendering);
                }
                // Alpha mode
                if (enableAlphaMode) {
                    engine.setAlphaMode(material.alphaMode);
                }
                // Clip planes
                BindClipPlane(effect, material, scene);
                // Selection ID
                let selectionId = this._meshUniqueIdToSelectionId[renderingMesh.uniqueId];
                // When using LOD, the rendering mesh is the LOD mesh, not the source mesh.
                // Look up the selection ID from the master (source) mesh.
                if (selectionId === undefined && renderingMesh._masterMesh) {
                    selectionId = this._meshUniqueIdToSelectionId[renderingMesh._masterMesh.uniqueId];
                    if (selectionId === undefined) {
                        // Prefer the ownerMesh (submesh owner) which is the actual instance
                        // being rendered during LOD, then fall back to the master (source) mesh.
                        selectionId =
                            ownerMesh.instancedBuffers?.[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName] ??
                                renderingMesh._masterMesh.instancedBuffers?.[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName];
                    }
                }
                if (!renderingMesh.hasInstances && !renderingMesh.hasThinInstances && !renderingMesh.isAnInstance && selectionId !== undefined) {
                    effect.setFloat("selectionId", selectionId);
                }
            }
            // Draw
            renderingMesh._processRendering(effectiveMesh, subMesh, effect, material.fillMode, batch, hardwareInstancedRendering, (isInstance, world) => effect.setMatrix("world", world));
        }
        else {
            // Need to reset refresh rate of the main map
            this._objectRenderer.resetRefreshCounter();
        }
        this.onAfterRenderMeshToEffect.notifyObservers(ownerMesh);
    }
    /** @internal */
    _internalCompose(effect, _renderIndex) {
        // Texture
        this.bindTexturesForCompose(effect);
        effect.setFloat2("screenSize", this.textureWidth, this.textureHeight);
        effect.setColor3("outlineColor", this.outlineColor);
        effect.setFloat("outlineThickness", this.outlineThickness);
        effect.setFloat("occlusionStrength", this.occlusionStrength);
        effect.setFloat("occlusionThreshold", this.occlusionThreshold);
        // Cache
        const engine = this._engine;
        const previousStencilBuffer = engine.getStencilBuffer();
        // Draw order
        engine.setStencilBuffer(false);
        engine.drawElementsType(Material.TriangleFillMode, 0, 6);
        // Draw order
        engine.setStencilBuffer(previousStencilBuffer);
    }
    /** @internal */
    _setEmissiveTextureAndColor(_mesh, _subMesh, _material) {
        // we don't use emissive texture or color for this layer
    }
    /**
     * Returns true if the layer contains information to display, otherwise false.
     * @returns true if the glow layer should be rendered
     */
    shouldRender() {
        return this._selection && super.shouldRender() ? true : false;
    }
    /** @internal */
    _shouldRenderMesh(mesh) {
        // Use the base class check (renderingGroupId) rather than this.hasMesh,
        // because LOD meshes won't be in _selection but still need to render.
        return super.hasMesh(mesh);
    }
    /** @internal */
    _addCustomEffectDefines(defines) {
        if (this._options.storeCameraSpaceZ) {
            defines.push("#define STORE_CAMERASPACE_Z");
        }
    }
    /**
     * Determine if a given mesh will be used in the current effect.
     * @param mesh mesh to test
     * @returns true if the mesh will be used
     */
    hasMesh(mesh) {
        if (!super.hasMesh(mesh) || !this._selection) {
            return false;
        }
        return this._selection.indexOf(mesh) !== -1;
    }
    /** @internal */
    _useMeshMaterial(_mesh) {
        return false;
    }
    _clearMeshSelection(mesh) {
        if (mesh._userInstancedBuffersStorage) {
            const kind = ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName;
            // Dispose per-pass VBOs for ALL render passes (WebGPU).
            // _processInstancedBuffers creates per-pass VBOs for every render pass
            // that renders this mesh (main scene, depth renderer, etc.), not just
            // this layer's own passes. We must clean them all up to avoid using
            // a destroyed GPU buffer on the next submit.
            if (mesh._userInstancedBuffersStorage.renderPasses) {
                for (const passId in mesh._userInstancedBuffersStorage.renderPasses) {
                    const renderPassId = Number(passId);
                    const passVBOs = mesh._userInstancedBuffersStorage.renderPasses[renderPassId];
                    if (passVBOs?.[kind]) {
                        passVBOs[kind].dispose();
                        delete passVBOs[kind];
                    }
                }
            }
            mesh._userInstancedBuffersStorage.vertexBuffers[kind]?.dispose();
            const vao = mesh._userInstancedBuffersStorage.vertexArrayObjects?.[kind];
            if (vao) {
                // invalidate VAO is very important to keep sync between VAO and vertex buffers
                this._engine.releaseVertexArrayObject(vao);
                delete mesh._userInstancedBuffersStorage.vertexArrayObjects[kind];
            }
            delete mesh._userInstancedBuffersStorage.data[kind];
            delete mesh._userInstancedBuffersStorage.vertexBuffers[kind];
            delete mesh._userInstancedBuffersStorage.strides[kind];
            delete mesh._userInstancedBuffersStorage.sizes[kind];
            if (Object.keys(mesh._userInstancedBuffersStorage.vertexBuffers).length === 0) {
                mesh._userInstancedBuffersStorage = undefined;
            }
        }
        if (mesh.instancedBuffers && ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName in mesh.instancedBuffers) {
            delete mesh.instancedBuffers[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName];
        }
        // In WebGPU non-compat mode, cached render bundles (fastBundle) bake
        // vertex-buffer GPU handles at record time. Because the new VBO has
        // the same format (and thus the same hashCode), the pipeline cache
        // won't detect the change and would replay the stale bundle.
        // Resetting the draw cache forces new bundles to be recorded.
        if (this._engine.isWebGPU && !this._engine.compatibilityMode) {
            mesh.resetDrawCache();
        }
        if (this._selection) {
            const index = this._selection.indexOf(mesh);
            if (~index) {
                this._selection.splice(index, 1);
            }
        }
    }
    /**
     * Remove all the meshes currently referenced in the selection outline layer
     */
    clearSelection() {
        if (!this._selection) {
            return;
        }
        for (let index = 0; index < this._selection.length; ++index) {
            const mesh = this._selection[index];
            this._clearMeshSelection(mesh);
        }
        // addSelection registers instanceSelectionId on the source mesh
        // (via sourceMesh.registerInstancedBuffer), but _selection contains
        // the instance, not the source. Clean up source meshes separately.
        this._instancedBufferSources.forEach((sourceMesh) => this._clearMeshSelection(sourceMesh));
        this._instancedBufferSources.clear();
        this._selection.length = 0;
        this._meshUniqueIdToSelectionId.length = 0;
        this._nextSelectionId = 1;
        this._shouldRender = false;
    }
    /**
     * Adds mesh or group of mesh to the current selection
     *
     * If a group of meshes is provided, they will outline as a single unit
     * @param meshOrGroup Meshes to add to the selection
     */
    addSelection(meshOrGroup) {
        if (!this._selection) {
            return;
        }
        const nextId = this._nextSelectionId;
        const group = Array.isArray(meshOrGroup) ? meshOrGroup : [meshOrGroup];
        if (group.length === 0) {
            return;
        }
        for (let meshIndex = 0; meshIndex < group.length; ++meshIndex) {
            const mesh = group[meshIndex];
            this._selection.push(mesh); // add to render list
            if (mesh.hasInstances || mesh.isAnInstance) {
                const sourceMesh = mesh.sourceMesh ?? mesh;
                if (!this._isDef(sourceMesh.instancedBuffers?.[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName])) {
                    sourceMesh.registerInstancedBuffer(ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName, 1);
                }
                this._instancedBufferSources.add(sourceMesh);
                mesh.instancedBuffers[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName] = nextId;
                // Also register instanceSelectionId on LOD meshes so hardware instancing
                // works during LOD transitions (LOD meshes replace the source mesh as the
                // renderingMesh, and _processInstancedBuffers only processes kinds that
                // are registered on the rendering mesh).
                const lodLevels = sourceMesh.getLODLevels();
                for (const level of lodLevels) {
                    if (level.mesh) {
                        if (level.mesh.instancedBuffers?.[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName] === undefined) {
                            level.mesh.registerInstancedBuffer(ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName, 1);
                        }
                        this._instancedBufferSources.add(level.mesh);
                    }
                }
            }
            else if (mesh.hasThinInstances) {
                const thinInstanceCount = mesh.thinInstanceCount;
                const selectionIdData = new Float32Array(thinInstanceCount);
                for (let i = 0; i < thinInstanceCount; i++) {
                    selectionIdData[i] = nextId;
                }
                mesh.thinInstanceSetBuffer(ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName, selectionIdData, 1);
            }
            else {
                this._meshUniqueIdToSelectionId[mesh.uniqueId] = nextId;
            }
        }
        this._nextSelectionId += 1;
        this._shouldRender = true;
    }
    /**
     * Adds mesh or group of mesh to the current selection
     * @param meshOrGroup Meshes to remove from the selection
     */
    removeSelection(meshOrGroup) {
        if (!this._selection) {
            return;
        }
        const group = Array.isArray(meshOrGroup) ? meshOrGroup : [meshOrGroup];
        if (group.length === 0) {
            return;
        }
        for (let i = 0; i < group.length; i++) {
            const mesh = group[i];
            this._clearMeshSelection(mesh);
            delete this._meshUniqueIdToSelectionId[mesh.uniqueId];
        }
        if (!this._selection || !this._selection.length) {
            this._meshUniqueIdToSelectionId.length = 0;
            this._nextSelectionId = 1;
            this._shouldRender = false;
        }
    }
    /**
     * Free any resources and references associated to a mesh.
     * Internal use
     * @param mesh The mesh to free.
     * @internal
     */
    _disposeMesh(mesh) {
        const selection = this._selection;
        if (!selection) {
            return;
        }
        const index = selection.indexOf(mesh);
        if (index !== -1) {
            selection.splice(index, 1);
            if (mesh.hasInstances) {
                mesh.removeVerticesData(ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName);
            }
            else if (mesh.hasThinInstances) {
                mesh.thinInstanceSetBuffer(ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName, null);
            }
            if (mesh.isAnInstance) {
                const sourceMesh = mesh.sourceMesh;
                // Only remove the source from tracking if no other selected
                // instance shares it, to avoid leaking its GPU resources.
                if (sourceMesh && !selection.some((m) => m !== mesh && m.isAnInstance && m.sourceMesh === sourceMesh)) {
                    this._instancedBufferSources.delete(sourceMesh);
                }
            }
            if (selection.length === 0) {
                this._shouldRender = false;
            }
        }
    }
    /**
     * Dispose the effect layer and free resources.
     */
    dispose() {
        this.clearSelection();
        this._selection = null;
        super.dispose();
    }
}
/**
 * Effect Name of the layer.
 */
ThinSelectionOutlineLayer.EffectName = "SelectionOutlineLayer";
/**
 * Name of the instance selection ID attribute
 * @internal
 */
ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName = "instanceSelectionId";
//# sourceMappingURL=thinSelectionOutlineLayer.js.map