import { Vector3 } from '@babylonjs/core';
import { setMax, setMin, nextPow2 } from '../math';
import { IntVector3Map } from './IntVector3Map';
/** Minimum side length for AABB grid cells in world units. */
const minAABBSideLen = 1;
/** Precomputed log2(minAABBSideLen) for level calculation optimization. */
const log2MinAABBSideLen = Math.log2(minAABBSideLen);
/**
 * AABB type configurations for categorizing meshes by size.
 * - `large`: Default type for meshes exceeding {@link small.maxLength}, no distance-based culling.
 * - `small`: For meshes smaller than {@link small.maxLength}, enables distance-based culling at 15 units.
 */
const aabbTypes = {
    large: { id: 1, k: 1 / minAABBSideLen, suffix: 'l', distance: -1 },
    small: { id: 0, k: 1 / minAABBSideLen, suffix: 's', maxLength: 0.3, distance: 15 }
};
/** Character codes for merge ID generation: 'o' for opaque, 't' for transparent. */
const opaqueOrTransparentIdChar = ['o', 't'];
/** Character codes for merge ID generation: 'c' for cull-on-camera-move, 'n' for no culling. */
const cullOnCameraMoveIdChar = ['c', 'n'];
/**
 * Temporary vectors used for calculations to avoid allocations.
 * `cubeCellId`: Used to store the id of a cube cell in the spatial grid.
 * {@link BimProductMesh} instances are grouped into these cells for merging based on spatial locality.
 */
const cubeCellId = Vector3.Zero();
const aabbSizeLengths = Vector3.Zero();
const aabbMin = setMax(Vector3.Zero());
const aabbMax = setMin(Vector3.Zero());
const parentCubeCellId = Vector3.Zero();
/**
 * Manages the creation and hierarchical merging of {@link CubeCellMergeId} instances.
 *
 * Meshes are spatially hashed into grid cells based on their AABB. Small cells with
 * insufficient geometry are progressively merged into larger parent cells to optimize
 * rendering by reducing draw calls while maintaining spatial locality.
 */
export class CubeCellMergeIdAlgorithm {
    prefix;
    /** Minimum index count required per cube to avoid merging.*/
    _minIndiceCountRequiredPerCube;
    /** Maps organized by mesh type (8 combinations of opacity, AABB type, and cull-on-move). */
    _mergeIdMapsByMeshType = Array.from({ length: 8 }, () => new Map());
    /** Maps level number to the set of {@link CubeCellMergeId} instances at that level. */
    _mergeIdsByLevel = new Map();
    /**
     * Creates a new MergeIdMap instance.
     * @param prefix - Prefix string used when generating merge ID strings.
     * @param minIndiceCountRequiredPerCube - Minimum index count required per cube to avoid merging. Defaults to 128k indices.
     */
    constructor(prefix, minIndiceCountRequiredPerCube = 1 << 17) {
        this.prefix = prefix;
        this._minIndiceCountRequiredPerCube = minIndiceCountRequiredPerCube;
    }
    /**
     * Adds a mesh to the appropriate {@link MergeId} based on its spatial location and properties.
     *
     * The mesh's AABB is calculated in world space and assigned to a grid cell. The cell size
     * is the smallest power-of-2 that can contain the mesh's AABB (minimum {@link minAABBSideLen}).
     *
     * @param mesh - The product mesh to add.
     * @param cullOnCameraMovePredicate - Predicate to determine if the mesh should be culled on camera move.
     * @returns The {@link MergeId} that the mesh was assigned to.
     */
    add(mesh, cullOnCameraMovePredicate) {
        // Calculate AABB of the mesh in world space. Then determine which cube cell this AABB can be placed into.
        // The AABB can be fit into a cubecell if its center is located within the cubecell AND if it has a equal or smaller side length that the cubecell.
        // This means that we may end up with objects that are not located entirely inside the cubecell (and in some cases not at all, depends on what the object looks like)-
        // However cubecells are only meant to group spatially close objects so this does not matter.
        // Think of cubecells as the cells in a rubiks cube. However they exist on many "levels" with sides that are a powerof 2. So 16x16x16, 32x32x32,64x64x64 etc.
        // NOTE: This method is called millions of times so it is important to avoid allocations and keep it optimized. This is why we
        // reuse vector3 objects etc.
        setMax(aabbMin);
        setMin(aabbMax);
        // Now we have the aabb of the mesh in world space in aabbMin and aabbMax
        // NOTE: We do not clear the transform cache here because same transform  used in aabb calculation will often be used again soon after
        mesh.aabb(aabbMin, aabbMax, false);
        // calculate length of mesh aabb sides. Result is stored in aabbSizeLengths
        aabbMax.subtractToRef(aabbMin, aabbSizeLengths);
        // aabbSizeLengths is guaranteed to be >= 0 so max side len is always >= 0
        const maxSideLen = Math.max(aabbSizeLengths.x, aabbSizeLengths.y, aabbSizeLengths.z);
        const aabbType = maxSideLen > aabbTypes.small.maxLength ? aabbTypes.large : aabbTypes.small;
        // Figure out the closest power of two side length that would fit the mesh. However it must be at least minAABBSideLen
        const pow2SideLen = Math.max(minAABBSideLen, nextPow2(maxSideLen));
        // Calculate cube id (ie not world space coordinates). We
        // Meshes that get the same cube id id will be spatially close and
        // can be merged into the same babylonjs mesh (if they also share other properties like opacity).
        aabbMin.addToRef(aabbMax, cubeCellId).scaleInPlace(0.5 / pow2SideLen);
        cubeCellId.x = Math.trunc(cubeCellId.x);
        cubeCellId.y = Math.trunc(cubeCellId.y);
        cubeCellId.z = Math.trunc(cubeCellId.z);
        const opacityId = Number(mesh.style.a < 255); // 0 or 1
        const cullOnCameraMoveId = Number(cullOnCameraMovePredicate(mesh, aabbType)); // 0 or 1
        const ret = this.getOrAddMergeId(cubeCellId, opacityId, cullOnCameraMoveId, aabbType, pow2SideLen, this._mergeIdMapsByMeshType);
        mesh._setMergeId(ret);
        ret.indiceCount += mesh.descriptor.i;
        ret.primitiveCount += mesh.descriptor.p;
        // After _setMergeId the mesh has a merge identity and is conceptually a BimProductMesh
        ret.ifcProductMeshes.push(mesh);
        return ret;
    }
    /**
     * Gets an existing {@link CubeCellMergeId} or creates a new one for the given parameters.
     *
     * @param cubeCellId - The grid cell coordinates (not world space).
     * @param opacityId - 0 for opaque, 1 for transparent.
     * @param cullOnCameraMoveId - 0 for no culling, 1 for cull on camera move.
     * @param aabbType - The AABB type configuration (large or small).
     * @param pow2SideLen - The power-of-2 side length of the grid cell.
     * @param dst - The destination map structure to store/retrieve the MergeId.
     * @returns The existing or newly created {@link CubeCellMergeId}.
     */
    getOrAddMergeId(cubeCellId, opacityId, cullOnCameraMoveId, aabbType, pow2SideLen, dst) {
        const meshType = opacityId | (aabbType.id << 1) | (cullOnCameraMoveId << 2);
        // TODO: Can be optimized further by precomputing log2 of pow2SideLen if needed.
        const level = Math.log2(pow2SideLen) - log2MinAABBSideLen;
        if (!Number.isFinite(level))
            throw new Error('Level is not finite. This is a BUG!');
        // NOTE: Here usage of map.getOrAdd() is ok because IntVector3Map.create already exists and takes no parameters.
        // hence no extra closure/allocation is needed.
        const levelMap = dst[meshType].getOrAdd(level, IntVector3Map.create);
        // NOTE: We avoid map.getOrAdd() here to reduce allocations because the factory function of getOrAdd only takes
        // one parameter and we then need to create a closure to capture all the other parameters. Since this method
        // is executed for millions of meshes we want to avoid that allocation.
        let existingMergeId = levelMap.get(cubeCellId);
        if (existingMergeId == null) {
            const cx = cubeCellId.x;
            const cy = cubeCellId.y;
            const cz = cubeCellId.z;
            const mergeId = `${this.prefix}:${opaqueOrTransparentIdChar[opacityId]}${cullOnCameraMoveIdChar[cullOnCameraMoveId]} ${level}@(${cx},${cy},${cz})->${pow2SideLen}|${aabbType.suffix}`;
            existingMergeId = {
                id: mergeId,
                meshType,
                opacityId,
                cullOnCameraMoveId,
                pow2SideLen,
                aabbType,
                cullingDistance: aabbType.distance,
                // Calculate center of the cube cell that this ifc product mesh will be merged into (in world space)
                cubeCellCenter: new Vector3((cx + 0.5) * pow2SideLen, (cy + 0.5) * pow2SideLen, (cz + 0.5) * pow2SideLen),
                cubeCellId: cubeCellId.clone(),
                cullOnCameraMove: !!cullOnCameraMoveId,
                indiceCount: 0,
                primitiveCount: 0,
                level: level,
                ifcProductMeshes: []
            };
            levelMap.set(cubeCellId, existingMergeId);
        }
        // NOTE: Use of getOrAdd here is ok because MergeIdSet.createMergeIdSet takes no parameters and so no closure/allocation is needed.
        this._mergeIdsByLevel
            .getOrAdd(existingMergeId.level, CubeCellMergeIdAlgorithm.createMergeIdSet)
            .add(existingMergeId);
        return existingMergeId;
    }
    /**
     * Builds the final list of {@link MergeId} instances by hierarchically merging small cubes into larger ones.
     *
     * Processing starts at level 0 (smallest cubes) and progresses to larger levels. MergeIds with
     * insufficient indices are merged into their parent cube at the next level. This continues until
     * either only one MergeId remains (nothing to merge with) or all MergeIds have sufficient indices.
     *
     * @returns Array of {@link MergeId} instances that have at least one assigned mesh.
     */
    build() {
        if (this._mergeIdsByLevel.size === 0)
            return [];
        const maxLevel = Math.max(...this._mergeIdsByLevel.keys());
        for (let level = 0;; level++) {
            const mergeIdsOnLevel = this._mergeIdsByLevel.get(level) ?? new Set();
            if (this.isMergingComplete(level, maxLevel, mergeIdsOnLevel))
                break;
            for (const mergeId of mergeIdsOnLevel) {
                // This mergeId has enough indices to justify its own babylonjs mesh. Leave it be.
                if (!this.isMergeCandidate(mergeId))
                    continue;
                // This will add a merge id to  mergeId.level + 1 in this._mergeIdsByLevel
                // level + 1 may be a entirely new level that did not exist before (we had no mergeIds at all for it).
                const parentMergeId = this.getOrAddParentCubeCell(mergeId, this._mergeIdMapsByMeshType);
                // Transfer all ifcProductMeshes to parent mergeId
                this.moveIfcProductMeshesTo(mergeId, parentMergeId);
                // Remove mergeId from current level set
                mergeIdsOnLevel.delete(mergeId);
            }
            if (mergeIdsOnLevel.size === 0) {
                this._mergeIdsByLevel.delete(level);
            }
        }
        // Sort levels ascending (smallest cubes first), then fan out from min corner within each level
        const levels = Array.from(this._mergeIdsByLevel.keys()).sort((a, b) => a - b);
        const ret = [];
        const cubeCellMin = setMax(Vector3.Zero());
        for (const level of levels) {
            const mergeIdsOnLevel = Array.from(this._mergeIdsByLevel.get(level));
            // Find min corner point for this level
            for (const m of mergeIdsOnLevel) {
                cubeCellMin.minimizeInPlace(m.cubeCellId);
            }
            // Sort by distance from min corner (fan out)
            mergeIdsOnLevel.sort((a, b) => {
                const distSqA = Vector3.DistanceSquared(a.cubeCellId, cubeCellMin);
                const distSqB = Vector3.DistanceSquared(b.cubeCellId, cubeCellMin);
                return distSqA - distSqB;
            });
            for (const mergeId of mergeIdsOnLevel) {
                // TODO: Consider forcing cullOnCameraMove for very small cubes if needed. Does give a little too
                //       weird of an effect in some cases though.
                // (mergeId.cullOnCameraMove as boolean) ||= mergeId.pow2SideLen <= 4; // Force cullOnCameraMove for very small cubes
                ret.push(mergeId);
            }
        }
        return ret;
    }
    getOrAddParentCubeCell(mergeId, dst) {
        // Closest parent cube cell id is mergeId.cubeCellId / 2 floored. Why? Thinks of cube cells as a grid.
        // Each parent cube cell contains 8 child cube cells. So to find the parent cube cell we just divide by two and floor.
        // Example: Child cube cell at (3,2,5) is contained in parent cube cell at (1,1,2):
        mergeId.cubeCellId.scaleToRef(0.5, parentCubeCellId);
        parentCubeCellId.x = Math.trunc(parentCubeCellId.x);
        parentCubeCellId.y = Math.trunc(parentCubeCellId.y);
        parentCubeCellId.z = Math.trunc(parentCubeCellId.z);
        const isFiniteCubeCellId = Number.isFinite(parentCubeCellId.x) &&
            Number.isFinite(parentCubeCellId.y) &&
            Number.isFinite(parentCubeCellId.z);
        if (!isFiniteCubeCellId)
            throw new Error('CubeCellId Infinity encountered when calculating parent cubeCellId. This is a BUG!');
        return this.getOrAddMergeId(parentCubeCellId, mergeId.opacityId, mergeId.cullOnCameraMoveId, mergeId.aabbType, mergeId.pow2SideLen * 2, dst);
    }
    /**
     * Transfer all ifcProductMeshes from one mergeId to another. Moving a ifc prdoduct mesh also means updating its mergeId reference
     * as it now belongs to a different mergeId.
     * @param mergeId {@link CubeCellMergeId} to move ifcProductMeshes from.
     * @param parentMergeId {@link CubeCellMergeId} to move ifcProductMeshes to.
     */
    moveIfcProductMeshesTo(mergeId, parentMergeId) {
        for (const ifcProductMesh of mergeId.ifcProductMeshes) {
            parentMergeId.ifcProductMeshes.push(ifcProductMesh);
            ifcProductMesh._setMergeId(parentMergeId);
        }
        parentMergeId.indiceCount += mergeId.indiceCount;
        parentMergeId.primitiveCount += mergeId.primitiveCount;
        mergeId.ifcProductMeshes.length = 0;
    }
    /**
     * Check if merging is complete at the given level.
     *
     * Two mergeIds can be merged if they:
     * 1. Are both merge candidates (indiceCount < threshold)
     * 2. Have the same meshType (items of different mesh types cannot merge together)
     *
     * @param level Current level being processed.
     * @param maxLevel Max level encountered at start of build.
     * @param mergeIdsOnLevel mergeIds present on {@link level}.
     * @returns `true` if merging is complete. Otherwise `false`.
     */
    isMergingComplete(level, maxLevel, mergeIdsOnLevel) {
        if (!Number.isFinite(level))
            throw new Error('Level is not finite. This is a BUG!');
        // Before maxLevel, always continue
        if (level < maxLevel)
            return false;
        // At or past maxLevel: check if any merging is possible
        // Merging requires ≥2 candidates of the SAME meshType
        const createEmptyMeshTypeCount = () => ({ count: 0 });
        const candidateCountByMeshType = new Map();
        for (const m of mergeIdsOnLevel) {
            if (!this.isMergeCandidate(m))
                continue;
            const meshType = candidateCountByMeshType.getOrAdd(m.meshType, createEmptyMeshTypeCount);
            meshType.count++;
            if (meshType.count >= 2)
                return false; // Found 2+ candidates of same type, continue merging
        }
        return true; // No mesh type has ≥2 candidates, stop
    }
    /**
     * Determine if the given mergeId is a candidate for merging into a larger cube.
     * @param mergeId {@link CubeCellMergeId} to evaluate.
     * @returns `true` if the mergeId is a candidate for merging. Otherwise `false`.
     */
    isMergeCandidate(mergeId) {
        return mergeId.indiceCount < this._minIndiceCountRequiredPerCube;
    }
    /**
     * Creates a new Set instance for storing {@link CubeCellMergeId} instances.
     * @returns A new Set instance for {@link CubeCellMergeId} objects.
     */
    static createMergeIdSet() {
        return new Set();
    }
}
//# sourceMappingURL=CubeCellMergeIdAlgorithm.js.map